Totally revamped the alt+f1 menu system. I think this is much better than the old version, and it also fixes a bug where the old menu didn't close when something was selected. It is experimental, and can be removed if people prefer the old menu, just let me know.
This commit is contained in:
parent
70f3cc749e
commit
002c5ce1d7
609
scripts/menu.py
609
scripts/menu.py
@ -16,7 +16,8 @@ from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
gi.require_version('Atk', '1.0')
|
||||
from gi.repository import Gtk, Gdk, GLib, Atk
|
||||
|
||||
def read_desktop_files(paths):
|
||||
desktopEntries = []
|
||||
@ -85,90 +86,574 @@ categoryMap = {
|
||||
"X-Xfce-Toplevel": "XFCE",
|
||||
}
|
||||
|
||||
categories = defaultdict(set)
|
||||
# First, gather all applications by category
|
||||
categoryApps = defaultdict(dict)
|
||||
subcategories = defaultdict(set)
|
||||
|
||||
for entry in desktopEntries:
|
||||
try:
|
||||
entryCategories = entry.get('Desktop Entry', 'Categories').split(';')
|
||||
# Check if NoDisplay=true is set
|
||||
try:
|
||||
noDisplay = entry.getboolean('Desktop Entry', 'NoDisplay', fallback=False)
|
||||
if noDisplay:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
|
||||
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:
|
||||
combinedCategory = categoryMap.get(category, category)
|
||||
name = entry.get('Desktop Entry', 'Name')
|
||||
execCommand = entry.get('Desktop Entry', 'Exec')
|
||||
# Use a tuple of name and execCommand as a unique identifier
|
||||
if (name, execCommand) not in categories[combinedCategory]:
|
||||
categories[combinedCategory].add((name, execCommand))
|
||||
except configparser.NoOptionError:
|
||||
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)
|
||||
|
||||
# If we found a category, add the application
|
||||
if mainCategory:
|
||||
categoryApps[mainCategory][name] = execCommand
|
||||
else:
|
||||
# If no category was found, add to "Other"
|
||||
categoryApps["Other"][name] = execCommand
|
||||
|
||||
except (configparser.NoOptionError, KeyError):
|
||||
continue
|
||||
|
||||
class Xdg_Menu_Window(Gtk.Window):
|
||||
# Ensure we have an "All Applications" category that contains everything
|
||||
allApps = {}
|
||||
for category, apps in categoryApps.items():
|
||||
allApps.update(apps)
|
||||
categoryApps["All Applications"] = allApps
|
||||
|
||||
class I38_Tab_Menu(Gtk.Window):
|
||||
def __init__(self):
|
||||
super().__init__(title="I38 Menu")
|
||||
self.set_default_size(400, 300)
|
||||
self.set_default_size(500, 400)
|
||||
self.set_border_width(10)
|
||||
|
||||
self.store = Gtk.TreeStore(str, str) # Columns: Category/Application Name, Exec Command
|
||||
# Main container
|
||||
self.mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.add(self.mainBox)
|
||||
|
||||
sortedCategories = sorted(categories.items()) # Sort categories alphabetically
|
||||
# Add search box at the top
|
||||
self.searchBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
for category, entries in sortedCategories:
|
||||
if category == "":
|
||||
# Create completion for the search entry
|
||||
self.completionStore = Gtk.ListStore(str, str) # Name, Exec
|
||||
self.completion = Gtk.EntryCompletion()
|
||||
self.completion.set_model(self.completionStore)
|
||||
self.completion.set_text_column(0)
|
||||
self.completion.set_minimum_key_length(1)
|
||||
self.completion.set_popup_completion(True)
|
||||
self.completion.set_inline_completion(False)
|
||||
self.completion.connect("match-selected", self.on_completion_match)
|
||||
|
||||
self.searchEntry = Gtk.Entry()
|
||||
self.searchEntry.set_completion(self.completion)
|
||||
self.searchEntry.set_placeholder_text("Search applications...")
|
||||
self.searchEntry.connect("changed", self.on_search_changed)
|
||||
self.searchEntry.connect("key-press-event", self.on_search_key_press)
|
||||
self.searchBox.pack_start(self.searchEntry, True, True, 0)
|
||||
|
||||
# Add search button for visual users
|
||||
self.searchButton = Gtk.Button.new_from_icon_name("search", Gtk.IconSize.BUTTON)
|
||||
self.searchButton.connect("clicked", self.on_search_activated)
|
||||
self.searchBox.pack_start(self.searchButton, False, False, 0)
|
||||
|
||||
self.mainBox.pack_start(self.searchBox, False, False, 0)
|
||||
|
||||
# Create notebook (tabbed interface)
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
|
||||
self.mainBox.pack_start(self.notebook, True, True, 0)
|
||||
|
||||
# Flag for search mode
|
||||
self.inSearchMode = False
|
||||
|
||||
# For incremental letter navigation
|
||||
self.typedText = ""
|
||||
self.typedTextTimer = None
|
||||
|
||||
# Add a tab for each major category
|
||||
self.treeViews = {} # Store TreeViews for each tab
|
||||
self.stores = {} # Store models for each tab
|
||||
|
||||
# Sort categories alphabetically, but ensure "All Applications" is first
|
||||
sortedCategories = sorted(categoryApps.keys())
|
||||
if "All Applications" in sortedCategories:
|
||||
sortedCategories.remove("All Applications")
|
||||
sortedCategories.insert(0, "All Applications")
|
||||
|
||||
# Create tabs
|
||||
for category in sortedCategories:
|
||||
if not categoryApps[category]: # Skip empty categories
|
||||
continue
|
||||
categoryIter = self.store.append(parent=None, row=[category, None])
|
||||
sortedEntries = sorted(entries, key=lambda e: e[0]) # Sort entries by name
|
||||
for name, execCommand in sortedEntries:
|
||||
self.store.append(parent=categoryIter, row=[name, execCommand])
|
||||
|
||||
self.treeview = Gtk.TreeView(model=self.store)
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Applications", renderer, text=0)
|
||||
self.treeview.append_column(column)
|
||||
self.treeview.set_headers_visible(False)
|
||||
# Create a TreeStore for this category
|
||||
store = Gtk.TreeStore(str, str) # Columns: Name, Exec
|
||||
self.stores[category] = store
|
||||
|
||||
self.treeview.connect("row-activated", self.on_row_activated)
|
||||
self.treeview.connect("key-press-event", self.on_key_press)
|
||||
# Add applications to this category's store
|
||||
sortedApps = sorted(categoryApps[category].items())
|
||||
|
||||
self.add(self.treeview)
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
self.treeview.connect("focus-out-event", self.on_focus_out)
|
||||
# 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
|
||||
|
||||
self.treeview.grab_focus()
|
||||
self.show_all()
|
||||
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
|
||||
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)
|
||||
|
||||
# Add the tab
|
||||
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.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)
|
||||
|
||||
# Connect notebook signals
|
||||
self.notebook.connect("switch-page", self.on_switch_page)
|
||||
|
||||
# Connect window signals
|
||||
self.connect("key-press-event", self.on_window_key_press)
|
||||
self.connect("focus-out-event", self.on_focus_out)
|
||||
|
||||
# Add all applications to the completion store
|
||||
self.populate_completion_store()
|
||||
|
||||
# Set accessibility properties
|
||||
windowAccessible = self.get_accessible()
|
||||
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 populate_completion_store(self):
|
||||
"""Populate completion store with all available applications"""
|
||||
self.completionStore.clear()
|
||||
|
||||
# Add all applications from all categories
|
||||
for tabName, appDict in categoryApps.items():
|
||||
for appName, execCommand in sorted(appDict.items()):
|
||||
self.completionStore.append([appName, execCommand])
|
||||
|
||||
def on_switch_page(self, notebook, page, pageNum):
|
||||
# Focus the TreeView in the newly selected tab
|
||||
tab = notebook.get_nth_page(pageNum)
|
||||
for child in tab.get_children():
|
||||
if isinstance(child, Gtk.TreeView):
|
||||
child.grab_focus()
|
||||
break
|
||||
|
||||
def on_row_activated(self, treeView, path, column):
|
||||
model = treeView.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
execCommand = model.get_value(treeIter, 1)
|
||||
|
||||
def on_row_activated(self, treeview, path, column):
|
||||
model = treeview.get_model()
|
||||
iter = model.get_iter(path)
|
||||
execCommand = model.get_value(iter, 1)
|
||||
if execCommand:
|
||||
os.system(execCommand)
|
||||
# Launch the application
|
||||
cmdParts = execCommand.split()
|
||||
# Remove field codes like %f, %F, %u, %U
|
||||
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
|
||||
cleanCmd = ' '.join(cleanCmd)
|
||||
|
||||
def on_focus_out(self, widget, event):
|
||||
Gtk.main_quit()
|
||||
# Use GLib.spawn_command_line_async for better process handling
|
||||
try:
|
||||
GLib.spawn_command_line_async(cleanCmd)
|
||||
Gtk.main_quit()
|
||||
except GLib.Error as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=f"Failed to launch application: {e.message}"
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def on_completion_match(self, completion, model, iterator):
|
||||
"""Handle when a completion item is selected"""
|
||||
appName = model[iterator][0]
|
||||
execCommand = model[iterator][1]
|
||||
|
||||
if execCommand:
|
||||
# Launch the application
|
||||
cmdParts = execCommand.split()
|
||||
# Remove field codes like %f, %F, %u, %U
|
||||
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
|
||||
cleanCmd = ' '.join(cleanCmd)
|
||||
|
||||
# Use GLib.spawn_command_line_async for better process handling
|
||||
try:
|
||||
GLib.spawn_command_line_async(cleanCmd)
|
||||
Gtk.main_quit()
|
||||
except GLib.Error as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=f"Failed to launch application: {e.message}"
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
return True
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
"""Handle search text changes"""
|
||||
searchText = entry.get_text().lower()
|
||||
self.inSearchMode = bool(searchText)
|
||||
|
||||
# If entering search mode, make sure "All Applications" tab is active
|
||||
if self.inSearchMode and not self.notebook.get_current_page() == 0:
|
||||
self.notebook.set_current_page(0)
|
||||
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
store = self.stores[tabName]
|
||||
|
||||
self.filter_applications(store, searchText)
|
||||
|
||||
# Update the completion model with filtered results
|
||||
self.update_completion_results(searchText)
|
||||
|
||||
# Set accessibility announcement
|
||||
if self.inSearchMode:
|
||||
treeView.get_accessible().set_name(f"Search results for {searchText}")
|
||||
else:
|
||||
treeView.get_accessible().set_name(f"{tabName} applications")
|
||||
|
||||
def update_completion_results(self, searchText):
|
||||
"""Update completion dropdown with matching applications"""
|
||||
if not searchText:
|
||||
# If empty, restore all applications
|
||||
self.populate_completion_store()
|
||||
return
|
||||
|
||||
# Clear and repopulate with filtered results
|
||||
self.completionStore.clear()
|
||||
|
||||
for tabName, appDict in categoryApps.items():
|
||||
for appName, execCommand in sorted(appDict.items()):
|
||||
if searchText.lower() in appName.lower():
|
||||
self.completionStore.append([appName, execCommand])
|
||||
|
||||
def filter_applications(self, store, searchText):
|
||||
"""Show/hide applications based on search text"""
|
||||
def filterRow(model, iterator):
|
||||
if not self.inSearchMode:
|
||||
return True
|
||||
|
||||
appName = model.get_value(iterator, 0).lower()
|
||||
|
||||
# Always show categories (rows with no exec command)
|
||||
execCommand = model.get_value(iterator, 1)
|
||||
if execCommand is None:
|
||||
# Check if any children match
|
||||
childIter = model.iter_children(iterator)
|
||||
while childIter:
|
||||
childName = model.get_value(childIter, 0).lower()
|
||||
if searchText in childName:
|
||||
return True
|
||||
childIter = model.iter_next(childIter)
|
||||
return False
|
||||
|
||||
# Show app if it matches search
|
||||
return searchText in appName
|
||||
|
||||
for tabName, treeStore in self.stores.items():
|
||||
# Replace store with filtered version
|
||||
filterStore = treeStore.filter_new()
|
||||
filterStore.set_visible_func(filterRow)
|
||||
|
||||
treeView = self.treeViews[tabName]
|
||||
treeView.set_model(filterStore)
|
||||
|
||||
# Expand all categories in search mode
|
||||
if self.inSearchMode:
|
||||
treeView.expand_all()
|
||||
else:
|
||||
treeView.collapse_all()
|
||||
|
||||
def on_search_key_press(self, entry, event):
|
||||
"""Handle keyboard input in search box"""
|
||||
keyval = event.keyval
|
||||
|
||||
if keyval == Gdk.KEY_Down:
|
||||
# Check if we can navigate to completion results
|
||||
if self.completion.get_popup_completion() and self.inSearchMode:
|
||||
# Try to activate completion popup and navigate to it
|
||||
self.completion.complete()
|
||||
return True
|
||||
|
||||
# If not in completion mode, move focus to the treeview
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
treeView.grab_focus()
|
||||
|
||||
# Set cursor to first item
|
||||
model = treeView.get_model()
|
||||
iterator = model.get_iter_first()
|
||||
if iterator:
|
||||
path = model.get_path(iterator)
|
||||
treeView.set_cursor(path)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Escape:
|
||||
if self.searchEntry.get_text():
|
||||
# Clear search if there's text
|
||||
self.searchEntry.set_text("")
|
||||
return True
|
||||
else:
|
||||
# Otherwise close the app
|
||||
Gtk.main_quit()
|
||||
return True
|
||||
|
||||
# Other keys pass through
|
||||
return False
|
||||
|
||||
def on_search_activated(self, widget):
|
||||
"""Process search when search button is clicked"""
|
||||
searchText = self.searchEntry.get_text()
|
||||
if searchText:
|
||||
# Focus on the first result if any
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
model = treeView.get_model()
|
||||
|
||||
# Find first visible row
|
||||
iterator = model.get_iter_first()
|
||||
if iterator:
|
||||
path = model.get_path(iterator)
|
||||
treeView.set_cursor(path)
|
||||
treeView.grab_focus()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
keyval = event.keyval
|
||||
state = event.state
|
||||
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
# Reset incremental typing and close app if no ongoing search
|
||||
if self.typedText:
|
||||
self.typedText = ""
|
||||
# Announce reset
|
||||
current_tab = self.notebook.get_current_page()
|
||||
tab_name = list(self.treeViews.keys())[current_tab]
|
||||
treeView = self.treeViews[tab_name]
|
||||
treeView.get_accessible().set_name(f"Type reset. {tab_name} applications")
|
||||
return True
|
||||
else:
|
||||
Gtk.main_quit()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_slash:
|
||||
# Forward slash activates search
|
||||
self.searchEntry.grab_focus()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Left:
|
||||
# If in a TreeView and Left key is pressed
|
||||
path = widget.get_cursor()[0]
|
||||
if path:
|
||||
model = widget.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
|
||||
if widget.row_expanded(path):
|
||||
# Collapse the current row if it's expanded
|
||||
widget.collapse_row(path)
|
||||
return True
|
||||
else:
|
||||
# Move to parent if possible
|
||||
parentIter = model.iter_parent(treeIter)
|
||||
if parentIter:
|
||||
parentPath = model.get_path(parentIter)
|
||||
widget.set_cursor(parentPath)
|
||||
return True
|
||||
|
||||
# If we couldn't handle it in the TreeView, try to switch tabs
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if currentPage > 0 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage - 1)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Right:
|
||||
# If in a TreeView and Right key is pressed
|
||||
path = widget.get_cursor()[0]
|
||||
if path:
|
||||
model = widget.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
|
||||
# Check if this row has children
|
||||
if model.iter_has_child(treeIter):
|
||||
if not widget.row_expanded(path):
|
||||
widget.expand_row(path, False)
|
||||
# Move to the first child
|
||||
childPath = model.get_path(model.iter_children(treeIter))
|
||||
widget.set_cursor(childPath)
|
||||
return True
|
||||
|
||||
# If we couldn't handle it in the TreeView, try to switch tabs
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage + 1)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Tab:
|
||||
# Control tab navigation
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if event.state & Gdk.ModifierType.SHIFT_MASK:
|
||||
# Shift+Tab -> previous tab
|
||||
if currentPage > 0 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage - 1)
|
||||
else:
|
||||
self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
|
||||
else:
|
||||
# Tab -> next tab
|
||||
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage + 1)
|
||||
else:
|
||||
self.notebook.set_current_page(0)
|
||||
return True
|
||||
|
||||
# Incremental letter navigation
|
||||
elif Gdk.KEY_a <= keyval <= Gdk.KEY_z or Gdk.KEY_A <= keyval <= Gdk.KEY_Z:
|
||||
# Cancel any pending timer
|
||||
if self.typedTextTimer:
|
||||
GLib.source_remove(self.typedTextTimer)
|
||||
self.typedTextTimer = None
|
||||
|
||||
# Add the new letter to the typed text
|
||||
letter = chr(keyval).lower()
|
||||
self.typedText += letter
|
||||
|
||||
# Find item matching typed text
|
||||
found = self.find_incremental_match(widget, self.typedText)
|
||||
|
||||
# Set timer to reset typed text after 1.5 seconds of inactivity
|
||||
self.typedTextTimer = GLib.timeout_add(1500, self.reset_typed_text)
|
||||
|
||||
# Announce the letters being typed
|
||||
current_tab = self.notebook.get_current_page()
|
||||
tab_name = list(self.treeViews.keys())[current_tab]
|
||||
treeView = self.treeViews[tab_name]
|
||||
treeView.get_accessible().set_name(f"Typed: {self.typedText}")
|
||||
|
||||
return True if found else False
|
||||
|
||||
return False
|
||||
|
||||
def reset_typed_text(self):
|
||||
"""Reset the typed text after timeout"""
|
||||
self.typedText = ""
|
||||
self.typedTextTimer = None
|
||||
return False # Don't call again
|
||||
|
||||
def find_incremental_match(self, treeView, text):
|
||||
"""Find items matching the incrementally typed text"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
model = treeView.get_model()
|
||||
found = False
|
||||
|
||||
def search_tree(model, path, treeIter, user_data):
|
||||
nonlocal found
|
||||
if found:
|
||||
return True # Stop iteration if already found
|
||||
|
||||
name = model.get_value(treeIter, 0)
|
||||
if name and name.lower().startswith(text.lower()):
|
||||
# Found a match
|
||||
treeView.set_cursor(path)
|
||||
treeView.scroll_to_cell(path, None, True, 0.5, 0.5)
|
||||
found = True
|
||||
return True # Stop iteration
|
||||
return False # Continue iteration
|
||||
|
||||
# Search the entire model
|
||||
model.foreach(search_tree, None)
|
||||
return found
|
||||
|
||||
def on_window_key_press(self, widget, event):
|
||||
# Handle window-level key events
|
||||
keyval = event.keyval
|
||||
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
Gtk.main_quit()
|
||||
elif keyval == Gdk.KEY_Left:
|
||||
path = self.treeview.get_cursor()[0]
|
||||
if path:
|
||||
iter = self.store.get_iter(path)
|
||||
if self.treeview.row_expanded(path):
|
||||
self.treeview.collapse_row(path)
|
||||
else:
|
||||
parent_iter = self.store.iter_parent(iter)
|
||||
if parent_iter:
|
||||
parent_path = self.store.get_path(parent_iter)
|
||||
self.treeview.collapse_row(parent_path)
|
||||
else:
|
||||
self.treeview.collapse_row(path)
|
||||
elif keyval == Gdk.KEY_Right:
|
||||
path = self.treeview.get_cursor()[0]
|
||||
if path:
|
||||
if not self.treeview.row_expanded(path):
|
||||
self.treeview.expand_row(path, open_all=False)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_slash:
|
||||
# Forward slash activates search
|
||||
self.searchEntry.grab_focus()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def on_focus_out(self, widget, event):
|
||||
# Quit when the window loses focus
|
||||
Gtk.main_quit()
|
||||
|
||||
window = I38_Tab_Menu()
|
||||
window.connect("destroy", Gtk.main_quit)
|
||||
window.show_all()
|
||||
|
||||
# Focus the first TreeView
|
||||
firstTab = window.notebook.get_nth_page(0)
|
||||
for child in firstTab.get_children():
|
||||
if isinstance(child, Gtk.TreeView):
|
||||
child.grab_focus()
|
||||
break
|
||||
|
||||
win = Xdg_Menu_Window()
|
||||
win.connect("destroy", Gtk.main_quit)
|
||||
win.show_all()
|
||||
Gtk.main()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user