From 0dfd727222e5bc9fb7f6b1325b016d6d24dd1957 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:33:39 +0900 Subject: [PATCH 1/9] change: use enum for popup mode --- helpers/ui.py | 70 ++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 9508760..cb5542d 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -19,7 +19,7 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango from enum import Enum -# 5.5.0 +# 5.6.0 app_name = "DZGUI" start_time = 0 @@ -297,25 +297,25 @@ def process_shell_return_code(transient_parent, msg, code, original_input): #TODO: add logger output to each case 0: # success with notice popup - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) case 1: # error with notice popup if msg == "": msg = "Something went wrong" - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) case 2: # warn and recurse (e.g. validation failed) - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(original_input, treeview) case 4: # for BM only - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(["Options", "Change Battlemetrics API key"], treeview) case 5: # for steam only - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) treeview = transient_parent.grid.scrollable_treelist.treeview process_tree_option(["Options", "Change Steam API key"], treeview) case 6: @@ -330,17 +330,17 @@ def process_shell_return_code(transient_parent, msg, code, original_input): config_vals.append(i) tooltip = format_metadata(col) transient_parent.grid.update_statusbar(tooltip) - spawn_dialog(transient_parent, msg, "NOTIFY") + spawn_dialog(transient_parent, msg, Popup.NOTIFY) return case 100: # final handoff before launch - final_conf = spawn_dialog(transient_parent, msg, "CONFIRM") + final_conf = spawn_dialog(transient_parent, msg, Popup.CONFIRM) treeview = transient_parent.grid.scrollable_treelist.treeview if final_conf == 1 or final_conf is None: return process_tree_option(["Handshake", ""], treeview) case 255: - spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", "NOTIFY") + spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY) Gtk.main_quit() @@ -369,7 +369,7 @@ def process_tree_option(input, treeview): proc = call_out(transient_parent, subproc, args) GLib.idle_add(_load) if bool is True: - wait_dialog = GenericDialog(transient_parent, msg, "WAIT") + wait_dialog = GenericDialog(transient_parent, msg, Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog)) thread.start() @@ -432,7 +432,7 @@ def process_tree_option(input, treeview): link_label = "Open Battlemetrics API page" prompt = "Enter new API key" - user_entry = EntryDialog(transient_parent, prompt, "ENTRY", link_label) + user_entry = EntryDialog(transient_parent, prompt, Popup.ENTRY, link_label) res = user_entry.get_input() if res is None: logger.info("User aborted entry dialog") @@ -711,7 +711,7 @@ class TreeView(Gtk.TreeView): iter = self.get_current_iter() server_store.remove(iter) msg = proc.stdout - res = spawn_dialog(parent, msg, "NOTIFY") + res = spawn_dialog(parent, msg, Popup.NOTIFY) case "Remove from history": record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8)) call_out(parent, context_menu_label, record) @@ -734,16 +734,16 @@ class TreeView(Gtk.TreeView): conf_msg = "Really delete the mod '%s'?" %(value) success_msg = "Successfully deleted the mod '%s'." %(value) fail_msg = "An error occurred during deletion. Aborting." - res = spawn_dialog(parent, conf_msg, "CONFIRM") + res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) symlink = self.get_column_at_index(1) dir = self.get_column_at_index(2) if res == 0: proc = call_out(parent, "delete", symlink, dir) if proc.returncode == 0: - spawn_dialog(parent, success_msg, "NOTIFY") + spawn_dialog(parent, success_msg, Popup.NOTIFY) self._update_quad_column("List installed mods") else: - spawn_dialog(parent, fail_msg, "NOTIFY") + spawn_dialog(parent, fail_msg, Popup.NOTIFY) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) @@ -805,7 +805,7 @@ class TreeView(Gtk.TreeView): diff = now - then cooldown = 30 - math.floor(diff) if ((start_time > 0) and (now - then) < 30): - spawn_dialog(parent, "Global refresh cooldown not met. Wait %s second(s)." %(str(cooldown)), "NOTIFY") + spawn_dialog(parent, "Global refresh cooldown not met. Wait %s second(s)." %(str(cooldown)), Popup.NOTIFY) return start_time = now @@ -935,7 +935,7 @@ class TreeView(Gtk.TreeView): wait_dialog.destroy() parent = self.get_outer_window() - wait_dialog = GenericDialog(parent, "Refreshing player count", "WAIT") + wait_dialog = GenericDialog(parent, "Refreshing player count", Popup.WAIT) wait_dialog.show_all() select = self.get_selection() sels = select.get_selected_rows() @@ -971,7 +971,7 @@ class TreeView(Gtk.TreeView): No servers returned. Possible network issue or API key on cooldown? Return to the main menu, wait 60s, and try again. If this issue persists, your API key may be defunct.""" - spawn_dialog(self.get_outer_window(), textwrap.dedent(api_warn_msg), "NOTIFY") + spawn_dialog(self.get_outer_window(), textwrap.dedent(api_warn_msg), Popup.NOTIFY) grid = self.get_outer_grid() right_panel = grid.right_panel @@ -1097,7 +1097,7 @@ class TreeView(Gtk.TreeView): return mode = mode + ":" + port - wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Fetching server metadata", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, mode)) thread.start() @@ -1153,12 +1153,12 @@ class TreeView(Gtk.TreeView): data = call_out(self, "show_log") res = parse_log_rows(data) if res == 1: - spawn_dialog(self.get_outer_window(), "Failed to load log file, possibly corrupted", "NOTIFY") + spawn_dialog(self.get_outer_window(), "Failed to load log file, possibly corrupted", Popup.NOTIFY) return transient_parent = self.get_outer_window() - wait_dialog = GenericDialog(transient_parent, "Checking mods", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Checking mods", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background_quad, args=(wait_dialog, mode)) thread.start() @@ -1181,7 +1181,7 @@ class TreeView(Gtk.TreeView): qport = self.get_column_at_index(8) record = "%s:%s" %(addr, str(qport)) - wait_dialog = GenericDialog(transient_parent, "Querying server and aligning mods", "WAIT") + wait_dialog = GenericDialog(transient_parent, "Querying server and aligning mods", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background_connection, args=(wait_dialog, record)) thread.start() @@ -1205,7 +1205,7 @@ class TreeView(Gtk.TreeView): if chosen_row == "Server browser": cooldown = call_out(self, "test_cooldown", "", "") if cooldown.returncode == 1: - spawn_dialog(self.get_outer_window(), cooldown.stdout, "NOTIFY") + spawn_dialog(self.get_outer_window(), cooldown.stdout, Popup.NOTIFY) return 1 for check in checks: toggle_signal(filters_vbox, check, '_on_check_toggle', False) @@ -1323,7 +1323,7 @@ def filter_servers(transient_parent, filters_vbox, treeview, context): toggle_signal(filters_vbox, filters_vbox, '_on_button_release', False) toggle_signal(filters_vbox, filters_vbox.maps_combo, '_on_map_changed', False) - dialog = GenericDialog(transient_parent, "Filtering results", "WAIT") + dialog = GenericDialog(transient_parent, "Filtering results", Popup.WAIT) dialog.show_all() server_store.clear() @@ -1343,6 +1343,12 @@ class Port(Enum): DEFAULT = 1 CUSTOM = 2 +class Popup(Enum): + WAIT = 1 + NOTIFY = 2 + CONFIRM = 3 + ENTRY = 4 + class GenericDialog(Gtk.MessageDialog): def __init__(self, parent, text, mode): @@ -1351,19 +1357,19 @@ class GenericDialog(Gtk.MessageDialog): return True match mode: - case "WAIT": + case Popup.WAIT: dialog_type = Gtk.MessageType.INFO button_type = Gtk.ButtonsType.NONE header_text = "Please wait" - case "NOTIFY": + case Popup.NOTIFY: dialog_type = Gtk.MessageType.INFO button_type = Gtk.ButtonsType.OK header_text = "Notice" - case "CONFIRM": + case Popup.CONFIRM: dialog_type = Gtk.MessageType.QUESTION button_type = Gtk.ButtonsType.OK_CANCEL header_text = "Confirmation" - case "ENTRY": + case Popup.ENTRY: dialog_type = Gtk.MessageType.QUESTION button_type = Gtk.ButtonsType.OK_CANCEL header_text = "User input required" @@ -1384,7 +1390,7 @@ class GenericDialog(Gtk.MessageDialog): modal=True, ) - if mode == "WAIT": + if mode == Popup.WAIT: dialogBox = self.get_content_area() spinner = Gtk.Spinner() dialogBox.pack_end(spinner, False, False, 0) @@ -1598,7 +1604,7 @@ class PingDialog(GenericDialog): dialogBox = self.get_content_area() self.set_default_response(Gtk.ResponseType.OK) self.set_size_request(500, 200) - wait_dialog = GenericDialog(parent, "Checking ping", "WAIT") + wait_dialog = GenericDialog(parent, "Checking ping", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread.start() @@ -1642,7 +1648,7 @@ class ModDialog(GenericDialog): column.set_sort_column_id(i) dialogBox.pack_end(self.scrollable, True, True, 0) - wait_dialog = GenericDialog(parent, "Fetching modlist", "WAIT") + wait_dialog = GenericDialog(parent, "Fetching modlist", Popup.WAIT) wait_dialog.show_all() thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread.start() @@ -1651,7 +1657,7 @@ class ModDialog(GenericDialog): def _load(): dialog.destroy() if data.returncode == 1: - spawn_dialog(parent, "Server has no mods installed or is unsupported in this mode", "NOTIFY") + spawn_dialog(parent, "Server has no mods installed or is unsupported in this mode", Popup.NOTIFY) return self.show_all() self.set_markup("Modlist (%s mods)" %(mod_count)) From 031df3a62f931cc71e9dbf6a73ab41c6f976afc6 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:35:50 +0900 Subject: [PATCH 2/9] fix: batch MSQ queries --- helpers/funcs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index b69640f..b0c57d7 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -o pipefail -version=5.5.0 +version=5.6.0 #CONSTANTS aid=221100 @@ -38,6 +38,7 @@ lock_file="$state_path/$prefix.lock" #CACHE cache_dir="$HOME/.cache/$app_name" _cache_servers="$cache_dir/$prefix.servers" +_cache_temp="$cache_dir/$prefix.temp" _cache_my_servers="$cache_dir/$prefix.my_servers" _cache_history="$cache_dir/$prefix.history" _cache_launch="$cache_dir/$prefix.launch_mods" @@ -380,9 +381,30 @@ get_dist(){ fi } get_remote_servers(){ - local limit=20000 - local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=$limit&key=$steam_api" - curl -Ls "$url" | jq -r '.response.servers' + params=( + "\\nor\1\map\chernarusplus\\nor\1\map\sakhal" + "\map\chernarusplus\empty\1" + "\map\chernarusplus\noplayers\1" + "\map\\sakhal" + ) + local limit=10000 + local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?" + + _fetch(){ + local param="$1" + curl -LsG "$url" \ + -d filter="\appid\221100${param}" \ + -d limit=$limit \ + -d key=$steam_api \ + | jq -M -r '.response.servers' + + } + + for ((i=0; i <${#params[@]}; i++ )); do + _fetch "${params[$i]}" > $_cache_temp.${i} + done + + jq -n '[ [inputs]|add ].[]' $_cache_temp.* && rm $_cache_temp.* } get_unique_maps(){ shift From 1eed01623a97637e9e8369fcbfa52f4854c71270 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 05:53:44 +0900 Subject: [PATCH 3/9] chore: clarify timeout message --- helpers/funcs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/funcs b/helpers/funcs index b0c57d7..59c3718 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -671,7 +671,7 @@ test_ping(){ local qport="$2" local res res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}') - [[ ! $? -eq 0 ]] && res="Unreachable" + [[ ! $? -eq 0 ]] && res="Timed out" printf "%s" "$res" } show_server_modlist(){ From 50d6bdf74f44ba2a912dc088abe505bf4109cf3f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 06:19:37 +0900 Subject: [PATCH 4/9] feat: add pluralize func --- helpers/ui.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index cb5542d..ae27bd4 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -169,6 +169,14 @@ status_tooltip = { "Hall of fame ⧉": "A list of significant contributors and testers", } +def pluralize(plural, count): + suffix = plural[-2:] + if suffix == "es": + base = plural[:-2] + return f"%s{'es'[:2*count^2]}" %(base) + else: + base = plural[:-1] + return f"%s{'s'[:count^1]}" %(base) def format_ping(ping): ms = " | Ping: %s" %(ping) @@ -997,7 +1005,8 @@ class TreeView(Gtk.TreeView): self.set_model(mod_store) self.grab_focus() size = locale.format_string('%.3f', total_size, grouping=True) - grid.update_statusbar("Found %s mods taking up %s MiB" %(f'{total_mods:n}', size)) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") toggle_signal(self, self, '_on_keypress', True) grid = self.get_outer_grid() @@ -1286,16 +1295,10 @@ def format_metadata(row_sel): return prefix -def format_tooltip(sum, hits): - if hits == 1: - hit_suffix = "match" - else: - hit_suffix = "matches" - if sum == 1: - player_suffix = "player" - else: - player_suffix = "players" - tooltip = "Found %s %s with %s %s" %(f'{hits:n}', hit_suffix, f'{sum:n}', player_suffix) +def format_tooltip(players, hits): + hits_pretty = pluralize("matches", hits) + players_pretty = pluralize("players", players) + tooltip = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}" return tooltip From 365b455b3c4c3595d4db280575af8d1c9a4e7022 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:34:05 +0900 Subject: [PATCH 5/9] feat: bulk mod deletion --- helpers/ui.py | 166 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 16 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index ae27bd4..99d8102 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -48,12 +48,14 @@ default_tooltip = "Select a row to see its detailed description" server_tooltip = [None, None] user_path = os.path.expanduser('~') +cache_path = '%s/.cache/dzgui' %(user_path) state_path = '%s/.local/state/dzgui' %(user_path) helpers_path = '%s/.local/share/dzgui/helpers' %(user_path) log_path = '%s/logs' %(state_path) changelog_path = '%s/CHANGELOG.md' %(state_path) geometry_path = '%s/dzg.cols.json' %(state_path) funcs = '%s/funcs' %(helpers_path) +mods_temp_file = '%s/dzg.mods_temp' %(cache_path) logger = logging.getLogger(__name__) log_file = '%s/DZGUI_DEBUG.log' %(log_path) @@ -169,6 +171,32 @@ status_tooltip = { "Hall of fame ⧉": "A list of significant contributors and testers", } +def relative_widget(child): + # get target widget relative to source widget + # chiefly used for transient modals and parent member functions + # when calling non-adjacent widgets + # positions are relative to grid members + grid = child.get_parent().get_parent() + treeview = grid.scrollable_treelist.treeview + outer = grid.get_parent() + + widgets = { + 'grid': grid, + 'treeview': treeview, + 'outer': outer + } + + supported = [ + "ModSelectionPanel", + "ButtonBox", + "TreeView" + ] + + if child.__class__.__name__ not in supported: + raise Exception("Unsupported child widget") + + return widgets + def pluralize(plural, count): suffix = plural[-2:] if suffix == "es": @@ -571,10 +599,17 @@ class ButtonBox(Gtk.Box): def _update_single_column(self, context): logger.info("Returning from multi-column view to monocolumn view for the context '%s'" %(context)) - treeview = self.get_treeview() + widgets = relative_widget(self) + + # only applicable when returning from mod list + grid = widgets["grid"] + grid.right_panel.remove(grid.sel_panel) right_panel = self.get_parent() right_panel.set_filter_visibility(False) + treeview = widgets["treeview"] + treeview.set_selection_mode(Gtk.SelectionMode.SINGLE) + """Block maps combo when returning to main menu""" toggle_signal(right_panel.filters_vbox, right_panel.filters_vbox.maps_combo, '_on_map_changed', False) right_panel.filters_vbox.keyword_entry.set_text("") @@ -749,14 +784,30 @@ class TreeView(Gtk.TreeView): proc = call_out(parent, "delete", symlink, dir) if proc.returncode == 0: spawn_dialog(parent, success_msg, Popup.NOTIFY) - self._update_quad_column("List installed mods") + self.update_quad_column("List installed mods") else: spawn_dialog(parent, fail_msg, Popup.NOTIFY) case "Open in Steam Workshop": record = self.get_column_at_index(2) call_out(parent, "open_workshop_page", record) + def toggle_selection(self, bool): + l = len(mod_store) + match bool: + case True: + for i in range (0, l): + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + case False: + for i in range (0, l): + path = Gtk.TreePath(i) + it = mod_store.get_iter(path) + self.get_selection().unselect_path(path) + def _on_button_release(self, widget, event): + if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: + return try: pathinfo = self.get_path_at_pos(event.x, event.y) if pathinfo is None: @@ -766,8 +817,6 @@ class TreeView(Gtk.TreeView): except AttributeError: pass - if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3: - return context = self.get_first_col() self.menu = Gtk.Menu() @@ -861,8 +910,6 @@ class TreeView(Gtk.TreeView): self.emit("on_distcalc_started") self.current_proc = CalcDist(self, addr, self.queue, cache) self.current_proc.start() - elif None: - return else: tooltip = format_metadata(row_sel) grid.update_statusbar(tooltip) @@ -1004,16 +1051,28 @@ class TreeView(Gtk.TreeView): dialog.destroy() self.set_model(mod_store) self.grab_focus() - size = locale.format_string('%.3f', total_size, grouping=True) - pretty = pluralize("mods", total_mods) - grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + if abort is False: + size = locale.format_string('%.3f', total_size, grouping=True) + pretty = pluralize("mods", total_mods) + grid.update_statusbar(f"Found {total_mods:n} {pretty} taking up {size} MiB") + #2024-11-12 + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) grid = self.get_outer_grid() right_panel = grid.right_panel + abort = False right_panel.set_filter_visibility(False) data = call_out(self, "list_mods", mode) + + # suppress errors if no mods available on system + if data.returncode == 1: + abort = True + GLib.idle_add(load) + spawn_dialog(self.get_outer_window(), data.stdout, Popup.NOTIFY) + return 1 + result = parse_mod_rows(data) total_size = result[0] total_mods = result[1] @@ -1091,6 +1150,8 @@ class TreeView(Gtk.TreeView): selected_map.clear() selected_map.append("Map=All maps") + self.set_selection_mode(Gtk.SelectionMode.SINGLE) + for check in checks: toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True) toggle_signal(self, self, '_on_keypress', True) @@ -1130,8 +1191,12 @@ class TreeView(Gtk.TreeView): cell.set_property('text', formatted) return - def _update_quad_column(self, mode): - # toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) + def set_selection_mode(self, mode): + sel = self.get_selection() + sel.set_mode(mode) + + def update_quad_column(self, mode): + toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) for column in self.get_columns(): self.remove_column(column) @@ -1141,6 +1206,10 @@ class TreeView(Gtk.TreeView): if mode == "List installed mods": cols = mod_cols self.set_model(mod_store) + # attach button panel + grid = self.get_parent().get_parent() + grid.right_panel.pack_start(grid.sel_panel, False, False, 0) + grid.show_all() else: cols = log_cols self.set_model(log_store) @@ -1152,12 +1221,10 @@ class TreeView(Gtk.TreeView): if i == 3: column.set_cell_data_func(renderer, self._format_float, func_data=None) column.set_sort_column_id(i) - #if (column_title == "Name"): - # column.set_fixed_width(600) self.append_column(column) if mode == "List installed mods": - pass + self.set_selection_mode(Gtk.SelectionMode.MULTIPLE) else: data = call_out(self, "show_log") res = parse_log_rows(data) @@ -1236,7 +1303,7 @@ class TreeView(Gtk.TreeView): self.grab_focus() elif chosen_row == "List installed mods" or chosen_row == "Show debug log": toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) - self._update_quad_column(chosen_row) + self.update_quad_column(chosen_row) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) elif any(map(context.__contains__, valid_contexts)): # implies activated row on any server list subcontext @@ -1747,7 +1814,7 @@ class Grid(Gtk.Grid): self.scrollable_treelist.set_vexpand(True) self.right_panel = RightPanel(is_steam_deck) - + self.sel_panel = ModSelectionPanel() self.bar = Gtk.Statusbar() self.scrollable_treelist.treeview.connect("on_distcalc_started", self._on_calclat_started) @@ -1853,6 +1920,73 @@ class App(Gtk.Application): self.win.halt_proc_and_quit(self, None) +class ModSelectionPanel(Gtk.Box): + def __init__(self): + super().__init__(spacing=6) + self.set_orientation(Gtk.Orientation.VERTICAL) + + labels = [ + "Select all", + "Unselect all", + "Delete selected" + ] + + for l in labels: + button = Gtk.Button(label=l) + button.set_margin_start(10) + button.set_margin_end(10) + button.connect("clicked", self._on_button_clicked) + self.pack_start(button, False, True, 0) + + def _on_button_clicked(self, button): + label = button.get_label() + widgets = relative_widget(self) + parent = widgets["outer"] + treeview = widgets["treeview"] + match label: + case "Select all": + treeview.toggle_selection(True) + case "Unselect all": + treeview.toggle_selection(False) + case "Delete selected": + (model, pathlist) = treeview.get_selection().get_selected_rows() + ct = len(pathlist) + if ct < 1: + return + + self._iterate_mod_deletion(model, pathlist, ct) + + def _iterate_mod_deletion(self, model, pathlist, ct): + # hedge against large number of arguments + widgets = relative_widget(self) + parent = widgets["outer"] + treeview = widgets["treeview"] + + pretty = pluralize("mods", ct) + conf_msg = f"You are going to delete {ct} {pretty}. Proceed?" + success_msg = f"Successfully deleted {ct} {pretty}." + fail_msg = "An error occurred during deletion. Aborting." + + res = spawn_dialog(parent, conf_msg, Popup.CONFIRM) + if res != 0: + return + + mods = [] + for i in pathlist: + it = model.get_iter(i) + symlink = model.get_value(it, 1) + path = model.get_value(it, 2) + concat = symlink + " " + path + "\n" + mods.append(concat) + with open(mods_temp_file, "w") as outfile: + outfile.writelines(mods) + proc = call_out(parent, "delete") + if proc.returncode == 0: + spawn_dialog(parent, success_msg, Popup.NOTIFY) + treeview.update_quad_column("List installed mods") + else: + spawn_dialog(parent, fail_msg, Popup.NOTIFY) + class FilterPanel(Gtk.Box): def __init__(self): super().__init__(spacing=6) From fbc500066f4558958f4a189df0a5eedc6d60a328 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:34:25 +0900 Subject: [PATCH 6/9] fix: focus first row --- helpers/ui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helpers/ui.py b/helpers/ui.py index 99d8102..1b55aa9 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -969,6 +969,11 @@ class TreeView(Gtk.TreeView): else: return False + def _focus_first_row(self): + path = Gtk.TreePath(0) + it = mod_store.get_iter(path) + self.get_selection().select_path(path) + def get_column_at_index(self, index): select = self.get_selection() sels = select.get_selected_rows() @@ -1058,6 +1063,7 @@ class TreeView(Gtk.TreeView): #2024-11-12 toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True) toggle_signal(self, self, '_on_keypress', True) + self._focus_first_row() grid = self.get_outer_grid() right_panel = grid.right_panel From c254de2aec6f8c4dd2fbbb6174b951984d301bf0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:42:01 +0900 Subject: [PATCH 7/9] chore: add comments --- helpers/ui.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/helpers/ui.py b/helpers/ui.py index 1b55aa9..16cd43e 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -172,10 +172,11 @@ status_tooltip = { } def relative_widget(child): - # get target widget relative to source widget - # chiefly used for transient modals and parent member functions - # when calling non-adjacent widgets - # positions are relative to grid members + # returns collection of outer widgets relative to source widget + # chiefly used for transient modals and accessing non-adjacent widget methods + # positions are always relative to grid sub-children + # containers and nested buttons should never need to call this function directly + grid = child.get_parent().get_parent() treeview = grid.scrollable_treelist.treeview outer = grid.get_parent() @@ -187,9 +188,9 @@ def relative_widget(child): } supported = [ - "ModSelectionPanel", - "ButtonBox", - "TreeView" + "ModSelectionPanel", # Grid < RightPanel < ModSelectionPanel + "ButtonBox", # Grid < RightPanel < ButtonBox + "TreeView" # Grid < ScrollableTree < TreeView ] if child.__class__.__name__ not in supported: @@ -590,6 +591,7 @@ class ButtonBox(Gtk.Box): button.set_size_request(10, 10) else: button.set_size_request(50,50) + #TODO: explore a more intuitive way of highlighting the active context button.set_opacity(0.6) self.buttons.append(button) button.connect("clicked", self._on_selection_button_clicked) From d6d8dde2aad09d0e2aa01cabeffa87b6b4f45599 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:43:23 +0900 Subject: [PATCH 8/9] feat: iterate mod deletion --- helpers/funcs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/helpers/funcs b/helpers/funcs index 59c3718..9c42da7 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -38,6 +38,7 @@ lock_file="$state_path/$prefix.lock" #CACHE cache_dir="$HOME/.cache/$app_name" _cache_servers="$cache_dir/$prefix.servers" +_cache_mods_temp="$cache_dir/$prefix.mods_temp" _cache_temp="$cache_dir/$prefix.temp" _cache_my_servers="$cache_dir/$prefix.my_servers" _cache_history="$cache_dir/$prefix.history" @@ -580,12 +581,23 @@ parse_server_json(){ } delete_local_mod(){ shift - local symlink="$1" - local dir="$2" - [[ ! -d $workshop_dir/$dir ]] && return 1 - [[ ! -L $game_dir/$symlink ]] && return 1 - #SC2115 - rm -rf "${workshop_dir:?}/$dir" && unlink "$game_dir/$symlink" || return 1 + if [[ -z $1 ]]; then + # use multi mode + readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp) + readarray -t ids < <(awk '{print $2}' $_cache_mods_temp) + rm "$_cache_mods_temp" + else + local symlink="$1" + local dir="$2" + readarray -t symlinks <<< "$symlink" + readarray -t ids <<< "$dir" + fi + for ((i=0; i<${#symlinks[@]}; i++)); do + [[ ! -d $workshop_dir/${ids[$i]} ]] && return 1 + [[ ! -L $game_dir/${symlinks[$i]} ]] && return 1 + #SC2115 + rm -rf "${workshop_dir:?}/${ids[$i]}" && unlink "$game_dir/${symlinks[$i]}" || return 1 + done } test_cooldown(){ [[ ! -f $_cache_cooldown ]] && return 0 From 2087f4ffa8d4797d563ca5c5989a241db311460b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 13 Nov 2024 09:52:49 +0900 Subject: [PATCH 9/9] chore: update changelog --- CHANGELOG.md | 10 ++++++++++ dzgui.sh | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0982e75..45f0ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [5.6.0-beta.1] 2024-11-12 +### Added +- Bulk delete mods (via 'List installed mods' list). Not compatible with Manual Mod install mode +### Fixed +- Fix for server list truncation causing some servers to not appear in results +- Suppress signal emission when switching menu contexts +- Focus first row when opening mods list +### Changed +- Clarify some error messages and normalize text formatting + ## [5.5.0-beta.5] 2024-11-03 ### Changed - Use updated A2S_RULES logic diff --git a/dzgui.sh b/dzgui.sh index 5a0bac8..3330fc8 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.5.0-beta.5 +version=5.6.0-beta.1 #CONSTANTS aid=221100 @@ -569,10 +569,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="dd7aa34df1d374739127cca3033a3f67" + ["ui.py"]="680ff0e4071681f26409fa3592a41e46" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="d8ae2662fbc3c62bdb5a51dec1935705" + ["funcs"]="fa5eb43c454e6bf2903e94884fe64644" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist"