1
0
Fork 0
mirror of https://github.com/aclist/dztui.git synced 2024-12-29 13:52:03 +01:00

Merge pull request #165 from aclist/release/5.6.0.beta-1

Release/5.6.0-beta.1
This commit is contained in:
aclist 2024-11-13 09:55:45 +09:00 committed by GitHub
commit 1b7752588c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 266 additions and 71 deletions

View file

@ -1,5 +1,15 @@
# Changelog # 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 ## [5.5.0-beta.5] 2024-11-03
### Changed ### Changed
- Use updated A2S_RULES logic - Use updated A2S_RULES logic

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version=5.5.0-beta.5 version=5.6.0-beta.1
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@ -569,10 +569,10 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file" [[ -f "$config_file" ]] && source "$config_file"
declare -A sums declare -A sums
sums=( sums=(
["ui.py"]="dd7aa34df1d374739127cca3033a3f67" ["ui.py"]="680ff0e4071681f26409fa3592a41e46"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["funcs"]="d8ae2662fbc3c62bdb5a51dec1935705" ["funcs"]="fa5eb43c454e6bf2903e94884fe64644"
["lan"]="c62e84ddd1457b71a85ad21da662b9af" ["lan"]="c62e84ddd1457b71a85ad21da662b9af"
) )
local author="aclist" local author="aclist"

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -o pipefail set -o pipefail
version=5.5.0 version=5.6.0
#CONSTANTS #CONSTANTS
aid=221100 aid=221100
@ -38,6 +38,8 @@ lock_file="$state_path/$prefix.lock"
#CACHE #CACHE
cache_dir="$HOME/.cache/$app_name" cache_dir="$HOME/.cache/$app_name"
_cache_servers="$cache_dir/$prefix.servers" _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_my_servers="$cache_dir/$prefix.my_servers"
_cache_history="$cache_dir/$prefix.history" _cache_history="$cache_dir/$prefix.history"
_cache_launch="$cache_dir/$prefix.launch_mods" _cache_launch="$cache_dir/$prefix.launch_mods"
@ -380,9 +382,30 @@ get_dist(){
fi fi
} }
get_remote_servers(){ get_remote_servers(){
local limit=20000 params=(
local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?filter=\appid\221100&limit=$limit&key=$steam_api" "\\nor\1\map\chernarusplus\\nor\1\map\sakhal"
curl -Ls "$url" | jq -r '.response.servers' "\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(){ get_unique_maps(){
shift shift
@ -558,12 +581,23 @@ parse_server_json(){
} }
delete_local_mod(){ delete_local_mod(){
shift shift
local symlink="$1" if [[ -z $1 ]]; then
local dir="$2" # use multi mode
[[ ! -d $workshop_dir/$dir ]] && return 1 readarray -t symlinks < <(awk '{print $1}' $_cache_mods_temp)
[[ ! -L $game_dir/$symlink ]] && return 1 readarray -t ids < <(awk '{print $2}' $_cache_mods_temp)
#SC2115 rm "$_cache_mods_temp"
rm -rf "${workshop_dir:?}/$dir" && unlink "$game_dir/$symlink" || return 1 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(){ test_cooldown(){
[[ ! -f $_cache_cooldown ]] && return 0 [[ ! -f $_cache_cooldown ]] && return 0
@ -649,7 +683,7 @@ test_ping(){
local qport="$2" local qport="$2"
local res local res
res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}') 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" printf "%s" "$res"
} }
show_server_modlist(){ show_server_modlist(){

View file

@ -19,7 +19,7 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango from gi.repository import Gtk, GLib, Gdk, GObject, Pango
from enum import Enum from enum import Enum
# 5.5.0 # 5.6.0
app_name = "DZGUI" app_name = "DZGUI"
start_time = 0 start_time = 0
@ -48,12 +48,14 @@ default_tooltip = "Select a row to see its detailed description"
server_tooltip = [None, None] server_tooltip = [None, None]
user_path = os.path.expanduser('~') user_path = os.path.expanduser('~')
cache_path = '%s/.cache/dzgui' %(user_path)
state_path = '%s/.local/state/dzgui' %(user_path) state_path = '%s/.local/state/dzgui' %(user_path)
helpers_path = '%s/.local/share/dzgui/helpers' %(user_path) helpers_path = '%s/.local/share/dzgui/helpers' %(user_path)
log_path = '%s/logs' %(state_path) log_path = '%s/logs' %(state_path)
changelog_path = '%s/CHANGELOG.md' %(state_path) changelog_path = '%s/CHANGELOG.md' %(state_path)
geometry_path = '%s/dzg.cols.json' %(state_path) geometry_path = '%s/dzg.cols.json' %(state_path)
funcs = '%s/funcs' %(helpers_path) funcs = '%s/funcs' %(helpers_path)
mods_temp_file = '%s/dzg.mods_temp' %(cache_path)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
log_file = '%s/DZGUI_DEBUG.log' %(log_path) log_file = '%s/DZGUI_DEBUG.log' %(log_path)
@ -169,6 +171,41 @@ status_tooltip = {
"Hall of fame ⧉": "A list of significant contributors and testers", "Hall of fame ⧉": "A list of significant contributors and testers",
} }
def relative_widget(child):
# 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()
widgets = {
'grid': grid,
'treeview': treeview,
'outer': outer
}
supported = [
"ModSelectionPanel", # Grid < RightPanel < ModSelectionPanel
"ButtonBox", # Grid < RightPanel < ButtonBox
"TreeView" # Grid < ScrollableTree < 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":
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): def format_ping(ping):
ms = " | Ping: %s" %(ping) ms = " | Ping: %s" %(ping)
@ -297,25 +334,25 @@ def process_shell_return_code(transient_parent, msg, code, original_input):
#TODO: add logger output to each #TODO: add logger output to each
case 0: case 0:
# success with notice popup # success with notice popup
spawn_dialog(transient_parent, msg, "NOTIFY") spawn_dialog(transient_parent, msg, Popup.NOTIFY)
case 1: case 1:
# error with notice popup # error with notice popup
if msg == "": if msg == "":
msg = "Something went wrong" msg = "Something went wrong"
spawn_dialog(transient_parent, msg, "NOTIFY") spawn_dialog(transient_parent, msg, Popup.NOTIFY)
case 2: case 2:
# warn and recurse (e.g. validation failed) # 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 treeview = transient_parent.grid.scrollable_treelist.treeview
process_tree_option(original_input, treeview) process_tree_option(original_input, treeview)
case 4: case 4:
# for BM only # for BM only
spawn_dialog(transient_parent, msg, "NOTIFY") spawn_dialog(transient_parent, msg, Popup.NOTIFY)
treeview = transient_parent.grid.scrollable_treelist.treeview treeview = transient_parent.grid.scrollable_treelist.treeview
process_tree_option(["Options", "Change Battlemetrics API key"], treeview) process_tree_option(["Options", "Change Battlemetrics API key"], treeview)
case 5: case 5:
# for steam only # for steam only
spawn_dialog(transient_parent, msg, "NOTIFY") spawn_dialog(transient_parent, msg, Popup.NOTIFY)
treeview = transient_parent.grid.scrollable_treelist.treeview treeview = transient_parent.grid.scrollable_treelist.treeview
process_tree_option(["Options", "Change Steam API key"], treeview) process_tree_option(["Options", "Change Steam API key"], treeview)
case 6: case 6:
@ -330,17 +367,17 @@ def process_shell_return_code(transient_parent, msg, code, original_input):
config_vals.append(i) config_vals.append(i)
tooltip = format_metadata(col) tooltip = format_metadata(col)
transient_parent.grid.update_statusbar(tooltip) transient_parent.grid.update_statusbar(tooltip)
spawn_dialog(transient_parent, msg, "NOTIFY") spawn_dialog(transient_parent, msg, Popup.NOTIFY)
return return
case 100: case 100:
# final handoff before launch # 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 treeview = transient_parent.grid.scrollable_treelist.treeview
if final_conf == 1 or final_conf is None: if final_conf == 1 or final_conf is None:
return return
process_tree_option(["Handshake", ""], treeview) process_tree_option(["Handshake", ""], treeview)
case 255: 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() Gtk.main_quit()
@ -369,7 +406,7 @@ def process_tree_option(input, treeview):
proc = call_out(transient_parent, subproc, args) proc = call_out(transient_parent, subproc, args)
GLib.idle_add(_load) GLib.idle_add(_load)
if bool is True: if bool is True:
wait_dialog = GenericDialog(transient_parent, msg, "WAIT") wait_dialog = GenericDialog(transient_parent, msg, Popup.WAIT)
wait_dialog.show_all() wait_dialog.show_all()
thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog)) thread = threading.Thread(target=_background, args=(subproc, args, wait_dialog))
thread.start() thread.start()
@ -432,7 +469,7 @@ def process_tree_option(input, treeview):
link_label = "Open Battlemetrics API page" link_label = "Open Battlemetrics API page"
prompt = "Enter new API key" 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() res = user_entry.get_input()
if res is None: if res is None:
logger.info("User aborted entry dialog") logger.info("User aborted entry dialog")
@ -554,6 +591,7 @@ class ButtonBox(Gtk.Box):
button.set_size_request(10, 10) button.set_size_request(10, 10)
else: else:
button.set_size_request(50,50) button.set_size_request(50,50)
#TODO: explore a more intuitive way of highlighting the active context
button.set_opacity(0.6) button.set_opacity(0.6)
self.buttons.append(button) self.buttons.append(button)
button.connect("clicked", self._on_selection_button_clicked) button.connect("clicked", self._on_selection_button_clicked)
@ -563,10 +601,17 @@ class ButtonBox(Gtk.Box):
def _update_single_column(self, context): def _update_single_column(self, context):
logger.info("Returning from multi-column view to monocolumn view for the context '%s'" %(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 = self.get_parent()
right_panel.set_filter_visibility(False) right_panel.set_filter_visibility(False)
treeview = widgets["treeview"]
treeview.set_selection_mode(Gtk.SelectionMode.SINGLE)
"""Block maps combo when returning to main menu""" """Block maps combo when returning to main menu"""
toggle_signal(right_panel.filters_vbox, right_panel.filters_vbox.maps_combo, '_on_map_changed', False) toggle_signal(right_panel.filters_vbox, right_panel.filters_vbox.maps_combo, '_on_map_changed', False)
right_panel.filters_vbox.keyword_entry.set_text("") right_panel.filters_vbox.keyword_entry.set_text("")
@ -711,7 +756,7 @@ class TreeView(Gtk.TreeView):
iter = self.get_current_iter() iter = self.get_current_iter()
server_store.remove(iter) server_store.remove(iter)
msg = proc.stdout msg = proc.stdout
res = spawn_dialog(parent, msg, "NOTIFY") res = spawn_dialog(parent, msg, Popup.NOTIFY)
case "Remove from history": case "Remove from history":
record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8)) record = "%s:%s" %(self.get_column_at_index(7), self.get_column_at_index(8))
call_out(parent, context_menu_label, record) call_out(parent, context_menu_label, record)
@ -734,21 +779,37 @@ class TreeView(Gtk.TreeView):
conf_msg = "Really delete the mod '%s'?" %(value) conf_msg = "Really delete the mod '%s'?" %(value)
success_msg = "Successfully deleted the mod '%s'." %(value) success_msg = "Successfully deleted the mod '%s'." %(value)
fail_msg = "An error occurred during deletion. Aborting." 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) symlink = self.get_column_at_index(1)
dir = self.get_column_at_index(2) dir = self.get_column_at_index(2)
if res == 0: if res == 0:
proc = call_out(parent, "delete", symlink, dir) proc = call_out(parent, "delete", symlink, dir)
if proc.returncode == 0: if proc.returncode == 0:
spawn_dialog(parent, success_msg, "NOTIFY") spawn_dialog(parent, success_msg, Popup.NOTIFY)
self._update_quad_column("List installed mods") self.update_quad_column("List installed mods")
else: else:
spawn_dialog(parent, fail_msg, "NOTIFY") spawn_dialog(parent, fail_msg, Popup.NOTIFY)
case "Open in Steam Workshop": case "Open in Steam Workshop":
record = self.get_column_at_index(2) record = self.get_column_at_index(2)
call_out(parent, "open_workshop_page", record) 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): def _on_button_release(self, widget, event):
if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return
try: try:
pathinfo = self.get_path_at_pos(event.x, event.y) pathinfo = self.get_path_at_pos(event.x, event.y)
if pathinfo is None: if pathinfo is None:
@ -758,8 +819,6 @@ class TreeView(Gtk.TreeView):
except AttributeError: except AttributeError:
pass pass
if event.type is Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return
context = self.get_first_col() context = self.get_first_col()
self.menu = Gtk.Menu() self.menu = Gtk.Menu()
@ -805,7 +864,7 @@ class TreeView(Gtk.TreeView):
diff = now - then diff = now - then
cooldown = 30 - math.floor(diff) cooldown = 30 - math.floor(diff)
if ((start_time > 0) and (now - then) < 30): 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 return
start_time = now start_time = now
@ -853,8 +912,6 @@ class TreeView(Gtk.TreeView):
self.emit("on_distcalc_started") self.emit("on_distcalc_started")
self.current_proc = CalcDist(self, addr, self.queue, cache) self.current_proc = CalcDist(self, addr, self.queue, cache)
self.current_proc.start() self.current_proc.start()
elif None:
return
else: else:
tooltip = format_metadata(row_sel) tooltip = format_metadata(row_sel)
grid.update_statusbar(tooltip) grid.update_statusbar(tooltip)
@ -914,6 +971,11 @@ class TreeView(Gtk.TreeView):
else: else:
return False 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): def get_column_at_index(self, index):
select = self.get_selection() select = self.get_selection()
sels = select.get_selected_rows() sels = select.get_selected_rows()
@ -935,7 +997,7 @@ class TreeView(Gtk.TreeView):
wait_dialog.destroy() wait_dialog.destroy()
parent = self.get_outer_window() 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() wait_dialog.show_all()
select = self.get_selection() select = self.get_selection()
sels = select.get_selected_rows() sels = select.get_selected_rows()
@ -971,7 +1033,7 @@ class TreeView(Gtk.TreeView):
No servers returned. Possible network issue or API key on cooldown? No servers returned. Possible network issue or API key on cooldown?
Return to the main menu, wait 60s, and try again. Return to the main menu, wait 60s, and try again.
If this issue persists, your API key may be defunct.""" 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() grid = self.get_outer_grid()
right_panel = grid.right_panel right_panel = grid.right_panel
@ -996,15 +1058,29 @@ class TreeView(Gtk.TreeView):
dialog.destroy() dialog.destroy()
self.set_model(mod_store) self.set_model(mod_store)
self.grab_focus() self.grab_focus()
size = locale.format_string('%.3f', total_size, grouping=True) if abort is False:
grid.update_statusbar("Found %s mods taking up %s MiB" %(f'{total_mods:n}', size)) 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) toggle_signal(self, self, '_on_keypress', True)
self._focus_first_row()
grid = self.get_outer_grid() grid = self.get_outer_grid()
right_panel = grid.right_panel right_panel = grid.right_panel
abort = False
right_panel.set_filter_visibility(False) right_panel.set_filter_visibility(False)
data = call_out(self, "list_mods", mode) 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) result = parse_mod_rows(data)
total_size = result[0] total_size = result[0]
total_mods = result[1] total_mods = result[1]
@ -1082,6 +1158,8 @@ class TreeView(Gtk.TreeView):
selected_map.clear() selected_map.clear()
selected_map.append("Map=All maps") selected_map.append("Map=All maps")
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
for check in checks: for check in checks:
toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True) toggle_signal(self.get_outer_grid().right_panel.filters_vbox, check, '_on_check_toggle', True)
toggle_signal(self, self, '_on_keypress', True) toggle_signal(self, self, '_on_keypress', True)
@ -1097,7 +1175,7 @@ class TreeView(Gtk.TreeView):
return return
mode = mode + ":" + port 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() wait_dialog.show_all()
thread = threading.Thread(target=self._background, args=(wait_dialog, mode)) thread = threading.Thread(target=self._background, args=(wait_dialog, mode))
thread.start() thread.start()
@ -1121,8 +1199,12 @@ class TreeView(Gtk.TreeView):
cell.set_property('text', formatted) cell.set_property('text', formatted)
return return
def _update_quad_column(self, mode): def set_selection_mode(self, mode):
# toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) 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(): for column in self.get_columns():
self.remove_column(column) self.remove_column(column)
@ -1132,6 +1214,10 @@ class TreeView(Gtk.TreeView):
if mode == "List installed mods": if mode == "List installed mods":
cols = mod_cols cols = mod_cols
self.set_model(mod_store) 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: else:
cols = log_cols cols = log_cols
self.set_model(log_store) self.set_model(log_store)
@ -1143,22 +1229,20 @@ class TreeView(Gtk.TreeView):
if i == 3: if i == 3:
column.set_cell_data_func(renderer, self._format_float, func_data=None) column.set_cell_data_func(renderer, self._format_float, func_data=None)
column.set_sort_column_id(i) column.set_sort_column_id(i)
#if (column_title == "Name"):
# column.set_fixed_width(600)
self.append_column(column) self.append_column(column)
if mode == "List installed mods": if mode == "List installed mods":
pass self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
else: else:
data = call_out(self, "show_log") data = call_out(self, "show_log")
res = parse_log_rows(data) res = parse_log_rows(data)
if res == 1: 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 return
transient_parent = self.get_outer_window() 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() wait_dialog.show_all()
thread = threading.Thread(target=self._background_quad, args=(wait_dialog, mode)) thread = threading.Thread(target=self._background_quad, args=(wait_dialog, mode))
thread.start() thread.start()
@ -1181,7 +1265,7 @@ class TreeView(Gtk.TreeView):
qport = self.get_column_at_index(8) qport = self.get_column_at_index(8)
record = "%s:%s" %(addr, str(qport)) 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() wait_dialog.show_all()
thread = threading.Thread(target=self._background_connection, args=(wait_dialog, record)) thread = threading.Thread(target=self._background_connection, args=(wait_dialog, record))
thread.start() thread.start()
@ -1205,7 +1289,7 @@ class TreeView(Gtk.TreeView):
if chosen_row == "Server browser": if chosen_row == "Server browser":
cooldown = call_out(self, "test_cooldown", "", "") cooldown = call_out(self, "test_cooldown", "", "")
if cooldown.returncode == 1: 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 return 1
for check in checks: for check in checks:
toggle_signal(filters_vbox, check, '_on_check_toggle', False) toggle_signal(filters_vbox, check, '_on_check_toggle', False)
@ -1227,7 +1311,7 @@ class TreeView(Gtk.TreeView):
self.grab_focus() self.grab_focus()
elif chosen_row == "List installed mods" or chosen_row == "Show debug log": elif chosen_row == "List installed mods" or chosen_row == "Show debug log":
toggle_signal(self, self.selected_row, '_on_tree_selection_changed', False) 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) toggle_signal(self, self.selected_row, '_on_tree_selection_changed', True)
elif any(map(context.__contains__, valid_contexts)): elif any(map(context.__contains__, valid_contexts)):
# implies activated row on any server list subcontext # implies activated row on any server list subcontext
@ -1286,16 +1370,10 @@ def format_metadata(row_sel):
return prefix return prefix
def format_tooltip(sum, hits): def format_tooltip(players, hits):
if hits == 1: hits_pretty = pluralize("matches", hits)
hit_suffix = "match" players_pretty = pluralize("players", players)
else: tooltip = f"Found {hits:n} {hits_pretty} with {players:n} {players_pretty}"
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)
return tooltip return tooltip
@ -1323,7 +1401,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, '_on_button_release', False)
toggle_signal(filters_vbox, filters_vbox.maps_combo, '_on_map_changed', 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() dialog.show_all()
server_store.clear() server_store.clear()
@ -1343,6 +1421,12 @@ class Port(Enum):
DEFAULT = 1 DEFAULT = 1
CUSTOM = 2 CUSTOM = 2
class Popup(Enum):
WAIT = 1
NOTIFY = 2
CONFIRM = 3
ENTRY = 4
class GenericDialog(Gtk.MessageDialog): class GenericDialog(Gtk.MessageDialog):
def __init__(self, parent, text, mode): def __init__(self, parent, text, mode):
@ -1351,19 +1435,19 @@ class GenericDialog(Gtk.MessageDialog):
return True return True
match mode: match mode:
case "WAIT": case Popup.WAIT:
dialog_type = Gtk.MessageType.INFO dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.NONE button_type = Gtk.ButtonsType.NONE
header_text = "Please wait" header_text = "Please wait"
case "NOTIFY": case Popup.NOTIFY:
dialog_type = Gtk.MessageType.INFO dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.OK button_type = Gtk.ButtonsType.OK
header_text = "Notice" header_text = "Notice"
case "CONFIRM": case Popup.CONFIRM:
dialog_type = Gtk.MessageType.QUESTION dialog_type = Gtk.MessageType.QUESTION
button_type = Gtk.ButtonsType.OK_CANCEL button_type = Gtk.ButtonsType.OK_CANCEL
header_text = "Confirmation" header_text = "Confirmation"
case "ENTRY": case Popup.ENTRY:
dialog_type = Gtk.MessageType.QUESTION dialog_type = Gtk.MessageType.QUESTION
button_type = Gtk.ButtonsType.OK_CANCEL button_type = Gtk.ButtonsType.OK_CANCEL
header_text = "User input required" header_text = "User input required"
@ -1384,7 +1468,7 @@ class GenericDialog(Gtk.MessageDialog):
modal=True, modal=True,
) )
if mode == "WAIT": if mode == Popup.WAIT:
dialogBox = self.get_content_area() dialogBox = self.get_content_area()
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
dialogBox.pack_end(spinner, False, False, 0) dialogBox.pack_end(spinner, False, False, 0)
@ -1598,7 +1682,7 @@ class PingDialog(GenericDialog):
dialogBox = self.get_content_area() dialogBox = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK) self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(500, 200) 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() wait_dialog.show_all()
thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record))
thread.start() thread.start()
@ -1642,7 +1726,7 @@ class ModDialog(GenericDialog):
column.set_sort_column_id(i) column.set_sort_column_id(i)
dialogBox.pack_end(self.scrollable, True, True, 0) 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() wait_dialog.show_all()
thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record)) thread = threading.Thread(target=self._background, args=(wait_dialog, parent, record))
thread.start() thread.start()
@ -1651,7 +1735,7 @@ class ModDialog(GenericDialog):
def _load(): def _load():
dialog.destroy() dialog.destroy()
if data.returncode == 1: 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 return
self.show_all() self.show_all()
self.set_markup("Modlist (%s mods)" %(mod_count)) self.set_markup("Modlist (%s mods)" %(mod_count))
@ -1738,7 +1822,7 @@ class Grid(Gtk.Grid):
self.scrollable_treelist.set_vexpand(True) self.scrollable_treelist.set_vexpand(True)
self.right_panel = RightPanel(is_steam_deck) self.right_panel = RightPanel(is_steam_deck)
self.sel_panel = ModSelectionPanel()
self.bar = Gtk.Statusbar() self.bar = Gtk.Statusbar()
self.scrollable_treelist.treeview.connect("on_distcalc_started", self._on_calclat_started) self.scrollable_treelist.treeview.connect("on_distcalc_started", self._on_calclat_started)
@ -1844,6 +1928,73 @@ class App(Gtk.Application):
self.win.halt_proc_and_quit(self, None) 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): class FilterPanel(Gtk.Box):
def __init__(self): def __init__(self):
super().__init__(spacing=6) super().__init__(spacing=6)