diff --git a/debian/control b/debian/control index cca03e8..9f26e2b 100644 --- a/debian/control +++ b/debian/control @@ -25,6 +25,7 @@ Depends: gir1.2-gtk-3.0, gir1.2-mate-desktop, mozo +Recommends: gir1.2-vte-2.91 Description: Advanced MATE menu One of the most advanced menus under Linux. MintMenu supports filtering, favorites, easy-uninstallation, autosession, and many other features. diff --git a/usr/lib/linuxmint/mintMenu/mintMenu.py b/usr/lib/linuxmint/mintMenu/mintMenu.py index 832141f..266e4d5 100755 --- a/usr/lib/linuxmint/mintMenu/mintMenu.py +++ b/usr/lib/linuxmint/mintMenu/mintMenu.py @@ -471,7 +471,7 @@ class MenuWin(object): self.keybinder.connect("activate", self.onBindingPress) self.keybinder.start() self.settings.connect("changed::hot-key", self.hotkeyChanged) - print("Binding to Hot Key: %s" % self.hotkeyText) + # print("Binding to Hot Key: %s" % self.hotkeyText) except Exception as e: self.keybinder = None print("** WARNING ** - Keybinder Error") diff --git a/usr/lib/linuxmint/mintMenu/mintMenuConfig.glade b/usr/lib/linuxmint/mintMenu/mintMenuConfig.glade index 863d735..c0a4f30 100644 --- a/usr/lib/linuxmint/mintMenu/mintMenuConfig.glade +++ b/usr/lib/linuxmint/mintMenu/mintMenuConfig.glade @@ -12,6 +12,18 @@ 1 1 + + 200 + 10000 + 600 + 10 + + + 200 + 10000 + 500 + 10 + 100 1 @@ -750,8 +762,8 @@ 7 7 7 - 5 - 2 + 6 + 12 Show application comments @@ -759,8 +771,8 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start 11 - 0.5 True @@ -776,7 +788,7 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 + start True @@ -787,12 +799,12 @@ - Hover + Open categories on hover True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 + start True @@ -823,7 +835,7 @@ False False adjustment4 - 100 + 99.999999999776477 1 @@ -893,7 +905,7 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 + start True @@ -909,7 +921,7 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 + start True @@ -925,7 +937,7 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0.5 + start True @@ -934,6 +946,101 @@ 2 + + + Enable run/open features for search entry + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + True + + + 0 + 9 + 2 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + 500 + False + False + adjustment12 + 500 + + + 1 + 12 + + + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + 600 + False + False + adjustment11 + 600 + + + 1 + 11 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + 18 + Output/terminal height (pixels): + + + 0 + 12 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + 18 + Output/terminal width (pixels): + + + 0 + 11 + + + + + Enable integrated terminal + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + start + 18 + True + + + 0 + 10 + 2 + + diff --git a/usr/lib/linuxmint/mintMenu/mintMenuConfig.py b/usr/lib/linuxmint/mintMenu/mintMenuConfig.py index 2971482..bf54483 100755 --- a/usr/lib/linuxmint/mintMenu/mintMenuConfig.py +++ b/usr/lib/linuxmint/mintMenu/mintMenuConfig.py @@ -134,6 +134,16 @@ class mintMenuConfig(object): self.bindGSettingsValueToWidget(self.settingsApplications, "bool", "remember-filter", self.rememberFilter, "toggled", self.rememberFilter.set_active, self.rememberFilter.get_active) self.bindGSettingsValueToWidget(self.settingsApplications, "bool", "enable-internet-search", self.enableInternetSearch, "toggled", self.enableInternetSearch.set_active, self.enableInternetSearch.get_active) + self.allow_execute = self.builder.get_object("allow-execute") + self.integrated_terminal_enabled = self.builder.get_object("integrated-terminal-enabled") + self.integrated_terminal_width = self.builder.get_object("integrated-terminal-width") + self.integrated_terminal_height = self.builder.get_object("integrated-terminal-height") + + self.bindGSettingsValueToWidget(self.settingsApplications, "bool", "allow-execute", self.allow_execute, "toggled", self.allow_execute.set_active, self.allow_execute.get_active) + self.bindGSettingsValueToWidget(self.settingsApplications, "bool", "integrated-terminal-enabled", self.integrated_terminal_enabled, "toggled", self.integrated_terminal_enabled.set_active, self.integrated_terminal_enabled.get_active) + self.bindGSettingsValueToWidget(self.settingsApplications, "int", "integrated-terminal-width", self.integrated_terminal_width, "value-changed", self.integrated_terminal_width.set_value, self.integrated_terminal_width.get_value) + self.bindGSettingsValueToWidget(self.settingsApplications, "int", "integrated-terminal-height", self.integrated_terminal_height, "value-changed", self.integrated_terminal_height.set_value, self.integrated_terminal_height.get_value) + self.bindGSettingsValueToWidget(self.settingsPlaces, "int", "icon-size", self.placesIconSize, "value-changed", self.placesIconSize.set_value, self.placesIconSize.get_value) self.bindGSettingsValueToWidget(self.settingsSystem, "int", "icon-size", self.systemIconSize, "value-changed", self.systemIconSize.set_value, self.systemIconSize.get_value) diff --git a/usr/lib/linuxmint/mintMenu/plugins/applications.glade b/usr/lib/linuxmint/mintMenu/plugins/applications.glade index 7c8d294..1fd2eb6 100644 --- a/usr/lib/linuxmint/mintMenu/plugins/applications.glade +++ b/usr/lib/linuxmint/mintMenu/plugins/applications.glade @@ -362,7 +362,7 @@ True False - Search + Search/Run: @@ -393,6 +393,33 @@ 1 + + + 28 + True + True + False + False + Try to execute or open your entry + none + + + True + False + True + True + gtk-execute + 1 + + + + + False + False + end + 2 + + 28 @@ -420,7 +447,7 @@ False False end - 2 + 3 diff --git a/usr/lib/linuxmint/mintMenu/plugins/applications.py b/usr/lib/linuxmint/mintMenu/plugins/applications.py index 4d76c88..0488ee9 100755 --- a/usr/lib/linuxmint/mintMenu/plugins/applications.py +++ b/usr/lib/linuxmint/mintMenu/plugins/applications.py @@ -19,6 +19,12 @@ from plugins.easybuttons import (ApplicationLauncher, CategoryButton, MenuApplicationLauncher) from plugins.easygsettings import EasyGSettings +try: + from plugins.terminal import IntegratedTerminal + hasVte = True +except: + hasVte = False + # i18n gettext.install("mintmenu", "/usr/share/linuxmint/locale") home = os.path.expanduser("~") @@ -29,6 +35,23 @@ class PackageDescriptor(): self.summary = summary self.description = description +class subprocess_thread(threading.Thread): + + def __init__(self, _cmd, parent): + threading.Thread.__init__(self) + self.cmd = _cmd + self.parent = parent + + def run(self): + try: + output = subprocess.check_output(self.cmd, + stderr=subprocess.STDOUT, cwd=home, shell=True) + if output: + GLib.timeout_add(0, self.parent.subprocess_thread_output, output, self.cmd) + except subprocess.CalledProcessError as e: + if e.output: + GLib.timeout_add(0, self.parent.subprocess_thread_output, e.output, self.cmd) + # import time # def print_timing(func): # def wrapper(*arg): @@ -181,9 +204,8 @@ class pluginclass(object): self.favoritesBox = self.builder.get_object("favoritesBox") self.applicationsScrolledWindow = self.builder.get_object("applicationsScrolledWindow") - - self.headingstocolor = [self.builder.get_object("label6"), self.builder.get_object("label2")] self.numApps = 0 + # These properties are NECESSARY to maintain consistency # Set 'window' property for the plugin (Must be the root widget) @@ -233,13 +255,14 @@ class pluginclass(object): self.settings.notifyAdd("use-apt", self.switchAPTUsage) self.settings.notifyAdd("fav-cols", self.changeFavCols) self.settings.notifyAdd("remember-filter", self.changeRememberFilter) - self.settings.notifyAdd("enable-internet-search", self.changeEnableInternetSearch) + self.settings.notifyAdd("allow-execute", self.update_allow_execute) self.settings.bindGSettingsEntryToVar("int", "category-hover-delay", self, "categoryhoverdelay") self.settings.bindGSettingsEntryToVar("bool", "do-not-filter", self, "donotfilterapps") self.settings.bindGSettingsEntryToVar("bool", "enable-internet-search", self, "enableInternetSearch") self.settings.bindGSettingsEntryToVar("string", "search-command", self, "searchtool") self.settings.bindGSettingsEntryToVar("int", "default-tab", self, "defaultTab") + self.settings.bindGSettingsEntryToVar("bool", "integrated-terminal-enabled", self, "integrated_terminal_enabled") except Exception as e: print(e) @@ -275,6 +298,8 @@ class pluginclass(object): self.panel_position = -1 self.builder.get_object("searchButton").connect("button-press-event", self.searchPopup) + self.builder.get_object("executeButton").connect("button-press-event", self.on_execute_button_pressed) + self.update_allow_execute() # self.icon_theme = Gtk.IconTheme.get_default() # self.icon_theme.connect("changed", self.on_icon_theme_changed) @@ -383,8 +408,19 @@ class pluginclass(object): def changeRememberFilter(self, settings, key, args): self.rememberFilter = settings.get_boolean(key) - def changeEnableInternetSearch(self, settings, key, args): - self.enableInternetSearch = settings.get_boolean(key) + def update_allow_execute(self, settings=None, key=None, args=None): + if settings and key: + self.allow_execute = settings.get_boolean(key) + #self.builder.get_object("executeButton").set_visible(self.allow_execute) + if self.allow_execute: + self.builder.get_object("executeButton").show() + searchLabel_text = _("Search/Run:") + else: + self.builder.get_object("executeButton").hide() + searchLabel_text = _("Search:") + searchLabel = self.builder.get_object("searchLabel") + searchLabel.set_markup("%s" % searchLabel_text) + searchLabel.show() def changeShowApplicationComments(self, settings, key, args): self.showapplicationcomments = settings.get_boolean(key) @@ -465,6 +501,10 @@ class pluginclass(object): self.useAPT = self.settings.get("bool", "use-apt") self.rememberFilter = self.settings.get("bool", "remember-filter") self.enableInternetSearch = self.settings.get("bool", "enable-internet-search") + self.allow_execute = self.settings.get("bool", "allow-execute") + self.integrated_terminal_enabled = self.settings.get("bool", "integrated-terminal-enabled") + self.integrated_terminal_width = self.settings.get("int", "integrated-terminal-width") + self.integrated_terminal_height = self.settings.get("int", "integrated-terminal-height") self.lastActiveTab = self.settings.get("int", "last-active-tab") self.defaultTab = self.settings.get("int", "default-tab") @@ -607,6 +647,22 @@ class pluginclass(object): self.applicationsBox.get_children()[-1].grab_focus() + def add_execute_suggestions(self, user_text): + # Wait to see if the keyword has changed.. before doing anything + text = self.searchEntry.get_text() + if user_text != text: + return + commands = user_text.split() + cmd = self.verify_command(commands[0]) + if cmd: + text = "%s" % cgi.escape(text) + commands[0] = cmd + if cmd.startswith("xdg-open"): + self.add_suggestion("document-open", _("Try to open %s") % text, None, self.execute_user_input, commands, False) + else: + self.add_suggestion("application-x-executable", _("Run %s") % text, None, self.execute_user_input, commands, False) + self.applicationsBox.get_children()[-1].grab_focus() + def add_apt_filter_results(self, keyword): try: # Wait to see if the keyword has changed.. before doing anything @@ -660,7 +716,7 @@ class pluginclass(object): for word in keywords: if word != "": name = name.replace(word, "%s" % word) - self.add_suggestion(Gtk.STOCK_ADD, + self.add_suggestion("package-x-generic", _("Install package '%s'") % name, "%s\n\n%s\n\n%s" % (pkg.name, pkg.summary, pkg.description), self.apturl_install, pkg.name) @@ -725,7 +781,7 @@ class pluginclass(object): if self.donotfilterapps: widget.set_text("") else: - text = widget.get_text() + text = widget.get_text().strip() if self.lastActiveTab != 1: self.changeTab(1, clear = False) text = widget.get_text() @@ -750,8 +806,11 @@ class pluginclass(object): if not showns: if len(text) >= 3: self.add_search_suggestions(text) + if self.allow_execute: + GLib.timeout_add(150, self.add_execute_suggestions, text) if self.useAPT: GLib.timeout_add(300, self.add_apt_filter_results, text) + for i in self.categoriesBox.get_children(): i.released() i.set_relief(Gtk.ReliefStyle.NONE) @@ -786,10 +845,17 @@ class pluginclass(object): self.Filter(widget, category) def keyPress(self, widget, event): - """ Forward all text to the search box """ + # Forward all text to the search box if event.string.strip() or event.keyval == Gdk.KEY_space: self.searchEntry.event(event) return True + + # Ctrl+Enter for the Run button + if self.allow_execute and \ + (event.state & Gdk.ModifierType.CONTROL_MASK) and \ + event.keyval == Gdk.KEY_Return: + self.on_execute_button_pressed(widget, event) + return True return False def favPopup(self, widget, event): @@ -931,6 +997,102 @@ class pluginclass(object): mTree.attach_to_widget(widget, None) mTree.popup(None, None, None, None, event.button, event.time) + def subprocess_thread_output(self, output, command): + output = output.strip() + if not output: + return + self.mintMenuWin.hide() + + def on_key_press_event(widget, event): + if event.keyval == Gdk.KEY_Escape or \ + event.keyval == Gdk.KEY_Return or \ + event.keyval == Gdk.KEY_KP_Enter: + widget.destroy() + + window = Gtk.Window() + window.set_title(_("Output from your command: %s" % command)) + window.set_transient_for(self.window) + window.set_icon_from_file("/usr/lib/linuxmint/mintMenu/icon.svg") + #window.set_skip_taskbar_hint(True) + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + output_box = Gtk.TextView() + output_box.set_editable(False) + output_box.set_monospace(True) + output_box.set_right_margin(16) + output_box.set_wrap_mode(Gtk.WrapMode.WORD) + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolled_window.set_hexpand(True) + scrolled_window.set_vexpand(True) + scrolled_window.set_size_request(self.integrated_terminal_width, self.integrated_terminal_height) + scrolled_window.add(output_box) + box.pack_start(scrolled_window, False, True, 0) + window.add(box) + + output_box.get_buffer().set_text(output) + window.show_all() + window.connect("key-press-event", on_key_press_event) + + @staticmethod + def find_file_in_path(filename): + if os.path.isfile(filename): + return filename + path = os.environ.get("PATH", os.defpath) + paths = path.split(os.pathsep) + paths.append(home) + for p in paths: + f = os.path.join(p, filename) + if os.path.isfile(f): + return f + return None + + def verify_command(self, cmd): + cmd = os.path.expanduser(os.path.expandvars(cmd)) + cmd = self.find_file_in_path(cmd) + if cmd and not os.access(cmd, os.X_OK): + cmd = "xdg-open %s" % cmd + # TODO: We could check mime type first to ensure this goes through + return cmd + + def on_execute_button_pressed(self, widget, event): + command = self.searchEntry.get_text().strip() + if command: + self.execute_user_input(widget, command.split()) + + def execute_user_input(self, widget, commands, verify=True): + self.mintMenuWin.hide() + if verify: + cmd = self.verify_command(commands[0]) + if not cmd: + # file not found + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, + _("Invalid command or file reference.")) + dialog.set_title("mintMenu") + dialog.run() + dialog.destroy() + return + commands[0] = cmd + command = " ".join(commands) + try: + global hasVte + if hasVte and self.integrated_terminal_enabled: + try: + IntegratedTerminal(command, + _("mintMenu Integrated Terminal"), + width=self.integrated_terminal_width, + height=self.integrated_terminal_height + ) + except Exception as e: + print("IntegratedTerminal exception:", e) + hasVte = False + if not hasVte or not self.integrated_terminal_enabled: + thread = subprocess_thread(command, self) + thread.start() + except: + pass + def searchPopup(self, widget, event): def add_menu_item(icon=None, text=None, callback=None): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) diff --git a/usr/lib/linuxmint/mintMenu/plugins/terminal.py b/usr/lib/linuxmint/mintMenu/plugins/terminal.py new file mode 100644 index 0000000..45cc71e --- /dev/null +++ b/usr/lib/linuxmint/mintMenu/plugins/terminal.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os + +import gi +gi.require_version("Gtk", "3.0") +gi.require_version('Vte', '2.91') +from gi.repository import Gtk, Gdk, GLib, Vte + +class IntegratedTerminal(Gtk.Window): + + def __init__(self, command, title=None, shell=None, cwd=None, width=600, height=500): + try: + self.terminal=Vte.Terminal() + self.terminal.set_scrollback_lines(-1) + # Setting the font doesn't work for some reason, let's leave it + # self.terminal.set_font(Pango.FontDescription(string='Monospace')) + self.command = command + self.ready = False + self.output_handler = self.terminal.connect("cursor-moved", + self.on_cursor_moved) + # apparently Vte.Terminal.spawn_sync() is deprecated in favour of the + # non-existent Vte.Terminal.spawn_async()... + self.terminal.spawn_sync( + Vte.PtyFlags.DEFAULT, # pty_flags + cwd or os.environ.get("HOME"), # working_directory + shell or [os.environ.get("SHELL")], # argv + [], # envv + GLib.SpawnFlags.DO_NOT_REAP_CHILD, # spawn_flags + None, # child_setup + None, # child_setup_data + None # cancellable + ) + except Exception as e: + self.close() + raise Exception(e) + + Gtk.Window.__init__(self, title=title or "mintMenu Integrated Terminal") + self.set_icon_name("utilities-terminal") + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.scrolled_window = Gtk.ScrolledWindow() + self.scrolled_window.set_hexpand(True) + self.scrolled_window.set_vexpand(True) + self.scrolled_window.set_size_request(width, height) + self.scrolled_window.add(self.terminal) + box.pack_start(self.scrolled_window, False, True, 0) + self.add(box) + self.connect("key-press-event", self.on_key_press_event) + self.terminal.connect("child-exited", self.exit) + self.terminal.connect("eof", self.exit) + self.terminal.set_rewrap_on_resize(True) + + def on_cursor_moved(self, terminal): + if not self.ready: + # we have to run the command on a callback because the + # spawn_sync() method doesn't wait for the shell to load, + # so instead we have to wait for the shell to create a prompt + self.ready = True + # if we don't show the terminal once after this it won't + # always receive output, so we show it and hide it again right + # away. Whatever works... + self.show_all() + self.hide() + # Now we can go: + command = "%s\n" % self.command + self.terminal.feed_child(command, len(command)) + return + # Unfortunately we cannot guarantee that the command is on the first + # line (the shell may display somethinge else first), so we have to + # search for it: + x, y = terminal.get_cursor_position() + contents, dummy = terminal.get_text_range(0, 0, x, y, None, None) + lines = contents.split("\n") + prefix = "" + command_found = False + for line in lines: + if not line: + continue + if command_found: + if not line.lstrip(prefix): + # we got a command prompt, exit + self.exit() + break + # the command generated output, show the terminal + terminal.disconnect(self.output_handler) + self.show_all() + break + if self.command in line: + # this is our command line + prefix = line.split(self.command, 1)[0] + command_found = True + + def on_key_press_event(self, widget, event): + if event.keyval == Gdk.KEY_Escape: + self.exit() + + def exit(self, *args): + # Gtk.main_quit() + self.close() diff --git a/usr/share/glib-2.0/schemas/com.linuxmint.mintmenu.gschema.xml b/usr/share/glib-2.0/schemas/com.linuxmint.mintmenu.gschema.xml index 65c00e9..4b0536e 100644 --- a/usr/share/glib-2.0/schemas/com.linuxmint.mintmenu.gschema.xml +++ b/usr/share/glib-2.0/schemas/com.linuxmint.mintmenu.gschema.xml @@ -307,6 +307,28 @@ + + + true + + + + + true + + + + + 600 + + + + + + 500 + + +