diff --git a/data/guake.glade b/data/guake.glade index 748c67e6..d031fcad 100644 --- a/data/guake.glade +++ b/data/guake.glade @@ -2,215 +2,6 @@ - - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - Copy - True - False - False - - - - - - Paste - True - False - False - - - - - - True - False - - - - - Toggle Fullscreen - True - False - False - - - - - - True - False - - - - - Save to File... - True - False - False - - - - - - Reset terminal - True - False - False - - - - - - Find... - True - False - False - - - - - - True - False - - - - - New Tab - True - False - False - - - - - - Rename Tab - True - False - False - - - - - - Close Tab - True - False - False - - - - - - True - False - - - - - Open link... - True - False - False - - - - - - Search on Web - True - False - False - - - - - - Quick Open - True - False - False - - - - - - True - False - - - - - gtk-preferences - True - False - True - True - - - - - - gtk-about - True - False - True - True - - - - - - True - False - - - - - Quit - True - False - False - - - - - - True - False - - - New Tab - True - False - False - - - - - - Rename - True - False - False - - - - - - Close - True - False - False - - - - True False diff --git a/guake/boxes.py b/guake/boxes.py index 55209f03..c7f7381f 100644 --- a/guake/boxes.py +++ b/guake/boxes.py @@ -1,6 +1,17 @@ import gi gi.require_version('Gtk', '3.0') +from gi.repository import Gdk from gi.repository import Gtk +from guake.callbacks import TerminalContextMenuCallbacks +from guake.dialogs import RenameDialog +from guake.menus import TabContextMenu +from guake.menus import TerminalContextMenu +from guake.utils import TabNameUtils +from locale import gettext as _ +gi.require_version('Vte', '2.91') # vte-0.42 +from gi.repository import Vte + +# TODO remove calls to guake class TerminalHolder(): @@ -17,6 +28,12 @@ class TerminalHolder(): def get_guake(self): pass + def get_window(self): + pass + + def get_settings(self): + pass + def get_root_box(self): pass @@ -57,6 +74,12 @@ class RootTerminalBox(Gtk.Box, TerminalHolder): def get_guake(self): return self.guake + def get_window(self): + return self.guake.window + + def get_settings(self): + return self.guake.settings + def get_root_box(self): return self @@ -81,6 +104,7 @@ class TerminalBox(Gtk.Box, TerminalHolder): raise RuntimeError("TerminalBox: terminal already set") self.terminal = terminal self.terminal.connect("focus", self.on_terminal_focus) + self.terminal.connect("button-press-event", self.on_button_press, None) self.pack_start(self.terminal, True, True, 0) self.terminal.show() self.add_scroll_bar() @@ -122,12 +146,38 @@ class TerminalBox(Gtk.Box, TerminalHolder): def get_guake(self): return self.get_parent().get_guake() + def get_window(self): + return self.get_parent().get_window() + + def get_settings(self): + return self.get_parent().get_settings() + def get_root_box(self): return self.get_parent() def on_terminal_focus(self, direction, user_data): self.get_root_box().set_last_terminal_focused(self.terminal) + def on_button_press(self, target, event, user_data): + if event.button == 3: + # First send to background process if handled, do nothing else + if not event.get_state() & Gdk.ModifierType.SHIFT_MASK: + if Vte.Terminal.do_button_press_event(self.terminal, event): + return True + + menu = TerminalContextMenu( + self.terminal, self.get_window(), self.get_settings(), + TerminalContextMenuCallbacks( + self.terminal, self.get_window(), self.get_settings(), + self.get_guake().notebook + ) + ) + menu.popup_at_pointer(event) + self.terminal.grab_focus() + return True + self.terminal.grab_focus() + return False + class DualTerminalBox(Gtk.Paned, TerminalHolder): @@ -172,5 +222,60 @@ class DualTerminalBox(Gtk.Paned, TerminalHolder): def get_guake(self): return self.get_parent().get_guake() + def get_window(self): + return self.get_parent().get_window() + + def get_settings(self): + return self.get_parent().get_settings() + def get_root_box(self): return self.get_parent() + + +class TabLabelEventBox(Gtk.EventBox): + + def __init__(self, notebook, text): + super().__init__() + self.notebook = notebook + self.label = Gtk.Label(text) + self.add(self.label) + self.connect("button-press-event", self.on_button_press, self.label) + self.label.show() + + def set_text(self, text): + self.label.set_text(text) + + def get_text(self): + return self.label.get_text() + + def on_button_press(self, target, event, user_data): + if event.button == 3: + menu = TabContextMenu(self) + menu.popup_at_pointer(event) + self.notebook.get_current_terminal().grab_focus() + return True + if event.button == 2: + self.notebook.delete_page_by_label(self) + return True + + self.notebook.get_current_terminal().grab_focus() + return False + + def on_new_tab(self, user_data): + self.notebook.new_page_with_focus() + + def on_rename(self, user_data): + self.notebook.guake.preventHide = True + dialog = RenameDialog(self.notebook.guake.window, self.label.get_text()) + r = dialog.run() + if r == Gtk.ResponseType.ACCEPT: + new_text = TabNameUtils.shorten(dialog.get_text(), self.notebook.guake.settings) + page_num = self.notebook.find_tab_index_by_label(self) + self.notebook.rename_page(page_num, new_text, True) + dialog.destroy() + self.notebook.guake.preventHide = False + # TODO + # self.set_terminal_focus() + + def on_close(self, user_data): + self.notebook.delete_page_by_label(self) diff --git a/guake/callbacks.py b/guake/callbacks.py new file mode 100644 index 00000000..1a3322e0 --- /dev/null +++ b/guake/callbacks.py @@ -0,0 +1,89 @@ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from guake.utils import FullscreenManager +from guake.utils import TabNameUtils +from guake.dialogs import RenameDialog +from guake.dialogs import SaveTerminalDialog +from guake.about import AboutDialog +from guake.prefs import PrefsDialog +from urllib.parse import quote_plus +from guake.utils import get_server_time + + +class TerminalContextMenuCallbacks(): + + def __init__(self, terminal, window, settings, notebook): + self.terminal = terminal + self.window = window + self.settings = settings + self.notebook = notebook + + def on_copy_clipboard(self, *args): + self.terminal.copy_clipboard() + + def on_paste_clipboard(self, *args): + self.terminal.paste_clipboard() + + def on_toggle_fullscreen(self, *args): + FullscreenManager(self.window).toggle() + + def on_save_to_file(self, *args): + SaveTerminalDialog(self.terminal, self.window).run() + + def on_reset_terminal(self, *args): + self.terminal.reset(True, True) + + def on_find(self): + # this is not implemented jet + pass + + def on_new_tab(self, *args): + self.notebook.new_page_with_focus( + directory=self.terminal.get_current_directory() + ) + + def on_rename_tab(self, *args): + page_num = self.notebook.find_page_index_by_terminal(self.terminal) + tab_text = self.notebook.get_tab_text_index(page_num) + dialog = RenameDialog(self.window, tab_text) + r = dialog.run() + if r == Gtk.ResponseType.ACCEPT: + new_text = TabNameUtils.shorten(dialog.get_text(), self.settings) + self.notebook.rename_page(page_num, new_text, True) + dialog.destroy() + + def on_close_tab(self, *args): + page_num = self.notebook.find_page_index_by_terminal(self.terminal) + self.notebook.delete_page(page_num, False, True) + + def on_open_link(self, *args): + self.terminal.browse_link_under_cursor() + + def on_search_on_web(self, *args): + if self.terminal.get_has_selection(): + self.terminal.copy_clipboard() + clipboard = Gtk.Clipboard.get_default(self.window.get_display()) + query = clipboard.wait_for_text() + query = quote_plus(query) + if query: + search_url = "https://www.google.com/#q={!s}&safe=off".format(query) + Gtk.show_uri(self.window.get_screen(), search_url, get_server_time(self.window)) + + def on_quick_open(self, *args): + if self.terminal.get_has_selection(): + self.terminal.quick_open() + + def on_command_selected(self, command): + self.terminal.execute_command(command) + + def on_show_preferences(self, *args): + self.notebook.guake.hide() + PrefsDialog(self.settings).show() + + def on_show_about(self, *args): + self.notebook.guake.hide() + AboutDialog() + + def on_quit(self, *args): + self.notebook.guake.accel_quit() diff --git a/guake/customcommands.py b/guake/customcommands.py new file mode 100644 index 00000000..595881ed --- /dev/null +++ b/guake/customcommands.py @@ -0,0 +1,82 @@ +import os +import json + +from locale import gettext as _ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +class CustomCommands(): + """ + Example for a custom commands file + [ + { + "type": "menu", + "description": "dir listing", + "items": [ + { + "description": "la", + "cmd":["ls", "-la"] + }, + { + "description": "tree", + "cmd":["tree", ""] + } + ] + }, + { + "description": "less ls", + "cmd": ["ls | less", ""] + } + ] + """ + + def __init__(self, settings, callback): + self.settings = settings + self.callback = callback + + def should_load(self): + file_path = self.settings.general.get_string('custom-command-file') + return file_path is not None + + def get_file_path(self): + return os.path.expanduser(self.settings.general.get_string('custom-command-file')) + + def _load_json(self, file_name): + try: + with open(file_name) as f: + data_file = f.read() + return json.loads(data_file) + except Exception as e: + log.exception("Invalid custom command file %s. Exception: %s", data_file, str(e)) + + def build_menu(self): + if not self.should_load(): + return None + menu = Gtk.Menu() + for obj in self._load_json(self.get_file_path()): + self._parse_custom_commands(obj, menu) + return menu + + def _parse_custom_commands(self, json_object, menu): + if json_object.get('type') == "menu": + newmenu = Gtk.Menu() + newmenuitem = Gtk.MenuItem(json_object['description']) + newmenuitem.set_submenu(newmenu) + newmenuitem.show() + menu.append(newmenuitem) + for item in json_object['items']: + self._parse_custom_commands(item, newmenu) + else: + menu_item = Gtk.MenuItem(json_object['description']) + custom_command = "" + space = "" + for command in json_object['cmd']: + custom_command += (space + command) + space = " " + menu_item.connect("activate", self.on_menu_item_activated, custom_command) + menu.append(menu_item) + menu_item.show() + + def on_menu_item_activated(self, item, cmd): + self.callback.on_command_selected(cmd) diff --git a/guake/dbusiface.py b/guake/dbusiface.py index c520f19d..be1bf4ae 100755 --- a/guake/dbusiface.py +++ b/guake/dbusiface.py @@ -76,11 +76,11 @@ class DbusManager(dbus.service.Object): @dbus.service.method(DBUS_NAME, out_signature='i') def get_selected_tab(self): - return self.guake.get_selected_tab() + return self.guake.notebook.get_current_page() @dbus.service.method(DBUS_NAME, out_signature='s') def get_selected_tablabel(self): - return self.guake.get_selected_tablabel() + return self.guake.notebook.get_tab_text_index(self.guake.notebook.get_current_page()) @dbus.service.method(DBUS_NAME, out_signature='i') def get_tab_count(self): @@ -108,7 +108,7 @@ class DbusManager(dbus.service.Object): @dbus.service.method(DBUS_NAME, in_signature='is') def rename_tab(self, tab_index, new_text): - self.guake.rename_tab(tab_index, new_text, True) + self.guake.notebook.rename_page(tab_index, new_text, True) @dbus.service.method(DBUS_NAME, in_signature='s') def rename_current_tab(self, new_text): diff --git a/guake/dialogs.py b/guake/dialogs.py new file mode 100644 index 00000000..f340b5d5 --- /dev/null +++ b/guake/dialogs.py @@ -0,0 +1,127 @@ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from locale import gettext as _ + + +class RenameDialog(Gtk.Dialog): + + def __init__(self, window, current_name): + super().__init__( + _("Rename tab"), window, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) + ) + self.entry = Gtk.Entry() + self.entry.set_text(current_name) + self.entry.set_property('can-default', True) + self.entry.show() + + vbox = Gtk.VBox() + vbox.set_border_width(6) + vbox.show() + + self.set_size_request(300, -1) + self.vbox.pack_start(vbox, True, True, 0) + self.set_border_width(4) + self.set_default_response(Gtk.ResponseType.ACCEPT) + self.add_action_widget(self.entry, Gtk.ResponseType.ACCEPT) + self.entry.reparent(vbox) + + def get_text(self): + return self.entry.get_text() + +class PromptQuitDialog(Gtk.MessageDialog): + + """Prompts the user whether to quit/close a tab. + """ + + def __init__(self, parent, procs, tabs): + super(PromptQuitDialog, self).__init__( + parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO + ) + + if tabs == -1: + primary_msg = _("Do you want to close the tab?") + tab_str = '' + else: + primary_msg = _("Do you really want to quit Guake?") + if tabs == 1: + tab_str = _(" and one tab open") + else: + tab_str = _(" and {0} tabs open").format(tabs) + + if procs == 0: + proc_str = _("There are no processes running") + elif procs == 1: + proc_str = _("There is a process still running") + else: + proc_str = _("There are {0} processes still running").format(procs) + + self.set_markup(primary_msg) + self.format_secondary_markup("{0}{1}.".format(proc_str, tab_str)) + + def quit(self): + """Run the "are you sure" dialog for quitting Guake + """ + # Stop an open "close tab" dialog from obstructing a quit + response = self.run() == Gtk.ResponseType.YES + self.destroy() + # Keep Guake focussed after dismissing tab-close prompt + # if tab == -1: + # self.window.present() + return response + + def close_tab(self): + response = self.run() == Gtk.ResponseType.YES + self.destroy() + # Keep Guake focussed after dismissing tab-close prompt + # if tab == -1: + # self.window.present() + return response + +class SaveTerminalDialog(Gtk.FileChooserDialog): + + def __init__(self, terminal, window): + super().__init__( + _("Save to..."), + window, + Gtk.FileChooserAction.SAVE, + ( + Gtk.STOCK_CANCEL, + Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, + Gtk.ResponseType.OK + ) + ) + self.set_default_response(Gtk.ResponseType.OK) + self.terminal = terminal + self.parent_window = window + + def run(self): + self.terminal.select_all() + self.terminal.copy_clipboard() + self.terminal.unselect_all() + clipboard = Gtk.Clipboard.get_default(self.parent_window.get_display()) + selection = clipboard.wait_for_text() + if not selection: + return + selection = selection.rstrip() + filter = Gtk.FileFilter() + filter.set_name(_("All files")) + filter.add_pattern("*") + self.add_filter(filter) + + filter = Gtk.FileFilter() + filter.set_name(_("Text and Logs")) + filter.add_pattern("*.log") + filter.add_pattern("*.txt") + self.add_filter(filter) + + response = super().run() + if response == Gtk.ResponseType.OK: + filename = self.get_filename() + with open(filename, "w") as f: + f.write(selection) + self.destroy() diff --git a/guake/guake_app.py b/guake/guake_app.py index fe7b26ef..33afdfde 100644 --- a/guake/guake_app.py +++ b/guake/guake_app.py @@ -28,7 +28,6 @@ import traceback import uuid from pathlib import Path -from textwrap import dedent from urllib.parse import quote_plus from xml.sax.saxutils import escape as xml_escape @@ -55,6 +54,7 @@ from guake import vte_version from guake.about import AboutDialog from guake.common import gladefile from guake.common import pixmapfile +from guake.dialogs import PromptQuitDialog from guake.globals import ALIGN_BOTTOM from guake.globals import ALIGN_CENTER from guake.globals import ALIGN_LEFT @@ -74,35 +74,13 @@ from guake.prefs import refresh_user_start from guake.settings import Settings from guake.simplegladeapp import SimpleGladeApp from guake.terminal import GuakeTerminal -from guake.theme import get_gtk_theme +from guake.theme import patch_gtk_theme from guake.theme import select_gtk_theme +from guake.utils import FullscreenManager +from guake.utils import TabNameUtils from guake.utils import get_server_time from locale import gettext as _ -libutempter = None -try: - # this allow to run some commands that requires libuterm to - # be injected in current process, as: wall - from atexit import register as at_exit_call - from ctypes import cdll - libutempter = cdll.LoadLibrary('libutempter.so.0') - if libutempter is not None: - # We absolutely need to remove the old tty from the utmp !!! - at_exit_call(libutempter.utempter_remove_added_record) -except Exception as e: - libutempter = None - sys.stderr.write("[WARN] ===================================================================\n") - sys.stderr.write("[WARN] Unable to load the library libutempter !\n") - sys.stderr.write( - "[WARN] Some feature might not work:\n" - "[WARN] - 'exit' command might freeze the terminal instead of closing the tab\n" - "[WARN] - the 'wall' command is know to work badly\n" - ) - sys.stderr.write("[WARN] Error: " + str(e) + '\n') - sys.stderr.write( - "[WARN] ===================================================================²\n" - ) - log = logging.getLogger(__name__) instance = None @@ -126,38 +104,6 @@ GDK_WINDOW_STATE_ABOVE = 32 MAX_TRANSPARENCY = 100 -class PromptQuitDialog(Gtk.MessageDialog): - - """Prompts the user whether to quit/close a tab. - """ - - def __init__(self, parent, procs, tabs): - super(PromptQuitDialog, self).__init__( - parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, - Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO - ) - - if tabs == -1: - primary_msg = _("Do you want to close the tab?") - tab_str = '' - else: - primary_msg = _("Do you really want to quit Guake?") - if tabs == 1: - tab_str = _(" and one tab open") - else: - tab_str = _(" and {0} tabs open").format(tabs) - - if procs == 0: - proc_str = _("There are no processes running") - elif procs == 1: - proc_str = _("There is a process still running") - else: - proc_str = _("There are {0} processes still running").format(procs) - - self.set_markup(primary_msg) - self.format_secondary_markup("{0}{1}.".format(proc_str, tab_str)) - - class Guake(SimpleGladeApp): """Guake main class. Handles specialy the main window. @@ -177,11 +123,11 @@ class Guake(SimpleGladeApp): try_to_compile_glib_schemas() schema_source = load_schema() self.settings = Settings(schema_source) - select_gtk_theme(self.settings) super(Guake, self).__init__(gladefile('guake.glade')) - self._patch_theme() + select_gtk_theme(self.settings) + patch_gtk_theme(self.get_widget("window-root").get_style_context(), self.settings) self.add_callbacks(self) self.debug_mode = self.settings.general.get_boolean('debug-mode') @@ -190,13 +136,10 @@ class Guake(SimpleGladeApp): log.info('VTE %s', vte_version()) log.info('Gtk %s', gtk_version()) - self.prompt_dialog = None self.hidden = True self.forceHide = False self.preventHide = False - self.custom_command_menuitem = None - # trayicon! Using SVG handles better different OS trays # img = pixmapfile('guake-tray.svg') # trayicon! @@ -230,25 +173,12 @@ class Guake(SimpleGladeApp): self.mainframe = self.get_widget('mainframe') self.mainframe.remove(self.get_widget('notebook-teminals')) self.notebook = TerminalNotebook(self) - self.notebook.set_name("notebook-teminals") + self.notebook.connect('terminal-spawned', self.terminal_spawned) + self.notebook.connect('page-deleted', self.page_deleted) - # help(Gtk.PositionType) - self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) - self.notebook.set_property("show-tabs", True) - self.notebook.set_property("enable-popup", False) - self.notebook.set_property("scrollable", True) - self.notebook.set_property("show-border", False) - self.notebook.set_property("visible", True) - self.notebook.set_property("has-focus", True) - self.notebook.set_property("can-focus", True) - self.notebook.set_property("is-focus", True) - self.notebook.set_property("expand", True) self.mainframe.add(self.notebook) self.set_tab_position() - self.toolbar = self.get_widget('toolbar') - self.mainframe = self.get_widget('mainframe') - # check and set ARGB for real transparency color = self.window.get_style_context().get_background_color(Gtk.StateFlags.NORMAL) self.window.set_app_paintable(True) @@ -275,14 +205,6 @@ class Guake(SimpleGladeApp): self.window.connect('draw', draw_callback) - # It's intended to know which tab was selected to - # close/rename. This attribute will be set in - # self.show_tab_menu - self.selected_tab = None - - # holds fullscreen status - self.is_fullscreen = False - # holds the timestamp of the losefocus event self.losefocus_time = 0 @@ -292,30 +214,11 @@ class Guake(SimpleGladeApp): # Controls the transparency state needed for function accel_toggle_transparency self.transparency_toggled = False - # load cumstom command menu and menuitems from config file - self.custom_command_menuitem = None - self.load_custom_commands() - # store the default window title to reset it when update is not wanted self.default_window_title = self.window.get_title() self.abbreviate = False - # Flag to prevent guake hide when window_losefocus is true and - # user tries to use the context menu. - self.showing_context_menu = False - - def hide_context_menu(menu): - """Turn context menu flag off to make sure it is not being - shown. - """ - self.showing_context_menu = False - - self.get_widget('context-menu').connect('hide', hide_context_menu) - - # setting up the TabContextMenuHelper - self.tab_context_menu_helper = TabContextMenuHelper(self.get_widget('tab-menu')) - self.window.connect('focus-out-event', self.on_window_losefocus) # Handling the delete-event of the main window to avoid @@ -352,8 +255,6 @@ class Guake(SimpleGladeApp): # adding the first tab on guake self.add_tab() - self.get_widget("context_find_tab").set_visible(enable_find) - if self.settings.general.get_boolean('start-fullscreen'): self.fullscreen() @@ -374,144 +275,6 @@ class Guake(SimpleGladeApp): log.info("Guake initialized") - def _patch_theme(self): - theme_name, variant = get_gtk_theme(self.settings) - - def rgba_to_hex(color): - return "#{0:02x}{1:02x}{2:02x}".format( - int(color.red * 255), int(color.green * 255), int(color.blue * 255) - ) - - style_context = self.get_widget("window-root").get_style_context() - # for n in [ - # "inverted_bg_color", - # "inverted_fg_color", - # "selected_bg_color", - # "selected_fg_color", - # "theme_inverted_bg_color", - # "theme_inverted_fg_color", - # "theme_selected_bg_color", - # "theme_selected_fg_color", - # ]: - # s = style_context.lookup_color(n) - # print(n, s, rgba_to_hex(s[1])) - selected_fg_color = rgba_to_hex(style_context.lookup_color("theme_selected_fg_color")[1]) - selected_bg_color = rgba_to_hex(style_context.lookup_color("theme_selected_bg_color")[1]) - log.debug( - "Patching theme '%s' (prefer dark = '%r'), overriding tab 'checked' state': " - "foreground: %r, background: %r", theme_name, "yes" - if variant == "dark" else "no", selected_fg_color, selected_bg_color - ) - css_data = dedent( - """ - .custom_tab:checked {{ - color: {selected_fg_color}; - background: {selected_bg_color}; - }} - """.format(selected_bg_color=selected_bg_color, selected_fg_color=selected_fg_color) - ).encode() - style_provider = Gtk.CssProvider() - style_provider.load_from_data(css_data) - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) - - # load the custom commands infrastucture - def load_custom_commands(self): - if self.custom_command_menuitem: - self.get_widget('context-menu').remove(self.custom_command_menuitem) - custom_commands_menu = Gtk.Menu() - if not self.get_custom_commands(custom_commands_menu): - return - menu = Gtk.MenuItem(_("Custom Commands")) - menu.set_submenu(custom_commands_menu) - menu.show() - self.custom_command_menuitem = menu - context_menu = self.get_widget('context-menu') - context_menu.insert(self.custom_command_menuitem, self.context_menu_get_insert_pos()) - - # returns position where the custom cmd must be placed on the context-menu - def context_menu_get_insert_pos(self): - # assuming that the quit menuitem is always the last one and with a - # separator before him - return len(self.get_widget('context-menu').get_children()) - 2 - - # function to read commands stored at /general/custom_command_file and - # launch the context menu builder - def get_custom_commands(self, menu): - """ - Example for a custom commands file - [ - { - "type": "menu", - "description": "dir listing", - "items": [ - { - "description": "la", - "cmd":["ls", "-la"] - }, - { - "description": "tree", - "cmd":["tree", ""] - } - ] - }, - { - "description": "less ls", - "cmd": ["ls | less", ""] - } - ] - """ - custom_command_file_path = self.settings.general.get_string('custom-command-file') - if not custom_command_file_path: - return False - file_name = os.path.expanduser(custom_command_file_path) - if not file_name: - return False - try: - with open(file_name) as f: - data_file = f.read() - except Exception as e: - data_file = None - if not data_file: - return False - - try: - custom_commands = json.loads(data_file) - for json_object in custom_commands: - self.parse_custom_commands(json_object, menu) - return True - - except Exception: - log.exception("Invalid custom command file %s. Exception:", data_file) - return False - - # function to build the custom commands menu and menuitems - def parse_custom_commands(self, json_object, menu): - if json_object.get('type') == "menu": - newmenu = Gtk.Menu() - newmenuitem = Gtk.MenuItem(json_object['description']) - newmenuitem.set_submenu(newmenu) - newmenuitem.show() - menu.append(newmenuitem) - for item in json_object['items']: - self.parse_custom_commands(item, newmenu) - else: - menu_item = Gtk.MenuItem(json_object['description']) - custom_command = "" - space = "" - for command in json_object['cmd']: - custom_command += (space + command) - space = " " - - menu_item.connect("activate", self.execute_context_menu_cmd, custom_command) - menu.append(menu_item) - menu_item.show() - - # execute contextual menu call - def execute_context_menu_cmd(self, item, cmd): - self.execute_command(cmd) - # new color methods should be moved to the GuakeTerminal class def _load_palette(self): @@ -591,11 +354,14 @@ class Guake(SimpleGladeApp): terminal.set_color_foreground(fgcolor) def execute_command(self, command, tab=None): + # TODO DBUS_ONLY """Execute the `command' in the `tab'. If tab is None, the command will be executed in the currently selected tab. Command should end with '\n', otherwise it will be appended to the string. """ + # TODO CONTEXTMENU this has to be rewriten and only serves the + # dbus interface, maybe this should be moved to dbusinterface.py if not self.notebook.has_page(): self.add_tab() @@ -604,11 +370,15 @@ class Guake(SimpleGladeApp): index = self.notebook.get_current_page() index = tab or self.notebook.get_current_page() - for terminal in self.notebook.get_terminals_for_page(index): + for terminal in self.notebook.get_terminals_for_page(index): # TODO NOTEBOOK + # this is a stupid quick fix I did while rewriting the notebook to + # make it work some how but running the command on all terminals in a + # page might be a bad idea... terminal.feed_child(command) break def execute_command_by_uuid(self, tab_uuid, command): + # TODO DBUS_ONLY """Execute the `command' in the tab whose terminal has the `tab_uuid' uuid """ if command[-1] != '\n': @@ -626,56 +396,10 @@ class Guake(SimpleGladeApp): for current_vte in terminals: current_vte.feed_child(command) - # TODO this is dead: 2eae380b1a91a24f6c1eb68c13dac33db98a6ea2 and - # 3f8c344519c9228deb9ca5f181cbdd5ef1d6acc0 - def on_resizer_drag(self, widget, event): - """Method that handles the resize drag. - - It does not actually move the main window. It just sets the new window size in gconf. - """ - - mod = event.get_state() - if 'GDK_BUTTON1_MASK' not in mod.value_names: - return - - screen = self.window.get_screen() - win, x, y, _ = screen.get_root_window().get_pointer() - screen_no = screen.get_monitor_at_point(x, y) - valignment = self.settings.general.get_int('window-valignment') - - max_height = screen.get_monitor_geometry(screen_no).height - if valignment == ALIGN_BOTTOM: - percent = 100 * (max_height - y) / max_height - else: - percent = 100 * y / max_height - - if percent < 1: - percent = 1 - - window_rect = self.window.get_size() - window_pos = self.window.get_position() - if valignment == ALIGN_BOTTOM: - self.window.resize(window_rect[0], max_height - y) - log.debug( - "Resizing on resizer drag to : %r, and moving to: %r, y: %r", - (window_rect[0], max_height - y), window_pos[0], y - ) - self.window.move(window_pos[0], y) - else: - self.window.resize(window_rect[0], y) - log.debug("Just moving on resizer drag to: %r, y=%r", window_rect[0], y) - self.settings.general.set_int('window-height', int(percent)) - def on_window_losefocus(self, window, event): """Hides terminal main window when it loses the focus and if the window_losefocus gconf variable is True. """ - if self.showing_context_menu: - return - - if self.prompt_dialog is not None: - return - if self.preventHide: return @@ -690,122 +414,11 @@ class Guake(SimpleGladeApp): """Show the tray icon menu. """ menu = self.get_widget('tray-menu') - menu.popup(None, None, None, Gtk.StatusIcon.position_menu, button, activate_time) - def show_context_menu(self, terminal, event): - """Show the context menu, only with a right click on a vte - Terminal. - """ - if event.button != 3: - return False - - if event.get_state() & Gdk.ModifierType.SHIFT_MASK: - # Force showing contextual menu on Ctrl+right click - self.showing_context_menu = True - else: - # First send to background process if handled, do nothing else - if Vte.Terminal.do_button_press_event(terminal, event): - log.info("Background app captured the right click event") - return True - - log.debug("showing context menu") - self.showing_context_menu = True - - guake_clipboard = Gtk.Clipboard.get_default(self.window.get_display()) - if not guake_clipboard.wait_is_text_available(): - self.get_widget('context_paste').set_sensitive(False) - else: - self.get_widget('context_paste').set_sensitive(True) - - current_selection = '' - current_term = self.notebook.get_current_terminal() - if current_term.get_has_selection(): - current_term.copy_clipboard() - guake_clipboard = Gtk.Clipboard.get_default(self.window.get_display()) - current_selection = guake_clipboard.wait_for_text() - if current_selection: - current_selection.rstrip() - - if current_selection and len(current_selection) > 20: - current_selection = current_selection[:17] + "..." - - self.get_widget('separator_search').set_visible(False) - if current_selection: - self.get_widget('context_search_on_web' - ).set_label(_("Search on Web: '%s'") % current_selection) - self.get_widget('context_search_on_web').set_visible(True) - self.get_widget('separator_search').set_visible(True) - else: - self.get_widget('context_search_on_web').set_label(_("Search on Web (no selection)")) - self.get_widget('context_search_on_web').set_visible(False) - - filename = None - if current_selection: - filename = self.get_current_terminal_filename_under_cursor(current_selection) - - if filename: - self.get_widget('context_quick_open').set_visible(True) - filename_str = str(filename) - if len(filename_str) >= 28: - self.get_widget('context_quick_open' - ).set_label(_("Quick Open: {!s}...").format(filename_str[:25])) - else: - self.get_widget('context_quick_open' - ).set_label(_("Quick Open: {!s}").format(filename_str)) - self.get_widget('separator_search').set_visible(True) - else: - self.get_widget('context_quick_open').set_label(_("Quick Open...")) - self.get_widget('context_quick_open').set_visible(False) - - link = self.get_current_terminal_link_under_cursor() - if link: - self.get_widget('context_browse_on_web').set_visible(True) - if len(link) >= 28: - self.get_widget('context_browse_on_web' - ).set_label(_("Open Link: {!s}...").format(link[:25])) - else: - self.get_widget('context_browse_on_web' - ).set_label(_("Open Link: {!s}").format(link)) - self.get_widget('separator_search').set_visible(True) - else: - self.get_widget('context_browse_on_web').set_label(_("Open Link...")) - self.get_widget('context_browse_on_web').set_visible(False) - - context_menu = self.get_widget('context-menu') - context_menu.popup(None, None, None, None, 3, event.get_time()) - return True - - def show_rename_current_tab_dialog(self, target, event): - """On double-click over a tab, show the rename dialog. - """ - if event.button == 1: - if event.type == Gdk.EventType._2BUTTON_PRESS: - self.accel_rename_current_tab() - self.set_terminal_focus() - self.selected_tab.pressed() - return True - - def show_tab_menu(self, target, event, user_data): - """Shows the tab menu with a right click. After that, the - focus come back to the terminal. - user_data is a Gtk.Label which is displayed in an eventbox in the clicked tab - """ - if event.button == 3: - self.tab_context_menu_helper.show(event, self.notebook.find_tab_index_label(user_data)) - self.notebook.get_current_terminal().grab_focus() - return True - self.notebook.get_current_terminal().grab_focus() - return False - - def middle_button_click(self, target, event, user_data): - """Closes a tab with a middle click - user_data is a Gtk.Label which is displayed in an eventbox in the clicked tab - """ - if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS: - self.notebook.delete_page_by_label(user_data) - def show_about(self, *args): + # TODO DBUS ONLY + # TODO TRAY ONLY """Hides the main window and creates an instance of the About Dialog. """ @@ -813,6 +426,8 @@ class Guake(SimpleGladeApp): AboutDialog() def show_prefs(self, *args): + # TODO DBUS ONLY + # TODO TRAY ONLY """Hides the main window and creates an instance of the Preferences window. """ @@ -949,7 +564,7 @@ class Guake(SimpleGladeApp): self.window.move(window_rect.x, window_rect.y) # this works around an issue in fluxbox - if not self.is_fullscreen: + if not FullscreenManager(self.window).is_fullscreen(): self.settings.general.triggerOnChangedValue(self.settings.general, 'window-height') time = get_server_time(self.window) @@ -1203,7 +818,7 @@ class Guake(SimpleGladeApp): if width_percents == 100 and height_percents == 100: log.debug("MAXIMIZING MAIN WINDOW") self.window.maximize() - elif not self.is_fullscreen: + elif not FullscreenManager(self.window).is_fullscreen(): log.debug("RESIZING MAIN WINDOW TO THE FOLLOWING VALUES:") self.window.unmaximize() log.debug(" window_rect.x: %s", window_rect.x) @@ -1240,7 +855,7 @@ class Guake(SimpleGladeApp): self.settings.general.triggerOnChangedValue(self.settings.general, 'mouse-display') self.settings.general.triggerOnChangedValue(self.settings.general, 'display-n') self.settings.general.triggerOnChangedValue(self.settings.general, 'window-ontop') - if not self.is_fullscreen: + if not FullscreenManager(self.window).is_fullscreen(): self.settings.general.triggerOnChangedValue(self.settings.general, 'window-height') self.settings.general.triggerOnChangedValue(self.settings.general, 'window-width') self.settings.general.triggerOnChangedValue(self.settings.general, 'use-scrollbar') @@ -1266,21 +881,6 @@ class Guake(SimpleGladeApp): self.settings.general.triggerOnChangedValue(self.settings.general, 'compat-backspace') self.settings.general.triggerOnChangedValue(self.settings.general, 'compat-delete') - def run_quit_dialog(self, procs, tab): - """Run the "are you sure" dialog for closing a tab, or quitting Guake - """ - # Stop an open "close tab" dialog from obstructing a quit - if self.prompt_dialog is not None: - self.prompt_dialog.destroy() - self.prompt_dialog = PromptQuitDialog(self.window, procs, tab) - response = self.prompt_dialog.run() == Gtk.ResponseType.YES - self.prompt_dialog.destroy() - self.prompt_dialog = None - # Keep Guake focussed after dismissing tab-close prompt - if tab == -1: - self.window.present() - return response - def accel_quit(self, *args): """Callback to prompt the user whether to quit Guake or not. """ @@ -1291,7 +891,7 @@ class Guake(SimpleGladeApp): # "Prompt on tab close" config overrides "prompt on quit" config if prompt_cfg or (prompt_tab_cfg == 1 and procs > 0) or (prompt_tab_cfg == 2): log.debug("Remaining procs=%r", procs) - if self.run_quit_dialog(procs, tabs): + if PromptQuitDialog(self.window, procs, tabs).quit(): log.info("Quitting Guake") Gtk.main_quit() else: @@ -1299,8 +899,13 @@ class Guake(SimpleGladeApp): Gtk.main_quit() def accel_reset_terminal(self, *args): + # TODO KEYBINDINGS ONLY """Callback to reset and clean the terminal""" - self.reset_terminal() + # TODO PREVENTHIDE remove + # self.preventHide = True + current_term = self.notebook.get_current_terminal() + current_term.reset(True, True) + # self.preventHide = False return True def accel_zoom_in(self, *args): @@ -1381,6 +986,7 @@ class Guake(SimpleGladeApp): return True def accel_move_tab_left(self, *args): + # TODO KEYBINDINGS ONLY """ Callback to move a tab to the left """ pos = self.notebook.get_current_page() if pos != 0: @@ -1388,12 +994,17 @@ class Guake(SimpleGladeApp): return True def accel_move_tab_right(self, *args): + # TODO KEYBINDINGS ONLY """ Callback to move a tab to the right """ pos = self.notebook.get_current_page() if pos != self.notebook.get_n_pages() - 1: self.move_tab(pos, pos + 1) return True + def move_tab(self, old_tab_pos, new_tab_pos): + self.notebook.reorder_child(self.notebook.get_nth_page(old_tab_pos), new_tab_pos) + self.notebook.set_current_page(new_tab_pos) + def gen_accel_switch_tabN(self, N): """Generates callback (which called by accel key) to go to the Nth tab. """ @@ -1414,71 +1025,45 @@ class Guake(SimpleGladeApp): """Callback to show the rename tab dialog. Called by the accel key. """ - self.on_rename_current_tab_activate(args) + page_num = self.notebook.get_current_page() + page = self.notebook.get_nth_page(page_num) + self.notebook.get_tab_label(page).on_rename(None) return True def accel_copy_clipboard(self, *args): + # TODO KEYBINDINGS ONLY """Callback to copy text in the shown terminal. Called by the accel key. """ self.notebook.get_current_terminal().copy_clipboard() - return True def accel_paste_clipboard(self, *args): + # TODO KEYBINDINGS ONLY """Callback to paste text in the shown terminal. Called by the accel key. """ self.notebook.get_current_terminal().paste_clipboard() return True - def accel_toggle_fullscreen(self, *args): - """Callback toggle the fullscreen status of the main - window. Called by the accel key. - """ - - if not self.is_fullscreen: - self.fullscreen() - else: - self.unfullscreen() - return True - def accel_toggle_hide_on_lose_focus(self, *args): """Callback toggle whether the window should hide when it loses focus. Called by the accel key. """ - if self.settings.general.get_boolean('window-losefocus'): self.settings.general.set_boolean('window-losefocus', False) else: self.settings.general.set_boolean('window-losefocus', True) return True - def fullscreen(self): - log.debug("FULLSCREEN: ON") - self.window.fullscreen() - self.is_fullscreen = True + def accel_toggle_fullscreen(self, *args): + FullscreenManager(self.window).toggle() - if not self.settings.general.get_boolean('toolbar-visible-in-fullscreen'): - self.toolbar.hide() + def fullscreen(self): + FullscreenManager(self.window).fullscreen() def unfullscreen(self): - - # Fixes "Guake cannot restore from fullscreen" (#628) - log.debug("UNMAXIMIZING") - self.window.unmaximize() - - self.set_final_window_rect() - log.debug("FULLSCREEN: OFF") - self.window.unfullscreen() - self.is_fullscreen = False - - # making sure that tabbar will come back to default state. - self.settings.general.triggerOnChangedValue(self.settings.general, 'window-tabbar') - - # make sure the window size is correct after returning - # from fullscreen. broken on old compiz/metacity versions :C - self.settings.general.triggerOnChangedValue(self.settings.general, 'window-height') + FullscreenManager(self.window).unfullscreen() # -- callbacks -- @@ -1488,9 +1073,7 @@ class Guake(SimpleGladeApp): `delete_tab' method to do the work. """ log.debug("Terminal exited: %s", term) - if libutempter is not None: - libutempter.utempter_remove_record(term.get_pty()) - self.delete_tab(self.notebook.find_page_index_for_terminal(term), kill=False, prompt=False) + self.delete_tab(self.notebook.find_page_index_by_terminal(term), kill=False, prompt=False) def recompute_tabs_titles(self): """Updates labels on all tabs. This is required when `self.abbreviate` @@ -1504,7 +1087,7 @@ class Guake(SimpleGladeApp): # page, this need to be rewritten for terminal in self.notebook.iter_terminals(): page_num = self.notebook.page_num(terminal.get_parent()) - self.rename_tab(page_num, self.compute_tab_title(terminal), False) + self.notebook.rename_page(page_num, self.compute_tab_title(terminal), False) def compute_tab_title(self, vte): """Abbreviate and cut vte terminal title when necessary @@ -1518,16 +1101,7 @@ class Guake(SimpleGladeApp): vte_title = vte_title[:len(vte_title) - len(current_directory)] + '/'.join(parts) except OSError: pass - return self._shorten_tab_title(vte_title) - - def _shorten_tab_title(self, text): - use_vte_titles = self.settings.general.get_boolean('use-vte-titles') - if not use_vte_titles: - return text - max_name_length = self.settings.general.get_int("max-tab-name-length") - if max_name_length != 0 and len(text) > max_name_length: - text = "..." + text[-max_name_length:] - return text + return TabNameUtils.shorten(vte_title, self.settings) def on_terminal_title_changed(self, vte, term): box = term.get_parent() @@ -1538,7 +1112,7 @@ class Guake(SimpleGladeApp): # if tab has been renamed by user, don't override. if not getattr(box, 'custom_label_set', False): title = self.compute_tab_title(vte) - self.rename_tab(page_num, title, False) + self.notebook.rename_page(page_num, title, False) self.update_window_title(title) else: text = self.notebook.get_tab_text_page(page) @@ -1551,294 +1125,45 @@ class Guake(SimpleGladeApp): else: self.window.set_title(self.default_window_title) - def on_rename_current_tab_activate(self, *args): - """Shows a dialog to rename the current tab. - """ - print(args) - entry = Gtk.Entry() - page_num = self.tab_context_menu_helper.last_invoked_on_tab_index - page = self.notebook.get_nth_page(page_num) - entry.set_text(self.notebook.get_tab_label(page).get_children()[0].get_text()) - entry.set_property('can-default', True) - entry.show() - - vbox = Gtk.VBox() - vbox.set_border_width(6) - vbox.show() - - dialog = Gtk.Dialog( - _("Rename tab"), self.window, - Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, - (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) - ) - - dialog.set_size_request(300, -1) - dialog.vbox.pack_start(vbox, True, True, 0) - dialog.set_border_width(4) - dialog.set_default_response(Gtk.ResponseType.ACCEPT) - dialog.add_action_widget(entry, Gtk.ResponseType.ACCEPT) - entry.reparent(vbox) - - # don't hide on lose focus until the rename is finished - self.preventHide = True - response = dialog.run() - self.preventHide = False - - if response == Gtk.ResponseType.ACCEPT: - new_text = entry.get_text() - new_text = self._shorten_tab_title(new_text) - - self.rename_tab(page_num, new_text, True) - - dialog.destroy() - self.set_terminal_focus() - - def on_close_activate(self, *args): - """Tab context menu close handler - """ - page_num = self.tab_context_menu_helper.last_invoked_on_tab_index - self.notebook.delete_page(page_num) - - def on_drag_data_received(self, widget, context, x, y, selection, target, timestamp, term): - box = term.get_parent() - droppeduris = selection.get_uris() - - # url-unquote the list, strip file:// schemes, handle .desktop-s - pathlist = [] - app = None - for uri in droppeduris: - scheme, _, path, _, _ = urlsplit(uri) - - if scheme != "file": - pathlist.append(uri) - else: - filename = url2pathname(path) - - desktopentry = DesktopEntry() - try: - desktopentry.parse(filename) - except xdg.Exceptions.ParsingError: - pathlist.append(filename) - continue - - if desktopentry.getType() == 'Link': - pathlist.append(desktopentry.getURL()) - - if desktopentry.getType() == 'Application': - app = desktopentry.getExec() - - if app and len(droppeduris) == 1: - text = app - else: - text = str.join("", (shell_quote(path) + " " for path in pathlist)) - - box.terminal.feed_child(text) - return True + # TODO PORT reimplement drag and drop text on terminal # -- tab related functions -- def close_tab(self, *args): """Closes the current tab. """ - page_num = self.notebook.get_current_page() - self.delete_page(page_num) + self.notebook.delete_page_current() + + def delete_tab(self, page_num, kill=True, prompt=True): + """This function will destroy the notebook page, terminal and + tab widgets and will call the function to kill interpreter + forked by vte. + """ + self.notebook.delete_page(page_num, kill=kill, prompt=prompt) def rename_tab_uuid(self, term_uuid, new_text, user_set=True): """Rename an already added tab by its UUID """ term_uuid = uuid.UUID(term_uuid) - # TODO NOTEBOOK this is not optimal (page reordering messes the lookup up) - # and should be fixed in the notebook rewrite page_index, = ( index for index, t in enumerate(self.notebook.iter_terminals()) if t.get_uuid() == term_uuid ) - self.rename_tab(tab_index, new_text, user_set) - - def rename_tab(self, page_index, new_text, user_set=False): - """Rename an already added tab by its index. Use user_set to define - if the rename was triggered by the user (eg. rename dialog) or by - an update from the vte (eg. vte:window-title-changed) - """ - page_box = self.notebook.get_nth_page(page_index) - if not getattr(page_box, "custom_label_set", False) or user_set: - eventbox = Gtk.EventBox() - label = Gtk.Label(new_text) - eventbox.add(label) - eventbox.connect("button-press-event", self.show_tab_menu, label) - eventbox.connect("button-press-event", self.middle_button_click, label) - self.notebook.set_tab_label(page_box, eventbox) - label.show() - - if user_set: - setattr(page_box, "custom_label_set", new_text != "-") + self.notebook.rename_page(page_index, new_text, user_set) def rename_current_tab(self, new_text, user_set=False): page_num = self.notebook.get_current_page() - self.rename_tab(page_num, new_text, user_set) + self.notebook.rename_page(page_num, new_text, user_set) - def get_current_dir(self): - """Gets the working directory of the current tab to create a - new one in the same dir. - """ - active_terminal = self.notebook.get_current_terminal() - if not active_terminal: - return os.path.expanduser('~') - return active_terminal.get_current_directory() - - def spawn_sync_pid(self, directory=None, terminal=None): - - argv = list() - user_shell = self.settings.general.get_string('default-shell') - if user_shell and os.path.exists(user_shell): - argv.append(user_shell) - else: - argv.append(os.environ['SHELL']) - - login_shell = self.settings.general.get_boolean('use-login-shell') - if login_shell: - argv.append('--login') - - # We can choose the directory to vte launch. It is important - # to be used by dbus interface. I'm testing if directory is a - # string because when binded to a signal, the first param can - # be a button not a directory. - - if isinstance(directory, str): - wd = directory - else: - wd = os.environ['HOME'] - try: - if self.settings.general.get_boolean('open-tab-cwd'): - wd = self.get_current_dir() - except: # pylint: disable=bare-except - pass - - # Environment variables are not actually parameters but they - # need to be set before calling terminal.fork_command() - # method. This is a good place to do it. - self.update_proxy_vars(terminal) - pid = terminal.spawn_sync( - Vte.PtyFlags.DEFAULT, wd, argv, [], GLib.SpawnFlags.DO_NOT_REAP_CHILD, None, None, None - ) - try: - tuple_type = gi._gi.ResultTuple # pylint: disable=c-extension-no-member - except: # pylint: disable=bare-except - tuple_type = tuple - if isinstance(pid, (tuple, tuple_type)): - # Return a tuple in 2.91 - # https://lazka.github.io/pgi-docs/Vte-2.91/classes/Terminal.html#Vte.Terminal.spawn_sync - pid = pid[1] - assert isinstance(pid, int) - return pid - - def update_proxy_vars(self, terminal=None): - """This method updates http{s,}_proxy environment variables - with values found in gconf. - """ - if terminal: - os.environ['GUAKE_TAB_UUID'] = str(terminal.get_uuid()) - else: - del os.environ['GUAKE_TAB_UUID'] - return - # TODO PORT port this code - proxy = '/system/http_proxy/' - if self.client.get_boolen(proxy + 'use_http_proxy'): - host = self.client.get_string(proxy + 'host') - port = self.client.get_int(proxy + 'port') - if self.client.get_bool(proxy + 'use_same_proxy'): - ssl_host = host - ssl_port = port - else: - ssl_host = self.client.get_string('/system/proxy/secure_host') - ssl_port = self.client.get_int('/system/proxy/secure_port') - - if self.client.get_bool(proxy + 'use_authentication'): - auth_user = self.client.get_string(proxy + 'authentication_user') - auth_pass = self.client.get_string(proxy + 'authentication_password') - auth_pass = quote_plus(auth_pass, '') - os.environ[ - 'http_proxy' - ] = "http://{!s}:{!s}@{!s}:{:d}".format(auth_user, auth_pass, host, port) - os.environ[ - 'https_proxy' - ] = "http://{!s}:{!s}@{!s}:{:d}".format(auth_user, auth_pass, ssl_host, ssl_port) - else: - os.environ['http_proxy'] = "http://{!s}:{:d}".format(host, port) - os.environ['https_proxy'] = "http://{!s}:{:d}".format(ssl_host, ssl_port) - - def setup_new_terminal(self, directory=None): - terminal = GuakeTerminal(self) - terminal.grab_focus() - - terminal.connect('button-press-event', self.show_context_menu) + def terminal_spawned(self, notebook, terminal, pid): + self.load_config() terminal.connect('child-exited', self.on_terminal_exited, terminal) terminal.connect('window-title-changed', self.on_terminal_title_changed, terminal) - terminal.connect('drag-data-received', self.on_drag_data_received, terminal) - - # TODO PORT is this still the case with the newer vte version? - # -- Ubuntu has a patch to libvte which disables mouse scrolling in apps - # -- like vim and less by default. If this is the case, enable it back. - if hasattr(terminal, "set_alternate_screen_scroll"): - terminal.set_alternate_screen_scroll(True) - - pid = self.spawn_sync_pid(directory, terminal) - - if libutempter is not None: - libutempter.utempter_add_record(terminal.get_pty().get_fd(), os.uname()[1]) - terminal.pid = pid - return terminal def add_tab(self, directory=None): """Adds a new tab to the terminal notebook. """ - # TODO NOTEBOOK - - box, page_num = self.notebook.new_page() - text = self.compute_tab_title(box.get_terminals()[0]) - self.notebook.set_current_page(page_num) - self.rename_tab(page_num, text, False) - self.load_config() - - if self.is_fullscreen: - self.fullscreen() - return str(box.get_terminals()[0].get_uuid()) - - def save_tab(self, directory=None): - self.preventHide = True - current_term = self.notebook.get_current_terminal() - current_term.select_all() - current_term.copy_clipboard() - current_term.unselect_all() - guake_clipboard = Gtk.Clipboard.get_default(self.window.get_display()) - current_selection = guake_clipboard.wait_for_text() - if not current_selection: - return - current_selection = current_selection.rstrip() - - dialog = Gtk.FileChooserDialog( - _("Save to..."), self.window, Gtk.FileChooserAction.SAVE, - (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK) - ) - dialog.set_default_response(Gtk.ResponseType.OK) - filter = Gtk.FileFilter() - filter.set_name(_("All files")) - filter.add_pattern("*") - dialog.add_filter(filter) - - filter = Gtk.FileFilter() - filter.set_name(_("Text and Logs")) - filter.add_pattern("*.log") - filter.add_pattern("*.txt") - dialog.add_filter(filter) - - response = dialog.run() - if response == Gtk.ResponseType.OK: - filename = dialog.get_filename() - with open(filename, "w") as f: - f.write(current_selection) - dialog.destroy() - self.preventHide = False + self.notebook.new_page_with_focus(directory) def find_tab(self, directory=None): log.debug("find") @@ -1888,21 +1213,7 @@ class Guake(SimpleGladeApp): # elif response_id == RESPONSE_BACKWARD: # buffer.search_backward(search_string, self) - def delete_tab(self, page_num, kill=True, prompt=True): - """This function will destroy the notebook page, terminal and - tab widgets and will call the function to kill interpreter - forked by vte. - """ - # Run prompt if necessary - if prompt: - procs = self.notebook.get_running_fg_processes_count_page(page_num) - prompt_cfg = self.settings.general.get_int('prompt-on-close-tab') - if (prompt_cfg == 1 and procs > 0) or (prompt_cfg == 2): - if not self.run_quit_dialog(procs, -1): - return - - self.notebook.delete_page(page_num, kill=kill) - + def page_deleted(self, *args): if not self.notebook.has_page(): self.hide() # avoiding the delay on next Guake show request @@ -1922,6 +1233,7 @@ class Guake(SimpleGladeApp): self.notebook.set_current_page(self.notebook.get_current_page()) def get_selected_uuidtab(self): + # TODO DBUS ONLY """Returns the uuid of the current selected terminal """ page_num = self.notebook.get_current_page() @@ -1931,6 +1243,7 @@ class Guake(SimpleGladeApp): def search_on_web(self, *args): """Search for the selected text on the web """ + # TODO KEYBINDINGS ONLY current_term = self.notebook.get_current_terminal() if current_term.get_has_selection(): @@ -1939,51 +1252,18 @@ class Guake(SimpleGladeApp): search_query = guake_clipboard.wait_for_text() search_query = quote_plus(search_query) if search_query: + # TODO search provider should be selectable (someone might + # prefer bing.com, the internet is a strange place ¯\_(ツ)_/¯ ) search_url = "https://www.google.com/#q={!s}&safe=off".format(search_query, ) Gtk.show_uri(self.window.get_screen(), search_url, get_server_time(self.window)) return True - def quick_open(self, *args): - """Open open selection - """ - current_term = self.notebook.get_current_terminal() - - if current_term.get_has_selection(): - self.notebook.get_current_terminal().quick_open() - return True - - def get_current_terminal_link_under_cursor(self): - current_term = self.notebook.get_current_terminal() - link = current_term.found_link - log.debug("Current link under cursor: %s", link) - if link: - return link - return None - - def get_current_terminal_filename_under_cursor(self, selected_text): - current_term = self.notebook.get_current_terminal() - filename, _1, _2 = current_term.is_file_on_local_server(selected_text) - log.info("Current filename under cursor: %s", filename) - if filename: - return filename - return None - - def browse_on_web(self, *args): - log.debug("browsing %s...", self.get_current_terminal_link_under_cursor()) - self.notebook.get_current_terminal().browse_link_under_cursor() - def set_tab_position(self, *args): if self.settings.general.get_boolean('tab-ontop'): self.notebook.set_tab_pos(Gtk.PositionType.TOP) else: self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) - def reset_terminal(self, directory=None): - self.preventHide = True - current_term = self.notebook.get_current_terminal() - current_term.reset(True, True) - self.preventHide = False - def execute_hook(self, event_name): """Execute shell commands related to current event_name""" hook = self.settings.hooks.get_string('{!s}'.format(event_name)) @@ -2002,23 +1282,3 @@ class Guake(SimpleGladeApp): log.debug(traceback.format_exc()) else: log.debug("hook on event %s has been executed", event_name) - - -class TabContextMenuHelper(): - - def __init__(self, menu): - self.menu = menu - self.menu.connect('hide', self._hide) - self.reset() - - def reset(self): - self.is_showing = False - self.last_invoked_on_tab_index = -1 - - def show(self, event, invoked_on_tab_index): - self.is_showing = True - self.last_invoked_on_tab_index = invoked_on_tab_index - self.menu.popup_at_pointer(event) - - def _hide(self, *args): - self.is_showing = False diff --git a/guake/keybindings.py b/guake/keybindings.py index 3b2909de..b719bf77 100644 --- a/guake/keybindings.py +++ b/guake/keybindings.py @@ -104,7 +104,6 @@ class Keybindings(): self.guake.window.remove_accel_group(self.accel_group) self.accel_group = Gtk.AccelGroup() self.guake.window.add_accel_group(self.accel_group) - self.guake.context_menu.set_accel_group(self.accel_group) self.load_accelerators() def load_accelerators(self): @@ -132,7 +131,9 @@ class Keybindings(): key, mask = Gtk.accelerator_parse(getk('close-tab')) if key > 0: - self.accel_group.connect(key, mask, Gtk.AccelFlags.VISIBLE, self.guake.close_tab) + self.accel_group.connect( + key, mask, Gtk.AccelFlags.VISIBLE, self.guake.notebook.delete_page_current + ) key, mask = Gtk.accelerator_parse(getk('previous-tab')) if key > 0: diff --git a/guake/menus.py b/guake/menus.py new file mode 100644 index 00000000..1eae064f --- /dev/null +++ b/guake/menus.py @@ -0,0 +1,163 @@ +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from locale import gettext as _ +gi.require_version('Vte', '2.91') # vte-0.42 +from gi.repository import Vte +from guake.customcommands import CustomCommands + +import logging +log = logging.getLogger(__name__) + + +def TabContextMenu(callback_object): + """Create the context menu for a notebook tab + """ + menu = Gtk.Menu() + mi1 = Gtk.MenuItem(_("New Tab")) + mi1.connect("activate", callback_object.on_new_tab) + menu.add(mi1) + mi2 = Gtk.MenuItem(_("Rename")) + mi2.connect("activate", callback_object.on_rename) + menu.add(mi2) + mi3 = Gtk.MenuItem(_("Close")) + mi3.connect("activate", callback_object.on_close) + menu.add(mi3) + menu.show_all() + return menu + + +SEARCH_SELECTION_LENGTH = 20 +FILE_SELECTION_LENGTH = 30 + +def TerminalContextMenu(terminal, window, settings, callback_object): + """Create the context menu for a terminal + """ + menu = Gtk.Menu() + mi = Gtk.MenuItem(_("Copy")) + mi.connect("activate", callback_object.on_copy_clipboard) + menu.add(mi) + mi = Gtk.MenuItem(_("Paste")) + mi.connect("activate", callback_object.on_paste_clipboard) + # check if clipboard has text, if not disable the paste menuitem + clipboard = Gtk.Clipboard.get_default(window.get_display()) + mi.set_sensitive(clipboard.wait_is_text_available()) + menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.MenuItem(_("Toggle Fullscreen")) + mi.connect("activate", callback_object.on_toggle_fullscreen) + menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.MenuItem(_("Save to File...")) + mi.connect("activate", callback_object.on_save_to_file) + menu.add(mi) + mi = Gtk.MenuItem(_("Reset terminal")) + mi.connect("activate", callback_object.on_reset_terminal) + menu.add(mi) + # TODO SEARCH uncomment menu.add() + mi = Gtk.MenuItem(_("Find...")) + mi.connect("activate", callback_object.on_find) + # menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.MenuItem(_("New Tab")) + mi.connect("activate", callback_object.on_new_tab) + menu.add(mi) + mi = Gtk.MenuItem(_("Rename Tab")) + mi.connect("activate", callback_object.on_rename_tab) + menu.add(mi) + mi = Gtk.MenuItem(_("Close Tab")) + mi.connect("activate", callback_object.on_close_tab) + menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.MenuItem(_("Open link...")) + mi.connect("activate", callback_object.on_open_link) + link = get_link_under_cursor(terminal) + # TODO CONTEXTMENU this is a mess Quick open should also be sensible + # if the text in the selection is a url the current terminal + # implementation does not support this at the moment + if link: + if len(link) >= FILE_SELECTION_LENGTH: + mi.set_label(_("Open Link: {!s}...").format(link[:FILE_SELECTION_LENGTH-3])) + else: + mi.set_label(_("Open Link: {!s}").format(link)) + mi.set_sensitive(True) + else: + mi.set_sensitive(False) + menu.add(mi) + mi = Gtk.MenuItem(_("Search on Web")) + mi.connect("activate", callback_object.on_search_on_web) + selection = get_current_selection(terminal, window) + if selection: + search_text = selection.rstrip() + if len(search_text) > SEARCH_SELECTION_LENGTH: + search_text = search_text[:SEARCH_SELECTION_LENGTH-3] + "..." + mi.set_label(_("Search on Web: '%s'") % search_text) + mi.set_sensitive(True) + else: + mi.set_sensitive(False) + menu.add(mi) + mi = Gtk.MenuItem(_("Quick Open...")) + mi.connect("activate", callback_object.on_quick_open) + if selection: + filename = get_filename_under_cursor(terminal, selection) + if filename: + filename_str = str(filename) + if len(filename_str) > FILE_SELECTION_LENGTH: + mi.set_label( + _("Quick Open: {!s}...").format( + filename_str[:FILE_SELECTION_LENGTH-3] + ) + ) + else: + mi.set_label(_("Quick Open: {!s}").format(filename_str)) + mi.set_sensitive(True) + else: + mi.set_sensitive(False) + else: + mi.set_sensitive(False) + menu.add(mi) + customcommands = CustomCommands(settings, callback_object) + if customcommands.should_load(): + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.MenuItem(_("Custom Commands")) + mi.set_submenu(customcommands.build_menu()) + menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.ImageMenuItem("gtk-preferences") + mi.set_use_stock(True) + mi.connect("activate", callback_object.on_show_preferences) + menu.add(mi) + mi = Gtk.ImageMenuItem("gtk-about") + mi.set_use_stock(True) + mi.connect("activate", callback_object.on_show_about) + menu.add(mi) + menu.add(Gtk.SeparatorMenuItem()) + mi = Gtk.ImageMenuItem(_("Quit")) + mi.connect("activate", callback_object.on_quit) + menu.add(mi) + menu.show_all() + return menu + + +def get_current_selection(terminal, window): + if terminal.get_has_selection(): + terminal.copy_clipboard() + clipboard = Gtk.Clipboard.get_default(window.get_display()) + return clipboard.wait_for_text() + return None + + +def get_filename_under_cursor(terminal, selection): + filename, _1, _2 = terminal.is_file_on_local_server(selection) + log.info("Current filename under cursor: %s", filename) + if filename: + return filename + return None + + +def get_link_under_cursor(terminal): + link = terminal.found_link + log.info("Current link under cursor: %s", link) + if link: + return link + return None diff --git a/guake/notebook.py b/guake/notebook.py index cbeab99d..519410b7 100644 --- a/guake/notebook.py +++ b/guake/notebook.py @@ -20,12 +20,17 @@ Boston, MA 02110-1301 USA from guake.boxes import DualTerminalBox from guake.boxes import RootTerminalBox +from guake.boxes import TabLabelEventBox from guake.boxes import TerminalBox +from guake.dialogs import PromptQuitDialog import gi +import os gi.require_version('Gtk', '3.0') +from gi.repository import GObject from gi.repository import Gtk from guake.terminal import GuakeTerminal +from locale import gettext as _ import logging import posix @@ -40,6 +45,24 @@ class TerminalNotebook(Gtk.Notebook): self.guake = guake self.last_terminal_focused = None + self.set_name("notebook-teminals") + self.set_tab_pos(Gtk.PositionType.BOTTOM) + self.set_property("show-tabs", True) + self.set_property("enable-popup", False) + self.set_property("scrollable", True) + self.set_property("show-border", False) + self.set_property("visible", True) + self.set_property("has-focus", True) + self.set_property("can-focus", True) + self.set_property("is-focus", True) + self.set_property("expand", True) + + GObject.signal_new( + 'terminal-spawned', self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT, GObject.TYPE_INT) + ) + GObject.signal_new('page-deleted', self, GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) + def set_last_terminal_focused(self, terminal): self.last_terminal_focused = terminal @@ -103,21 +126,48 @@ class TerminalNotebook(Gtk.Notebook): for page_num in range(self.get_n_pages()): yield self.get_nth_page(page_num) - def delete_page(self, page_num, kill=True): + def delete_page(self, page_num, kill=True, prompt=False): if page_num >= self.get_n_pages(): log.debug("Can not delete page %s no such index", page_num) return + # TODO NOTEBOOK it would be nice if none of the "ui" stuff + # (PromptQuitDialog) would be in here + if prompt: + procs = self.get_running_fg_processes_count_page(page_num) + # TODO NOTEBOOK remove call to guake + prompt_cfg = self.guake.settings.general.get_int('prompt-on-close-tab') + if (prompt_cfg == 1 and procs > 0) or (prompt_cfg == 2): + if not PromptQuitDialog(self.guake.window, procs, -1).close_tab(): + return + for terminal in self.get_terminals_for_page(page_num): if kill: terminal.kill() terminal.destroy() self.remove_page(page_num) + self.emit('page-deleted') def delete_page_by_label(self, label, kill=True): - self.delete_page(self.find_tab_index_label(label), True) + self.delete_page(self.find_tab_index_by_label(label), kill) + + def delete_page_current(self, kill=True): + self.delete_page(self.get_current_page(), kill) + + def new_page(self, directory=None): + terminal = GuakeTerminal(self.guake) + terminal.grab_focus() + if not isinstance(directory, str): + directory = os.environ['HOME'] + try: + if self.guake.settings.general.get_boolean('open-tab-cwd'): + active_terminal = self.get_current_terminal() + if not active_terminal: + directory = os.path.expanduser('~') + directory = active_terminal.get_current_directory() + except: # pylint: disable=bare-except + pass + terminal.spawn_sync_pid(directory) - def new_page(self): - terminal = self.guake.setup_new_terminal() terminal_box = TerminalBox() terminal_box.set_terminal(terminal) root_terminal_box = RootTerminalBox(self.guake) @@ -128,18 +178,37 @@ class TerminalNotebook(Gtk.Notebook): # this is needed to initially set the last_terminal_focused, # one could also call terminal.get_parent().on_terminal_focus() terminal.emit("focus", Gtk.DirectionType.TAB_FORWARD) - return root_terminal_box, page_num + self.emit('terminal-spawned', terminal, terminal.pid) + return root_terminal_box, page_num, terminal - def find_tab_index_eventbox(self, eventbox): + def new_page_with_focus(self, directory=None): + box, page_num, terminal = self.new_page() + self.set_current_page(page_num) + self.rename_page(page_num, _("Terminal"), False) + terminal.grab_focus() + + def rename_page(self, page_index, new_text, user_set=False): + """Rename an already added page by its index. Use user_set to define + if the rename was triggered by the user (eg. rename dialog) or by + an update from the vte (eg. vte:window-title-changed) + """ + page = self.get_nth_page(page_index) + if not getattr(page, "custom_label_set", False) or user_set: + old_label = self.get_tab_label(page) + if isinstance(old_label, TabLabelEventBox): + old_label.set_text(new_text) + else: + self.set_tab_label(page, TabLabelEventBox(self, new_text)) + if user_set: + setattr(page, "custom_label_set", new_text != "-") + + def find_tab_index_by_label(self, eventbox): for index, tab_eventbox in enumerate(self.iter_tabs()): if eventbox is tab_eventbox: return index return -1 - def find_tab_index_label(self, label): - return self.find_tab_index_eventbox(label.get_parent()) - - def find_page_index_for_terminal(self, terminal): + def find_page_index_by_terminal(self, terminal): for index, page in enumerate(self.iter_pages()): for t in page.iter_terminals(): if t is terminal: @@ -147,7 +216,7 @@ class TerminalNotebook(Gtk.Notebook): return -1 def get_tab_text_index(self, index): - return self.get_tab_label(self.get_nth_page(index)).get_children()[0].get_text() + return self.get_tab_label(self.get_nth_page(index)).get_text() def get_tab_text_page(self, page): - return self.get_tab_label(page).get_children()[0].get_text() + return self.get_tab_label(page).get_text() diff --git a/guake/terminal.py b/guake/terminal.py index 3215c8b9..0b888fb5 100644 --- a/guake/terminal.py +++ b/guake/terminal.py @@ -50,6 +50,30 @@ from guake.globals import TERMINAL_MATCH_TAGS log = logging.getLogger(__name__) +libutempter = None +try: + # this allow to run some commands that requires libuterm to + # be injected in current process, as: wall + from atexit import register as at_exit_call + from ctypes import cdll + libutempter = cdll.LoadLibrary('libutempter.so.0') + if libutempter is not None: + # We absolutely need to remove the old tty from the utmp !!! + at_exit_call(libutempter.utempter_remove_added_record) +except Exception as e: + libutempter = None + sys.stderr.write("[WARN] ===================================================================\n") + sys.stderr.write("[WARN] Unable to load the library libutempter !\n") + sys.stderr.write( + "[WARN] Some feature might not work:\n" + "[WARN] - 'exit' command might freeze the terminal instead of closing the tab\n" + "[WARN] - the 'wall' command is know to work badly\n" + ) + sys.stderr.write("[WARN] Error: " + str(e) + '\n') + sys.stderr.write( + "[WARN] ===================================================================²\n" + ) + def halt(loc): code.interact(local=loc) @@ -71,6 +95,8 @@ class GuakeTerminal(Vte.Terminal): self.configure_terminal() self.add_matches() self.connect('button-press-event', self.button_press) + self.connect('button-press-event', self.button_press) + self.connect('child-exited', self.on_child_exited) self.matched_value = '' self.font_scale_index = 0 self._pid = None @@ -102,6 +128,11 @@ class GuakeTerminal(Vte.Terminal): else: super().feed_child(resolved_cmdline, len(resolved_cmdline)) + def execute_command(self, command): + if command[-1] != '\n': + command += "\n" + self.feed_child(command) + def copy_clipboard(self): if self.get_has_selection(): super(GuakeTerminal, self).copy_clipboard() @@ -125,6 +156,12 @@ class GuakeTerminal(Vte.Terminal): if (Vte.MAJOR_VERSION, Vte.MINOR_VERSION) >= (0, 50): self.set_allow_hyperlink(True) + # TODO PORT is this still the case with the newer vte version? + # -- Ubuntu has a patch to libvte which disables mouse scrolling in apps + # -- like vim and less by default. If this is the case, enable it back. + if hasattr(self, "set_alternate_screen_scroll"): + self.set_alternate_screen_scroll(True) + self.set_can_default(True) self.set_can_focus(True) @@ -269,6 +306,10 @@ class GuakeTerminal(Vte.Terminal): self.found_link = self.handleTerminalMatch(matched_string) self.matched_value = matched_string[0] + def on_child_exited(self, target, status, *user_data): + if libutempter is not None: + libutempter.utempter_remove_record(self.get_pty()) + def quick_open(self): self.copy_clipboard() clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) @@ -427,3 +468,35 @@ class GuakeTerminal(Vte.Terminal): # if this part of code was reached, means that SIGTERM # did the work and SIGKILL wasnt needed. pass + + def spawn_sync_pid(self, directory): + + argv = list() + user_shell = self.guake.settings.general.get_string('default-shell') + if user_shell and os.path.exists(user_shell): + argv.append(user_shell) + else: + argv.append(os.environ['SHELL']) + + login_shell = self.guake.settings.general.get_boolean('use-login-shell') + if login_shell: + argv.append('--login') + + pid = self.spawn_sync( + Vte.PtyFlags.DEFAULT, directory, argv, [], GLib.SpawnFlags.DO_NOT_REAP_CHILD, None, + None, None + ) + try: + tuple_type = gi._gi.ResultTuple # pylint: disable=c-extension-no-member + except: # pylint: disable=bare-except + tuple_type = tuple + if isinstance(pid, (tuple, tuple_type)): + # Return a tuple in 2.91 + # https://lazka.github.io/pgi-docs/Vte-2.91/classes/Terminal.html#Vte.Terminal.spawn_sync + pid = pid[1] + assert isinstance(pid, int) + + if libutempter is not None: + libutempter.utempter_add_record(self.get_pty().get_fd(), os.uname()[1]) + self.pid = pid + return pid diff --git a/guake/theme.py b/guake/theme.py index 73b5467c..1479c156 100644 --- a/guake/theme.py +++ b/guake/theme.py @@ -8,6 +8,9 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import GLib from gi.repository import Gtk +from gi.repository import Gdk +from textwrap import dedent + from guake.paths import GUAKE_THEME_DIR @@ -58,3 +61,44 @@ def get_gtk_theme(settings): gtk_theme_name = settings.general.get_string('gtk-theme-name') prefer_dark_theme = settings.general.get_boolean('gtk-prefer-dark-theme') return (gtk_theme_name, "dark" if prefer_dark_theme else None) + +def patch_gtk_theme(style_context, settings): + theme_name, variant = get_gtk_theme(settings) + + def rgba_to_hex(color): + return "#{0:02x}{1:02x}{2:02x}".format( + int(color.red * 255), int(color.green * 255), int(color.blue * 255) + ) + + # for n in [ + # "inverted_bg_color", + # "inverted_fg_color", + # "selected_bg_color", + # "selected_fg_color", + # "theme_inverted_bg_color", + # "theme_inverted_fg_color", + # "theme_selected_bg_color", + # "theme_selected_fg_color", + # ]: + # s = style_context.lookup_color(n) + # print(n, s, rgba_to_hex(s[1])) + selected_fg_color = rgba_to_hex(style_context.lookup_color("theme_selected_fg_color")[1]) + selected_bg_color = rgba_to_hex(style_context.lookup_color("theme_selected_bg_color")[1]) + log.debug( + "Patching theme '%s' (prefer dark = '%r'), overriding tab 'checked' state': " + "foreground: %r, background: %r", theme_name, "yes" + if variant == "dark" else "no", selected_fg_color, selected_bg_color + ) + css_data = dedent( + """ + .custom_tab:checked {{ + color: {selected_fg_color}; + background: {selected_bg_color}; + }} + """.format(selected_bg_color=selected_bg_color, selected_fg_color=selected_fg_color) + ).encode() + style_provider = Gtk.CssProvider() + style_provider.load_from_data(css_data) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) diff --git a/guake/utils.py b/guake/utils.py index c719fe2b..b73d6655 100644 --- a/guake/utils.py +++ b/guake/utils.py @@ -35,3 +35,40 @@ def get_server_time(widget): # Use local timestamp instead ts = time.time() return ts + +class TabNameUtils(): + @classmethod + def shorten(cls, text, settings): + use_vte_titles = settings.general.get_boolean('use-vte-titles') + if not use_vte_titles: + return text + max_name_length = settings.general.get_int("max-tab-name-length") + if max_name_length != 0 and len(text) > max_name_length: + text = "..." + text[-max_name_length:] + return text + +class FullscreenManager(): + + def __init__(self, window): + self.window = window + self.is_in_fullscreen = False + + def is_fullscreen(self): + return getattr(self.window, 'is_fullscreen', False) + + def fullscreen(self): + self.window.fullscreen() + setattr(self.window, 'is_fullscreen', True) + + def unfullscreen(self): + # TODO do we still need this fix with gtk3? + # Fixes "Guake cannot restore from fullscreen" (#628) + self.window.unmaximize() + self.window.unfullscreen() + setattr(self.window, 'is_fullscreen', False) + + def toggle(self): + if self.is_fullscreen(): + self.unfullscreen() + else: + self.fullscreen()