diff --git a/scripts/menu.py b/scripts/menu.py index d92c7db..d0e6938 100755 --- a/scripts/menu.py +++ b/scripts/menu.py @@ -22,10 +22,15 @@ from gi.repository import Gtk, Gdk, GLib, Atk def read_desktop_files(paths): desktopEntries = [] for path in paths: + if not Path(path).exists(): + continue for file in Path(path).rglob('*.desktop'): - config = configparser.ConfigParser(interpolation=None) - config.read(file) - desktopEntries.append(config) + try: + config = configparser.ConfigParser(interpolation=None) + config.read(file, encoding='utf-8') + desktopEntries.append(config) + except (UnicodeDecodeError, configparser.Error): + continue return desktopEntries userApplicationsPath = Path.home() / '.local/share/applications' @@ -93,34 +98,24 @@ subcategories = defaultdict(set) for entry in desktopEntries: try: # Check if NoDisplay=true is set - try: - noDisplay = entry.getboolean('Desktop Entry', 'NoDisplay', fallback=False) - if noDisplay: - continue - except: - pass + if entry.getboolean('Desktop Entry', 'NoDisplay', fallback=False): + continue + + name = entry.get('Desktop Entry', 'Name', fallback=None) + execCommand = entry.get('Desktop Entry', 'Exec', fallback=None) + + if not name or not execCommand: + continue - name = entry.get('Desktop Entry', 'Name') - execCommand = entry.get('Desktop Entry', 'Exec') entryCategories = entry.get('Desktop Entry', 'Categories', fallback='').split(';') # For applications with categories mainCategory = None - for category in entryCategories: - if category: # Skip empty strings - mappedCategory = categoryMap.get(category, category) - if mainCategory is None: - mainCategory = mappedCategory - - # Check if this might be a subcategory - for other in entryCategories: - if other and other != category: - mappedOther = categoryMap.get(other, other) - if mappedCategory != mappedOther: - subcategories[mappedOther].add(mappedCategory) + validCategories = [cat for cat in entryCategories if cat.strip()] - # If we found a category, add the application - if mainCategory: + if validCategories: + # Use first valid category as main + mainCategory = categoryMap.get(validCategories[0], validCategories[0]) categoryApps[mainCategory][name] = execCommand else: # If no category was found, add to "Other" @@ -194,58 +189,17 @@ class I38_Tab_Menu(Gtk.Window): sortedCategories.remove("All Applications") sortedCategories.insert(0, "All Applications") - # Create tabs - for category in sortedCategories: + # Create tabs - defer TreeView creation for performance + self.tabCategories = sortedCategories + self.createdTabs = set() # Track which tabs have been created + + for i, category in enumerate(sortedCategories): if not categoryApps[category]: # Skip empty categories continue - # Create a TreeStore for this category - store = Gtk.TreeStore(str, str) # Columns: Name, Exec - self.stores[category] = store - - # Add applications to this category's store - sortedApps = sorted(categoryApps[category].items()) - - # Check for potential subcategories within this category - categorySubcategories = {} - for appName, appExec in sortedApps: - subcatFound = False - for subcat in subcategories.get(category, []): - if appName in categoryApps.get(subcat, {}): - if subcat not in categorySubcategories: - categorySubcategories[subcat] = [] - categorySubcategories[subcat].append((appName, appExec)) - subcatFound = True - break - - if not subcatFound: - # Add directly to the category's root - store.append(None, [appName, appExec]) - - # Add any subcategories - for subcat, subcatApps in sorted(categorySubcategories.items()): - subcatIter = store.append(None, [subcat, None]) - for appName, appExec in sorted(subcatApps): - store.append(subcatIter, [appName, appExec]) - - # Create TreeView for this category - treeView = Gtk.TreeView(model=store) - treeView.set_headers_visible(False) - self.treeViews[category] = treeView - - # Add column for application names - renderer = Gtk.CellRendererText() - column = Gtk.TreeViewColumn("Applications", renderer, text=0) - treeView.append_column(column) - - # Set up scrolled window + # Create placeholder scrolled window scrolledWindow = Gtk.ScrolledWindow() scrolledWindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - scrolledWindow.add(treeView) - - # Connect signals - treeView.connect("row-activated", self.on_row_activated) - treeView.connect("key-press-event", self.on_key_press) # Create tab label tabLabel = Gtk.Label(label=category) @@ -254,13 +208,14 @@ class I38_Tab_Menu(Gtk.Window): self.notebook.append_page(scrolledWindow, tabLabel) # Set tab accessibility properties for screen readers - tabChild = self.notebook.get_nth_page(self.notebook.get_n_pages() - 1) - # Get the accessible object and set properties on it instead - accessible = tabChild.get_accessible() + accessible = scrolledWindow.get_accessible() accessible.set_name(f"{category} applications") - # Use Atk role instead of Gtk.AccessibleRole which isn't available in GTK 3.0 accessible.set_role(Atk.Role.LIST) + # Create first tab immediately + if sortedCategories: + self.create_tab_content(0) + # Connect notebook signals self.notebook.connect("switch-page", self.on_switch_page) @@ -276,6 +231,48 @@ class I38_Tab_Menu(Gtk.Window): windowAccessible.set_name("I38 Application Menu") windowAccessible.set_description("Tab-based application launcher menu. Press slash to search, type app name and use down arrow to navigate results. Type letters to incrementally navigate to matching applications.") + def create_tab_content(self, tabIndex): + """Create TreeView content for a tab on demand""" + if tabIndex in self.createdTabs or tabIndex >= len(self.tabCategories): + return + + category = self.tabCategories[tabIndex] + if not categoryApps[category]: + return + + # Create a TreeStore for this category + store = Gtk.TreeStore(str, str) # Columns: Name, Exec + self.stores[category] = store + + # Add applications to this category's store (simplified - no subcategories) + sortedApps = sorted(categoryApps[category].items()) + for appName, appExec in sortedApps: + store.append(None, [appName, appExec]) + + # Create TreeView for this category + treeView = Gtk.TreeView(model=store) + treeView.set_headers_visible(False) + self.treeViews[category] = treeView + + # Add column for application names + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Applications", renderer, text=0) + treeView.append_column(column) + + # Get the scrolled window for this tab + scrolledWindow = self.notebook.get_nth_page(tabIndex) + scrolledWindow.add(treeView) + + # Connect signals + treeView.connect("row-activated", self.on_row_activated) + treeView.connect("key-press-event", self.on_key_press) + + # Show the new content + treeView.show() + + # Mark this tab as created + self.createdTabs.add(tabIndex) + def populate_completion_store(self): """Populate completion store with all available applications""" self.completionStore.clear() @@ -286,6 +283,9 @@ class I38_Tab_Menu(Gtk.Window): self.completionStore.append([appName, execCommand]) def on_switch_page(self, notebook, page, pageNum): + # Create tab content if not already created + self.create_tab_content(pageNum) + # Focus the TreeView in the newly selected tab tab = notebook.get_nth_page(pageNum) for child in tab.get_children():