diff --git a/src/Application.vala b/src/Application.vala index 0f8ec0b900..bdf8c84cc9 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -213,6 +213,16 @@ public class Terminal.Application : Gtk.Application { set_accels_for_action ("app.new-window", { "N" }); set_accels_for_action ("app.quit", { "Q" }); + + set_accels_for_action (TerminalWidget.ACTION_COPY, TerminalWidget.ACCELS_COPY); + set_accels_for_action (TerminalWidget.ACTION_COPY_OUTPUT, TerminalWidget.ACCELS_COPY_OUTPUT); + set_accels_for_action (TerminalWidget.ACTION_PASTE, TerminalWidget.ACCELS_PASTE); + set_accels_for_action (TerminalWidget.ACTION_RELOAD, TerminalWidget.ACCELS_RELOAD); + set_accels_for_action (TerminalWidget.ACTION_SCROLL_TO_COMMAND, TerminalWidget.ACCELS_SCROLL_TO_COMMAND); + set_accels_for_action (TerminalWidget.ACTION_SELECT_ALL, TerminalWidget.ACCELS_SELECT_ALL); + set_accels_for_action (TerminalWidget.ACTION_ZOOM_DEFAULT, TerminalWidget.ACCELS_ZOOM_DEFAULT); + set_accels_for_action (TerminalWidget.ACTION_ZOOM_IN, TerminalWidget.ACCELS_ZOOM_IN); + set_accels_for_action (TerminalWidget.ACTION_ZOOM_OUT, TerminalWidget.ACCELS_ZOOM_OUT); } protected override int command_line (ApplicationCommandLine command_line) { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index b2bb6b24e2..6265507e6c 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -56,28 +56,14 @@ namespace Terminal { public const string ACTION_FULLSCREEN = "action-fullscreen"; public const string ACTION_NEW_TAB = "action-new-tab"; public const string ACTION_DUPLICATE_TAB = "action-duplicate-tab"; - public const string ACTION_RELOAD_TAB = "action-reload-tab"; public const string ACTION_NEXT_TAB = "action-next-tab"; public const string ACTION_PREVIOUS_TAB = "action-previous-tab"; public const string ACTION_MOVE_TAB_RIGHT = "action-move-tab-right"; public const string ACTION_MOVE_TAB_LEFT = "action-move-tab-left"; - public const string ACTION_ZOOM_DEFAULT_FONT = "action-zoom-default-font"; - public const string ACTION_ZOOM_IN_FONT = "action-zoom-in-font"; - public const string ACTION_ZOOM_OUT_FONT = "action-zoom-out-font"; - public const string ACTION_COPY = "action-copy"; - public const string ACTION_COPY_LAST_OUTPUT = "action-copy-last-output"; - public const string ACTION_PASTE = "action-paste"; public const string ACTION_SEARCH = "action-search"; public const string ACTION_SEARCH_NEXT = "action-search-next"; public const string ACTION_SEARCH_PREVIOUS = "action-search-previous"; - public const string ACTION_SELECT_ALL = "action-select-all"; - public const string ACTION_SCROLL_TO_LAST_COMMAND = "action-scroll-to-last-command"; public const string ACTION_OPEN_IN_BROWSER = "action-open-in-browser"; - public const string ACTION_RELOAD_PREFERRED_ACCEL = "R"; // Shown in context menu - - public const string[] ACCELS_ZOOM_DEFAULT_FONT = { "0", "KP_0", null }; - public const string[] ACCELS_ZOOM_IN_FONT = { "plus", "equal", "KP_Add", null }; - public const string[] ACCELS_ZOOM_OUT_FONT = { "minus", "KP_Subtract", null }; private static Gee.MultiMap action_accelerators = new Gee.HashMultiMap (); @@ -86,22 +72,13 @@ namespace Terminal { { ACTION_FULLSCREEN, action_fullscreen }, { ACTION_NEW_TAB, action_new_tab }, { ACTION_DUPLICATE_TAB, action_duplicate_tab }, - { ACTION_RELOAD_TAB, action_reload_tab }, { ACTION_NEXT_TAB, action_next_tab }, { ACTION_PREVIOUS_TAB, action_previous_tab }, { ACTION_MOVE_TAB_RIGHT, action_move_tab_right}, { ACTION_MOVE_TAB_LEFT, action_move_tab_left}, - { ACTION_ZOOM_DEFAULT_FONT, action_zoom_default_font }, - { ACTION_ZOOM_IN_FONT, action_zoom_in_font }, - { ACTION_ZOOM_OUT_FONT, action_zoom_out_font }, - { ACTION_COPY, action_copy }, - { ACTION_COPY_LAST_OUTPUT, action_copy_last_output }, - { ACTION_PASTE, action_paste }, { ACTION_SEARCH, action_search, null, "false" }, { ACTION_SEARCH_NEXT, action_search_next }, { ACTION_SEARCH_PREVIOUS, action_search_previous }, - { ACTION_SELECT_ALL, action_select_all }, - { ACTION_SCROLL_TO_LAST_COMMAND, action_scroll_to_last_command }, { ACTION_OPEN_IN_BROWSER, action_open_in_browser } }; @@ -121,21 +98,14 @@ namespace Terminal { action_accelerators[ACTION_FULLSCREEN] = "F11"; action_accelerators[ACTION_NEW_TAB] = "t"; action_accelerators[ACTION_DUPLICATE_TAB] = "d"; - action_accelerators[ACTION_RELOAD_TAB] = ACTION_RELOAD_PREFERRED_ACCEL; - action_accelerators[ACTION_RELOAD_TAB] = "F5"; action_accelerators[ACTION_NEXT_TAB] = "Tab"; action_accelerators[ACTION_NEXT_TAB] = "Page_Down"; action_accelerators[ACTION_PREVIOUS_TAB] = "Tab"; action_accelerators[ACTION_PREVIOUS_TAB] = "Page_Up"; action_accelerators[ACTION_MOVE_TAB_RIGHT] = "Right"; action_accelerators[ACTION_MOVE_TAB_LEFT] = "Left"; - action_accelerators[ACTION_COPY] = "c"; - action_accelerators[ACTION_COPY_LAST_OUTPUT] = "c"; - action_accelerators[ACTION_PASTE] = "v"; action_accelerators[ACTION_SEARCH] = "f"; - action_accelerators[ACTION_SELECT_ALL] = "a"; action_accelerators[ACTION_OPEN_IN_BROWSER] = "e"; - action_accelerators[ACTION_SCROLL_TO_LAST_COMMAND] = "Up"; } construct { @@ -154,18 +124,12 @@ namespace Terminal { app.set_accels_for_action (ACTION_PREFIX + action, accels_array); } - app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_DEFAULT_FONT, ACCELS_ZOOM_DEFAULT_FONT); - app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_IN_FONT, ACCELS_ZOOM_IN_FONT); - app.set_accels_for_action (ACTION_PREFIX + ACTION_ZOOM_OUT_FONT, ACCELS_ZOOM_OUT_FONT); - set_visual (Gdk.Screen.get_default ().get_rgba_visual ()); title = TerminalWidget.DEFAULT_LABEL; restore_saved_state (); clipboard = Gtk.Clipboard.get (Gdk.Atom.intern ("CLIPBOARD", false)); - clipboard.owner_change.connect (update_context_menu); - primary_selection = Gtk.Clipboard.get (Gdk.Atom.intern ("PRIMARY", false)); var open_in_browser_menuitem = new Gtk.MenuItem () { @@ -177,27 +141,27 @@ namespace Terminal { open_in_browser_menuitem.add (open_in_browser_menuitem_label); var copy_menuitem = new Gtk.MenuItem () { - action_name = ACTION_PREFIX + ACTION_COPY + action_name = TerminalWidget.ACTION_COPY }; - copy_menuitem.add (new Granite.AccelLabel.from_action_name (_("Copy"), copy_menuitem.action_name)); + copy_menuitem.add (new Granite.AccelLabel.from_action_name (_("Copy"), TerminalWidget.ACTION_COPY)); var copy_last_output_menuitem = new Gtk.MenuItem () { - action_name = ACTION_PREFIX + ACTION_COPY_LAST_OUTPUT + action_name = TerminalWidget.ACTION_COPY_OUTPUT }; copy_last_output_menuitem.add ( - new Granite.AccelLabel.from_action_name (_("Copy Last Output"), copy_last_output_menuitem.action_name) + new Granite.AccelLabel.from_action_name (_("Copy Last Output"), TerminalWidget.ACTION_COPY_OUTPUT) ); var paste_menuitem = new Gtk.MenuItem () { - action_name = ACTION_PREFIX + ACTION_PASTE + action_name = TerminalWidget.ACTION_PASTE }; - paste_menuitem.add (new Granite.AccelLabel.from_action_name (_("Paste"), paste_menuitem.action_name)); + paste_menuitem.add (new Granite.AccelLabel.from_action_name (_("Paste"), TerminalWidget.ACTION_PASTE)); var select_all_menuitem = new Gtk.MenuItem () { - action_name = ACTION_PREFIX + ACTION_SELECT_ALL + action_name = TerminalWidget.ACTION_SELECT_ALL }; select_all_menuitem.add ( - new Granite.AccelLabel.from_action_name (_("Select All"), select_all_menuitem.action_name) + new Granite.AccelLabel.from_action_name (_("Select All"), TerminalWidget.ACTION_SELECT_ALL) ); var search_menuitem = new Gtk.MenuItem () { @@ -216,10 +180,6 @@ namespace Terminal { menu.append (search_menuitem); menu.insert_action_group ("win", actions); - menu.popped_up.connect (() => { - update_copy_output_sensitive (); - }); - setup_ui (); show_all (); @@ -326,11 +286,6 @@ namespace Terminal { unowned Gtk.StyleContext header_context = header.get_style_context (); header_context.add_class ("default-decoration"); - get_simple_action (ACTION_COPY).set_enabled (false); - get_simple_action (ACTION_COPY_LAST_OUTPUT).set_enabled (false); - get_simple_action (ACTION_SCROLL_TO_LAST_COMMAND).set_enabled (false); - - notebook = new Granite.Widgets.DynamicNotebook.with_accellabels ( new Granite.AccelLabel.from_action_name (_("New Tab"), ACTION_PREFIX + ACTION_NEW_TAB) ) { @@ -397,11 +352,6 @@ namespace Terminal { search_toolbar.next_search (); } return true; - } else { - current_terminal.remember_position (); - get_simple_action (ACTION_SCROLL_TO_LAST_COMMAND).set_enabled (true); - current_terminal.remember_command_end_position (); - get_simple_action (ACTION_COPY_LAST_OUTPUT).set_enabled (false); } break; @@ -435,90 +385,10 @@ namespace Terminal { } break; - case Gdk.Key.Up: - case Gdk.Key.Down: - current_terminal.remember_command_start_position (); - break; - - case Gdk.Key.Menu: - long col, row; - current_terminal.get_cursor_position (out col, out row); - - var cell_width = current_terminal.get_char_width (); - var cell_height = current_terminal.get_char_height (); - var vadj = current_terminal.vadjustment.value; - - Gdk.Rectangle rect = { - (int) (col * cell_width), - (int) ((row - vadj) * cell_height), - (int) cell_width, - (int) cell_height - }; - - unowned var rect_window = current_terminal.get_window (); - update_context_menu (); - - // Popup context menu below cursor position - menu.popup_at_rect (rect_window, rect, SOUTH_WEST, NORTH_WEST); - menu.select_first (false); - break; - default: - if (!(Gtk.accelerator_get_default_mod_mask () in modifiers)) { - current_terminal.remember_command_start_position (); - } break; } - // Use hardware keycodes so the key used is unaffected by internationalized layout - bool match_keycode (uint keyval, uint code) { - Gdk.KeymapKey[] keys; - - var keymap = Gdk.Keymap.get_for_display (get_display ()); - if (keymap.get_entries_for_keyval (keyval, out keys)) { - foreach (var key in keys) { - if (code == key.keycode) { - return true; - } - } - } - - return false; - } - - if (CONTROL_MASK in modifiers && Application.settings.get_boolean ("natural-copy-paste")) { - if (match_keycode (Gdk.Key.c, keycode)) { - if (current_terminal.get_has_selection ()) { - current_terminal.copy_clipboard (); - if (!(SHIFT_MASK in modifiers)) { // Shift not pressed - current_terminal.unselect_all (); - } - return true; - } else { - current_terminal.last_key_was_return = true; // Ctrl-c: Command cancelled - } - } else if (match_keycode (Gdk.Key.v, keycode)) { - if (search_toolbar.search_entry.has_focus) { - return false; - } else if (clipboard.wait_is_text_available ()) { - action_paste (); - return true; - } - - return false; - } - } - - if (MOD1_MASK in modifiers) { - if (keyval == Gdk.Key.Up) { - return !get_simple_action (ACTION_SCROLL_TO_LAST_COMMAND).enabled; - } - - if (match_keycode (Gdk.Key.c, keycode)) { // Alt-c - update_copy_output_sensitive (); - } - } - return false; } @@ -665,9 +535,6 @@ namespace Terminal { } public void update_context_menu () { - /* Update the "Paste" menu option */ - clipboard.request_targets (update_context_menu_cb); - /* Update the "Show in ..." menu option */ get_current_selection_link_or_pwd ((clipboard, uri) => { update_menu_label (Utils.sanitize_path (uri, current_terminal.get_shell_location ())); @@ -722,20 +589,6 @@ namespace Terminal { return appinfo; } - private void update_context_menu_cb (Gtk.Clipboard clipboard_, Gdk.Atom[]? atoms) { - bool can_paste = false; - - if (atoms != null && atoms.length > 0) { - can_paste = Gtk.targets_include_text (atoms) || Gtk.targets_include_uri (atoms); - } - - get_simple_action (ACTION_PASTE).set_enabled (can_paste); - } - - private void update_copy_output_sensitive () { - get_simple_action (ACTION_COPY_LAST_OUTPUT).set_enabled (current_terminal.has_output ()); - } - protected override bool configure_event (Gdk.EventConfigure event) { // triggered when the size, position or stacking of the window has changed // it is delayed 400ms to prevent spamming gsettings @@ -778,7 +631,7 @@ namespace Terminal { title = current_terminal.window_title != "" ? current_terminal.window_title : current_terminal.current_working_directory; - update_copy_output_sensitive (); + menu.insert_action_group ("term", current_terminal.get_action_group ("term")); current_terminal.grab_focus (); } @@ -896,6 +749,7 @@ namespace Terminal { } }); + terminal_widget.notify["font-scale"].connect (() => save_opened_terminals (false, true)); terminal_widget.window_title_changed.connect (check_for_tabs_with_same_name); terminal_widget.cwd_changed.connect (cwd_changed); @@ -962,7 +816,7 @@ namespace Terminal { /* Granite.Accel.from_action_name () does not allow control of which accel is used when * there are multiple so we have to use the other constructor to specify it. */ var reload_menu_item = new Gtk.MenuItem () { - child = new Granite.AccelLabel (_("Reload"), ACTION_RELOAD_PREFERRED_ACCEL) + child = new Granite.AccelLabel (_("Reload"), TerminalWidget.ACCELS_RELOAD[0]), }; tab.menu.append (reload_menu_item); reload_menu_item.activate.connect (term.reload); @@ -1038,74 +892,6 @@ namespace Terminal { base.destroy (); } - private void on_get_text (Gtk.Clipboard board, string? intext) { - if (Application.settings.get_boolean ("unsafe-paste-alert") && !unsafe_ignored ) { - - if (intext == null) { - return; - } - - if (!intext.validate ()) { - warning ("Dropping invalid UTF-8 paste"); - return; - } - - var text = intext.strip (); - - string? unsafe_warning = null; - - if ((text.index_of ("sudo") > -1) && (text.index_of ("\n") != 0)) { - unsafe_warning = _("The pasted text may be trying to gain administrative access"); - } else if (text.index_of ("\n") != -1) { - unsafe_warning = _("The pasted text may contain multiple commands"); - } - - if (unsafe_warning != null) { - var unsafe_paste_dialog = new UnsafePasteDialog ( - this, - unsafe_warning, - text - ); - - if (unsafe_paste_dialog.run () != Gtk.ResponseType.ACCEPT) { - unsafe_paste_dialog.destroy (); - return; - } - - unsafe_paste_dialog.destroy (); - } - } - - current_terminal.remember_command_start_position (); - - if (board == primary_selection) { - current_terminal.paste_primary (); - } else { - current_terminal.paste_clipboard (); - } - } - - private void action_copy () { - if (current_terminal.link_uri != null && ! current_terminal.get_has_selection ()) - clipboard.set_text (current_terminal.link_uri, - current_terminal.link_uri.length); - else - current_terminal.copy_clipboard (); - } - - private void action_copy_last_output () { - string output = current_terminal.get_last_output (); - Gtk.Clipboard.get_default (Gdk.Display.get_default ()).set_text (output, output.length); - } - - private void action_paste () { - clipboard.request_text (on_get_text); - } - - private void action_select_all () { - current_terminal.select_all (); - } - private void action_open_in_browser () { get_current_selection_link_or_pwd ((clipboard, uri) => { string? to_open = Utils.sanitize_path (uri, current_terminal.get_shell_location ()); @@ -1137,12 +923,6 @@ namespace Terminal { } } - private void action_scroll_to_last_command () { - current_terminal.scroll_to_last_command (); - /* Repeated presses are ignored */ - get_simple_action (ACTION_SCROLL_TO_LAST_COMMAND).set_enabled (false); - } - private void action_close_tab () { current_terminal.tab.close (); current_terminal.grab_focus (); @@ -1161,25 +941,6 @@ namespace Terminal { new_tab (current_terminal.get_shell_location ()); } - private void action_reload_tab () { - current_terminal.reload (); - } - - private void action_zoom_in_font () { - current_terminal.increment_size (); - save_opened_terminals (false, true); - } - - private void action_zoom_out_font () { - current_terminal.decrement_size (); - save_opened_terminals (false, true); - } - - private void action_zoom_default_font () { - current_terminal.set_default_font_size (); - save_opened_terminals (false, true); - } - private void action_next_tab () { notebook.next_page (); } diff --git a/src/Widgets/SettingsPopover.vala b/src/Widgets/SettingsPopover.vala index be73ab82cb..adc266d20c 100644 --- a/src/Widgets/SettingsPopover.vala +++ b/src/Widgets/SettingsPopover.vala @@ -13,6 +13,7 @@ public sealed class Terminal.SettingsPopover : Gtk.Popover { set { terminal_binding.source = value; + insert_action_group ("term", value.get_action_group ("term")); } } @@ -34,28 +35,28 @@ public sealed class Terminal.SettingsPopover : Gtk.Popover { construct { var zoom_out_button = new Gtk.Button.from_icon_name ("zoom-out-symbolic", MENU) { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_ZOOM_OUT_FONT, tooltip_markup = Granite.markup_accel_tooltip ( - MainWindow.ACCELS_ZOOM_OUT_FONT, + TerminalWidget.ACCELS_ZOOM_OUT, _("Zoom out") ) }; + zoom_out_button.set_detailed_action_name (TerminalWidget.ACTION_ZOOM_OUT); var zoom_default_button = new Gtk.Button () { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_ZOOM_DEFAULT_FONT, tooltip_markup = Granite.markup_accel_tooltip ( - MainWindow.ACCELS_ZOOM_DEFAULT_FONT, + TerminalWidget.ACCELS_ZOOM_DEFAULT, _("Default zoom level") ) }; + zoom_default_button.set_detailed_action_name (TerminalWidget.ACTION_ZOOM_DEFAULT); var zoom_in_button = new Gtk.Button.from_icon_name ("zoom-in-symbolic", MENU) { - action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_ZOOM_IN_FONT, tooltip_markup = Granite.markup_accel_tooltip ( - MainWindow.ACCELS_ZOOM_IN_FONT, + TerminalWidget.ACCELS_ZOOM_IN, _("Zoom in") ) }; + zoom_in_button.set_detailed_action_name (TerminalWidget.ACTION_ZOOM_IN); var font_size_box = new Gtk.Box (HORIZONTAL, 0) { homogeneous = true, diff --git a/src/Widgets/TerminalWidget.vala b/src/Widgets/TerminalWidget.vala index 6125df854a..27f7a27592 100644 --- a/src/Widgets/TerminalWidget.vala +++ b/src/Widgets/TerminalWidget.vala @@ -67,6 +67,26 @@ namespace Terminal { } } + public const string ACTION_COPY = "term.copy"; + public const string ACTION_COPY_OUTPUT = "term.copy-output"; + public const string ACTION_PASTE = "term.paste"; + public const string ACTION_RELOAD = "term.reload"; + public const string ACTION_SCROLL_TO_COMMAND = "term.scroll-to-command"; + public const string ACTION_SELECT_ALL = "term.select-all"; + public const string ACTION_ZOOM_DEFAULT = "term.zoom::default"; + public const string ACTION_ZOOM_IN = "term.zoom::in"; + public const string ACTION_ZOOM_OUT = "term.zoom::out"; + + public const string[] ACCELS_COPY = { "C", null }; + public const string[] ACCELS_COPY_OUTPUT = { "C", null }; + public const string[] ACCELS_PASTE = { "V", null }; + public const string[] ACCELS_RELOAD = { "R", "F5", null }; + public const string[] ACCELS_SCROLL_TO_COMMAND = { "Up", null }; + public const string[] ACCELS_SELECT_ALL = { "A", null }; + public const string[] ACCELS_ZOOM_DEFAULT = { "0", "KP_0", null }; + public const string[] ACCELS_ZOOM_IN = { "plus", "equal", "KP_Add", null }; + public const string[] ACCELS_ZOOM_OUT = { "minus", "KP_Subtract", null }; + public int default_size; const string SEND_PROCESS_FINISHED_BASH = "dbus-send --type=method_call " + "--session --dest=io.elementary.terminal " + @@ -119,6 +139,13 @@ namespace Terminal { private set; } + private unowned Gtk.Clipboard clipboard; + + private GLib.SimpleAction copy_action; + private GLib.SimpleAction copy_output_action; + private GLib.SimpleAction paste_action; + private GLib.SimpleAction scroll_to_command_action; + private long remembered_position; /* Only need to remember row at the moment */ private long remembered_command_start_row = 0; /* Only need to remember row at the moment */ private long remembered_command_end_row = 0; /* Only need to remember row at the moment */ @@ -129,6 +156,7 @@ namespace Terminal { private Gtk.EventControllerKey key_controller; private Gtk.GestureMultiPress press_gesture; + private bool modifier_pressed = false; private double scroll_delta = 0.0; public signal void cwd_changed (); @@ -177,6 +205,17 @@ namespace Terminal { key_controller.key_pressed.connect (key_pressed); key_controller.key_released.connect (() => scroll_controller.flags = NONE); + // XXX(Gtk3): This is used to stop the key_pressed() handler from breaking the copy last output action, + // when a modifier is pressed, since it won't be in the modifier mask there (neither here). + // + // TODO(Gtk4): check if the modifier emission was fixed. + key_controller.modifiers.connect (() => { + // if two modifers are pressed in sequence (like -> ), modifier_pressed will be false. + // However, the modifer mask in key_pressed() will already contain the previous modifier. + modifier_pressed = !modifier_pressed; + return true; + }); + press_gesture = new Gtk.GestureMultiPress (this) { propagation_phase = TARGET, button = 0 @@ -187,18 +226,16 @@ namespace Terminal { // send events to key controller manually, since key_released isn't emitted in any propagation phase event.connect (key_controller.handle_event); - selection_changed.connect (() => { - window.get_simple_action (MainWindow.ACTION_COPY).set_enabled (get_has_selection ()); - window.update_context_menu (); - }); - - size_allocate.connect (() => { - resized = true; - }); - + selection_changed.connect (() => copy_action.set_enabled (get_has_selection ())); + size_allocate.connect (() => resized = true); contents_changed.connect (check_cwd_changed); - child_exited.connect (on_child_exited); + ulong once = 0; + once = realize.connect (() => { + clipboard = get_clipboard (Gdk.SELECTION_CLIPBOARD); + clipboard.owner_change.connect (setup_menu); + disconnect (once); + }); /* target entries specify what kind of data the terminal widget accepts */ Gtk.TargetEntry uri_entry = { "text/uri-list", Gtk.TargetFlags.OTHER_APP, DropTargets.URILIST }; @@ -215,6 +252,53 @@ namespace Terminal { /* Make Links Clickable */ this.drag_data_received.connect (drag_received); this.clickable (REGEX_STRINGS); + + // Setup Actions + var action_group = new GLib.SimpleActionGroup (); + insert_action_group ("term", action_group); + + copy_action = new GLib.SimpleAction ("copy", null); + copy_action.set_enabled (false); + copy_action.activate.connect (() => copy_clipboard.emit ()); + action_group.add_action (copy_action); + + copy_output_action = new GLib.SimpleAction ("copy-output", null); + copy_output_action.set_enabled (false); + copy_output_action.activate.connect (copy_output); + action_group.add_action (copy_output_action); + + paste_action = new GLib.SimpleAction ("paste", null); + paste_action.activate.connect (() => paste_clipboard.emit ()); + action_group.add_action (paste_action); + + var reload_action = new GLib.SimpleAction ("reload", null); + reload_action.activate.connect (reload); + action_group.add_action (reload_action); + + scroll_to_command_action = new GLib.SimpleAction ("scroll-to-command", null); + scroll_to_command_action.set_enabled (false); + scroll_to_command_action.activate.connect (scroll_to_command); + action_group.add_action (scroll_to_command_action); + + var select_all_action = new GLib.SimpleAction ("select-all", null); + select_all_action.activate.connect (select_all); + action_group.add_action (select_all_action); + + var zoom_action = new GLib.SimpleAction ("zoom", VariantType.STRING); + zoom_action.activate.connect ((p) => { + switch ((string) p) { + case "in": + increase_font_size (); + break; + case "out": + decrease_font_size (); + break; + case "default": + font_scale = 1.0; + break; + } + }); + action_group.add_action (zoom_action); } private void pointer_focus () { @@ -229,11 +313,12 @@ namespace Terminal { link_uri = get_link (gesture.get_last_event (null)); if (link_uri != null) { - window.get_simple_action (MainWindow.ACTION_COPY).set_enabled (true); + copy_action.set_enabled (true); } Gdk.Rectangle rect = { (int) x, (int) y }; window.update_context_menu (); + setup_menu (); menu.popup_at_rect (get_window (), rect, SOUTH_WEST, NORTH_WEST); menu.select_first (false); @@ -261,22 +346,184 @@ namespace Terminal { scroll_delta += y; if (scroll_delta >= 0.5) { - window.get_simple_action (MainWindow.ACTION_ZOOM_OUT_FONT).activate (null); + decrease_font_size (); scroll_delta = 0.0; } else if (scroll_delta <= -0.5) { - window.get_simple_action (MainWindow.ACTION_ZOOM_IN_FONT).activate (null); + increase_font_size (); scroll_delta = 0.0; } } private bool key_pressed (uint keyval, uint keycode, Gdk.ModifierType modifiers) { - if (keyval == Gdk.Key.Control_R || keyval == Gdk.Key.Control_L) { - scroll_controller.flags = VERTICAL; + switch (keyval) { + case Gdk.Key.Control_R: + case Gdk.Key.Control_L: + scroll_controller.flags = VERTICAL; + break; + + case Gdk.Key.Return: + remember_position (); + scroll_to_command_action.set_enabled (true); + remember_command_end_position (); + copy_output_action.set_enabled (false); + break; + + case Gdk.Key.Up: + case Gdk.Key.Down: + remember_command_start_position (); + break; + + case Gdk.Key.Menu: + long col, row; + + get_cursor_position (out col, out row); + + var cell_width = get_char_width (); + var cell_height = get_char_height (); + var vadj = vadjustment.value; + + Gdk.Rectangle rect = { + (int) (col * cell_width), + (int) ((row - vadj) * cell_height), + (int) cell_width, + (int) cell_height + }; + + window.update_context_menu (); + setup_menu (); + + // Popup context menu below cursor position + menu.popup_at_rect (get_window (), rect, SOUTH_WEST, NORTH_WEST); + menu.select_first (false); + break; + + case Gdk.Key.Alt_L: + case Gdk.Key.Alt_R: + // enable/disable the action before we try to use + copy_output_action.set_enabled (!resized && get_last_output ().length > 0); + break; + + default: + if (!modifier_pressed || !(Gtk.accelerator_get_default_mod_mask () in modifiers)) { + remember_command_start_position (); + } + break; + } + + // Use hardware keycodes so the key used is unaffected by internationalized layout + bool match_keycode (uint keyval, uint code) { + Gdk.KeymapKey[] keys; + + var keymap = Gdk.Keymap.get_for_display (get_display ()); + if (keymap.get_entries_for_keyval (keyval, out keys)) { + foreach (var key in keys) { + if (code == key.keycode) { + return true; + } + } + } + + return false; + } + + if (CONTROL_MASK in modifiers && Application.settings.get_boolean ("natural-copy-paste")) { + if (match_keycode (Gdk.Key.c, keycode)) { + if (get_has_selection ()) { + copy_clipboard (); + if (!(SHIFT_MASK in modifiers)) { // Shift not pressed + unselect_all (); + } + return true; + } else { + last_key_was_return = true; // Ctrl-c: Command cancelled + } + } else if (match_keycode (Gdk.Key.v, keycode) && clipboard.wait_is_text_available ()) { + paste_clipboard (); + return true; + } + } + + if (MOD1_MASK in modifiers && keyval == Gdk.Key.Up) { + return !scroll_to_command_action.enabled; } return false; } + private void setup_menu () { + // Update the "Paste" menu option + clipboard.request_targets ((clipboard, atoms) => { + bool can_paste = false; + + if (atoms != null && atoms.length > 0) { + can_paste = Gtk.targets_include_text (atoms) || Gtk.targets_include_uri (atoms); + } + + paste_action.set_enabled (can_paste); + }); + + // Update the "Copy Last Outptut" menu option + var has_output = !resized && get_last_output ().length > 0; + copy_output_action.set_enabled (has_output); + } + + protected override void copy_clipboard () { + if (link_uri != null && !get_has_selection ()) { + clipboard.set_text (link_uri, link_uri.length); + } else { + base.copy_clipboard (); + } + } + + private void copy_output () { + var output = get_last_output (); + clipboard.set_text (output, output.length); + } + + protected override void paste_clipboard () { + clipboard.request_text ((clipboard, text) => { + if (text == null) { + return; + } + + if (!text.validate ()) { + warning ("Dropping invalid UTF-8 paste"); + return; + } + + unowned var toplevel = (MainWindow) get_toplevel (); + + if (!toplevel.unsafe_ignored && Application.settings.get_boolean ("unsafe-paste-alert")) { + string? warn_text = null; + text._strip (); + + if ("\n" in text) { + warn_text = _("The pasted text may contain multiple commands"); + } else if ("sudo" in text) { + warn_text = _("The pasted text may be trying to gain administrative access"); + } + + if (warn_text != null) { + var dialog = new UnsafePasteDialog (toplevel, warn_text, text); + dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.ACCEPT) { + remember_command_start_position (); + base.paste_clipboard (); + } + + dialog.destroy (); + }); + + dialog.present (); + return; + } + } + + remember_command_start_position (); + base.paste_clipboard (); + }); + } + private void update_theme () { var gtk_settings = Gtk.Settings.get_default (); var theme_palette = new Gdk.RGBA[Themes.PALETTE_SIZE]; @@ -464,16 +711,12 @@ namespace Terminal { } } - public void increment_size () { - font_scale = (font_scale + 0.1).clamp (MIN_SCALE, MAX_SCALE); + protected override void increase_font_size () { + font_scale += 0.1; } - public void decrement_size () { - font_scale = (font_scale - 0.1).clamp (MIN_SCALE, MAX_SCALE); - } - - public void set_default_font_size () { - font_scale = 1.0; + protected override void decrease_font_size () { + font_scale -= 0.1; } public bool is_init_complete () { @@ -573,31 +816,35 @@ namespace Terminal { return get_text_range (start_row, 0, output_end_row - 1, 1000, null, null) + "\n"; } - public void scroll_to_last_command () { - long col, row; - get_cursor_position (out col, out row); - int delta = (int)(remembered_position - row); - vadjustment.set_value ( - vadjustment.get_value () + delta + get_window ().get_height () / get_char_height () - 1 - ); - } + private void scroll_to_command (GLib.SimpleAction action, GLib.Variant? parameter) { + long row, delta; + + get_cursor_position (null, out row); + delta = remembered_position - row; - public bool has_output () { - return !resized && get_last_output ().length > 0; + vadjustment.value += (int) delta + get_window ().get_height () / get_char_height () - 1; + action.set_enabled (false); // Repeated presses are ignored } public void reload () { + var old_loc = get_shell_location (); + if (has_foreground_process ()) { - var dialog = new ForegroundProcessDialog.before_tab_reload (window); - var response_type = dialog.run (); - dialog.destroy (); + var dialog = new ForegroundProcessDialog.before_tab_reload ((MainWindow) get_toplevel ()); + dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.ACCEPT) { + Posix.kill (child_pid, Posix.Signal.TERM); + reset (true, true); + active_shell (old_loc); + } - if (response_type != Gtk.ResponseType.ACCEPT) { - return; - } + dialog.destroy (); + }); + + dialog.present (); + return; } - var old_loc = get_shell_location (); Posix.kill (child_pid, Posix.Signal.TERM); reset (true, true); active_shell (old_loc);