660 lines
26 KiB
Python
Executable File
660 lines
26 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# This file is part of I38.
|
|
|
|
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
|
# either version 3 of the License, or (at your option) any later version.
|
|
|
|
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
# PURPOSE. See the GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import configparser
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
import gi
|
|
gi.require_version('Gtk', '3.0')
|
|
gi.require_version('Atk', '1.0')
|
|
from gi.repository import Gtk, Gdk, GLib, Atk
|
|
|
|
def read_desktop_files(paths):
|
|
desktopEntries = []
|
|
for path in paths:
|
|
for file in Path(path).rglob('*.desktop'):
|
|
config = configparser.ConfigParser(interpolation=None)
|
|
config.read(file)
|
|
desktopEntries.append(config)
|
|
return desktopEntries
|
|
|
|
userApplicationsPath = Path.home() / '.local/share/applications'
|
|
systemApplicationsPath = Path('/usr/share/applications')
|
|
userFlatpakApplicationsPath = Path.home() / '.local/share/flatpak/exports/share/applications'
|
|
systemFlatpakApplicationsPath = Path('/var/lib/flatpak/exports/share/applications')
|
|
desktopEntries = read_desktop_files([userApplicationsPath, systemApplicationsPath, userFlatpakApplicationsPath, systemFlatpakApplicationsPath])
|
|
|
|
# Combine some of the categories
|
|
categoryMap = {
|
|
"2DGraphics": "Office",
|
|
"Accessibility": "Settings",
|
|
"Audio": "Audio and Video",
|
|
"AudioVideo": "Audio and Video",
|
|
"AudioVideoEditing": "Audio and Video",
|
|
"ActionGame": "Games",
|
|
"Building": "Development",
|
|
"Calendar": "Office",
|
|
"Chat": "Communication",
|
|
"Database": "Office",
|
|
"DesktopSettings": "Settings",
|
|
"Emulator": "Games",
|
|
"FlowChart": "Office",
|
|
"Game": "Games",
|
|
"HardwareSettings": "Settings",
|
|
"IDE": "Development",
|
|
"InstantMessaging": "Communication",
|
|
"Math": "Education",
|
|
"Midi": "Audio and Video",
|
|
"Mixer": "Audio and Video",
|
|
"Player": "Audio and Video",
|
|
"Presentation": "Office",
|
|
"Recorder": "Audio and Video",
|
|
"Science": "Education",
|
|
"Spreadsheet": "Office",
|
|
"Telephony": "Communication",
|
|
"Terminal": "Utilities",
|
|
"TerminalEmulator": "Utilities",
|
|
"TV": "Audio and Video",
|
|
"Utility": "Utilities",
|
|
"VectorGraphics": "Office",
|
|
"Video": "Audio and Video",
|
|
"WebDevelopment": "Development",
|
|
"WordProcessor": "Office",
|
|
"X-Alsa": "Audio and Video",
|
|
"X-Fedora": "Utilities",
|
|
"X-Jack": "Audio and Video",
|
|
"X-LXDE-Settings": "Settings",
|
|
"X-MATE-NetworkSettings": "Settings",
|
|
"X-MATE-PersonalSettings": "Settings",
|
|
"X-Red-Hat-Base": "Utilities",
|
|
"X-SuSE-Core-Office": "Office",
|
|
"X-XFCE": "XFCE",
|
|
"X-XFCE-HardwareSettings": "Settings",
|
|
"X-XFCE-PersonalSettings": "Settings",
|
|
"X-XFCE-SettingsDialog": "Settings",
|
|
"X-Xfce": "XFCE",
|
|
"X-Xfce-Toplevel": "XFCE",
|
|
}
|
|
|
|
# First, gather all applications by category
|
|
categoryApps = defaultdict(dict)
|
|
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
|
|
|
|
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)
|
|
|
|
# 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
|
|
|
|
# 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(500, 400)
|
|
self.set_border_width(10)
|
|
|
|
# Main container
|
|
self.mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
self.add(self.mainBox)
|
|
|
|
# Add search box at the top
|
|
self.searchBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
|
|
|
# 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
|
|
|
|
# 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
|
|
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)
|
|
|
|
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()
|
|
|
|
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
|
|
|
|
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()
|
|
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
|
|
|
|
Gtk.main()
|