From 40be21887d4bb286ce0587bb5ca832c8fa92a49b Mon Sep 17 00:00:00 2001 From: heliguy4599 Date: Sat, 12 Oct 2024 22:43:23 -0400 Subject: [PATCH] Add keyboard shortcuts! --- src/gtk/help-overlay.blp | 86 +++--------- src/main.py | 161 +++++++++++++++++------ src/packages_page/packages_page.py | 88 +++++++------ src/packages_page/uninstall_dialog.py | 23 +++- src/remotes_page/add_remote_dialog.py | 12 ++ src/remotes_page/remotes_page.py | 5 +- src/snapshot_page/new_snapshot_dialog.py | 10 +- src/snapshot_page/snapshot_box.py | 45 ++++--- src/snapshot_page/snapshot_page.py | 56 +++++--- src/user_data_page/user_data_page.py | 23 +++- 10 files changed, 312 insertions(+), 197 deletions(-) diff --git a/src/gtk/help-overlay.blp b/src/gtk/help-overlay.blp index d0eb7c0..289cf6e 100644 --- a/src/gtk/help-overlay.blp +++ b/src/gtk/help-overlay.blp @@ -1,70 +1,24 @@ using Gtk 4.0; ShortcutsWindow help_overlay { - modal: true; - - ShortcutsSection { - section-name: "shortcuts"; - // max-height: 8; - - ShortcutsGroup { - title: _("App Management"); - - ShortcutsShortcut { - title: _("Search"); - action-name: "app.search"; - } - - ShortcutsShortcut { - title: _("Set Filters"); - action-name: "app.set-filter"; - } - - ShortcutsShortcut { - title: _("Refresh"); - action-name: "app.refresh"; - } - - ShortcutsShortcut { - title: _("Toggle Selection Mode"); - action-name: "app.toggle-batch-mode"; - } - } - ShortcutsGroup { - title: _("More Functions"); - - ShortcutsShortcut { - title: _("Manage Leftover Data"); - action-name: "app.manage-data-folders"; - } - - ShortcutsShortcut { - title: _("Manage Remotes"); - action-name: "app.show-remotes-window"; - } - - ShortcutsShortcut { - title: _("Install From File"); - action-name: "app.install-from-file"; - } - } - ShortcutsGroup { - title: _("General"); - - ShortcutsShortcut { - title: _("Open Menu"); - action-name: "app.open-menu"; - } - - ShortcutsShortcut { - title: _("Show Shortcuts"); - action-name: "win.show-help-overlay"; - } - - ShortcutsShortcut { - title: _("Quit"); - action-name: "app.quit"; - } - } - } + modal: true; + ShortcutsSection { + section-name: "shortcuts"; + // max-height: 8; + ShortcutsGroup { + title: _("General"); + ShortcutsShortcut { + title: _("Open Menu"); + action-name: "app.open-menu"; + } + ShortcutsShortcut { + title: _("Show Shortcuts"); + action-name: "win.show-help-overlay"; + } + ShortcutsShortcut { + title: _("Quit"); + action-name: "app.quit"; + } + } + } } diff --git a/src/main.py b/src/main.py index 11f3dcf..cbd17c8 100644 --- a/src/main.py +++ b/src/main.py @@ -32,10 +32,10 @@ from .const import Config class WarehouseApplication(Adw.Application): """The main application singleton class.""" - + troubleshooting = "OS: {os}\nWarehouse version: {wv}\nGTK: {gtk}\nlibadwaita: {adw}\nApp ID: {app_id}\nProfile: {profile}\nLanguage: {lang}" version = Config.VERSION - + def __init__(self): super().__init__( application_id="io.github.flattool.Warehouse", @@ -43,9 +43,6 @@ class WarehouseApplication(Adw.Application): ) self.create_action("about", self.on_about_action) self.create_action("preferences", self.on_preferences_action) - self.create_action("quit", lambda *_: self.quit(), ["q"]) - self.create_action("refresh", self.on_refresh_shortcut, ["r", "F5"]) - self.create_action("open-menu", lambda *_: self.props.active_window.main_menu.popup(), ["F10"]) self.create_action("show-packages-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("p"), ["p"]) self.create_action("show-remotes-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("m"), ["m"]) @@ -53,8 +50,20 @@ class WarehouseApplication(Adw.Application): self.create_action("show-snapshots-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("s"), ["s"]) self.create_action("show-install-page", lambda *_: self.props.active_window.switch_page_shortcut_handler("i"), ["i"]) + self.create_action("quit", lambda *_: self.quit(), ["q"]) + self.create_action("refresh", lambda *_: self.props.active_window.refresh_handler(), ["r", "F5"]) + self.create_action("open-menu", lambda *_: self.props.active_window.main_menu.popup(), ["F10"]) + self.create_action("open-files", self.on_open_files_shortcut, ["o"]) + self.create_action("toggle-select-mode", self.on_toggle_select_mode_shortcut, ["b", "Return", "KP_Enter"]) + self.create_action("toggle-search-mode", self.on_toggle_search_mode_shortcut, ["f"]) + self.create_action("filter", self.on_filter_shortcut, ["t"]) + self.create_action("new", self.on_new_shortcut, ["n"]) + self.create_action("delete", self.on_delete_shortcut, ["BackSpace", "Delete"]) + self.create_action("active-data-view", lambda *_: self.on_data_view_shortcut(True), ["1"]) + self.create_action("leftover-data-view", lambda *_: self.on_data_view_shortcut(False), ["2"]) + self.is_dialog_open = False - + gtk_version = ( str(Gtk.MAJOR_VERSION) + "." @@ -71,7 +80,7 @@ class WarehouseApplication(Adw.Application): ) os_string = GLib.get_os_info("NAME") + " " + GLib.get_os_info("VERSION") lang = GLib.environ_getenv(GLib.get_environ(), "LANG") - + self.troubleshooting = self.troubleshooting.format( os=os_string, wv=self.version, @@ -82,33 +91,108 @@ class WarehouseApplication(Adw.Application): lang=lang, ) - def on_refresh_shortcut(self, *args): - self.props.active_window.refresh_handler() - - # def file_callback(self, object, result): - # window = self.props.active_window - # try: - # file = object.open_finish(result) - # window.install_file(file.get_path()) - # except GLib.GError: - # pass - - # def install_from_file(self, widget, _a): - # window = self.props.active_window - - # filter = Gtk.FileFilter(name=_("Flatpaks")) - # filter.add_suffix("flatpak") - # filter.add_suffix("flatpakref") - # filters = Gio.ListStore.new(Gtk.FileFilter) - # filters.append(filter) - # file_chooser = Gtk.FileDialog() - # file_chooser.set_filters(filters) - # file_chooser.set_default_filter(filter) - # file_chooser.open(window, None, self.file_callback) - + def on_open_files_shortcut(self, *args): + window = self.props.active_window + + def file_choose_callback(object, result): + files = object.open_multiple_finish(result) + window.on_file_drop(None, files, None, None) + + file_filter = Gtk.FileFilter(name=_("Flatpaks & Remotes")) + file_filter.add_suffix("flatpak") + file_filter.add_suffix("flatpakref") + file_filter.add_suffix("flatpakrepo") + filters = Gio.ListStore.new(Gtk.FileFilter) + filters.append(file_filter) + file_chooser = Gtk.FileDialog() + file_chooser.set_filters(filters) + file_chooser.set_default_filter(file_filter) + file_chooser.open_multiple(window, None, file_choose_callback) + + def on_toggle_select_mode_shortcut(self, *args): + try: + button = self.props.active_window.stack.get_visible_child().select_button + button.set_active(not button.get_active()) + except AttributeError: + pass + + def on_toggle_search_mode_shortcut(self, *args): + try: + button = self.props.active_window.stack.get_visible_child().search_button + button.set_active(not button.get_active()) + except AttributeError: + pass + + def on_filter_shortcut(self, *args): + try: + button = self.props.active_window.stack.get_visible_child().filter_button + button.set_active(not button.get_active()) + except AttributeError: + pass + + try: + button = self.props.active_window.stack.get_visible_child().sort_button + button.set_active(True) + except AttributeError: + pass + + try: + button = self.props.active_window.stack.get_visible_child().show_disabled_button + if button.get_visible(): + button.set_active(not button.get_active()) + except AttributeError: + pass + + def on_new_shortcut(self, *args): + page = self.props.active_window.stack.get_visible_child() + try: + page.new_custom_handler() + except AttributeError: + pass + + try: + page.on_new() + except AttributeError: + pass + + def on_delete_shortcut(self, *args): + page = self.props.active_window.stack.get_visible_child() + try: + if not page.select_button.get_active(): + return + + page.selection_uninstall() + except AttributeError: + pass + + try: + if not page.select_button.get_active(): + return + + page.trash_handler() + except AttributeError: + pass + + try: + if not page.select_button.get_active(): + return + + page.select_trash_handler() + except AttributeError: + pass + + def on_data_view_shortcut(self, is_active): + page = self.props.active_window.stack.get_visible_child() + try: + adp = page.adp + ldp = page.ldp + page.stack.set_visible_child(adp if is_active else ldp) + except AttributeError: + pass + def do_activate(self): """Called when the application is activated. - + We raise the application's main window, creating it if necessary. """ @@ -116,7 +200,7 @@ class WarehouseApplication(Adw.Application): if not win: win = WarehouseWindow(application=self) win.present() - + def on_about_action(self, widget, _a): """Callback for the app.about action.""" about = Adw.AboutDialog( @@ -156,18 +240,17 @@ class WarehouseApplication(Adw.Application): ], ) about.present(self.props.active_window) - + def on_preferences_action(self, widget, _): """Callback for the app.preferences action.""" print("app.preferences action activated") - + def create_action(self, name, callback, shortcuts=None): """Add an application action. - + Args: name: the name of the action - callback: the function to be called when the action is - activated + callback: the function to be called when the action is activated shortcuts: an optional list of accelerators """ action = Gio.SimpleAction.new(name, None) @@ -175,7 +258,7 @@ class WarehouseApplication(Adw.Application): self.add_action(action) if shortcuts: self.set_accels_for_action(f"app.{name}", shortcuts) - + def main(version): """The application's entry point.""" app = WarehouseApplication() diff --git a/src/packages_page/packages_page.py b/src/packages_page/packages_page.py index fd1ec8b..630cc24 100644 --- a/src/packages_page/packages_page.py +++ b/src/packages_page/packages_page.py @@ -1,4 +1,4 @@ -from gi.repository import Adw, Gtk, GLib, Gio +from gi.repository import Adw, Gtk, GLib, Gio, Gdk from .host_info import HostInfo from .app_row import AppRow from .error_toast import ErrorToast @@ -47,13 +47,13 @@ class PackagesPage(Adw.BreakpointBin): uninstall_button = gtc() properties_page = gtc() filters_page = gtc() - + # Referred to in the main window # It is used to determine if a new page should be made or not # This must be set to the created object from within the class's __init__ method instance = None page_name = "packages" - + def set_status(self, to_set): if to_set is self.scrolled_window: @@ -61,12 +61,12 @@ class PackagesPage(Adw.BreakpointBin): self.select_button.set_sensitive(True) self.filter_button.set_sensitive(True) self.filters_page.set_sensitive(True) - + self.search_button.set_sensitive(True) self.search_entry.set_editable(True) else: self.select_button.set_sensitive(False) - + if to_set is self.no_packages: self.properties_page.stack.set_visible_child(self.properties_page.error_tbv) self.filter_button.set_sensitive(False) @@ -78,7 +78,7 @@ class PackagesPage(Adw.BreakpointBin): self.filters_page.set_sensitive(True) if not self.packages_split.get_collapsed(): self.filter_button.set_active(True) - + if to_set is self.no_results: self.filters_page.set_sensitive(False) @@ -93,7 +93,7 @@ class PackagesPage(Adw.BreakpointBin): else: self.stack.set_visible_child(self.packages_split) self.status_stack.set_visible_child(to_set) - + def apply_filters(self): i = 0 show_apps = self.filter_settings.get_boolean("show-apps") @@ -112,20 +112,20 @@ class PackagesPage(Adw.BreakpointBin): visible = False if runtimes_list != "all" and (row.package.is_runtime or row.package.dependant_runtime and not row.package.dependant_runtime.info["ref"] in runtimes_list): visible = False - + row.set_visible(visible) if visible: total_visible += 1 else: row.check_button.set_active(False) - + if total_visible == 0: self.set_status(self.no_filter_results) else: GLib.idle_add(lambda *_: self.set_status(self.scrolled_window)) if self.current_row_for_properties and not self.current_row_for_properties.get_visible(): self.select_first_visible_row() - + def select_first_visible_row(self): first_visible_row = None i = 0 @@ -135,11 +135,11 @@ class PackagesPage(Adw.BreakpointBin): first_visible_row = row self.current_row_for_properties = row break - + if not first_visible_row is None: self.packages_list_box.select_row(first_visible_row) self.properties_page.set_properties(first_visible_row.package) - + def row_select_handler(self, row): if row.check_button.get_active(): self.selected_rows.append(row) @@ -154,17 +154,17 @@ class PackagesPage(Adw.BreakpointBin): self.packages_navpage.set_title(_("Packages")) self.copy_button.set_sensitive(False) self.uninstall_button.set_sensitive(False) - + def select_all_handler(self, *args): i = 0 while row := self.packages_list_box.get_row_at_index(i): i += 1 row.check_button.set_active(row.get_visible()) - + def row_rclick_handler(self, row): self.select_button.set_active(True) GLib.idle_add(lambda *_, button=row.check_button: button.set_active(not button.get_active())) - + def generate_list(self, *args): self.properties_page.nav_view.pop_to_page(self.properties_page.inner_nav_page) self.packages_list_box.remove_all() @@ -175,7 +175,7 @@ class PackagesPage(Adw.BreakpointBin): if len(HostInfo.flatpaks) == 0: self.set_status(self.no_packages) return - + for package in HostInfo.flatpaks: row = AppRow(package, self.row_rclick_handler) package.app_row = row @@ -191,19 +191,19 @@ class PackagesPage(Adw.BreakpointBin): self.packages_toast_overlay.add_toast(ErrorToast(_("Error getting Flatpak '{}'").format(package.info["name"]), str(e)).toast) self.packages_list_box.append(row) - + self.apply_filters() self.select_first_visible_row() - + self.scrolled_window.set_vadjustment(Gtk.Adjustment.new(0,0,0,0,0,0)) # Scroll list to top - + def row_activate_handler(self, list_box, row): self.properties_page.set_properties(row.package) self.properties_page.nav_view.pop() self.packages_split.set_show_content(True) self.filter_button.set_active(False) self.current_row_for_properties = row - + def filter_func(self, row): search_text = self.search_entry.get_text().lower() title = row.get_title().lower() @@ -211,14 +211,14 @@ class PackagesPage(Adw.BreakpointBin): if row.get_visible() and (search_text in title or search_text in subtitle): self.is_result = True return True - + def set_selection_mode(self, is_enabled): i = 0 while row := self.packages_list_box.get_row_at_index(i): i += 1 GLib.idle_add(row.check_button.set_active, False) GLib.idle_add(row.check_button.set_visible, is_enabled) - + def selection_copy(self, box, row): self.copy_pop.popdown() info = "" @@ -233,7 +233,7 @@ class PackagesPage(Adw.BreakpointBin): case self.copy_refs: info = "ref" feedback = _("Refs") - + to_copy = [] for row in self.selected_rows: to_copy.append(row.package.info[info]) @@ -243,8 +243,11 @@ class PackagesPage(Adw.BreakpointBin): self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Copied {}").format(feedback))) except Exception as e: self.packages_toast_overlay.add_toast(ErrorToast(_("Could not copy {}").format(feedback), str(e)).toast) - + def selection_uninstall(self, *args): + if len(self.selected_rows) < 1: + return + def on_response(should_trash): GLib.idle_add(lambda *_: self.set_status(self.uninstalling)) error = [None] @@ -256,7 +259,7 @@ class PackagesPage(Adw.BreakpointBin): cmd.append(row.package.info["ref"]) if should_trash and os.path.exists(row.package.data_path): to_trash.append(row.package.data_path) - + try: subprocess.run(cmd, check=True, capture_output=True) if should_trash and len(to_trash) > 0: @@ -265,7 +268,7 @@ class PackagesPage(Adw.BreakpointBin): error[0] = cpe except Exception as e: error[0] = e - + def callback(*args): self.main_window.refresh_handler() HostInfo.main_window.remove_refresh_lockout("batch uninstalling packages") @@ -274,49 +277,57 @@ class PackagesPage(Adw.BreakpointBin): GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(ErrorToast(_("Could not uninstall packages"), details).toast)) else: GLib.idle_add(lambda *args: self.packages_toast_overlay.add_toast(Adw.Toast(title=_("Uninstalled Packages")))) - + Gio.Task.new(None, None, callback).run_in_thread(thread) - + dialog = UninstallDialog(on_response, True) dialog.present(self.main_window) - + def start_loading(self): self.packages_navpage.set_title(_("Packages")) self.select_button.set_active(False) self.set_status(self.loading_packages) - + def end_loading(self): GLib.idle_add(lambda *_: self.generate_list()) - + def select_button_handler(self, button): self.set_selection_mode(button.get_active()) - + def filter_button_handler(self, button): if button.get_active(): self.content_stack.set_visible_child(self.filters_page) self.packages_split.set_show_content(True) else: self.content_stack.set_visible_child(self.properties_page) - + self.packages_split.set_show_content(False) + def filter_page_handler(self, *args): if self.packages_split.get_collapsed() and not self.packages_split.get_show_content(): self.filter_button.set_active(False) - + def on_invalidate(self, row): current_status = self.status_stack.get_visible_child() if not current_status is self.no_results: self.prev_status = current_status - + self.is_result = False self.packages_list_box.invalidate_filter() if self.is_result: self.set_status(self.prev_status) else: self.set_status(self.no_results) - + def sort_func(self, row1, row2): return row1.package.info["name"].lower() > row2.package.info["name"].lower() - + + def key_handler(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + if self.select_button.get_active(): + self.select_button.set_active(False) + elif self.filter_button.get_active(): + self.filter_button.set_active(False) + def __init__(self, main_window, **kwargs): super().__init__(**kwargs) @@ -334,8 +345,10 @@ class PackagesPage(Adw.BreakpointBin): self.prev_status = None self.selected_rows = [] self.current_row_for_properties = None + event_controller = Gtk.EventControllerKey() # Apply + self.add_controller(event_controller) self.loading_view.set_content(self.loading_packages) self.packages_list_box.set_filter_func(self.filter_func) self.packages_list_box.set_sort_func(self.sort_func) @@ -344,6 +357,7 @@ class PackagesPage(Adw.BreakpointBin): self.__class__.instance = self # Connections + event_controller.connect("key-pressed", self.key_handler) self.search_entry.connect("search-changed", self.on_invalidate) self.search_bar.set_key_capture_widget(main_window) self.packages_list_box.connect("row-activated", self.row_activate_handler) diff --git a/src/packages_page/uninstall_dialog.py b/src/packages_page/uninstall_dialog.py index 7b4b2ac..2c1969d 100644 --- a/src/packages_page/uninstall_dialog.py +++ b/src/packages_page/uninstall_dialog.py @@ -4,30 +4,39 @@ from gi.repository import Adw, Gtk, GLib, Gio, Pango class UninstallDialog(Adw.AlertDialog): __gtype_name__ = "UninstallDialog" gtc = Gtk.Template.Child - + group = gtc() trash = gtc() - + is_open = False + def on_response(self, dialog, response): + self.__class__.is_open = False if response != "continue": return - + self.continue_callback(self.trash.get_active()) - + + def present(self, *args, **kwargs): + if self.__class__.is_open: + return + + self.__class__.is_open = True + super().present(*args, **kwargs) + def __init__(self, continue_callback, show_trash_option, package_name=None, **kwargs): super().__init__(**kwargs) - + if package_name: self.set_heading(GLib.markup_escape_text(_("Uninstall {}?").format(package_name))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use {} after removal").format(package_name))) else: self.set_heading(GLib.markup_escape_text(_("Uninstall Packages?"))) self.set_body(GLib.markup_escape_text(_("It will not be possible to use these packages after removal"))) - + self.continue_callback = continue_callback self.add_response("cancel", _("Cancel")) self.add_response("continue", _("Uninstall")) self.set_response_appearance("continue", Adw.ResponseAppearance.DESTRUCTIVE) self.connect("response", self.on_response) self.group.set_title(GLib.markup_escape_text(_("App Settings & Content"))) - self.group.set_visible(show_trash_option) \ No newline at end of file + self.group.set_visible(show_trash_option) diff --git a/src/remotes_page/add_remote_dialog.py b/src/remotes_page/add_remote_dialog.py index 41b8273..654e915 100644 --- a/src/remotes_page/add_remote_dialog.py +++ b/src/remotes_page/add_remote_dialog.py @@ -18,6 +18,7 @@ class AddRemoteDialog(Adw.Dialog): name_row = gtc() url_row = gtc() installation_chooser = gtc() + is_open = False def on_apply(self, *args): self.parent_page.status_stack.set_visible_child(self.parent_page.adding_view) @@ -75,6 +76,16 @@ class AddRemoteDialog(Adw.Dialog): self.apply_button.set_sensitive(self.title_passes and self.name_passes and self.url_passes) + def present(self, *args, **kwargs): + if self.__class__.is_open: + return + + self.__class__.is_open = True + super().present(*args, **kwargs) + + def on_close(self, *args): + self.__class__.is_open = False + def __init__(self, main_window, parent_page, remote_info=None, **kwargs): super().__init__(**kwargs) @@ -112,6 +123,7 @@ class AddRemoteDialog(Adw.Dialog): self.apply_button.set_sensitive(False) # Connections + self.connect("closed", self.on_close) self.cancel_button.connect("clicked", lambda *_: self.close()) self.apply_button.connect("clicked", self.on_apply) self.title_row.connect("changed", self.check_entries) diff --git a/src/remotes_page/remotes_page.py b/src/remotes_page/remotes_page.py index c5d872d..9357400 100644 --- a/src/remotes_page/remotes_page.py +++ b/src/remotes_page/remotes_page.py @@ -260,6 +260,9 @@ class RemotesPage(Adw.NavigationPage): total_visible += 1 self.none_visible.set_visible(total_visible == 0) + + def new_custom_handler(self, *args): + AddRemoteDialog(self.main_window, self).present(self.main_window) def __init__(self, main_window, **kwargs): super().__init__(**kwargs) @@ -274,7 +277,7 @@ class RemotesPage(Adw.NavigationPage): # Connections self.file_remote_row.connect("activated", lambda *_: self.add_file_handler()) - self.custom_remote_row.connect("activated", lambda *_: AddRemoteDialog(main_window, self).present(main_window)) + self.custom_remote_row.connect("activated", self.new_custom_handler) self.search_entry.connect("search-changed", self.on_search) self.show_disabled_button.connect("toggled", self.show_disabled_handler) diff --git a/src/snapshot_page/new_snapshot_dialog.py b/src/snapshot_page/new_snapshot_dialog.py index e4a8a16..f7d25c8 100644 --- a/src/snapshot_page/new_snapshot_dialog.py +++ b/src/snapshot_page/new_snapshot_dialog.py @@ -24,6 +24,7 @@ class NewSnapshotDialog(Adw.Dialog): scrolled_window = gtc() no_results = gtc() stack = gtc() + is_open = False def row_gesture_handler(self, row): row.check_button.set_active(not row.check_button.get_active()) @@ -68,6 +69,7 @@ class NewSnapshotDialog(Adw.Dialog): return False def on_close(self, *args): + self.__class__.is_open = False self.search_button.set_active(False) for row in self.selected_rows.copy(): GLib.idle_add(lambda *_, row=row: row.check_button.set_active(False)) @@ -149,16 +151,20 @@ class NewSnapshotDialog(Adw.Dialog): row.set_activatable(False) self.selected_rows.append(row) self.listbox.append(row) - + def enter_handler(self, *args): if self.create_button.get_sensitive(): self.create_button.activate() def present(self, *args, **kwargs): + if self.__class__.is_open: + return + super().present(*args, **kwargs) + self.__class__.is_open = True if not self.search_button.get_visible(): self.name_entry.grab_focus() - + def __init__(self, snapshot_page, loading_status, on_done=None, packages=None, **kwargs): super().__init__(**kwargs) diff --git a/src/snapshot_page/snapshot_box.py b/src/snapshot_page/snapshot_box.py index 9970049..08879bb 100644 --- a/src/snapshot_page/snapshot_box.py +++ b/src/snapshot_page/snapshot_box.py @@ -8,7 +8,7 @@ import os, subprocess, json, re class SnapshotBox(Gtk.Box): __gtype_name__ = "SnapshotBox" gtc = Gtk.Template.Child - + title = gtc() date = gtc() version = gtc() @@ -18,7 +18,7 @@ class SnapshotBox(Gtk.Box): rename_entry = gtc() apply_rename = gtc() trash_button = gtc() - + def create_json(self): try: data = { @@ -31,7 +31,7 @@ class SnapshotBox(Gtk.Box): except Exception as e: self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) - + def update_json(self, key, value): try: with open(self.json_path, 'r+') as file: @@ -40,14 +40,14 @@ class SnapshotBox(Gtk.Box): file.seek(0) json.dump(data, file, indent=4) file.truncate() - + except Exception as e: self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) - + def load_from_json(self): if not os.path.exists(self.json_path): self.create_json() - + try: with open(self.json_path, 'r') as file: data = json.load(file) @@ -59,15 +59,15 @@ class SnapshotBox(Gtk.Box): except Exception as e: self.toast_overlay.add_toast(ErrorToast(_("Could not write data"), str(e)).toast) - + def on_rename(self, widget): if not self.valid_checker(): return - + self.update_json('name', self.rename_entry.get_text().strip()) self.load_from_json() self.rename_menu.popdown() - + def valid_checker(self, *args): text = self.rename_entry.get_text().strip() valid = not ("/" in text or "\0" in text) and len(text) > 0 @@ -78,10 +78,13 @@ class SnapshotBox(Gtk.Box): self.rename_entry.add_css_class("error") return valid - + def on_trash(self, button): error = [None] path = f"{self.snapshots_path}{self.folder}" + if self.snapshot_page.is_trash_dialog_open: + return + def thread(*args): try: subprocess.run(['gio', 'trash', path], capture_output=True, text=True, check=True) @@ -89,21 +92,23 @@ class SnapshotBox(Gtk.Box): error[0] = cpe.stderr except Exception as e: error[0] = str(e) - + def callback(*args): if not error[0] is None: self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshot"), error[0]).toast) return - + self.parent_page.on_trash() self.toast_overlay.add_toast(Adw.Toast.new(_("Trashed snapshot"))) - + def on_response(_, response): + self.snapshot_page.is_trash_dialog_open = False if response != "continue": return - + Gio.Task.new(None, None, callback).run_in_thread(thread) - + + self.snapshot_page.is_trash_dialog_open = True dialog = Adw.AlertDialog(heading=_("Trash Snapshot?"), body=_("This snapshot will be sent to the trash")) dialog.add_response("cancel", _("Cancel")) dialog.add_response("continue", _("Trash")) @@ -128,7 +133,7 @@ class SnapshotBox(Gtk.Box): return False # Stop the timeout else: return True # Continue the timeout - + def on_apply(self, button): def on_response(dialog, response): if response != "continue": @@ -141,7 +146,7 @@ class SnapshotBox(Gtk.Box): self.snapshot_page.workers.append(self.worker) self.worker.extract() GLib.timeout_add(200, self.get_fraction) - + has_data = os.path.exists(self.worker.new_path) dialog = Adw.AlertDialog( heading=_("Apply Snapshot?"), @@ -154,7 +159,7 @@ class SnapshotBox(Gtk.Box): def __init__(self, parent_page, folder, snapshots_path, toast_overlay, **kwargs): super().__init__(**kwargs) - + self.snapshot_page = parent_page.parent_page self.toast_overlay = toast_overlay self.app_id = snapshots_path.split('/')[-2].strip() @@ -163,11 +168,11 @@ class SnapshotBox(Gtk.Box): new_path=f"{HostInfo.home}/.var/app/{self.app_id}/", toast_overlay=self.toast_overlay, ) - + split_folder = folder.split('_') if len(split_folder) < 2: return - + self.parent_page = parent_page self.folder = folder self.snapshots_path = snapshots_path diff --git a/src/snapshot_page/snapshot_page.py b/src/snapshot_page/snapshot_page.py index aebdcc7..c06c172 100644 --- a/src/snapshot_page/snapshot_page.py +++ b/src/snapshot_page/snapshot_page.py @@ -1,4 +1,4 @@ -from gi.repository import Adw, Gtk, GLib, Gio +from gi.repository import Adw, Gtk, GLib, Gio, Gdk from .host_info import HostInfo from .error_toast import ErrorToast from .app_row import AppRow @@ -12,7 +12,7 @@ import os, subprocess class LeftoverSnapshotRow(Adw.ActionRow): __gtype_name__ = "LeftoverSnapshotRow" - + def idle_stuff(self): self.set_title(self.name) icon = Gtk.Image.new_from_icon_name("application-x-executable-symbolic") @@ -22,7 +22,7 @@ class LeftoverSnapshotRow(Adw.ActionRow): def gesture_handler(self, *args): self.on_long_press(self) - + def __init__(self, folder, on_long_press, **kwargs): super().__init__(**kwargs) @@ -44,7 +44,7 @@ class LeftoverSnapshotRow(Adw.ActionRow): # Connections self.rclick_gesture.connect("released", self.gesture_handler) self.long_press_gesture.connect("pressed", self.gesture_handler) - + @Gtk.Template(resource_path="/io/github/flattool/Warehouse/snapshot_page/snapshot_page.ui") class SnapshotPage(Adw.BreakpointBin): __gtype_name__ = "SnapshotPage" @@ -87,6 +87,7 @@ class SnapshotPage(Adw.BreakpointBin): # This must be set to the created object from within the class's __init__ method instance = None page_name = "snapshots" + is_trash_dialog_open = False def sort_snapshots(self, *args): self.active_snapshot_paks.clear() @@ -126,11 +127,11 @@ class SnapshotPage(Adw.BreakpointBin): subprocess.run(['gio', 'trash', f'{HostInfo.snapshots_path}{folder}']) except Exception: pass - + def long_press_handler(self, row): self.select_button.set_active(True) row.check_button.set_active(not row.check_button.get_active()) - + def generate_active_list(self): for pak in self.active_snapshot_paks: row = AppRow(pak, self.long_press_handler) @@ -359,7 +360,7 @@ class SnapshotPage(Adw.BreakpointBin): if len(packages) == 0: self.toast_overlay.add_toast(Adw.Toast(title=_("No apps in your selection can be snapshotted"))) return - + self.new_snapshot_dialog = NewSnapshotDialog(self, self.snapshotting_status, self.refresh, packages) self.new_snapshot_dialog.present(HostInfo.main_window) @@ -378,7 +379,7 @@ class SnapshotPage(Adw.BreakpointBin): id_to_tar[app_id] = tarlist if len(tarlist) < 1: id_to_tar.pop(app_id, None) - + return id_to_tar def get_total_fraction(self): @@ -422,7 +423,7 @@ class SnapshotPage(Adw.BreakpointBin): biggest_tar = tar id_to_tar[app_id] = tar - + for app_id, tar in id_to_tar.items(): worker = TarWorker( existing_path=f"{HostInfo.snapshots_path}{app_id}/{tar}", @@ -431,7 +432,7 @@ class SnapshotPage(Adw.BreakpointBin): ) self.workers.append(worker) worker.extract() - + if len(self.workers) > 0: self.snapshotting_status.title_label.set_label(_("Applying Snapshots")) self.snapshotting_status.progress_bar.set_fraction(0.0) @@ -456,7 +457,14 @@ class SnapshotPage(Adw.BreakpointBin): AttemptInstallDialog(package_names, lambda is_valid: self.select_button.set_active(not is_valid)) def select_trash_handler(self): + if ( + self.is_trash_dialog_open + or len(self.selected_active_rows) + len(self.selected_leftover_rows) < 1 + ): + return + def on_response(dialog, response): + self.is_trash_dialog_open = False to_trash = [] if response != "continue": return @@ -474,7 +482,8 @@ class SnapshotPage(Adw.BreakpointBin): self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed snapshots"))) except subprocess.CalledProcessError as cpe: self.toast_overlay.add_toast(ErrorToast(_("Could not trash snapshots"), cpe.stderr).toast) - + + self.is_trash_dialog_open = True dialog = Adw.AlertDialog(heading=_("Trash Snapshots?"), body=_("These apps' snapshots will be sent to the trash")) dialog.add_response("cancel", _("Cancel")) dialog.add_response("continue", _("Trash")) @@ -494,7 +503,11 @@ class SnapshotPage(Adw.BreakpointBin): self.install_handler() case self.trash_snapshots: self.select_trash_handler() - + + def key_handler(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + self.select_button.set_active(False) + def __init__(self, main_window, **kwargs): super().__init__(**kwargs) @@ -509,8 +522,19 @@ class SnapshotPage(Adw.BreakpointBin): self.list_page = SnapshotsListPage(self) self.snapshotting_status = LoadingStatus("Initial Title", _("This could take a while"), True, self.on_cancel) self.new_snapshot_dialog = None + event_controller = Gtk.EventControllerKey() + + # Apply + self.add_controller(event_controller) + self.search_bar.set_key_capture_widget(HostInfo.main_window) + self.loading_view.set_content(LoadingStatus(_("Loading Snapshots"), _("This should only take a moment"))) + self.snapshotting_view.set_content(self.snapshotting_status) + self.split_view.set_content(self.list_page) + self.active_listbox.set_sort_func(self.sort_func) + self.leftover_listbox.set_sort_func(self.sort_func) # Connections + event_controller.connect("key-pressed", self.key_handler) self.active_listbox.connect("row-activated", self.active_select_handler) self.leftover_listbox.connect("row-activated", self.leftover_select_handler) self.open_button.connect("clicked", self.open_snapshots_folder) @@ -522,11 +546,3 @@ class SnapshotPage(Adw.BreakpointBin): self.select_all_button.connect("clicked", self.select_all_handler) self.copy_button.connect("clicked", self.select_copy_handler) self.more_menu.connect("row-activated", self.more_menu_handler) - - # Apply - self.search_bar.set_key_capture_widget(HostInfo.main_window) - self.loading_view.set_content(LoadingStatus(_("Loading Snapshots"), _("This should only take a moment"))) - self.snapshotting_view.set_content(self.snapshotting_status) - self.split_view.set_content(self.list_page) - self.active_listbox.set_sort_func(self.sort_func) - self.leftover_listbox.set_sort_func(self.sort_func) diff --git a/src/user_data_page/user_data_page.py b/src/user_data_page/user_data_page.py index be54381..a6861ad 100644 --- a/src/user_data_page/user_data_page.py +++ b/src/user_data_page/user_data_page.py @@ -1,4 +1,4 @@ -from gi.repository import Adw, Gtk, GLib, Gio, Pango +from gi.repository import Adw, Gtk, GLib, Gio, Gdk from .error_toast import ErrorToast from .data_box import DataBox from .data_subpage import DataSubpage @@ -12,7 +12,7 @@ import os, subprocess class UserDataPage(Adw.BreakpointBin): __gtype_name__ = 'UserDataPage' gtc = Gtk.Template.Child - + bpt = gtc() status_stack = gtc() loading_view = gtc() @@ -52,6 +52,7 @@ class UserDataPage(Adw.BreakpointBin): page_name = "user-data" data_path = f"{HostInfo.home}/.var/app" bpt_is_applied = False + is_trash_dialog_open = False def sort_data(self, *args): self.data_flatpaks.clear() @@ -156,6 +157,7 @@ class UserDataPage(Adw.BreakpointBin): def trash_handler(self, *args): error = [None] + child = self.stack.get_visible_child() def thread(path): cmd = ['gio', 'trash'] + path @@ -175,10 +177,10 @@ class UserDataPage(Adw.BreakpointBin): self.toast_overlay.add_toast(Adw.Toast(title=_("Trashed data"))) def on_response(dialog, response): + self.is_trash_dialog_open = False if response != "continue": return - child = self.stack.get_visible_child() to_trash = [] for box in child.selected_boxes: to_trash.append(box.data_path) @@ -191,6 +193,10 @@ class UserDataPage(Adw.BreakpointBin): child.set_visible_child(child.loading_data) Gio.Task.new(None, None, callback).run_in_thread(lambda *_: thread(to_trash)) + if len(child.selected_boxes) < 1 or self.is_trash_dialog_open: + return + + self.is_trash_dialog_open = True dialog = Adw.AlertDialog(heading=_("Trash Data?"), body=_("Data will be sent to the trash")) dialog.add_response("cancel", _("Cancel")) dialog.add_response("continue", _("Continue")) @@ -230,6 +236,10 @@ class UserDataPage(Adw.BreakpointBin): self.install_handler() case self.more_trash: self.trash_handler() + + def key_handler(self, controller, keyval, keycode, state): + if keyval == Gdk.KEY_Escape: + self.select_button.set_active(False) def __init__(self, main_window, **kwargs): super().__init__(**kwargs) @@ -243,17 +253,19 @@ class UserDataPage(Adw.BreakpointBin): self.leftover_data = [] self.total_items = 0 self.settings = Gio.Settings.new("io.github.flattool.Warehouse.data_page") - self.sort_modes_to_buttons = { "name": self.sort_name, "id": self.sort_id, "size": self.sort_size, } self.buttons_to_sort_modes = {} + event_controller = Gtk.EventControllerKey() + + # Apply + self.add_controller(event_controller) for key, button in self.sort_modes_to_buttons.items(): self.buttons_to_sort_modes[button] = key - # Apply self.stack.add_titled_with_icon( child=self.adp, name="active", @@ -268,6 +280,7 @@ class UserDataPage(Adw.BreakpointBin): ) # Connections + event_controller.connect("key-pressed", self.key_handler) self.open_button.connect("clicked", self.open_data_folder) self.stack.connect("notify::visible-child", self.view_change_handler) self.select_button.connect("toggled", self.select_toggle_handler)