From a19eba19b97226011c16f84d8daca365233321ec Mon Sep 17 00:00:00 2001 From: OmegaPhil Date: Tue, 18 Jul 2017 19:31:38 +0100 Subject: [PATCH 5/5] Further work on launcher category removal/addition Category removal/addition on the selected launcher now removes or adds the launcher to the relevant top-level directories without having to restart menulibre Implements https://bugs.launchpad.net/menulibre/+bug/1307002 --- menulibre/MenulibreApplication.py | 102 ++++++++++++++++++++++++++++++++------ menulibre/MenulibreTreeview.py | 36 ++++++++++---- menulibre/util.py | 64 ++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/menulibre/MenulibreApplication.py b/menulibre/MenulibreApplication.py index 2f95721..9937df6 100644 --- a/menulibre/MenulibreApplication.py +++ b/menulibre/MenulibreApplication.py @@ -35,6 +35,7 @@ from .util import MenuItemTypes, check_keypress, getBasename import menulibre_lib import logging + logger = logging.getLogger('menulibre') session = os.getenv("DESKTOP_SESSION") @@ -1602,8 +1603,6 @@ class MenulibreWindow(Gtk.ApplicationWindow): """Save the current launcher details, remove from the current directory if it no longer has the required category.""" - remove_launcher = False - if temp: filename = tempfile.mkstemp('.desktop', 'menulibre-')[1] else: @@ -1643,13 +1642,6 @@ class MenulibreWindow(Gtk.ApplicationWindow): self.set_editor_categories(';'.join(all_categories)) - # Detecting when the user has removed all categories required for - # an item to show in a directory - i.e. that it should be removed - # from the directory (but not deleted as it may be allowed in other - # directories) - if not set(required_categories) & set(all_categories): - remove_launcher = True - # Cleanup invalid entries and reorder the Categories and Actions self.cleanup_categories() self.cleanup_actions() @@ -1697,11 +1689,10 @@ class MenulibreWindow(Gtk.ApplicationWindow): self.treeview.update_menus() - # Removing from directory (but not deleting the launcher) when it no - # longer has the required categories - i.e. user has explicitly removed - # them - if remove_launcher: - self.treeview.remove_selected(True) + # Check and make sure that the launcher has been added to/removed from + # directories that its category configuration dictates - this is not + # deleting the launcher but removing it from various places in the UI + self.update_launcher_category_dirs() def update_launcher_categories(self, remove, add): original_filename = self.get_value('Filename') @@ -1764,6 +1755,89 @@ class MenulibreWindow(Gtk.ApplicationWindow): row_data[6] = save_filename self.treeview.update_launcher_instances(original_filename, row_data) + def update_launcher_category_dirs(self): + """Make sure launcher is present or absent from in all top-level + directories that its categories dictate.""" + + # Prior to menulibre being restarted, addition of a category does not + # result in the launcher being added to or removed from the relevant + # top-level directories - making sure this happens + + # Fetching model and launcher information - removing empty category + # at end of category split + # Note that a user can remove all categories now if they want, which + # would naturally remove the launcher from all top-level directories - + # alacarte doesn't save any categories by default with a new launcher, + # however to reach this point, any required categories (minus those the + # user has explicitly deleted) will be added, so this shouldn't be a + # problem + model, row_data = self.treeview.get_selected_row_data() + if row_data[2]: + categories = row_data[2].split(';')[:-1] + else: + categories = [] + filename = row_data[6] + + # Obtaining a dictionary of iters to launcher instances in top-level + # directories + launcher_instances = self.treeview._get_launcher_instances(filename) + launchers_in_top_level_dirs = {} + for instance in launcher_instances: + + # Make sure the launcher isn't top-level and is in a directory. + # Must pass a model otherwise it gets the current selection iter + # regardless... + _, parent = self.treeview.get_parent(model, instance) + if (parent is not None + and model[parent][3] == MenuItemTypes.DIRECTORY): + + # Adding if the directory returned is top level + _, parent_parent = self.treeview.get_parent(model, parent) + if parent_parent is None: + launchers_in_top_level_dirs[model[parent][0]] = instance + + # Obtaining a lookup of top-level directories -> iters + top_level_dirs = {} + for row in model: + if row[3] == MenuItemTypes.DIRECTORY: + top_level_dirs[row[0]] = model.get_iter(row.path) + + # Looping through all set categories - category specified is at maximum + # detail level, this needs to be converted to the parent group name, + # and this needs to be converted into the directory name as it would + # appear in the menu + required_category_directories = set() + for category in categories: + category_group = category_lookup[category] + directory_name = util.getDirectoryNameFromCategory(category_group) + + # Adding to directories the launcher should be in + if directory_name not in launchers_in_top_level_dirs: + treeiter = self.treeview.add_child(row_data, + top_level_dirs[directory_name], + model, False) + launchers_in_top_level_dirs[directory_name] = treeiter + + # Building set of required category directories to detect + # superfluous ones later + if directory_name not in required_category_directories: + required_category_directories.add(directory_name) + + # Removing launcher from directories it should no longer be in + superfluous_dirs = (set(launchers_in_top_level_dirs.keys()) + - required_category_directories) + _, parent_data = self.treeview.get_parent_row_data() + for directory_name in superfluous_dirs: + + # Removing selected launcher from the UI if it is in the current + # directory, otherwise just from the model + if directory_name == parent_data[0]: + self.treeview.remove_selected(True) + + else: + self.treeview.remove_iter(model, + launchers_in_top_level_dirs[directory_name]) + def delete_separator(self): """Remove a separator row from the treeview, update the menu files.""" self.treeview.remove_selected() diff --git a/menulibre/MenulibreTreeview.py b/menulibre/MenulibreTreeview.py index 602a2c6..2b454e9 100644 --- a/menulibre/MenulibreTreeview.py +++ b/menulibre/MenulibreTreeview.py @@ -130,14 +130,19 @@ class Treeview(GObject.GObject): return new_iter - def add_child(self, row_data): - """Add a new child launcher to the current selected one.""" - model, treeiter = self._get_selected_iter() + def add_child(self, row_data, treeiter=None, model=None, do_select=True): + """Add a new child launcher to the current selected one, or the + specified iter if adding elsewhere in the tree, with optional + selection.""" + if treeiter is None or model is None: + model, treeiter = self._get_selected_iter() new_iter = model.prepend(treeiter) - self._treeview.expand_row(model[treeiter].path, False) - self._populate_and_select_iter(model, new_iter, row_data) + if do_select: + self._treeview.expand_row(model[treeiter].path, False) + + self._populate_and_select_iter(model, new_iter, row_data, do_select) return new_iter @@ -221,6 +226,15 @@ class Treeview(GObject.GObject): self.update_menus() + def remove_iter(self, model, treeiter): + """Remove launcher pointed to by iter from the model only - use this + when you need to remove a launcher from a non-selected directory, e.g. + when the associated category has been removed""" + + # This feels a bit redundant for a function, but it keeps the + # functionality close to remove_selected + model.remove(treeiter) + # Get def get_parent(self, model=None, treeiter=None): """Get the parent iterator for the current treeiter""" @@ -389,14 +403,16 @@ class Treeview(GObject.GObject): model, treeiter = self._treeview.get_selection().get_selected() return model, treeiter - def _populate_and_select_iter(self, model, treeiter, row_data): - """Fill the specified treeiter with data and select it.""" + def _populate_and_select_iter(self, model, treeiter, row_data, + do_select=True): + """Fill the specified treeiter with data and optionally select it.""" for i in range(len(row_data)): model[treeiter][i] = row_data[i] - # Select the new iter. - path = model.get_path(treeiter) - self._treeview.set_cursor(path) + # Select the new iter if requested + if do_select: + path = model.get_path(treeiter) + self._treeview.set_cursor(path) def _get_deletable_launcher(self, filename): """Return True if the launcher is available for deletion.""" diff --git a/menulibre/util.py b/menulibre/util.py index d5d5c66..b74a1ce 100644 --- a/menulibre/util.py +++ b/menulibre/util.py @@ -205,6 +205,10 @@ def getSystemLauncherPath(basename): def getDirectoryName(directory_str): """Return the directory name to be used in the XML file.""" + + # Note: When adding new logic here, please see if + # getDirectoryNameFromCategory should also be updated + # Get the menu prefix prefix = getDefaultMenuPrefix() has_prefix = False @@ -271,6 +275,66 @@ def getDirectoryName(directory_str): return name +def getDirectoryNameFromCategory(name): + """Guess at the directory name a category should cause its launcher to + appear in. This is used to add launchers to or remove from the right + directories after category addition without having to restart menulibre.""" + + # Note: When adding new logic here, please see if + # getDirectoryName should also be updated + + # I don't want to overload the use of getDirectoryName, so have spun out + # this similar function + + # Only interested in generic categories here, so no need to handle + # categories named after desktop environments + + # Cleanup ArcadeGames and others as per the norm. + if name.endswith('Games') and name != 'Games': + condensed = name[:-5] + non_camel = re.sub('(?!^)([A-Z]+)', r' \1', condensed) + return non_camel + + # GNOME... + if name == 'AudioVideo' or name == 'Audio-Video': + return 'Multimedia' + + if name == 'Game': + return 'Games' + + if name == 'Network' and prefix != 'xfce-': + return 'Internet' + + if name == 'Utility': + return 'Accessories' + + if name == 'System-Tools': + if prefix == 'lxde-': + return 'Administration' + else: + return 'System' + + if name == 'Settings': + if prefix == 'lxde-': + return 'DesktopSettings' + elif has_prefix and prefix == 'xfce-': + return name + else: + return 'Preferences' + + if name == 'Settings-System': + return 'Administration' + + if name == 'GnomeScience': + return 'Science' + + if name == 'Utility-Accessibility': + return 'Universal Access' + + # We tried, just return the name. + return name + + def getRequiredCategories(directory): """Return the list of required categories for a directory string.""" prefix = getDefaultMenuPrefix() -- 2.13.2