/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #include "libpika/pika.h" #include "libpika/pikaui.h" #include #include "script-fu-console.h" #include "script-fu-console-editor.h" #include "script-fu-console-history.h" #include "script-fu-console-total.h" #include "script-fu-lib.h" #include "script-fu-intl.h" #define TEXT_WIDTH 480 #define TEXT_HEIGHT 400 #define PROC_NAME "plug-in-script-fu-console" typedef struct { GtkWidget *dialog; GtkTextBuffer *total_history; GtkWidget *editor; GtkWidget *history_view; GtkWidget *proc_browser; GtkWidget *save_dialog; CommandHistory history; } ConsoleInterface; enum { RESPONSE_CLEAR, RESPONSE_SAVE }; /* * Local Functions */ static void script_fu_console_response (GtkWidget *widget, gint response_id, ConsoleInterface *console); static void script_fu_console_save_dialog (ConsoleInterface *console); static void script_fu_console_save_response (GtkWidget *dialog, gint response_id, ConsoleInterface *console); static void script_fu_browse_callback (GtkWidget *widget, ConsoleInterface *console); static void script_fu_browse_response (GtkWidget *widget, gint response_id, ConsoleInterface *console); static void script_fu_browse_row_activated (GtkDialog *dialog); static gboolean script_fu_editor_key_function (GtkWidget *widget, GdkEventKey *event, ConsoleInterface *console); static void script_fu_console_scroll_end (GtkWidget *view); static void script_fu_output_to_console (gboolean is_error, const gchar *text, gint len, gpointer user_data); static void script_fu_models_from_settings (ConsoleInterface *console, PikaProcedureConfig *config); static void script_fu_command_to_history (ConsoleInterface *console, const gchar *command); /* * Function definitions */ PikaValueArray * script_fu_console_run (PikaProcedure *procedure, const PikaValueArray *args) { ConsoleInterface console = { 0, }; GtkWidget *vbox; GtkWidget *button; GtkWidget *scrolled_window; GtkWidget *hbox; PikaProcedureConfig *config; script_fu_set_print_flag (1); pika_ui_init ("script-fu"); /* Create model early so we can fill from settings. */ console.total_history = console_total_history_new (); console_history_init (&console.history); console_total_append_welcome (console.total_history); /* Get previous or default settings into config. */ config = pika_procedure_create_config (procedure); pika_procedure_config_begin_run (config, NULL, PIKA_RUN_INTERACTIVE, args); script_fu_models_from_settings (&console, config); console.dialog = pika_dialog_new (_("Script Console"), "pika-script-fu-console", NULL, 0, pika_standard_help_func, PROC_NAME, _("_Save"), RESPONSE_SAVE, _("C_lear"), RESPONSE_CLEAR, _("_Close"), GTK_RESPONSE_CLOSE, NULL); gtk_window_set_default_size (GTK_WINDOW (console.dialog), TEXT_WIDTH, TEXT_HEIGHT); pika_dialog_set_alternative_button_order (GTK_DIALOG (console.dialog), GTK_RESPONSE_CLOSE, RESPONSE_CLEAR, RESPONSE_SAVE, -1); g_object_add_weak_pointer (G_OBJECT (console.dialog), (gpointer) &console.dialog); g_signal_connect (console.dialog, "response", G_CALLBACK (script_fu_console_response), &console); /* The main vbox */ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (console.dialog))), vbox, TRUE, TRUE, 0); gtk_widget_show (vbox); /* A view of the total history. */ scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS); gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); gtk_widget_show (scrolled_window); console.history_view = gtk_text_view_new_with_buffer (console.total_history); /* View keeps reference. Unref our ref so buffer is destroyed with view. */ g_object_unref (console.total_history); gtk_text_view_set_editable (GTK_TEXT_VIEW (console.history_view), FALSE); gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (console.history_view), GTK_WRAP_WORD); gtk_text_view_set_left_margin (GTK_TEXT_VIEW (console.history_view), 6); gtk_text_view_set_right_margin (GTK_TEXT_VIEW (console.history_view), 6); gtk_widget_set_size_request (console.history_view, TEXT_WIDTH, TEXT_HEIGHT); gtk_container_add (GTK_CONTAINER (scrolled_window), console.history_view); gtk_widget_show (console.history_view); /* An editor of a command to be executed. */ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); console.editor = console_editor_new (); gtk_box_pack_start (GTK_BOX (hbox), console.editor, TRUE, TRUE, 0); gtk_widget_grab_focus (console.editor); gtk_widget_show (console.editor); g_signal_connect (console.editor, "key-press-event", G_CALLBACK (script_fu_editor_key_function), &console); button = gtk_button_new_with_mnemonic (_("_Browse...")); g_object_set (gtk_bin_get_child (GTK_BIN (button)), "margin-start", 2, "margin-end", 2, NULL); gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, TRUE, 0); gtk_widget_show (button); g_signal_connect (button, "clicked", G_CALLBACK (script_fu_browse_callback), &console); gtk_widget_show (console.dialog); /* The history model may fill the view, scroll. */ script_fu_console_scroll_end (console.history_view); gtk_main (); if (console.save_dialog) gtk_widget_destroy (console.save_dialog); if (console.dialog) gtk_widget_destroy (console.dialog); /* Update config with user's change to history */ console_history_to_settings (&console.history, config); /* Persist config */ pika_procedure_config_end_run (config, PIKA_PDB_SUCCESS); return pika_procedure_new_return_values (procedure, PIKA_PDB_SUCCESS, NULL); } static void script_fu_console_response (GtkWidget *widget, gint response_id, ConsoleInterface *console) { switch (response_id) { case RESPONSE_CLEAR: console_total_history_clear (console->total_history); break; case RESPONSE_SAVE: script_fu_console_save_dialog (console); break; default: gtk_main_quit (); break; } } static void script_fu_console_save_dialog (ConsoleInterface *console) { if (! console->save_dialog) { console->save_dialog = gtk_file_chooser_dialog_new (_("Save Script-Fu Console Output"), GTK_WINDOW (console->dialog), GTK_FILE_CHOOSER_ACTION_SAVE, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Save"), GTK_RESPONSE_OK, NULL); gtk_dialog_set_default_response (GTK_DIALOG (console->save_dialog), GTK_RESPONSE_OK); pika_dialog_set_alternative_button_order (GTK_DIALOG (console->save_dialog), GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1); gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (console->save_dialog), TRUE); g_object_add_weak_pointer (G_OBJECT (console->save_dialog), (gpointer) &console->save_dialog); g_signal_connect (console->save_dialog, "response", G_CALLBACK (script_fu_console_save_response), console); } gtk_window_present (GTK_WINDOW (console->save_dialog)); } static void script_fu_console_save_response (GtkWidget *dialog, gint response_id, ConsoleInterface *console) { if (response_id == GTK_RESPONSE_OK) { gchar *filename; gchar *str; FILE *fh; filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); fh = g_fopen (filename, "w"); if (! fh) { g_message (_("Could not open '%s' for writing: %s"), pika_filename_to_utf8 (filename), g_strerror (errno)); g_free (filename); return; } str = console_total_history_get_text (console->total_history); fputs (str, fh); fclose (fh); g_free (str); } gtk_widget_hide (dialog); } static void script_fu_browse_callback (GtkWidget *widget, ConsoleInterface *console) { if (! console->proc_browser) { console->proc_browser = pika_proc_browser_dialog_new (_("Script-Fu Procedure Browser"), "script-fu-procedure-browser", pika_standard_help_func, PROC_NAME, _("_Apply"), GTK_RESPONSE_APPLY, _("_Close"), GTK_RESPONSE_CLOSE, NULL); gtk_dialog_set_default_response (GTK_DIALOG (console->proc_browser), GTK_RESPONSE_APPLY); pika_dialog_set_alternative_button_order (GTK_DIALOG (console->proc_browser), GTK_RESPONSE_CLOSE, GTK_RESPONSE_APPLY, -1); g_object_add_weak_pointer (G_OBJECT (console->proc_browser), (gpointer) &console->proc_browser); g_signal_connect (console->proc_browser, "response", G_CALLBACK (script_fu_browse_response), console); g_signal_connect (console->proc_browser, "row-activated", G_CALLBACK (script_fu_browse_row_activated), console); } gtk_window_present (GTK_WINDOW (console->proc_browser)); } static void script_fu_browse_response (GtkWidget *widget, gint response_id, ConsoleInterface *console) { PikaProcBrowserDialog *dialog = PIKA_PROC_BROWSER_DIALOG (widget); PikaProcedure *procedure; gchar *proc_name; GParamSpec **pspecs; gint n_pspecs; gint i; GString *text; if (response_id != GTK_RESPONSE_APPLY) { gtk_widget_destroy (widget); return; } proc_name = pika_proc_browser_dialog_get_selected (dialog); if (proc_name == NULL) return; procedure = pika_pdb_lookup_procedure (pika_get_pdb (), proc_name); pspecs = pika_procedure_get_arguments (procedure, &n_pspecs); text = g_string_new ("("); text = g_string_append (text, proc_name); for (i = 0; i < n_pspecs; i++) { text = g_string_append_c (text, ' '); text = g_string_append (text, pspecs[i]->name); } text = g_string_append_c (text, ')'); gtk_window_set_focus (GTK_WINDOW (console->dialog), console->editor); console_editor_set_text_and_position (console->editor, text->str, g_utf8_pointer_to_offset ( text->str, text->str + strlen (proc_name) + 2)); g_string_free (text, TRUE); gtk_window_present (GTK_WINDOW (console->dialog)); g_free (proc_name); } static void script_fu_browse_row_activated (GtkDialog *dialog) { gtk_dialog_response (dialog, GTK_RESPONSE_APPLY); } static gboolean script_fu_console_idle_scroll_end (GtkWidget *view) { GtkWidget *parent = gtk_widget_get_parent (view); if (parent) { GtkAdjustment *adj; adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (parent)); gtk_adjustment_set_value (adj, gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj)); } g_object_unref (view); return FALSE; } static void script_fu_console_scroll_end (GtkWidget *view) { /* the text view idle updates, so we need to idle scroll too */ g_object_ref (view); g_idle_add ((GSourceFunc) script_fu_console_idle_scroll_end, view); } /* Write result of eval to the console view. * But not put results in the history model. */ static void script_fu_output_to_console (gboolean is_error_msg, const gchar *result_text, gint len, gpointer user_data) { ConsoleInterface *console = user_data; if (console && console->history_view) { if (! is_error_msg) console_total_append_text_normal (console->total_history, result_text, len); else console_total_append_text_emphasize (console->total_history, result_text, len); script_fu_console_scroll_end (console->history_view); } } static gboolean script_fu_editor_key_function (GtkWidget *widget, GdkEventKey *event, ConsoleInterface *console) { gint direction = 0; GString *output; gboolean is_error; const gchar *command; switch (event->keyval) { case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_ISO_Enter: if (console_editor_is_empty (console->editor)) return TRUE; command = g_strdup (console_editor_get_text (console->editor)); script_fu_command_to_history (console, command); /* Assert history advanced to new, empty tail. */ script_fu_console_scroll_end (console->history_view); console_editor_clear (console->editor); output = g_string_new (NULL); script_fu_redirect_output_to_gstr (output); pika_plug_in_set_pdb_error_handler (pika_get_plug_in (), PIKA_PDB_ERROR_HANDLER_PLUGIN); is_error = script_fu_interpret_string (command); script_fu_output_to_console (is_error, output->str, output->len, console); pika_plug_in_set_pdb_error_handler (pika_get_plug_in (), PIKA_PDB_ERROR_HANDLER_INTERNAL); g_string_free (output, TRUE); pika_displays_flush (); return TRUE; break; case GDK_KEY_KP_Up: case GDK_KEY_Up: direction = -1; break; case GDK_KEY_KP_Down: case GDK_KEY_Down: direction = 1; break; case GDK_KEY_P: case GDK_KEY_p: if (event->state & GDK_CONTROL_MASK) direction = -1; break; case GDK_KEY_N: case GDK_KEY_n: if (event->state & GDK_CONTROL_MASK) direction = 1; break; default: /* Any other key is the user editing. * Set cursor to tail: user is done scrolling history. * Must do this to ensure edited command line is saved in history. */ console_history_cursor_to_tail (&console->history); break; } if (direction) { /* Tail was preallocated and usually empty. * Keep the editor contents in the tail as cursor is moved away from tail. * So any edited text is not lost if user moves cursor back to tail. */ command = console_editor_get_text (console->editor); /* command can be NULL */ if (console_history_is_cursor_at_tail (&console->history)) console_history_set_tail (&console->history, g_strdup (command)); /* Now move cursor and replace editor contents. */ console_history_move_cursor (&console->history, direction); command = console_history_get_at_cursor (&console->history); /* command can be NULL. */ console_editor_set_text_and_position (console->editor, command, -1); return TRUE; } return FALSE; } /* Restore models from settings. * This understands how to get history as a GStrv from settings * and how to put GStrv into both models. * * Just the model. The view does not exist yet. */ static void script_fu_models_from_settings (ConsoleInterface *console, PikaProcedureConfig *config) { GStrv strings_in; /* Assert the History model is empty, recently init. */ strings_in = console_history_from_settings (&console->history, config); /* The history setting can be empty, and GStrv can be NULL. * !!! But g_strv_length requires its arg!=NULL */ if (strings_in==NULL) return; /* Adding requires a new tail. */ console_history_new_tail (&console->history); /* Order of the GStrv is earliest command first. * Iterate ascending, i.e. earliest command to history first. * Not concerned with performance. */ for (gint i = 0; i < g_strv_length (strings_in); i++) script_fu_command_to_history (console, g_strdup (strings_in[i])); g_strfreev (strings_in); } /* Append a command to history. * * Knows to put to both TotalHistory and History models. * * Transfers ownership of command to History model. * Caller may retain a reference, for a short time. * * !!! The History model is finite and limits itself. * While the TotalHistory model is nearly unlimited. * More commands in the view than in the History model. */ static void script_fu_command_to_history (ConsoleInterface *console, const gchar *command) { /* Require new_tail called previously. */ /* To History model. */ console_history_set_tail (&console->history, command); /* Advance history, editor wants preallocated tail. */ console_history_new_tail (&console->history); /* Decorated command to TotalHistory model. */ console_total_append_command (console->total_history, command); /* Ensure there is a new tail. */ }