diff --git a/files/etc/xdg/menus/cinnamon-applications.menu b/files/etc/xdg/menus/cinnamon-applications.menu index 292cf674aa..530f560bac 100644 --- a/files/etc/xdg/menus/cinnamon-applications.menu +++ b/files/etc/xdg/menus/cinnamon-applications.menu @@ -1,6 +1,7 @@ + Applications @@ -20,10 +21,10 @@ Utility - + Accessibility System @@ -129,12 +130,6 @@ - - - System - cinnamon-system-tools.directory - - Other @@ -176,36 +171,27 @@ Administration cinnamon-settings-system.directory - - Settings - System - - - - - System - Settings - + System - - Accessories - Education - Games - Graphics - Internet - Office - Other - Development - Multimedia - System - Universal Access - wine-wine - Preferences - Administration - + + Accessories + Education + Games + Graphics + Internet + Office + Other + Development + Science + Multimedia + Universal Access + wine-wine + Preferences + Administration + diff --git a/files/usr/bin/cinnamon-menu-editor b/files/usr/bin/cinnamon-menu-editor index b2af779f95..ae5c8bb0af 100755 --- a/files/usr/bin/cinnamon-menu-editor +++ b/files/usr/bin/cinnamon-menu-editor @@ -13,14 +13,7 @@ from cme import MainWindow def main(): - try: - from MenuEditor import config - datadir = config.pkgdatadir - version = config.VERSION - except Exception: - datadir = '.' - version = '0.9' - app = MainWindow.MainWindow(datadir, version) + app = MainWindow.MainWindow() app.run() diff --git a/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py b/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py index ba879b4a2b..637d9ef074 100755 --- a/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py +++ b/files/usr/share/cinnamon/cinnamon-desktop-editor/cinnamon-desktop-editor.py @@ -9,15 +9,13 @@ import subprocess from setproctitle import setproctitle from pathlib import Path +import xml.etree.ElementTree as ET import gi gi.require_version("Gtk", "3.0") gi.require_version("CMenu", "3.0") from gi.repository import GLib, Gtk, Gio, CMenu -sys.path.insert(0, '/usr/share/cinnamon/cinnamon-menu-editor') -from cme import util - sys.path.insert(0, '/usr/share/cinnamon/cinnamon-settings') from bin import JsonSettingsWidgets @@ -37,22 +35,88 @@ def escape_space(string): return string.replace(" ", r"\ ") - def ask(msg): - dialog = Gtk.MessageDialog(None, - Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL, - Gtk.MessageType.QUESTION, - Gtk.ButtonsType.YES_NO, - None) + dialog = Gtk.MessageDialog( + transient_for=None, + modal=True, + destroy_with_parent=True, + message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.YES_NO, + ) dialog.set_markup(msg) - dialog.show_all() + response = dialog.run() dialog.destroy() return response == Gtk.ResponseType.YES +def show_error_dialog(msg): + dialog = Gtk.MessageDialog( + transient_for=None, + modal=True, + destroy_with_parent=True, + message_type=Gtk.MessageType.WARNING, + buttons=Gtk.ButtonsType.OK, + ) + dialog.set_markup(msg) + + dialog.run() + dialog.destroy() DESKTOP_GROUP = GLib.KEY_FILE_DESKTOP_GROUP - +KEY_FILE_FLAGS = GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS + +def getUserItemPath(): + item_dir = os.path.join(GLib.get_user_data_dir(), 'applications') + if not os.path.isdir(item_dir): + os.makedirs(item_dir) + return item_dir + +def getUserDirectoryPath(): + menu_dir = os.path.join(GLib.get_user_data_dir(), 'desktop-directories') + if not os.path.isdir(menu_dir): + os.makedirs(menu_dir) + return menu_dir + +def is_system_launcher(filename): + for path in GLib.get_system_data_dirs(): + if os.path.exists(os.path.join(path, "applications", filename)): + return True + return False + +def is_system_directory(filename): + for path in GLib.get_system_data_dirs(): + if os.path.exists(os.path.join(path, "desktop-directories", filename)): + return True + return False + +# from cs_startup.py +def get_locale(): + current_locale = None + locales = GLib.get_language_names() + for locale in locales: + if locale.find(".") == -1: + current_locale = locale + break + return current_locale + +def fillKeyFile(keyfile, items): + LOCALIZABLE_KEYS = ("Name", "GenericName", "Comment", "Keywords") + locale = get_locale() + + for key, item in items.items(): + if item is None: + continue + + if isinstance(item, bool): + keyfile.set_boolean(DESKTOP_GROUP, key, item) + elif isinstance(item, str): + keyfile.set_string(DESKTOP_GROUP, key, item) + if key in LOCALIZABLE_KEYS and locale: + keyfile.set_locale_string(DESKTOP_GROUP, key, locale, item) + elif isinstance(item, list): + keyfile.set_string_list(DESKTOP_GROUP, key, item) + if key in LOCALIZABLE_KEYS and locale: + keyfile.set_locale_string_list(DESKTOP_GROUP, key, locale, item) class ItemEditor(object): ui_file = None @@ -132,6 +196,22 @@ def validate_exec_line(self, string): def get_keyfile_edits(self): raise NotImplementedError() + def pick_exec(self, button): + chooser = Gtk.FileChooserDialog( + title=_("Choose a command"), + parent=self.dialog, + action=Gtk.FileChooserAction.OPEN + ) + chooser.add_buttons( + "_Cancel", Gtk.ResponseType.REJECT, + "_Open", Gtk.ResponseType.ACCEPT + ) + response = chooser.run() + + if response == Gtk.ResponseType.ACCEPT: + self.builder.get_object('exec-entry').set_text(escape_space(chooser.get_filename())) + chooser.destroy() + def set_text(self, ctl, name): try: val = self.keyfile.get_locale_string(DESKTOP_GROUP, name, None) @@ -154,38 +234,29 @@ def set_icon(self, name): except GLib.GError: pass else: - print(val) self.icon_chooser.set_icon(val) self.starting_icon = val - print('icon:', self.icon_chooser.get_icon()) def load(self): self.keyfile = GLib.KeyFile() path = self.item_path or "" try: - self.keyfile.load_from_file(path, util.KEY_FILE_FLAGS) + self.keyfile.load_from_file(path, KEY_FILE_FLAGS) except GLib.GError: pass def save(self): - util.fillKeyFile(self.keyfile, self.get_keyfile_edits()) + fillKeyFile(self.keyfile, self.get_keyfile_edits()) contents, length = self.keyfile.to_data() - need_exec = False - if self.destdir is not None: - self.item_path = os.path.join(self.destdir, self.builder.get_object('name-entry').get_text() + ".desktop") - need_exec = True try: with open(self.item_path, 'w') as f: f.write(contents) - if need_exec: - os.chmod(self.item_path, 0o755) - subprocess.Popen(['update-desktop-database', util.getUserItemPath()], env=os.environ) except IOError as e: - if ask(_("Cannot create the launcher at this location. Add to the desktop instead?")): - self.destdir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP) - self.save() + msg = f"Error writing file: {e}" + print(msg) + show_error_dialog(msg) def run(self): self.dialog.present() @@ -197,15 +268,90 @@ def on_response(self, dialog, response): else: self.callback(False, self.item_path) -class LauncherEditor(ItemEditor): +class NemoLauncherEditor(ItemEditor): ui_file = '/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui' def build_ui(self): self.builder.get_object('exec-browse').connect('clicked', self.pick_exec) + self.builder.get_object('name-entry').connect('changed', self.resync_validity) + self.builder.get_object('exec-entry').connect('changed', self.resync_validity) + + # Hide LauncherEditor widgets not relevant to NemoLauncherEditor + self.builder.get_object('nodisplay-check').set_visible(False) + self.builder.get_object('nodisplay-check').set_no_show_all(True) + self.builder.get_object('category-section').set_visible(False) + self.builder.get_object('category-section').set_no_show_all(True) + def resync_validity(self, *args): + name_text = self.builder.get_object('name-entry').get_text().strip() + exec_text = self.builder.get_object('exec-entry').get_text().strip() + name_valid = name_text != "" + exec_valid = self.validate_exec_line(exec_text) + self.sync_widgets(name_valid, exec_valid) + + def load(self): + super(NemoLauncherEditor, self).load() + self.set_text('name-entry', "Name") + self.set_text('exec-entry', "Exec") + self.set_text('comment-entry', "Comment") + self.set_check('terminal-check', "Terminal") + self.set_check('offload-gpu-check', "PrefersNonDefaultGPU") + self.set_icon("Icon") + + def get_keyfile_edits(self): + return dict(Name=self.builder.get_object('name-entry').get_text(), + Exec=self.builder.get_object('exec-entry').get_text(), + Comment=self.builder.get_object('comment-entry').get_text(), + Terminal=self.builder.get_object('terminal-check').get_active(), + PrefersNonDefaultGPU=self.builder.get_object("offload-gpu-check").get_active(), + Icon=self.icon_chooser.get_icon(), + Type="Application") + + def check_custom_path(self): + if self.item_path: + self.item_path = os.path.join(getUserItemPath(), os.path.split(self.item_path)[1]) + + def save(self): + fillKeyFile(self.keyfile, self.get_keyfile_edits()) + contents, length = self.keyfile.to_data() + need_exec = False + if self.destdir is not None: + self.item_path = os.path.join(self.destdir, self.builder.get_object('name-entry').get_text() + ".desktop") + need_exec = True + + try: + with open(self.item_path, 'w') as f: + f.write(contents) + if need_exec: + os.chmod(self.item_path, 0o755) + + subprocess.Popen(['update-desktop-database', getUserItemPath()], env=os.environ) + except IOError as e: + if ask(_("Cannot create the launcher at this location. Add to the desktop instead?")): + self.destdir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP) + self.save() + +class LauncherEditor(ItemEditor): + ui_file = '/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui' + + def __init__(self, item_path=None, callback=None, destdir=None, icon_size=24, show_categories=False): + self.show_categories = show_categories + super(LauncherEditor, self).__init__(item_path, callback, destdir, icon_size) + + def build_ui(self): + self.builder.get_object('exec-browse').connect('clicked', self.pick_exec) self.builder.get_object('name-entry').connect('changed', self.resync_validity) self.builder.get_object('exec-entry').connect('changed', self.resync_validity) + if self.show_categories: + self.category_widgets = {} # Map ID -> CheckButton + self._setup_categories_list() + self.fdo_categories = [] + else: + cat_section = self.builder.get_object('category-section') + cat_section.set_visible(False) + cat_section.set_no_show_all(True) + def resync_validity(self, *args): name_text = self.builder.get_object('name-entry').get_text().strip() exec_text = self.builder.get_object('exec-entry').get_text().strip() @@ -213,6 +359,56 @@ def resync_validity(self, *args): exec_valid = self.validate_exec_line(exec_text) self.sync_widgets(name_valid, exec_valid) + def _setup_categories_list(self): + flowbox = self.builder.get_object('category-flowbox') + DONT_SHOWS = ["Other"] + + tree = CMenu.Tree.new("cinnamon-applications.menu", CMenu.TreeFlags.INCLUDE_NODISPLAY | CMenu.TreeFlags.SHOW_EMPTY) + if tree.load_sync(): + root = tree.get_root_directory() + it = root.iter() + while True: + item_type = it.next() + if item_type == CMenu.TreeItemType.INVALID: + break + if item_type == CMenu.TreeItemType.DIRECTORY: + dir_item = it.get_directory() + name = dir_item.get_name() + cat_id = dir_item.get_menu_id() + if cat_id and cat_id not in DONT_SHOWS: + cb = Gtk.CheckButton(label=name) + cb.set_visible(True) + flowbox.add(cb) + cb.get_parent().set_can_focus(False) + self.category_widgets[cat_id] = cb + cb.connect("toggled", self.on_category_toggled, cat_id) + + def on_category_toggled(self, button, cat_id): + if not button.get_active(): + return + + # These mutually exclusive categories are based on /etc/xdg/menus/cinnamon-applications.menu + def uncheck(cat): + if cat in self.category_widgets: + self.category_widgets[cat].set_active(False) + + if cat_id == "Accessories": + uncheck("Universal Access") + uncheck("Administration") + elif cat_id == "Universal Access": + uncheck("Accessories") + uncheck("Preferences") + elif cat_id == "Preferences": + uncheck("Universal Access") + uncheck("Administration") + elif cat_id == "Administration": + uncheck("Preferences") + uncheck("Accessories") + elif cat_id == "Education": + uncheck("Science") + elif cat_id == "Science": + uncheck("Education") + def load(self): super(LauncherEditor, self).load() self.set_text('name-entry', "Name") @@ -220,30 +416,152 @@ def load(self): self.set_text('comment-entry', "Comment") self.set_check('terminal-check', "Terminal") self.set_check('offload-gpu-check', "PrefersNonDefaultGPU") + self.set_check('nodisplay-check', "NoDisplay") self.set_icon("Icon") + if self.show_categories: + # Preselect existing categories + try: + flowbox = self.builder.get_object('category-flowbox') + self.fdo_categories = self.keyfile.get_string_list(DESKTOP_GROUP, "Categories") + cinnamon_categories = self._fdo_to_cinnamon(self.fdo_categories) + for cat_id in cinnamon_categories: + if cat_id in self.category_widgets: + self.category_widgets[cat_id].set_active(True) + except GLib.GError: + pass + else: + try: + self.old_categories = self.keyfile.get_locale_string(DESKTOP_GROUP, "Categories", None) + except GLib.GError: + self.old_categories = "" + def get_keyfile_edits(self): + if self.show_categories: + self.fdo_categories = self._cinnamon_to_fdo(self.fdo_categories) + categories_val = ";".join(self.fdo_categories) + if categories_val: + categories_val += ";" + else: + categories_val = self.old_categories + return dict(Name=self.builder.get_object('name-entry').get_text(), Exec=self.builder.get_object('exec-entry').get_text(), Comment=self.builder.get_object('comment-entry').get_text(), Terminal=self.builder.get_object('terminal-check').get_active(), PrefersNonDefaultGPU=self.builder.get_object("offload-gpu-check").get_active(), + NoDisplay=self.builder.get_object("nodisplay-check").get_active(), + Categories=categories_val, Icon=self.icon_chooser.get_icon(), Type="Application") - def pick_exec(self, button): - chooser = Gtk.FileChooserDialog(title=_("Choose a command"), - parent=self.dialog, - buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) - response = chooser.run() - if response == Gtk.ResponseType.ACCEPT: - self.builder.get_object('exec-entry').set_text(escape_space(chooser.get_filename())) - chooser.destroy() - def check_custom_path(self): - if self.item_path: - self.item_path = os.path.join(util.getUserItemPath(), os.path.split(self.item_path)[1]) + # If item_path is a system file, we create an override in user item path, otherwise + # we edit it directly (including items in subdirectories of user path e.g. wine apps) + file_name = os.path.basename(self.item_path) + if is_system_launcher(file_name): + self.item_path = os.path.join(getUserItemPath(), file_name) + else: + # If launcher appears to be from a user installed app (e.g. steam, wine) rather + # than a user created custom launcher, we show a warning that 'restore' is not available. + if not file_name.startswith("alacarte-"): + self.builder.get_object('restore-info-bar').show() + + def _fdo_to_cinnamon(self, fdo_cats): + # These conversions are based on /etc/xdg/menus/cinnamon-applications.menu + cats = list(fdo_cats) + + mappings = { + "Game": "Games", + "Network": "Internet", + "AudioVideo": "Multimedia", + "Wine": "wine-wine" + } + for fdo, cinn in mappings.items(): + if fdo in cats: + cats.append(cinn) + + if "Utility" in cats and "Accessibility" not in cats and "System" not in cats: + cats.append("Accessories") + + if "Accessibility" in cats and "Settings" not in cats: + cats.append("Universal Access") + + if "Education" in cats and "Science" in cats: + cats.remove("Education") + + if "Settings" in cats and "System" not in cats: + cats.append("Preferences") + + if "System" in cats: + cats.append("Administration") + + return cats + + def _cinnamon_to_fdo(self, fdo_cats): + # These conversions are based on /etc/xdg/menus/cinnamon-applications.menu + mappings = { + "Accessories": "Utility", + "Games": "Game", + "Internet": "Network", + "Multimedia": "AudioVideo", + "Universal Access": "Accessibility", + "wine-wine": "Wine", + "Preferences": "Settings", + "Administration": "System" + } + + for cinn_cat, button in self.category_widgets.items(): + fdo_cat = mappings.get(cinn_cat, cinn_cat) + if button.get_active(): + if fdo_cat not in fdo_cats: + fdo_cats.append(fdo_cat) + else: + if fdo_cat in fdo_cats: + fdo_cats.remove(fdo_cat) + + return fdo_cats + + def _clear_menu_overrides(self): + """Removes and rules for this item from the menu XML.""" + if not self.item_path: + return + + desktop_id = os.path.basename(self.item_path) + menu_path = os.path.join(GLib.get_user_config_dir(), "menus", "cinnamon-applications.menu") + + if not os.path.exists(menu_path): + return + + try: + tree = ET.parse(menu_path) + root = tree.getroot() + modified = False + + for parent in root.iter(): + for node in list(parent): + if node.tag in ('Include', 'Exclude'): + for filename_node in list(node.findall('Filename')): + if filename_node.text == desktop_id: + node.remove(filename_node) + modified = True + + if len(node) == 0: + parent.remove(node) + modified = True + + if modified: + tree.write(menu_path, encoding='utf-8', xml_declaration=True) + + except (ET.ParseError, OSError) as e: + msg = f"Desktop Editor: Could not update .menu file: {e}" + print(msg) + show_error_dialog(msg) + + def save(self): + super(LauncherEditor, self).save() + subprocess.Popen(['update-desktop-database', getUserItemPath()], env=os.environ) + self._clear_menu_overrides() class DirectoryEditor(ItemEditor): ui_file = '/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui' @@ -260,16 +578,27 @@ def load(self): super(DirectoryEditor, self).load() self.set_text('name-entry', "Name") self.set_text('comment-entry', "Comment") + self.set_check('nodisplay-check', "NoDisplay") self.set_icon("Icon") def get_keyfile_edits(self): return dict(Name=self.builder.get_object('name-entry').get_text(), Comment=self.builder.get_object('comment-entry').get_text(), + NoDisplay=self.builder.get_object('nodisplay-check').get_active(), Icon=self.icon_chooser.get_icon(), Type="Directory") def check_custom_path(self): - self.item_path = os.path.join(util.getUserDirectoryPath(), os.path.split(self.item_path)[1]) + # If item_path is a system file, we create an override in user's + # desktop-directories path, otherwise we edit it directly. + file_name = os.path.basename(self.item_path) + if is_system_directory(file_name): + self.item_path = os.path.join(getUserDirectoryPath(), file_name) + else: + # If directory appears to be from a user installed app (e.g. wine) rather + # than a user created directory, we show a warning that 'restore' is not available. + if not file_name.startswith("alacarte-"): + self.builder.get_object('restore-info-bar').show() class CinnamonLauncherEditor(ItemEditor): ui_file = '/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui' @@ -280,6 +609,11 @@ def build_ui(self): self.builder.get_object('name-entry').connect('changed', self.resync_validity) self.builder.get_object('exec-entry').connect('changed', self.resync_validity) + self.builder.get_object('nodisplay-check').set_visible(False) + self.builder.get_object('nodisplay-check').set_no_show_all(True) + self.builder.get_object('category-section').set_visible(False) + self.builder.get_object('category-section').set_no_show_all(True) + def check_custom_path(self): dir = Gio.file_new_for_path(PANEL_LAUNCHER_PATH) if not dir.query_exists(None): @@ -346,17 +680,6 @@ def in_hicolor(self, path): return False - def pick_exec(self, button): - chooser = Gtk.FileChooserDialog(title=_("Choose a command"), - parent=self.dialog, - buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) - response = chooser.run() - if response == Gtk.ResponseType.ACCEPT: - self.builder.get_object('exec-entry').set_text(escape_space(chooser.get_filename())) - chooser.destroy() - - class Main: def __init__(self): parser = OptionParser() @@ -365,6 +688,7 @@ def __init__(self): parser.add_option("-f", "--file", dest="desktop_file", help="Name of desktop file (i.e. gnome-terminal.desktop)", metavar="DESKTOP_NAME") parser.add_option("-m", "--mode", dest="mode", default=None, help="Mode to run in: launcher, directory, panel-launcher or nemo-launcher") parser.add_option("-i", "--icon-size", dest="icon_size", type=int, default=24, help="Size to set the icon picker for (panel-launcher only)") + parser.add_option("--show-categories", action="store_true", dest="show_categories", default=False, help="Show the category selection section (launcher mode only)") (options, args) = parser.parse_args() if not options.mode: @@ -385,6 +709,7 @@ def __init__(self): self.orig_file = options.original_desktop_file self.desktop_file = options.desktop_file self.dest_dir = options.destination_directory + self.show_categories = options.show_categories if options.mode == "cinnamon-launcher": self.json_path = args[0] @@ -397,13 +722,13 @@ def __init__(self): editor = DirectoryEditor(self.orig_file, self.directory_cb) editor.dialog.show_all() elif self.mode == "launcher": - editor = LauncherEditor(self.orig_file, self.launcher_cb) + editor = LauncherEditor(self.orig_file, self.launcher_cb, show_categories=self.show_categories) editor.dialog.show_all() elif self.mode == "cinnamon-launcher": editor = CinnamonLauncherEditor(self.orig_file, self.panel_launcher_cb, icon_size=self.icon_size) editor.dialog.show_all() elif self.mode == "nemo-launcher": - editor = LauncherEditor(self.orig_file, self.nemo_launcher_cb, self.dest_dir) + editor = NemoLauncherEditor(self.orig_file, self.nemo_launcher_cb, self.dest_dir) editor.dialog.show_all() else: print("Invalid args") @@ -437,7 +762,7 @@ def nemo_launcher_cb(self, success, dest_path): def ask_menu_launcher(self, dest_path): if ask(_("Would you like to add this launcher to the menu also? It will be placed in the Other category initially.")): - new_file_path = os.path.join(util.getUserItemPath(), os.path.split(dest_path)[1]) + new_file_path = os.path.join(getUserItemPath(), os.path.split(dest_path)[1]) shutil.copy(dest_path, new_file_path) def get_desktop_path(self): diff --git a/files/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui b/files/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui index bfefcafdcd..45c02b7d5e 100644 --- a/files/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui +++ b/files/usr/share/cinnamon/cinnamon-desktop-editor/directory-editor.ui @@ -1,81 +1,55 @@ - - + - False - 4 + False + 12 Directory Properties - dialog + dialog + 420 + 150 - False + False vertical - 4 - - - False - end - - - gtk-cancel - True - True - True - True - - - False - True - 0 - - - - - gtk-ok - True - True - True - True + 12 + + + False + True + warning + + + False + 16 + + + True + False + Note: 'Restore' to system default is not available for this item. + True + 0 + + - - False - True - 1 - - - False - True - end - 0 - True - False - 10 + False + 12 - + True - False - 1 - 0 - 0 - - - True - True - True - center - center - dialog - cinnamon-panel-launcher - - + True + True + end + start + dialog + cinnamon-panel-launcher False @@ -86,86 +60,109 @@ True - False - 6 - 10 + False + 6 + 12 True - False + False 1 - Name: + _Name: + True + name-entry - 0 - 0 - 1 - 1 + 0 + 0 True - False + False 1 - Comment: + Co_mment: + True + comment-entry - 0 - 1 - 1 - 1 + 0 + 1 True - True - True + True + True True - - 1 - 0 - 1 - 1 + 1 + 0 True - True + True True - - 1 - 1 - 1 - 1 + 1 + 1 + + + + + _Hide (NoDisplay) + True + True + True + True + + + 1 + 2 True True - end 1 - - True - True - 1 - + + + + False + end + + + _Cancel + True + True + True + + + + + _OK + True + True + True + + + diff --git a/files/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui b/files/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui index de65e3ae90..0abeb49275 100644 --- a/files/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui +++ b/files/usr/share/cinnamon/cinnamon-desktop-editor/launcher-editor.ui @@ -1,87 +1,55 @@ - False - 4 + 12 Launcher Properties dialog + 500 False vertical - 4 - - + 12 + + False - end - - - gtk-cancel - True - True - True - True - - - False - True - 0 - - - - - gtk-ok - True - True - True - True + True + warning + + + False + 16 + + + True + False + Note: 'Restore' to system default is not available for this launcher. + True + 0 + + - - False - True - 1 - - - False - True - end - 0 - True False - 10 + 12 - + True - False - 1 - 0 - 0 - - - True - True - True - center - center - dialog - cinnamon-panel-launcher - - + True + True + end + start + dialog + cinnamon-panel-launcher - - False - True - 0 - @@ -89,12 +57,13 @@ True False 6 - 10 + 12 True - False - Name: + _Name: + True + name-entry 1 @@ -108,8 +77,9 @@ True - False - Command: + Comman_d: + True + exec-entry 1 @@ -123,8 +93,9 @@ True - False - Comment: + Co_mment: + True + comment-entry 1 @@ -141,7 +112,6 @@ True True True - 1 @@ -151,33 +121,20 @@ True - False 10 True True True - - - True - True - 0 - - Browse + _Browse + True True - True - True - - False - True - 1 - @@ -189,8 +146,6 @@ True True - True - 1 @@ -199,12 +154,9 @@ - Launch in Terminal? + Launch in _Terminal? + True True - True - False - start - True 1 @@ -213,12 +165,9 @@ - Use dedicated GPU if available + Use dedicated _GPU if available + True True - True - False - start - True 1 @@ -226,41 +175,92 @@ - - - - - - - - - - - - - - - - + + _Hide (NoDisplay) + True + True + + + 1 + 5 + + + + True + True + 1 + + + + + + + True + vertical + 5 + + + True + Menu Categorie_s: + True + category-flowbox + 0 + + + + + + True + True + in + 120 - + + True + False + True + 2 + 6 + none + False + True True - end - 1 True True - 1 + + + False + end + + + _Cancel + True + True + True + + + + + _OK + True + True + True + True + + + + diff --git a/files/usr/share/cinnamon/cinnamon-menu-editor/cinnamon-menu-editor.ui b/files/usr/share/cinnamon/cinnamon-menu-editor/cinnamon-menu-editor.ui index c1c5e09504..d4772e6641 100644 --- a/files/usr/share/cinnamon/cinnamon-menu-editor/cinnamon-menu-editor.ui +++ b/files/usr/share/cinnamon/cinnamon-menu-editor/cinnamon-menu-editor.ui @@ -1,98 +1,54 @@ - - False - Main Menu - 800 - 500 - normal + False + 6 + Cinnamon Menu Editor + 750 + 500 + normal - False - 6 + False + 6 + 6 vertical - 10 - - - False - True - - - Restore System Configuration - True - True - True - False - Restore the default menu layout - - - - True - True - 0 - True - - - - - Close - True - True - True - True - False - - - - True - True - 1 - True - - - - - False - False - 0 - - + 0 True - False - 6 - 6 + False + 6 + 6 8 - + True - True + True 225 - True - True + True + True True - False + False vertical True - True - in + True + in True - True - False + True + False @@ -111,23 +67,22 @@ - False + True True True - True - in + True + in True - True - True + True + True - @@ -150,41 +105,40 @@ - + True - False + False True vertical 4 - start _New Menu True - True - True - False - True + True + True + False + True - True + False True 0 - Ne_w Item + Ne_w Launcher True - True - True - False - True - + True + True + False + True + - True + False True 1 @@ -192,120 +146,132 @@ True - False - 6 - 6 + False + 6 + 6 - True + False True 2 - True - - Cut + + Delete Item + True + True + True + + + + False + False + 6 + + + + + Restore Item True - True - True - + True + True + Restore this item to the system default + False False - 3 + 7 - - Copy + True - True - True - + False + 6 + 6 False False - 4 + 7 - - Paste + + _Properties True - True - True - + True + True + True + False False - 5 + 9 - - Delete + + _Edit Desktop File True - True - True - + True + True + True + False False - 6 + 10 - + True - False - 6 - 6 + False + 6 + 6 - True + False True - 7 - True + 11 - - Properties + True - True - True - + False - False - False - 8 + True + True + 12 - - Edit Desktop File + + Restore All True - True - True - + True + True + Restore the default menu layout + False - False - 9 + True + 13 False True - end + end 8 @@ -318,23 +284,20 @@ - - restore_button - close_button - - - - + + + + diff --git a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MainWindow.py b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MainWindow.py index 1db6753d0d..1c7a8ea4c9 100644 --- a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MainWindow.py +++ b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MainWindow.py @@ -20,14 +20,15 @@ import gi gi.require_version('Gtk', '3.0') gi.require_version('CMenu', '3.0') -from gi.repository import Gtk, GObject, Gdk, CMenu, GLib +from gi.repository import GLib, Gtk, Gdk, CMenu import html import os +from pathlib import Path import gettext import subprocess from cme import config -gettext.bindtextdomain(config.GETTEXT_PACKAGE, config.localedir) +gettext.bindtextdomain(config.GETTEXT_PACKAGE, config.LOCALEDIR) gettext.textdomain(config.GETTEXT_PACKAGE) _ = gettext.gettext @@ -35,67 +36,73 @@ from cme import util class MainWindow(object): - timer = None - #hack to make editing menu properties work - edit_pool = [] - - def __init__(self, datadir, version): - self.file_path = datadir - self.version = version + def __init__(self): self.editor = MenuEditor() - self.editor.tree.connect("changed", self.menuChanged) - Gtk.Window.set_default_icon_name('alacarte') + self.editor.tree.connect("changed", self._menuChanged) + Gtk.Window.set_default_icon_name('menu-editor') self.tree = Gtk.Builder() self.tree.set_translation_domain(config.GETTEXT_PACKAGE) - self.tree.add_from_file('/usr/share/cinnamon/cinnamon-menu-editor/cinnamon-menu-editor.ui') + ui_path = os.path.join(config.PKGDATADIR, 'cinnamon-menu-editor.ui') + self.tree.add_from_file(ui_path) self.tree.connect_signals(self) - self.setupMenuTree() - self.setupItemTree() + self._setupMenuTree() + self._setupItemTree() self.popup_menu = Gtk.Menu() - self.cut_menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-cut") - self.cut_menu_item.connect("activate", self.on_edit_cut_activate) - self.popup_menu.append(self.cut_menu_item) - - self.copy_menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-copy") - self.copy_menu_item.connect("activate", self.on_edit_copy_activate) - self.popup_menu.append(self.copy_menu_item) - - self.paste_menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-paste") - self.paste_menu_item.connect("activate", self.on_edit_paste_activate) - self.popup_menu.append(self.paste_menu_item) - - self.delete_menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-delete") - self.delete_menu_item.connect("activate", self.on_edit_delete_activate) + self.delete_menu_item = self._create_popup_menu_item(_("Delete"), "edit-delete") + self.delete_menu_item.connect("activate", self._on_edit_delete_restore_activate) self.popup_menu.append(self.delete_menu_item) - - self.properties_menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-properties") - self.properties_menu_item.connect("activate", self.on_edit_properties_activate) + self.restore_menu_item = self._create_popup_menu_item(_("Restore"), "edit-undo") + self.restore_menu_item.connect("activate", self._on_edit_delete_restore_activate) + self.popup_menu.append(self.restore_menu_item) + self.properties_menu_item = self._create_popup_menu_item(_("Properties"), "document-properties") + self.properties_menu_item.connect("activate", self._on_edit_properties_activate) self.popup_menu.append(self.properties_menu_item) self.popup_menu.show_all() - self.cut_copy_buffer = None self.file_id = None self.last_tree = None self.main_window = self.tree.get_object('mainwindow') - self.tree.get_object("action_box").set_layout(Gtk.ButtonBoxStyle.EDGE) + self.paned = self.tree.get_object('main_paned') + self.main_window.connect("map", self._on_window_mapped) + + def _create_popup_menu_item(self, label, icon_name): + item = Gtk.ImageMenuItem(label=label) + image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU) + item.set_image(image) + return item def run(self): - self.loadMenus() + self._loadMenus() self.main_window.show_all() Gtk.main() - def menuChanged(self, *a): - self.loadUpdates() - - def loadUpdates(self): + def _on_window_mapped(self, widget): + allocation = self.paned.get_allocation() + self.paned.set_position(allocation.width // 2) + + def _show_message(self, title, message, buttons=Gtk.ButtonsType.OK, msg_type=Gtk.MessageType.INFO): + dialog = Gtk.MessageDialog( + transient_for=self.main_window, + flags=0, + message_type=msg_type, + buttons=buttons, + text=message + ) + dialog.set_title(title) + response = dialog.run() + dialog.destroy() + return response + + def _menuChanged(self, *a): + self._loadUpdates() + + def _loadUpdates(self): menu_tree = self.tree.get_object('menu_tree') item_tree = self.tree.get_object('item_tree') - item_tree_pos = item_tree.get_vadjustment().get_value() - items, iter = item_tree.get_selection().get_selected() update_items = False update_type = None @@ -106,24 +113,22 @@ def loadUpdates(self): item_id = items[iter][3].get_desktop_file_id() update_type = CMenu.TreeItemType.ENTRY elif isinstance(items[iter][3], CMenu.TreeDirectory): - item_id = os.path.split(items[iter][3].get_desktop_file_path())[1] + item_id = Path(items[iter][3].get_desktop_file_path()).name update_type = CMenu.TreeItemType.DIRECTORY - elif isinstance(items[iter][3], CMenu.TreeSeparator): - item_id = items.get_path(iter) - update_type = CMenu.TreeItemType.SEPARATOR + menus, iter = menu_tree.get_selection().get_selected() update_menus = False menu_id = None if iter: if menus[iter][3].get_desktop_file_path(): - menu_id = os.path.split(menus[iter][3].get_desktop_file_path())[1] + menu_id = Path(menus[iter][3].get_desktop_file_path()).name else: menu_id = menus[iter][3].get_menu_id() update_menus = True - self.loadMenus() + self._loadMenus() #find current menu in new tree if update_menus: - menu_tree.get_model().foreach(self.findMenu, menu_id) + menu_tree.get_model().foreach(self._findMenu, menu_id) menus, iter = menu_tree.get_selection().get_selected() if iter: self.on_menu_tree_cursor_changed(menu_tree) @@ -132,34 +137,19 @@ def loadUpdates(self): i = 0 for item in item_tree.get_model(): found = False - if update_type != CMenu.TreeItemType.SEPARATOR: - if isinstance (item[3], CMenu.TreeEntry) and item[3].get_desktop_file_id() == item_id: - found = True - if isinstance (item[3], CMenu.TreeDirectory) and item[3].get_desktop_file_path() and update_type == CMenu.TreeItemType.DIRECTORY: - if os.path.split(item[3].get_desktop_file_path())[1] == item_id: - found = True - if isinstance(item[3], CMenu.TreeSeparator): - if not isinstance(item_id, tuple): - #we may not skip the increment via "continue" - i += 1 - continue - #separators have no id, have to find them manually - #probably won't work with two separators together - if (item_id[0] - 1,) == (i,): - found = True - elif (item_id[0] + 1,) == (i,): - found = True - elif (item_id[0],) == (i,): + if isinstance (item[3], CMenu.TreeEntry) and item[3].get_desktop_file_id() == item_id: + found = True + if isinstance (item[3], CMenu.TreeDirectory) and item[3].get_desktop_file_path() and update_type == CMenu.TreeItemType.DIRECTORY: + if Path(item[3].get_desktop_file_path()).name == item_id: found = True if found: item_tree.get_selection().select_path((i,)) self.on_item_tree_cursor_changed(item_tree) - GLib.idle_add(lambda: item_tree.get_vadjustment().set_value(item_tree_pos)) break i += 1 return False - def findMenu(self, menus, path, iter, menu_id): + def _findMenu(self, menus, path, iter, menu_id): if not menus[path][3].get_desktop_file_path(): if menu_id == menus[path][3].get_menu_id(): menu_tree = self.tree.get_object('menu_tree') @@ -167,41 +157,34 @@ def findMenu(self, menus, path, iter, menu_id): menu_tree.get_selection().select_path(path) return True return False - if os.path.split(menus[path][3].get_desktop_file_path())[1] == menu_id: + if Path(menus[path][3].get_desktop_file_path()).name == menu_id: menu_tree = self.tree.get_object('menu_tree') menu_tree.expand_to_path(path) menu_tree.get_selection().select_path(path) return True - def setupMenuTree(self): + def _setupMenuTree(self): self.menu_store = Gtk.TreeStore(object, str, bool, object) # bool is unused, just a placeholder menus = self.tree.get_object('menu_tree') # so object is the same index for column = Gtk.TreeViewColumn(_("Name")) # the menu tree and item tree column.set_spacing(4) cell = Gtk.CellRendererPixbuf() column.pack_start(cell, False) - column.set_cell_data_func(cell, self.icon_data_func, 0) + column.set_cell_data_func(cell, self._icon_data_func, 0) cell = Gtk.CellRendererText() column.pack_start(cell, True) column.add_attribute(cell, 'markup', 1) menus.append_column(column) menus.get_selection().set_mode(Gtk.SelectionMode.BROWSE) - def setupItemTree(self): + def _setupItemTree(self): items = self.tree.get_object('item_tree') - column = Gtk.TreeViewColumn(_("Show")) - cell = Gtk.CellRendererToggle() - cell.connect('toggled', self.on_item_tree_show_toggled) - column.pack_start(cell, True) - column.add_attribute(cell, 'active', 0) - #hide toggle for separators - column.set_cell_data_func(cell, self._cell_data_toggle_func) - items.append_column(column) column = Gtk.TreeViewColumn(_("Item")) + column.set_expand(True) column.set_spacing(4) cell = Gtk.CellRendererPixbuf() column.pack_start(cell, False) - column.set_cell_data_func(cell, self.icon_data_func, 1) + column.set_cell_data_func(cell, self._icon_data_func, 1) cell = Gtk.CellRendererText() column.pack_start(cell, True) column.add_attribute(cell, 'markup', 2) @@ -209,20 +192,16 @@ def setupItemTree(self): self.item_store = Gtk.ListStore(bool, object, str, object) items.set_model(self.item_store) - def icon_data_func(self, column, cell, model, iter, data=None): + def _icon_data_func(self, column, cell, model, iter, data=None): wrapper = model.get_value(iter, data) if wrapper: cell.set_property("surface", wrapper.surface) - def _cell_data_toggle_func(self, tree_column, renderer, model, treeiter, data=None): - if isinstance(model[treeiter][3], CMenu.TreeSeparator): - renderer.set_property('visible', False) - else: - renderer.set_property('visible', True) - - def loadMenus(self): + def _loadMenus(self): self.menu_store.clear() - self.loadMenu({ None: None }) + root_menu = self.editor.tree.get_root_directory() + self._loadMenu({root_menu: None}, root_menu) + menu_tree = self.tree.get_object('menu_tree') menu_tree.set_model(self.menu_store) @@ -231,17 +210,17 @@ def loadMenus(self): menu_tree.get_selection().select_path((0,)) self.on_menu_tree_cursor_changed(menu_tree) - def loadMenu(self, iters, parent=None): + def _loadMenu(self, iters, parent=None): for menu, show in self.editor.getMenus(parent): name = html.escape(menu.get_name()) if not show: - name = "%s" % (name,) + name = "%s" % (name,) icon = util.getIcon(menu, self.main_window) iters[menu] = self.menu_store.append(iters[parent], (icon, name, False, menu)) - self.loadMenu(iters, menu) + self._loadMenu(iters, menu) - def loadItems(self, menu): + def _loadItems(self, menu): self.item_store.clear() for item, show in self.editor.getItems(menu): icon = util.getIcon(item, self.main_window) @@ -249,136 +228,124 @@ def loadItems(self, menu): name = item.get_name() elif isinstance(item, CMenu.TreeEntry): name = item.get_app_info().get_display_name() - elif isinstance(item, CMenu.TreeSeparator): - name = '---' else: assert False, 'should not be reached' name = html.escape(name) if not show: - name = "%s" % (name,) + name = "%s" % (name,) self.item_store.append((show, icon, name, item)) - #this is a little timeout callback to insert new items after - #gnome-desktop-item-edit has finished running - def waitForNewItemProcess(self, process, parent_id, file_path): - if process.poll() is not None: - if os.path.isfile(file_path): - self.editor.insertExternalItem(os.path.split(file_path)[1], parent_id) - return False - return True - - def waitForNewMenuProcess(self, process, parent_id, file_path): - if process.poll() is not None: - if os.path.isfile(file_path): - self.editor.insertExternalMenu(os.path.split(file_path)[1], parent_id) - return False - return True + def _on_new_menu_editor_exited(self, pid, status, file_path): + GLib.spawn_close_pid(pid) + if Path(file_path).is_file(): + self.editor.insertExternalMenu(Path(file_path).name) - #this callback keeps you from editing the same item twice - def waitForEditProcess(self, process, file_path): - if process.poll() is not None: - self.edit_pool.remove(file_path) - return False - return True + def _on_launcher_editor_exited(self, pid, status): + GLib.spawn_close_pid(pid) def on_new_menu_button_clicked(self, button): - menu_tree = self.tree.get_object('menu_tree') - menus, iter = menu_tree.get_selection().get_selected() - if not iter: - parent = menus[(0,)][3] - menu_tree.expand_to_path((0,)) - menu_tree.get_selection().select_path((0,)) - else: - parent = menus[iter][3] - file_path = os.path.join(util.getUserDirectoryPath(), util.getUniqueFileId('alacarte-made', '.directory')) - process = subprocess.Popen(['cinnamon-desktop-editor', '-mdirectory', '-o' + file_path], env=os.environ) - GObject.timeout_add(100, self.waitForNewMenuProcess, process, parent.get_menu_id(), file_path) + file_path = Path(util.getUserDirectoryDir()) / util.getUniqueFileId('alacarte', '.directory') + process = subprocess.Popen(['cinnamon-desktop-editor', '-mdirectory', '-o' + str(file_path)], env=os.environ) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, process.pid, self._on_new_menu_editor_exited, file_path) - def on_new_item_button_clicked(self, button): - menu_tree = self.tree.get_object('menu_tree') - menus, iter = menu_tree.get_selection().get_selected() - if not iter: - parent = menus[(0,)][3] - menu_tree.expand_to_path((0,)) - menu_tree.get_selection().select_path((0,)) - else: - parent = menus[iter][3] - file_path = os.path.join(util.getUserItemPath(), util.getUniqueFileId('alacarte-made', '.desktop')) - process = subprocess.Popen(['cinnamon-desktop-editor', '-mlauncher', '-o' + file_path], env=os.environ) - GObject.timeout_add(100, self.waitForNewItemProcess, process, parent.get_menu_id(), file_path) + def on_new_launcher_button_clicked(self, button): + file_path = Path(util.getUserItemDir()) / util.getUniqueFileId('alacarte', '.desktop') + process = subprocess.Popen(['cinnamon-desktop-editor', '-mlauncher', '-o' + str(file_path), "--show-categories"], env=os.environ) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, process.pid, self._on_launcher_editor_exited) - def on_edit_delete_activate(self, menu): + def _on_edit_delete_restore_activate(self, menu): item_tree = self.tree.get_object('item_tree') items, iter = item_tree.get_selection().get_selected() + if not iter: - return - item = items[iter][3] - if isinstance(item, CMenu.TreeEntry): - self.editor.deleteItem(item) - elif isinstance(item, CMenu.TreeDirectory): - self.editor.deleteMenu(item) - elif isinstance(item, CMenu.TreeSeparator): - self.editor.deleteSeparator(item) + menu_tree = self.tree.get_object('menu_tree') + items, iter = menu_tree.get_selection().get_selected() - def on_edit_properties_activate(self, menu): - item_tree = self.tree.get_object(self.last_tree) - items, iter = item_tree.get_selection().get_selected() if not iter: return item = items[iter][3] - if not isinstance(item, CMenu.TreeEntry) and not isinstance(item, CMenu.TreeDirectory): - return if isinstance(item, CMenu.TreeEntry): - file_type = 'launcher' - elif isinstance(item, CMenu.TreeDirectory): - file_type = 'directory' - - file_path = item.get_desktop_file_path() + file_id = item.get_desktop_file_id() + match self.editor.getIsItemUserOrSystem(item): + case "user only": + res = self._show_message( + _("Delete Menu Entry"), + _("This will delete '%s' launcher from all menu categories and delete its associated .desktop file. Are you sure?") % item.get_app_info().get_name(), + Gtk.ButtonsType.YES_NO, + Gtk.MessageType.WARNING + ) + if res == Gtk.ResponseType.YES: + self.editor.deleteUserDesktopFile(file_id) + case "both": + res = self._show_message( + _("Restore Entry"), + _("Restore entry to system default?"), + Gtk.ButtonsType.OK_CANCEL, + Gtk.MessageType.QUESTION + ) + if res == Gtk.ResponseType.OK: + self.editor.deleteUserDesktopFile(file_id) + case "system only": + self._show_message( + _("Cannot Delete"), + _("This is a system entry and cannot be deleted."), + Gtk.ButtonsType.OK, + Gtk.MessageType.ERROR + ) - if file_path not in self.edit_pool: - self.edit_pool.append(file_path) - process = subprocess.Popen(['cinnamon-desktop-editor', '-m' + file_type, '-o' + file_path], env=os.environ) - GObject.timeout_add(100, self.waitForEditProcess, process, file_path) - - def on_edit_cut_activate(self, menu): - item_tree = self.tree.get_object('item_tree') + elif isinstance(item, CMenu.TreeDirectory): + match self.editor.getIsItemUserOrSystem(item): + case "user only": + res = self._show_message( + _("Delete Menu Category?"), + _("Delete the menu '%s' and its associated .directory file? Items inside the menu will not be deleted.") % item.get_name(), + Gtk.ButtonsType.YES_NO, + Gtk.MessageType.WARNING + ) + if res == Gtk.ResponseType.YES: + self.editor.removeCustomMenu(item) + case "both": + res = self._show_message( + _("Reset Menu Category"), + _("Reset menu '%s' to system default?") % item.get_name(), + Gtk.ButtonsType.OK_CANCEL, + Gtk.MessageType.QUESTION + ) + if res == Gtk.ResponseType.OK: + self.editor.removeCustomMenu(item) + case "system only": + self._show_message( + _("Cannot Delete"), + _("This is a system menu and cannot be deleted."), + Gtk.ButtonsType.OK, + Gtk.MessageType.ERROR + ) + case "neither": + res = self._show_message( + _("Delete Menu Category?"), + _("There is no .directory file associated with '%s'? Remove from the menu anyway?") % item.get_name(), + Gtk.ButtonsType.YES_NO, + Gtk.MessageType.WARNING + ) + if res == Gtk.ResponseType.YES: + self.editor.removeCustomMenu(item) + + def _on_edit_properties_activate(self, menu): + item_tree = self.tree.get_object(self.last_tree) items, iter = item_tree.get_selection().get_selected() - item = items[iter][3] - if not iter: - return - if not isinstance(item, CMenu.TreeEntry): - return - (self.cut_copy_buffer, self.file_id) = self.editor.cutItem(item) + if not iter: return - def on_edit_copy_activate(self, menu): - item_tree = self.tree.get_object('item_tree') - items, iter = item_tree.get_selection().get_selected() item = items[iter][3] - if not iter: - return - if not isinstance(item, CMenu.TreeEntry): - return - (self.cut_copy_buffer, self.file_id) = self.editor.copyItem(item) + if not isinstance(item, (CMenu.TreeEntry, CMenu.TreeDirectory)): return - def on_edit_paste_activate(self, menu): - item_tree = self.tree.get_object('item_tree') - items, iter = item_tree.get_selection().get_selected() - if not iter: - menu_tree = self.tree.get_object('menu_tree') - items, iter = menu_tree.get_selection().get_selected() - if not iter: - return - item = items[iter][3] - if not isinstance(item, CMenu.TreeDirectory): - return - if self.cut_copy_buffer is not None: - success = self.editor.pasteItem(self.cut_copy_buffer, item, self.file_id) - if success: - self.cut_copy_buffer = None - self.file_id = None + file_type = 'launcher' if isinstance(item, CMenu.TreeEntry) else 'directory' + file_path = item.get_desktop_file_path() + + process = subprocess.Popen(['cinnamon-desktop-editor', '-m' + file_type, '-o' + file_path, "--show-categories"], env=os.environ) + GLib.child_watch_add(GLib.PRIORITY_DEFAULT, process.pid, self._on_launcher_editor_exited) def on_menu_tree_cursor_changed(self, treeview): selection = treeview.get_selection() @@ -393,31 +360,14 @@ def on_menu_tree_cursor_changed(self, treeview): item_tree = self.tree.get_object('item_tree') item_tree.get_selection().unselect_all() - self.loadItems(self.menu_store[menu_path][3]) - self.set_cut_sensitive(False) - self.set_copy_sensitive(False) - self.set_delete_sensitive(False) - self.set_properties_sensitive(True) - - can_paste = isinstance(menu, CMenu.TreeDirectory) and self.cut_copy_buffer is not None - self.set_paste_sensitive(can_paste) - - index = menus.get_path(iter).get_indices()[menus.get_path(iter).get_depth() - 1] - parent_iter = menus.iter_parent(iter) - count = menus.iter_n_children(parent_iter) - can_go_up = index > 0 and isinstance(menu, CMenu.TreeDirectory) - can_go_down = index < count - 1 and isinstance(menu, CMenu.TreeDirectory) - self.last_tree = "menu_tree" + self._loadItems(self.menu_store[menu_path][3]) - def on_item_tree_show_toggled(self, cell, path): - item = self.item_store[path][3] - if isinstance(item, CMenu.TreeSeparator): - return - if self.item_store[path][0]: - self.editor.setVisible(item, False) - else: - self.editor.setVisible(item, True) - self.item_store[path][0] = not self.item_store[path][0] + item_status = self.editor.getIsItemUserOrSystem(menu) + self._set_delete_sensitive(item_status in ("user only", "neither")) + self._set_restore_sensitive(item_status == "both") + self._set_properties_sensitive(True) + + self.last_tree = "menu_tree" def on_item_tree_cursor_changed(self, treeview): selection = treeview.get_selection() @@ -428,43 +378,24 @@ def on_item_tree_cursor_changed(self, treeview): return item = items[iter][3] - self.set_delete_sensitive(True) - - can_edit = not isinstance(item, CMenu.TreeSeparator) - self.set_properties_sensitive(can_edit) - - can_cut_copy = not isinstance(item, CMenu.TreeDirectory) - self.set_cut_sensitive(can_cut_copy) - self.set_copy_sensitive(can_cut_copy) - - can_paste = isinstance(item, CMenu.TreeDirectory) and self.cut_copy_buffer is not None - self.set_paste_sensitive(can_paste) - - index = items.get_path(iter).get_indices()[0] - can_go_up = index > 0 and isinstance(item, CMenu.TreeDirectory) - can_go_down = index < len(items) - 1 and isinstance(item, CMenu.TreeDirectory) + item_status = self.editor.getIsItemUserOrSystem(item) + self._set_delete_sensitive(item_status == "user only") + self._set_restore_sensitive(item_status == "both") + self._set_properties_sensitive(True) self.last_tree = "item_tree" def on_item_tree_row_activated(self, treeview, path, column): - self.on_edit_properties_activate(None) - - def set_cut_sensitive(self, sensitive): - self.tree.get_object("cut_button").set_sensitive(sensitive) - self.cut_menu_item.set_sensitive(sensitive) - - def set_copy_sensitive(self, sensitive): - self.tree.get_object("copy_button").set_sensitive(sensitive) - self.copy_menu_item.set_sensitive(sensitive) + self._on_edit_properties_activate(None) - def set_paste_sensitive(self, sensitive): - self.tree.get_object("paste_button").set_sensitive(sensitive) - self.paste_menu_item.set_sensitive(sensitive) - - def set_delete_sensitive(self, sensitive): + def _set_delete_sensitive(self, sensitive): self.tree.get_object("delete_button").set_sensitive(sensitive) self.delete_menu_item.set_sensitive(sensitive) - def set_properties_sensitive(self, sensitive): + def _set_restore_sensitive(self, sensitive): + self.tree.get_object("restore_item_button").set_sensitive(sensitive) + self.restore_menu_item.set_sensitive(sensitive) + + def _set_properties_sensitive(self, sensitive): self.tree.get_object("properties_button").set_sensitive(sensitive) self.properties_menu_item.set_sensitive(sensitive) @@ -516,28 +447,16 @@ def on_menu_tree_popup_menu(self, menu_tree, event=None): def on_item_tree_key_press_event(self, item_tree, event): if event.keyval == Gdk.KEY_Delete: - self.on_edit_delete_activate(item_tree) - - def on_restore_button_clicked(self, button): - self.editor.restoreToSystem() - - def on_close_button_clicked(self, button): - self.quit() - - def on_properties_button_clicked(self, button): - self.on_edit_properties_activate(None) + self._on_edit_delete_restore_activate(None) def on_delete_button_clicked(self, button): - self.on_edit_delete_activate(None) - - def on_cut_button_clicked(self, button): - self.on_edit_cut_activate(None) + self._on_edit_delete_restore_activate(None) - def on_copy_button_clicked(self, button): - self.on_edit_copy_activate(None) + def on_restore_item_button_clicked(self, button): + self._on_edit_delete_restore_activate(None) - def on_paste_button_clicked(self, button): - self.on_edit_paste_activate(None) + def on_properties_button_clicked(self, button): + self._on_edit_properties_activate(None) def on_open_desktop_file_button_clicked(self, button): item_tree = self.tree.get_object(self.last_tree) @@ -548,14 +467,21 @@ def on_open_desktop_file_button_clicked(self, button): if not isinstance(item, CMenu.TreeEntry) and not isinstance(item, CMenu.TreeDirectory): return - if isinstance(item, CMenu.TreeEntry): - file_type = 'launcher' - elif isinstance(item, CMenu.TreeDirectory): - file_type = 'directory' - file_path = item.get_desktop_file_path() - subprocess.run(["xdg-open", file_path]) + def on_restore_all_button_clicked(self, button): + res = self._show_message( + _("Restore System Configuration"), + _("Restore all modified menus and launchers to system defaults? User created menus and launchers will not be deleted."), + Gtk.ButtonsType.OK_CANCEL, + Gtk.MessageType.QUESTION + ) + if res == Gtk.ResponseType.OK: + self.editor.restoreToSystem() + + def on_close_button_clicked(self, button): + self.quit() + def quit(self): Gtk.main_quit() diff --git a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MenuEditor.py b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MenuEditor.py index d717a4d715..3b7ceb4067 100644 --- a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MenuEditor.py +++ b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/MenuEditor.py @@ -17,86 +17,99 @@ # Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA import os -import xml.dom.minidom -import xml.parsers.expat +import tempfile +from pathlib import Path +import xml.etree.ElementTree as ET from gi.repository import CMenu, GLib from cme import util class MenuEditor(object): def __init__(self, name='cinnamon-applications.menu'): self.name = name - - self.tree = CMenu.Tree.new(name, CMenu.TreeFlags.SHOW_EMPTY|CMenu.TreeFlags.INCLUDE_EXCLUDED|CMenu.TreeFlags.INCLUDE_NODISPLAY|CMenu.TreeFlags.SHOW_ALL_SEPARATORS|CMenu.TreeFlags.SORT_DISPLAY_NAME) - self.tree.connect('changed', self.menuChanged) - self.load() + self.tree = CMenu.Tree.new(name, CMenu.TreeFlags.SHOW_EMPTY|CMenu.TreeFlags.INCLUDE_EXCLUDED|CMenu.TreeFlags.INCLUDE_NODISPLAY|CMenu.TreeFlags.SORT_DISPLAY_NAME) + self.tree.connect('changed', self._menuChanged) + self._load() self.path = os.path.join(util.getUserMenuPath(), self.tree.props.menu_basename) - self.loadDOM() + self._loadDOM() - def loadDOM(self): + def _loadDOM(self): try: - self.dom = xml.dom.minidom.parse(self.path) - except (IOError, xml.parsers.expat.ExpatError) as e: - self.dom = xml.dom.minidom.parseString(util.getUserMenuXml(self.tree)) - util.removeWhitespaceNodes(self.dom) - - def load(self): + self.dom_tree = ET.parse(self.path) + self.root = self.dom_tree.getroot() + except (FileNotFoundError, ET.ParseError): + # If file doesn't exist or is corrupt, create from string + xml_string = util.getUserMenuXml(self.tree) + self.root = ET.fromstring(xml_string) + self.dom_tree = ET.ElementTree(self.root) + + def _load(self): if not self.tree.load_sync(): raise ValueError("can not load menu tree %r" % (self.name,)) - def menuChanged(self, *a): - self.load() + def _menuChanged(self, *a): + self._load() - def save(self): - fd = open(self.path, 'w') - fd.write(self.dom.toprettyxml()) - fd.close() + def _save(self): + xml_header = b"\n" + dtd_header = b"\n" + + dir_name = os.path.dirname(self.path) + temp_name = None - def restoreToSystem(self): - self.restoreTree(self.tree.get_root_directory()) - path = os.path.join(util.getUserMenuPath(), os.path.basename(self.tree.get_canonical_menu_path())) try: - os.unlink(path) - except OSError: - pass + ET.indent(self.dom_tree, space=" ", level=0) + + with tempfile.NamedTemporaryFile('wb', dir=dir_name, delete=False) as tf: + temp_name = tf.name + tf.write(xml_header) + tf.write(dtd_header) + self.dom_tree.write(tf, encoding="utf-8", xml_declaration=False) + + os.replace(temp_name, self.path) + except Exception as e: + if temp_name and os.path.exists(temp_name): + os.unlink(temp_name) - self.loadDOM() + print(f"Error saving menu ({type(e).__name__}): {e}") + raise - def restoreTree(self, menu): + def restoreToSystem(self): + self._restoreTree(self.tree.get_root_directory()) + self._loadDOM() + + def _restoreTree(self, menu): item_iter = menu.iter() item_type = item_iter.next() while item_type != CMenu.TreeItemType.INVALID: if item_type == CMenu.TreeItemType.DIRECTORY: item = item_iter.get_directory() - self.restoreTree(item) + self._restoreTree(item) elif item_type == CMenu.TreeItemType.ENTRY: item = item_iter.get_entry() - self.restoreItem(item) + self._restoreItem(item) item_type = item_iter.next() - self.restoreMenu(menu) + self._restoreMenu(menu) - def restoreItem(self, item): - if not self.canRevert(item): + def _restoreItem(self, item): + if self.getIsItemUserOrSystem(item) != "both": return try: - os.remove(item.get_desktop_file_path()) + Path(item.get_desktop_file_path()).unlink() except OSError: pass - self.save() - def restoreMenu(self, menu): - if not self.canRevert(menu): + def _restoreMenu(self, menu): + if self.getIsItemUserOrSystem(menu) != "both": return - #wtf happened here? oh well, just bail if not menu.get_desktop_file_path(): return - file_id = os.path.split(menu.get_desktop_file_path())[1] - path = os.path.join(util.getUserDirectoryPath(), file_id) + file_id = Path(menu.get_desktop_file_path()).name + path = Path(util.getUserDirectoryDir()) / file_id try: - os.remove(path) + path.unlink() except OSError: pass - self.save() def getMenus(self, parent): if parent is None: @@ -115,28 +128,6 @@ def getMenus(self, parent): for item in items: yield item, self.isVisible(item) - def getContents(self, item): - contents = [] - item_iter = item.iter() - item_type = item_iter.next() - - while item_type != CMenu.TreeItemType.INVALID: - item = None - if item_type == CMenu.TreeItemType.DIRECTORY: - item = item_iter.get_directory() - elif item_type == CMenu.TreeItemType.ENTRY: - item = item_iter.get_entry() - elif item_type == CMenu.TreeItemType.HEADER: - item = item_iter.get_header() - elif item_type == CMenu.TreeItemType.ALIAS: - item = item_iter.get_alias() - elif item_type == CMenu.TreeItemType.SEPARATOR: - item = item_iter.get_separator() - if item: - contents.append(item) - item_type = item_iter.next() - return contents - def getItems(self, menu): item_iter = menu.iter() item_type = item_iter.next() @@ -150,175 +141,72 @@ def getItems(self, menu): item = item_iter.get_header() elif item_type == CMenu.TreeItemType.ALIAS: item = item_iter.get_alias() - elif item_type == CMenu.TreeItemType.SEPARATOR: - item = item_iter.get_separator() yield item, self.isVisible(item) item_type = item_iter.next() - def canRevert(self, item): + def getIsItemUserOrSystem(self, item): if isinstance(item, CMenu.TreeEntry): - if util.getItemPath(item.get_desktop_file_id()) is not None: - path = util.getUserItemPath() - if os.path.isfile(os.path.join(path, item.get_desktop_file_id())): - return True + file_id = item.get_desktop_file_id().removesuffix(":flatpak") + user_path = Path(util.getUserItemDir()) / file_id + user_exists = user_path.is_file() + system_exists = util.getSystemItemFilepath(file_id) is not None elif isinstance(item, CMenu.TreeDirectory): if item.get_desktop_file_path(): - file_id = os.path.split(item.get_desktop_file_path())[1] + file_id = Path(item.get_desktop_file_path()).name else: file_id = item.get_menu_id() + '.directory' - if util.getDirectoryPath(file_id) is not None: - path = util.getUserDirectoryPath() - if os.path.isfile(os.path.join(path, file_id)): - return True - return False + system_exists = util.getSystemDirectoryFilepath(file_id) is not None + user_path = Path(util.getUserDirectoryDir()) / file_id + user_exists = user_path.is_file() + else: + return "n/a" # This shouldn't normally happen. + + if user_exists and not system_exists: + return "user only" + elif not user_exists and system_exists: + return "system only" + elif user_exists and system_exists: + return "both" + else: + return "neither" - def setVisible(self, item, visible): - dom = self.dom - if isinstance(item, CMenu.TreeEntry): - menu_xml = self.getXmlMenu(self.getPath(item.get_parent()), dom.documentElement, dom) - if visible: - self.addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Include') - self.writeItem(item, NoDisplay=False) - else: - self.addXmlFilename(menu_xml, dom, item.get_desktop_file_id(), 'Exclude') - self.addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom) - elif isinstance(item, CMenu.TreeDirectory): - item_iter = item.iter() - first_child_type = item_iter.next() - #don't mess with it if it's empty - if first_child_type == CMenu.TreeItemType.INVALID: - return - menu_xml = self.getXmlMenu(self.getPath(item), dom.documentElement, dom) - for node in self.getXmlNodesByName(['Deleted', 'NotDeleted'], menu_xml): - node.parentNode.removeChild(node) - self.writeMenu(item, NoDisplay=not visible) - self.addXmlTextElement(menu_xml, 'DirectoryDir', util.getUserDirectoryPath(), dom) - self.save() - - def createItem(self, parent, before, after, **kwargs): - file_id = self.writeItem(None, **kwargs) - self.insertExternalItem(file_id, parent.get_menu_id(), before, after) - - def insertExternalItem(self, file_id, parent_id, before=None, after=None): - parent = self.findMenu(parent_id) - dom = self.dom - self.addItem(parent, file_id, dom) - self.positionItem(parent, ('Item', file_id), before, after) - self.save() - - def insertExternalMenu(self, file_id, parent_id, before=None, after=None): + def insertExternalMenu(self, file_id): menu_id = file_id.rsplit('.', 1)[0] - parent = self.findMenu(parent_id) - dom = self.dom - self.addXmlDefaultLayout(self.getXmlMenu(self.getPath(parent), dom.documentElement, dom) , dom) - menu_xml = self.getXmlMenu(self.getPath(parent) + [menu_id], dom.documentElement, dom) - self.addXmlTextElement(menu_xml, 'Directory', file_id, dom) - self.positionItem(parent, ('Menu', menu_id), before, after) - self.save() - - def editItem(self, item, icon, name, comment, command, use_term, parent=None, final=True): - #if nothing changed don't make a user copy - app_info = item.get_app_info() - if icon == app_info.get_icon() and name == app_info.get_display_name() and comment == item.get_comment() and command == item.get_exec() and use_term == item.get_launch_in_terminal(): - return - #hack, item.get_parent() seems to fail a lot - if not parent: - parent = item.get_parent() - self.writeItem(item, Icon=icon, Name=name, Comment=comment, Exec=command, Terminal=use_term) - if final: - dom = self.dom - menu_xml = self.getXmlMenu(self.getPath(parent), dom.documentElement, dom) - self.addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), dom) - self.save() - - def editMenu(self, menu, icon, name, comment, final=True): - #if nothing changed don't make a user copy - if icon == menu.get_icon() and name == menu.get_name() and comment == menu.get_comment(): - return - #we don't use this, we just need to make sure the exists - #otherwise changes won't show up - dom = self.dom - menu_xml = self.getXmlMenu(self.getPath(menu), dom.documentElement, dom) - self.writeMenu(menu, Icon=icon, Name=name, Comment=comment) - if final: - self.addXmlTextElement(menu_xml, 'DirectoryDir', util.getUserDirectoryPath(), dom) - self.save() - - def copyItem(self, item): - dom = self.dom - file_path = item.get_desktop_file_path() - copy_buffer = GLib.KeyFile() - copy_buffer.load_from_file(file_path, util.KEY_FILE_FLAGS) - return copy_buffer, None - - def cutItem(self, item): - copy_buffer, file_id = self.copyItem(item) - file_id = self.deleteItem(item) - return copy_buffer, file_id - - def pasteItem(self, cut_copy_buffer, menu, file_id = None): - try: - path = self.getPath(menu) - util.fillKeyFile(cut_copy_buffer, dict(Hidden=False, NoDisplay=False)) - name = util.getNameFromKeyFile(cut_copy_buffer) - if file_id is None: - file_id = util.getUniqueFileId(name.replace(os.sep, '-'), '.desktop') - out_path = os.path.join(util.getUserItemPath(), file_id) - contents, length = cut_copy_buffer.to_data() - f = open(out_path, 'w') - f.write(contents) - f.close() - menu_xml = self.getXmlMenu(path, self.dom.documentElement, self.dom) - self.addXmlFilename(menu_xml, self.dom, file_id, 'Include') - self.addXmlTextElement(menu_xml, 'AppDir', util.getUserItemPath(), self.dom) - self.save() - return True - except: - return False - - def deleteItem(self, item): - file_id = self.writeItem(item, Hidden=True) - item_xml = self.getXmlMenu(self.getPath(item.get_parent()), self.dom.documentElement, self.dom) - - self.removeXmlFilename(item_xml, self.dom, file_id) - - self.save() - return file_id - - def deleteMenu(self, menu): - dom = self.dom - menu_xml = self.getXmlMenu(self.getPath(menu), dom.documentElement, dom) - self.addDeleted(menu_xml, dom) - self.save() - - def deleteSeparator(self, item): - parent = item.get_parent() - contents = self.getContents(parent) - contents.remove(item) - layout = self.createLayout(contents) - dom = self.dom - menu_xml = self.getXmlMenu(self.getPath(parent), dom.documentElement, dom) - self.addXmlLayout(menu_xml, layout, dom) - self.save() - - def findMenu(self, menu_id, parent=None): - if parent is None: - parent = self.tree.get_root_directory() + self._addXmlDefaultLayout(self.root) - if menu_id == parent.get_menu_id(): - return parent + menu_xml = self._getXmlMenu(menu_id, self.root) + self._addXmlTextElement(menu_xml, 'Directory', file_id) + self._addXmlCategory(menu_xml, menu_id) + self._save() - item_iter = parent.iter() - item_type = item_iter.next() - while item_type != CMenu.TreeItemType.INVALID: - if item_type == CMenu.TreeItemType.DIRECTORY: - item = item_iter.get_directory() - if item.get_menu_id() == menu_id: - return item - menu = self.findMenu(menu_id, item) - if menu is not None: - return menu - item_type = item_iter.next() + def removeCustomMenu(self, menu): + file_path = menu.get_desktop_file_path() + if file_path and file_path.startswith(util.getUserDirectoryDir()): + try: + Path(file_path).unlink() + except OSError: + pass + + menu_id = menu.get_menu_id() + # Find the specific Menu element by its Name child + for node in self.root.findall('Menu'): + name_node = node.find('Name') + if name_node is not None and name_node.text == menu_id: + self.root.remove(node) + break + + self._save() + + def deleteUserDesktopFile(self, file_id): + file_id = file_id.removesuffix(":flatpak") + user_path = Path(util.getUserItemDir()) / file_id + if user_path.is_file(): + try: + user_path.unlink() + return True + except OSError: + return False + return False def isVisible(self, item): if isinstance(item, CMenu.TreeEntry): @@ -328,285 +216,49 @@ def isVisible(self, item): return not item.get_is_nodisplay() return True - def getPath(self, menu): - names = [] - current = menu - while current is not None: - try: - names.append(current.get_menu_id()) - except: - names.append(current.get_desktop_file_id()) - current = current.get_parent() - - # XXX - don't append root menu name, alacarte doesn't - # expect it. look into this more. - names.pop(-1) - return names[::-1] - - def getXmlMenuPart(self, element, name): - for node in self.getXmlNodesByName('Menu', element): - for child in self.getXmlNodesByName('Name', node): - if child.childNodes[0].nodeValue == name: - return node + # Logic for finding/creating XML elements + def _getXmlMenuPart(self, element, name): + for node in element.findall('Menu'): + name_node = node.find('Name') + if name_node is not None and name_node.text == name: + return node return None - def getXmlMenu(self, path, element, dom): - for name in path: - found = self.getXmlMenuPart(element, name) - if found is not None: - element = found - else: - element = self.addXmlMenuElement(element, name, dom) - return element - - def addXmlMenuElement(self, element, name, dom): - node = dom.createElement('Menu') - self.addXmlTextElement(node, 'Name', name, dom) - return element.appendChild(node) - - def addXmlTextElement(self, element, name, text, dom): - for temp in element.childNodes: - if temp.nodeName == name: - if temp.childNodes[0].nodeValue == text: - return - node = dom.createElement(name) - text = dom.createTextNode(text) - node.appendChild(text) - return element.appendChild(node) - - def addXmlFilename(self, element, dom, filename, type = 'Include'): - # remove old filenames - for node in self.getXmlNodesByName(['Include', 'Exclude'], element): - if node.childNodes[0].nodeName == 'Filename' and node.childNodes[0].childNodes[0].nodeValue == filename: - element.removeChild(node) - - # add new filename - node = dom.createElement(type) - node.appendChild(self.addXmlTextElement(node, 'Filename', filename, dom)) - return element.appendChild(node) - - def removeXmlFilename(self, element, dom, filename): - for node in self.getXmlNodesByName(['Include'], element): - if node.childNodes[0].nodeName == 'Filename' and node.childNodes[0].childNodes[0].nodeValue == filename: - element.removeChild(node) - - def addDeleted(self, element, dom): - node = dom.createElement('Deleted') - return element.appendChild(node) - - def makeKeyFile(self, file_path, kwargs): - if 'KeyFile' in kwargs: - return kwargs['KeyFile'] - - keyfile = GLib.KeyFile() - - if file_path is not None: - keyfile.load_from_file(file_path, util.KEY_FILE_FLAGS) - - util.fillKeyFile(keyfile, kwargs) - return keyfile - - def writeItem(self, item, **kwargs): - if item is not None: - file_path = item.get_desktop_file_path() - else: - file_path = None - - keyfile = self.makeKeyFile(file_path, kwargs) - - if item is not None: - file_id = item.get_desktop_file_id() + def _getXmlMenu(self, name, element): + found = self._getXmlMenuPart(element, name) + if found is not None: + element = found else: - file_id = util.getUniqueFileId(keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, 'Name'), '.desktop') - - contents, length = keyfile.to_data() - - f = open(os.path.join(util.getUserItemPath(), file_id), 'w') - f.write(contents) - f.close() - return file_id - - def writeMenu(self, menu, **kwargs): - if menu is not None: - file_id = os.path.split(menu.get_desktop_file_path())[1] - file_path = menu.get_desktop_file_path() - keyfile = GLib.KeyFile() - keyfile.load_from_file(file_path, util.KEY_FILE_FLAGS) - elif menu is None and 'Name' not in kwargs: - raise Exception('New menus need a name') - else: - file_id = util.getUniqueFileId(kwargs['Name'], '.directory') - keyfile = GLib.KeyFile() - - util.fillKeyFile(keyfile, kwargs) - - contents, length = keyfile.to_data() - - f = open(os.path.join(util.getUserDirectoryPath(), file_id), 'w') - f.write(contents) - f.close() - return file_id - - def getXmlNodesByName(self, name, element): - for child in element.childNodes: - if child.nodeType == xml.dom.Node.ELEMENT_NODE: - if isinstance(name, str) and child.nodeName == name: - yield child - elif isinstance(name, list) or isinstance(name, tuple): - if child.nodeName in name: - yield child - - def addXmlMove(self, element, old, new, dom): - if not self.undoMoves(element, old, new, dom): - node = dom.createElement('Move') - node.appendChild(self.addXmlTextElement(node, 'Old', old, dom)) - node.appendChild(self.addXmlTextElement(node, 'New', new, dom)) - #are parsed in reverse order, need to put at the beginning - return element.insertBefore(node, element.firstChild) - - def addXmlLayout(self, element, layout, dom): - # remove old layout - for node in self.getXmlNodesByName('Layout', element): - element.removeChild(node) + element = self._addXmlMenuElement(element, name) + return element - # add new layout - node = dom.createElement('Layout') - for order in layout: - if order[0] == 'Separator': - child = dom.createElement('Separator') - node.appendChild(child) - elif order[0] == 'Filename': - child = self.addXmlTextElement(node, 'Filename', order[1], dom) - elif order[0] == 'Menuname': - child = self.addXmlTextElement(node, 'Menuname', order[1], dom) - elif order[0] == 'Merge': - child = dom.createElement('Merge') - child.setAttribute('type', order[1]) - node.appendChild(child) - return element.appendChild(node) - - def addXmlDefaultLayout(self, element, dom): + def _addXmlMenuElement(self, element, name): + node = ET.SubElement(element, 'Menu') + self._addXmlTextElement(node, 'Name', name) + return node + + def _addXmlTextElement(self, element, name, text): + # Check if it already exists with this text + for child in element.findall(name): + if child.text == text: + return child + node = ET.SubElement(element, name) + node.text = text + return node + + def _addXmlCategory(self, element, category_id): + include = ET.SubElement(element, 'Include') + cat = ET.SubElement(include, 'Category') + cat.text = category_id + return include + + def _addXmlDefaultLayout(self, element): # remove old default layout - for node in self.getXmlNodesByName('DefaultLayout', element): - element.removeChild(node) + for node in element.findall('DefaultLayout'): + element.remove(node) # add new layout - node = dom.createElement('DefaultLayout') - node.setAttribute('inline', 'false') - return element.appendChild(node) + node = ET.SubElement(element, 'DefaultLayout') + node.set('inline', 'false') + return node - def createLayout(self, items): - layout = [('Merge', 'menus')] - for item in items: - if isinstance(item, CMenu.TreeDirectory): - layout.append(('Menuname', item.get_menu_id())) - elif isinstance(item, CMenu.TreeEntry): - layout.append(('Filename', item.get_desktop_file_id())) - elif isinstance(item, CMenu.TreeSeparator): - layout.append(('Separator',)) - else: - layout.append(item) - layout.append(('Merge', 'files')) - return layout - - def addItem(self, parent, file_id, dom): - xml_parent = self.getXmlMenu(self.getPath(parent), dom.documentElement, dom) - self.addXmlFilename(xml_parent, dom, file_id, 'Include') - - def moveItem(self, parent, item, before=None, after=None): - self.positionItem(parent, item, before=before, after=after) - self.save() - - def getIndex(self, item, contents): - index = -1 - if isinstance(item, CMenu.TreeDirectory): - for i in range(len(contents)): - if type(item) is not type(contents[i]): - continue - if item.get_menu_id() == contents[i].get_menu_id(): - index = i - return index - elif isinstance(item, CMenu.TreeEntry): - for i in range(len(contents)): - if type(item) is not type(contents[i]): - continue - if item.get_desktop_file_id() == contents[i].get_desktop_file_id(): - index = i - return index - return index - - def positionItem(self, parent, item, before=None, after=None): - contents = self.getContents(parent) - index = -1 - if after: - index = self.getIndex(after, contents) + 1 - # index = contents.index(after) + 1 - elif before: - index = self.getIndex(before, contents) - # index = contents.index(before) - else: - # append the item to the list - index = len(contents) - #if this is a move to a new parent you can't remove the item - item_index = self.getIndex(item, contents) - if item_index > -1: - # decrease the destination index, if we shorten the list - if (before and (item_index < index)) \ - or (after and (item_index < index - 1)): - index -= 1 - contents.remove(contents[item_index]) - contents.insert(index, item) - layout = self.createLayout(contents) - dom = self.dom - menu_xml = self.getXmlMenu(self.getPath(parent), dom.documentElement, dom) - self.addXmlLayout(menu_xml, layout, dom) - - def undoMoves(self, element, old, new, dom): - nodes = [] - matches = [] - original_old = old - final_old = old - #get all elements - for node in self.getXmlNodesByName(['Move'], element): - nodes.insert(0, node) - #if the matches our old parent we've found a stage to undo - for node in nodes: - xml_old = node.getElementsByTagName('Old')[0] - xml_new = node.getElementsByTagName('New')[0] - if xml_new.childNodes[0].nodeValue == old: - matches.append(node) - #we should end up with this path when completed - final_old = xml_old.childNodes[0].nodeValue - #undoing s - for node in matches: - element.removeChild(node) - if len(matches) > 0: - for node in nodes: - xml_old = node.getElementsByTagName('Old')[0] - xml_new = node.getElementsByTagName('New')[0] - path = os.path.split(xml_new.childNodes[0].nodeValue) - if path[0] == original_old: - element.removeChild(node) - for node in dom.getElementsByTagName('Menu'): - name_node = node.getElementsByTagName('Name')[0] - name = name_node.childNodes[0].nodeValue - if name == os.path.split(new)[1]: - #copy app and dir directory info from old - root_path = dom.getElementsByTagName('Menu')[0].getElementsByTagName('Name')[0].childNodes[0].nodeValue - xml_menu = self.getXmlMenu(root_path + '/' + new, dom.documentElement, dom) - for app_dir in node.getElementsByTagName('AppDir'): - xml_menu.appendChild(app_dir) - for dir_dir in node.getElementsByTagName('DirectoryDir'): - xml_menu.appendChild(dir_dir) - parent = node.parentNode - parent.removeChild(node) - node = dom.createElement('Move') - node.appendChild(self.addXmlTextElement(node, 'Old', xml_old.childNodes[0].nodeValue, dom)) - node.appendChild(self.addXmlTextElement(node, 'New', os.path.join(new, path[1]), dom)) - element.appendChild(node) - if final_old == new: - return True - node = dom.createElement('Move') - node.appendChild(self.addXmlTextElement(node, 'Old', final_old, dom)) - node.appendChild(self.addXmlTextElement(node, 'New', new, dom)) - return element.appendChild(node) diff --git a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/config.py b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/config.py index 71b8cb6095..f7e2c09f29 100644 --- a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/config.py +++ b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/config.py @@ -1,8 +1,4 @@ -prefix="/usr" -datadir="/usr/share" -localedir=datadir+"/locale" -libdir="/usr/share/cinnamon" -libexecdir="/usr/share/cinnamon/cinnamon-menu-editor" -PACKAGE="cinnamon-menu-editor" +PKGDATADIR="/usr/share/cinnamon/cinnamon-menu-editor" VERSION="1.6.1" GETTEXT_PACKAGE="cinnamon" +LOCALEDIR = "/usr/share/locale" diff --git a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/util.py b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/util.py index c4d8f77eeb..605f61e621 100644 --- a/files/usr/share/cinnamon/cinnamon-menu-editor/cme/util.py +++ b/files/usr/share/cinnamon/cinnamon-menu-editor/cme/util.py @@ -17,128 +17,58 @@ # Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA import os -import xml.dom.minidom +from pathlib import Path +import xml.etree.ElementTree as ET import uuid -import sys from typing import Optional - -if sys.version_info[:2] >= (3, 8): - from collections.abc import Sequence -else: - from collections import Sequence from gi.repository import Gtk, GdkPixbuf, CMenu, GLib, Gdk -DESKTOP_GROUP = GLib.KEY_FILE_DESKTOP_GROUP -KEY_FILE_FLAGS = GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS - -# from cs_startup.py -def get_locale(): - current_locale = None - locales = GLib.get_language_names() - for locale in locales: - if locale.find(".") == -1: - current_locale = locale - break - - return current_locale - -def fillKeyFile(keyfile, items) -> None: - for key, item in items.items(): - if item is None: - continue - - if isinstance(item, bool): - keyfile.set_boolean(DESKTOP_GROUP, key, item) - elif isinstance(item, str): - keyfile.set_string(DESKTOP_GROUP, key, item) - keyfile.set_locale_string(DESKTOP_GROUP, key, get_locale(), item) - elif isinstance(item, Sequence): - keyfile.set_string_list(DESKTOP_GROUP, key, item) - keyfile.set_locale_string_list(DESKTOP_GROUP, key, get_locale(), item) - -def getNameFromKeyFile(keyfile): - return keyfile.get_string(DESKTOP_GROUP, "Name") - def getUniqueFileId(name, extension): - while 1: - filename = name + '-' + str(uuid.uuid1()) + extension - if extension == '.desktop': - path = getUserItemPath() - if not os.path.isfile(os.path.join(path, filename)) and not getItemPath(filename): - break - elif extension == '.directory': - path = getUserDirectoryPath() - if not os.path.isfile(os.path.join(path, filename)) and not getDirectoryPath(filename): - break - return filename - -def getUniqueRedoFile(filepath) -> str: - while 1: - new_filepath = filepath + '.redo-' + str(uuid.uuid1()) - if not os.path.isfile(new_filepath): - break - return new_filepath - -def getUniqueUndoFile(filepath) -> str: - filename, extension = os.path.split(filepath)[1].rsplit('.', 1) - while 1: - if extension == 'desktop': - path = getUserItemPath() - elif extension == 'directory': - path = getUserDirectoryPath() - elif extension == 'menu': - path = getUserMenuPath() - new_filepath = os.path.join(path, filename + '.' + extension + '.undo-' + str(uuid.uuid1())) - if not os.path.isfile(new_filepath): - break - return new_filepath - -def getItemPath(file_id) -> Optional[str]: + return f"{name}-{uuid.uuid4().hex[:8]}{extension}" + +def getSystemItemFilepath(file_id) -> Optional[str]: for path in GLib.get_system_data_dirs(): - file_path = os.path.join(path, 'applications', file_id) - if os.path.isfile(file_path): - return file_path + file_path = Path(path) / 'applications' / file_id + if file_path.is_file(): + return str(file_path) return None -def getUserItemPath() -> str: - item_dir = os.path.join(GLib.get_user_data_dir(), 'applications') - if not os.path.isdir(item_dir): - os.makedirs(item_dir) - return item_dir +def getUserItemDir() -> str: + item_dir = Path(GLib.get_user_data_dir()) / 'applications' + if not item_dir.is_dir(): + Path(item_dir).mkdir(parents=True, exist_ok=True) + return str(item_dir) -def getDirectoryPath(file_id) -> Optional[str]: +def getSystemDirectoryFilepath(file_id) -> Optional[str]: for path in GLib.get_system_data_dirs(): - file_path = os.path.join(path, 'desktop-directories', file_id) - if os.path.isfile(file_path): - return file_path + file_path = Path(path) / 'desktop-directories' / file_id + if file_path.is_file(): + return str(file_path) return None -def getUserDirectoryPath() -> str: - menu_dir = os.path.join(GLib.get_user_data_dir(), 'desktop-directories') - if not os.path.isdir(menu_dir): - os.makedirs(menu_dir) - return menu_dir +def getUserDirectoryDir() -> str: + path = Path(GLib.get_user_data_dir()) / 'desktop-directories' + path.mkdir(parents=True, exist_ok=True) + return str(path) def getUserMenuPath() -> str: - menu_dir = os.path.join(GLib.get_user_config_dir(), 'menus') - if not os.path.isdir(menu_dir): - os.makedirs(menu_dir) - return menu_dir - -def getSystemMenuPath(file_id) -> Optional[str]: - for path in GLib.get_system_config_dirs(): - file_path = os.path.join(path, 'menus', file_id) - if os.path.isfile(file_path): - return file_path - return None + path = Path(GLib.get_user_config_dir()) / 'menus' + path.mkdir(parents=True, exist_ok=True) + return str(path) def getUserMenuXml(tree) -> str: - system_file = getSystemMenuPath(os.path.basename(tree.get_canonical_menu_path())) + system_file = tree.get_canonical_menu_path() name = tree.get_root_directory().get_menu_id() - menu_xml = "\n" - menu_xml += "\n " + name + "\n " - menu_xml += "" + system_file + "\n\n" - return menu_xml + + return ( + '\n' + '\n' + f'\n' + f' {name}\n' + f' {system_file}\n' + '' + ) class SurfaceWrapper: def __init__(self, surface): @@ -178,18 +108,6 @@ def getIcon(item, widget) -> SurfaceWrapper: wrapper.surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, widget.get_scale_factor(), widget.get_window()) return wrapper -def removeWhitespaceNodes(node) -> None: - remove_list = [] - for child in node.childNodes: - if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: - child.data = child.data.strip() - if not child.data.strip(): - remove_list.append(child) - elif child.hasChildNodes(): - removeWhitespaceNodes(child) - for node in remove_list: - node.parentNode.removeChild(node) - def menuSortKey(node): prefCats = ["administration", "preferences"] key = node.get_menu_id().lower()