diff --git a/CHANGELOG.md b/CHANGELOG.md index 676d7f1..befe399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,51 @@ # Changelog -## [5.6.6] 2024-02-09 +## [5.8.0] 2025-07-06 +## Added +- Filter servers by official/unofficial status +- Automatically fetch geolocation records +## Changed +- Updated internal versioning of helper files +## Fixed +- Corrected erroneous 2024 date in prior changelog entries + +## [5.7.0] 2025-04-17 +## Added +- Save application dimensions when quitting and restore on subsequent boot +## Fixed +- Issues with the window exceeding the bounds of the screen when displaying some table contexts +## Changed +- More direct server ping query method that should return marginally more accurate times + +## [5.6.7] 2025-04-04 +## Dropped +- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet + +## [5.6.6] 2025-03-16 +## Changed +- Update IP database records for 2025-03 + +## [5.6.5] 2025-03-04 +## Fixed +- Livonia server results being dropped from batch queries + +## [5.6.4] 2025-02-10 +### Changed +- Drop launch flag and check for invocation through Steam automatically + +## [5.6.3] 2025-02-09 ### Changed - Update IP database records for 2025-02 -## [5.6.2] 2024-01-22 +## [5.6.2] 2025-01-22 ### Fixed - Resolve regression introduced with IP resolution feature in 5.6.0 (restores functionality of right-click action: Add to My Servers) -## [5.6.1] 2024-01-10 +## [5.6.1] 2025-01-10 ### Fixed - Add fallback support for jq 1.6 -## [5.6.0] 2024-01-06 +## [5.6.0] 2025-01-06 ### Added - Application header bar and controls - Menu context subtitle in header bar diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4e302b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Introduction + +Thank you for your interest in DZGUI! + +This guide goes over development conventions and best practices for contributors. +If you are a developer, you can skip to the end. + +# Requesting help + +If you encounter a problem with DZGUI, you can submit tickets on the GitHub +(issue tracker)[https://github.com/aclist/dzgui-devel/issues] under the +`troubleshooting` tag. + +# How can I help the project? + +There are several ways to help this project. +1. Report bugs that you find +2. Request features that you would like +3. Contribute features and fixes to the codebase +4. Contribute documentation to the project + +Before making any contribution, please read the `CODE_OF_CONDUCT.md` and act +accordingly. + +## Submitting a ticket + +Navigate to the GitHub (issue tracker)[https://github.com/aclist/dzgui-devel/issues]. +From there, follow the onscreen prompts. You will be asked questions such as: + +- What version are you using? +- What distribution are you using? +- What is the issue that you found? +- How can we reproduce the issue? + +You can also attach screenshots, logs, or other data that can help us. + +## Requesting a feature + +You can also request features via the same issue tracker. It is good practice to +first search for your idea to see if a similar one has already been posted. +If not, open a ticket where you describe your feature and its possible benefits. + +Please note that this is a community project, so it takes time for us to develop +features. Putting in a feature request does not mean that it will be implemented, +but we will do our best to support as many cool ideas as possible. + +## Contributing code or documentation + +If you would like to take ownership or assist with an issue on the issue tracker, or +contribute a new change, please follow the guidelines below. + +Fork this repository and check out the code up to `prerelease/`. +If there is no pending prerelease PR, you can base your changes off of +the `testing` branch. You may later be asked to retarget your PR to the prerelease +branch once a new one is made. This is because out-of-band PRs are consolidated into +the next release before merging. + +The following naming conventions apply for PRs: +- fix/ - patch/hotfix branches +- feat/ - feature branches +- doc/ - documentation branches +- infra/ - infrastructure branches + +Implement your changes and test them locally. If they work you may +open a merge request, and then we review your changes. If everything is OK, it +will be merged to `prerelease/`, then `testing`, and after a live +testing phase, to `stable` as well. + +It is recommended to follow +(Conventional Commits)[https://www.conventionalcommits.org/en/v1.0.0/], as this +integrates well with tooling and helps the project review your code. diff --git a/dzgui.sh b/dzgui.sh index ae93854..02f5274 100755 --- a/dzgui.sh +++ b/dzgui.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.3 +version=5.8.0 #CONSTANTS aid=221100 @@ -49,10 +49,9 @@ freedesktop_path="$HOME/.local/share/applications" #HELPERS ui_helper="$helpers_path/ui.py" -geo_file="$helpers_path/ips.csv" km_helper="$helpers_path/latlon" -sums_path="$helpers_path/sums.md5" func_helper="$helpers_path/funcs" +geo_helper="$helpers_path/ips.csv" #REMOTE remote_host=gh @@ -63,7 +62,6 @@ stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" releases_url="https://github.com/$author/$repo/releases/download/browser" km_helper_url="$releases_url/latlon" -geo_file_url="$releases_url/ips.csv.gz" set_im_module(){ @@ -587,10 +585,10 @@ fetch_helpers_by_sum(){ [[ -f "$config_file" ]] && source "$config_file" declare -A sums sums=( - ["ui.py"]="5a876efacf208d12b5fe761996425412" + ["ui.py"]="f128a97e744e9e11036d707198feb8a8" ["query_v2.py"]="55d339ba02512ac69de288eb3be41067" ["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397" - ["funcs"]="417bd5eaffbefc905a843985c691dc64" + ["funcs"]="94287c75a5ae0a06a95dd439711e6fba" ["lan"]="c62e84ddd1457b71a85ad21da662b9af" ) local author="aclist" @@ -637,18 +635,8 @@ fetch_helpers_by_sum(){ done return 0 } -fetch_geo_file(){ - # for binary releases - local geo_sum="9824e9b9a75a4830a2423932cc188b06" +fetch_km_helper(){ local km_sum="b038fdb8f655798207bd28de3a004706" - local gzip="$helpers_path/ips.csv.gz" - if [[ ! -f $geo_file ]] || [[ $(get_hash $geo_file) != $geo_sum ]]; then - local res=$(get_response_code "$geo_file_url") - [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$geo_file_url'" - curl -Ls "$geo_file_url" > "$gzip" - #force overwrite - gunzip -f "$gzip" - fi if [[ ! -f $km_helper ]] || [[ $(get_hash $km_helper) != $km_sum ]]; then local res=$(get_response_code "$km_helper_url") [[ $res -ne 200 ]] && raise_error_and_quit "Remote resource unavailable: '$km_helper_url'" @@ -656,10 +644,119 @@ fetch_geo_file(){ chmod +x "$km_helper" fi } +get_response_code(){ + local url="$1" + curl -Ls -I -o /dev/null -w "%{http_code}" "$url" +} +raise_error_and_quit(){ + echo "$1" + exit 1 +} +fetch_ip_db(){ + parse_dl_url(){ + curl -Ls "$url" \ + | grep "csv.gz" \ + | awk -F"['']" '{print $2}' + } + + parse_dl_url_date(){ + local url="$1" + <<< "$url" \ + awk -F/ '{print $5}' \ + | awk -F"dbip-city-lite-" '{print $2}' \ + | awk -F'.csv.gz' '{print $1}' + } + + fetch(){ + logger INFO "Triggering fetch routine" + local url="$1" + + curl -Ls "$url" > "$base_file" + if [[ $? -ne 0 ]]; then + logger WARN "Abnormal exit while accessing '$url'" + return + fi + + gunzip -f "$base_file" + if [[ $? -ne 0 ]]; then + rm "$base_file" + logger WARN "Abnormal exit while unpacking gzip '$base_file'" + return + fi + + < "$extracted_file" grep -vE "^[a-z0-9]{4}:" | grep -v "::" > "$ip_file" + if [[ $? -ne 0 ]]; then + logger WARN "Abnormal exit while parsing IPs" + return + fi + rm "${state_path}/${month}.csv" + + readarray -t records < <(cat $ip_file | awk -F, 'NR==1 {print $1} END {print $1}') + if [[ ${records[0]} != "0.0.0.0" ]] && [[ ${records[1]} != "224.0.0.0" ]]; then + logger WARN "Anchor records missing in '$ip_file', possibly malformed" + rm "$ip_file" + return + fi + + mv "$ip_file" "$geo_helper" + if [[ $? -ne 0 ]]; then + logger WARN "Abnormal exit while moving '$ip_file' to '$helpers_path'" + rm "$ip_file" + fi + + echo "$month" > "$month_file" + logger INFO "Wrote '$month' to stub '$month_file'" + logger INFO "Updated '$ip_file'" + } + + local url="https://db-ip.com/db/download/ip-to-city-lite" + local month_file="${state_path}/.month" + local ip_file="${state_path}/ips.csv" + + # test main url + local res=$(get_response_code "$url") + if [[ $res -ne 200 ]]; then + logger WARN "Failed to retrieve remote resource: '$url' ($res)" + return + fi + logger INFO "Resolved remote URL: '$url'" + + # test dl url + local dl_url="$(parse_dl_url)" + local res=$(get_response_code "$dl_url") + if [[ $res -ne 200 ]]; then + logger WARN "Remote resource unavailable: '$dl_url' ($res)" + return + fi + logger INFO "Resolved download URL: '$dl_url'" + + local month=$(parse_dl_url_date "$dl_url") + local base_file="${state_path}/${month}.csv.gz" + local extracted_file="${state_path}/${month}.csv" + + # no stub file + if [[ ! -f $month_file ]]; then + logger WARN "No stub file '$month_file' present" + fetch "$dl_url" + return + fi + + # local needs update + local last_month=$(< "$month_file") + if [[ $last_month != "$month" ]]; then + logger WARN "Local stub '$last_month' does not match remote stub '$month'" + fetch "$dl_url" + return + fi + + # if stub is same date, abort + logger INFO "Local stub '$last_month' is identical to remote, skipping" +} fetch_helpers(){ fetch_a2s fetch_dzq - fetch_geo_file + fetch_km_helper + fetch_ip_db fetch_helpers_by_sum [[ ! -f $share_path/icon.png ]] && freedesktop_dirs fetch_icons @@ -876,21 +973,14 @@ get_response_code(){ curl -Ls -I -o /dev/null -w "%{http_code}" "$url" } test_connection(){ - source "$config_file" declare -A hr local res1 local res2 local str="No connection could be established to the remote server" hr=( - ["steampowered.com"]="https://api.steampowered.com/IGameServersService/GetServerList/v1/?key=$steam_api" ["github.com"]="https://github.com/$author" ["codeberg.org"]="https://codeberg.org/$author" ) - # steam API is mandatory, except on initial setup - if [[ -n $steam_api ]]; then - res=$(get_response_code "${hr["steampowered.com"]}") - [[ $res -ne 200 ]] && raise_error_and_quit "$str ("steampowered.com")" - fi res=$(get_response_code "${hr["github.com"]}") if [[ $res -ne 200 ]]; then @@ -906,7 +996,6 @@ test_connection(){ stable_url="$url_prefix/dzgui" testing_url="$url_prefix/testing" km_helper_url="$releases_url/latlon" - geo_file_url="$releases_url/ips.csv.gz" fi } legacy_cols(){ @@ -922,14 +1011,14 @@ stale_mod_signatures(){ readarray -t old_mod_ids < <(awk -F, '{print $1}' $versions_file) for ((i=0; i<${#old_mod_ids[@]}; ++i)); do if [[ ! -d $workshop_dir/${old_mod_ids[$i]} ]]; then - "$HOME/.local/share/$app_name/helpers/funcs" "align_local" "${old_mod_ids[$i]}" + "$func_helper" "align_local" "${old_mod_ids[$i]}" fi done fi } create_new_links(){ - "$HOME/.local/share/$app_name/helpers/funcs" "update_symlinks" + "$func_helper" "update_symlinks" } initial_setup(){ setup_dirs @@ -1007,10 +1096,7 @@ main(){ uninstall && exit 0 fi - if [[ $1 == "--steam" ]] || [[ $1 == "-s" ]]; then - export STEAM_LAUNCH=1 - fi - + set_im_module printf "Initializing setup...\n" diff --git a/helpers/funcs b/helpers/funcs index c747c82..31b5f59 100755 --- a/helpers/funcs +++ b/helpers/funcs @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -o pipefail -version=5.6.0 +version="5.8.0" #CONSTANTS aid=221100 @@ -59,7 +59,6 @@ geo_file="$helpers_path/ips.csv" km_helper="$helpers_path/latlon" sums_path="$helpers_path/sums.md5" query_helper="$helpers_path/query_v2.py" -func_helper="$helpers_path/funcs" lan_helper="$helpers_path/lan" #STEAM PATHS @@ -453,10 +452,15 @@ get_dist(){ } get_remote_servers(){ params=( - "\\nor\1\map\chernarusplus\\nor\1\map\sakhal" + "\\nor\1\map\chernarusplus\\nor\1\map\sakhal\\nor\1\map\enoch\empty\1\\nor\1\map\namalsk" + "\map\namalsk\empty\1" + "\map\namalsk\noplayers\1" "\map\chernarusplus\empty\1" "\map\chernarusplus\noplayers\1" - "\map\\sakhal" + "\map\\sakhal\empty\1" + "\map\\sakhal\noplayers\1" + "\map\\enoch\empty\1" + "\map\\enoch\noplayers\1" ) local limit=10000 local url="https://api.steampowered.com/IGameServersService/GetServerList/v1/?" @@ -601,6 +605,20 @@ filter_servers(){ <<< "$filtered" awk -F$separator '!seen[$1]++' fi } + filter_official(){ + if [[ ${filters[*]} =~ Official ]]; then + echo -n "$filtered" + else + <<< "$filtered" awk -F$separator '$10 == "Community"' + fi + } + filter_community(){ + if [[ ${filters[*]} =~ Unoffic ]]; then + echo -n "$filtered" + else + <<< "$filtered" awk -F$separator '$10 == "Official"' + fi + } filtered=$(filter_perspective) filtered=$(filter_full) @@ -611,6 +629,8 @@ filter_servers(){ filtered=$(filter_ascii) filtered=$(filter_duplicates) filtered=$(filter_keyword) + filtered=$(filter_official) + filtered=$(filter_community) if [[ -z "$filtered" ]]; then logger WARN "Filter result is empty" @@ -648,7 +668,8 @@ parse_server_json(){ "\(.max_players)␞" + "\(if .gametype == null then "0" elif .gametype|split("lqs")[1] == null then "0" else .gametype|split("lqs")[1]|split(",")[0] end)␞" + "\(.addr|split(":")[0]):\(if .gameport == null then "XXXX" else .gameport end)␞" + - "\(.addr|split(":")[1])" + "\(.addr|split(":")[1])␞" + + "\(if .gametype == null then "null" else (.gametype|split(",")|if any(. == "external") then "Community" else "Official" end) end)" ' | sort -k1 } align_versions_file(){ @@ -780,12 +801,17 @@ logger(){ } test_ping(){ shift + local time + local res local ip="$1" local qport="$2" - local res - res=$(ping -c1 -4 -W0.5 $1 | grep time= | awk -F= '{print $4}') - [[ ! $? -eq 0 ]] && res="Timed out" - printf "%s" "$res" + res=$(a2s $ip $qport info) + if [[ -z $res ]]; then + time="Timed out" + else + time=$(<<< "$res" jq -r '.[].ping') + fi + printf "%s" "$time" } show_server_modlist(){ shift @@ -1271,7 +1297,7 @@ symlinks(){ link="@$encoded_id" mod="${arr[$i]}" logger INFO "Creating link '$game_dir/$link' for '$workshop_dir/$mod'" - [[ $STEAM_LAUNCH -eq 1 ]] && echo "# Creating mod link $((i+1))/${#arr[@]}" + [[ -n $STEAMSCRIPT ]] && echo "# Creating mod link $((i+1))/${#arr[@]}" ln -s "$workshop_dir/$mod" "$game_dir/$link" done } @@ -1286,7 +1312,7 @@ symlinks(){ if [[ ${#links[@]} -eq 0 ]]; then logger INFO "No symlinks present in '$game_dir', creating them" - if [[ $STEAM_LAUNCH -eq 1 ]]; then + if [[ -n $STEAMSCRIPT ]]; then _create_links "${mods[@]}" > >(_pulse) else _create_links "${mods[@]}" @@ -1304,7 +1330,7 @@ symlinks(){ # update missing targets logger INFO "Found ${#hits[@]} unlinked mods" - if [[ $STEAM_LAUNCH -eq 1 ]]; then + if [[ -n $STEAMSCRIPT ]]; then _create_links "${hits[@]}" > >(_pulse) else _create_links "${hits[@]}" diff --git a/helpers/ui.py b/helpers/ui.py index ae28ede..e167277 100644 --- a/helpers/ui.py +++ b/helpers/ui.py @@ -17,7 +17,6 @@ import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango -# 5.6.0 app_name = "DZGUI" cache = {} @@ -38,7 +37,7 @@ mod_store = Gtk.ListStore(str, str, str, float, str) #cf. log_cols log_store = Gtk.ListStore(str, str, str, str) #cf. browser_cols -server_store = Gtk.ListStore(str, str, str, str, int, int, int, str, int) +server_store = Gtk.ListStore(str, str, str, str, int, int, int, str, int, str) default_tooltip = "Select a row to see its detailed description" server_tooltip = [None, None] @@ -50,6 +49,7 @@ 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) +res_path = '%s/dzg.res.json' %(state_path) funcs = '%s/funcs' %(helpers_path) mods_temp_file = '%s/dzg.mods_temp' %(cache_path) stale_mods_temp_file = '%s/dzg.stale_mods_temp' %(cache_path) @@ -88,14 +88,16 @@ log_cols = [ ] filters = { "1PP": True, - "3PP": True, "Day": True, - "Night": True, "Empty": False, + "3PP": True, + "Night": True, "Full": False, "Low pop": True, "Non-ASCII": False, - "Duplicate": False + "Duplicate": False, + "Official": True, + "Unoffic.": True, } @@ -552,7 +554,7 @@ def parse_server_rows(data): lines = data.stdout.splitlines() reader = csv.reader(lines, delimiter=delimiter) try: - rows = [[row[0], row[1], row[2], row[3], int(row[4]), int(row[5]), int(row[6]), row[7], int(row[8])] for row in reader if row] + rows = [[row[0], row[1], row[2], row[3], int(row[4]), int(row[5]), int(row[6]), row[7], int(row[8]), row[9]] for row in reader if row] except IndexError: return 1 for row in rows: @@ -685,7 +687,7 @@ def process_shell_return_code(transient_parent, msg, code, original_input): process_tree_option([treeview.view, RowType.HANDSHAKE], treeview) case 255: spawn_dialog(transient_parent, "Update complete. Please close DZGUI and restart.", Popup.NOTIFY) - Gtk.main_quit() + save_res_and_quit(transient_parent) def process_tree_option(input, treeview): @@ -874,19 +876,36 @@ class OuterWindow(Gtk.Window): self.add(self.grid) if is_game_mode is True: self.fullscreen() + elif query_config(None, "fullscreen")[0] == "true": + logger.info("User preference for 'fullscreen' is 'true'") + self.maximize() else: - if query_config(None, "fullscreen")[0] == "true": - self.maximize() + if os.path.isfile(res_path): + with open(res_path, "r") as infile: + try: + data = json.load(infile) + valid_json = True + except json.decoder.JSONDecodeError: + logger.critical("JSON decode error in '%s'" %(res_path)) + valid_json = False + else: + valid_json = False + if valid_json: + res = data["res"] + w = res["width"] + h = res["height"] + logger.info("Restoring window size to %s,%s" %(w,h)) + self.set_default_size(w, h) - # Hide FilterPanel on main menu self.show_all() + # Hide FilterPanel on main menu self.grid.right_panel.set_filter_visibility(False) self.grid.sel_panel.set_visible(False) self.grid.scrollable_treelist.treeview.grab_focus() def halt_proc_and_quit(self, window, event): self.grid.terminate_treeview_process() - Gtk.main_quit() + save_res_and_quit(window) class ScrollableTree(Gtk.ScrolledWindow): @@ -896,7 +915,6 @@ class ScrollableTree(Gtk.ScrolledWindow): self.treeview = TreeView(is_steam_deck) self.add(self.treeview) - class RightPanel(Gtk.Box): def __init__(self, is_steam_deck): super().__init__(spacing=6) @@ -925,8 +943,7 @@ class RightPanel(Gtk.Box): self.question_button.connect("clicked", self._on_button_clicked) self.pack_start(self.debug_toggle, False, True, 0) - if is_steam_deck is False: - self.pack_start(self.question_button, False, True, 0) + self.pack_start(self.question_button, False, True, 0) def _on_button_toggled(self, button, command): grid = self.get_parent() @@ -971,7 +988,7 @@ class ButtonBox(Gtk.Box): button.set_opacity(0.6) self.buttons.append(button) button.connect("clicked", self._on_selection_button_clicked) - self.pack_start(button, False, False, True) + self.pack_start(button, False, False, 0) self.buttons[0].set_opacity(1.0) @@ -1043,7 +1060,9 @@ class ButtonBox(Gtk.Box): if context == ButtonType.EXIT: logger.info("Normal user exit") - Gtk.main_quit() + widgets = relative_widget(self) + window = widgets["outer"] + save_res_and_quit(window) return cols = treeview.get_columns() @@ -1073,12 +1092,13 @@ class ButtonBox(Gtk.Box): class CalcDist(multiprocessing.Process): - def __init__(self, widget, addr, result_queue, cache): + def __init__(self, widget, addr, qport, result_queue, cache): super().__init__() self.widget = widget self.result_queue = result_queue self.addr = addr + self.qport = str(qport) self.ip = addr.split(':')[0] def run(self): @@ -1087,7 +1107,7 @@ class CalcDist(multiprocessing.Process): self.result_queue.put([self.addr, cache[self.addr][0], cache[self.addr][1]]) return proc = call_out(self.widget, "get_dist", self.ip) - proc2 = call_out(self.widget, "test_ping", self.ip) + proc2 = call_out(self.widget, "test_ping", self.ip, self.qport) km = proc.stdout ping = proc2.stdout self.result_queue.put([self.addr, km, ping]) @@ -1314,6 +1334,7 @@ class TreeView(Gtk.TreeView): if self.view == WindowContext.TABLE_API or self.view == WindowContext.TABLE_SERVER: addr = self.get_column_at_index(7) + qport = self.get_column_at_index(8) if addr is None: server_tooltip[0] = format_tooltip() grid.update_statusbar(server_tooltip[0]) @@ -1327,7 +1348,7 @@ class TreeView(Gtk.TreeView): grid.update_statusbar(tooltip) return self.emit("on_distcalc_started") - self.current_proc = CalcDist(self, addr, self.queue, cache) + self.current_proc = CalcDist(self, addr, qport, self.queue, cache) self.current_proc.start() else: tooltip = format_metadata(row_sel) @@ -1582,13 +1603,15 @@ class TreeView(Gtk.TreeView): column_title = "Name" saved_size = data["cols"][column_title] column.set_fixed_width(saved_size) + column.set_expand(True) else: if ("Name" in column_title): column.set_fixed_width(800) if (column_title == "Map"): column.set_fixed_width(300) - self.append_column(column) + if i != 10: + self.append_column(column) self.update_first_col(mode.dict["label"]) @@ -2330,12 +2353,8 @@ class Grid(Gtk.Grid): self._version = "%s %s" %(app_name, sys.argv[2]) self.scrollable_treelist = ScrollableTree(is_steam_deck) - if is_steam_deck is True: - self.scrollable_treelist.set_hexpand(False) - self.scrollable_treelist.set_vexpand(True) - else: - self.scrollable_treelist.set_hexpand(True) - self.scrollable_treelist.set_vexpand(True) + self.scrollable_treelist.set_hexpand(False) + self.scrollable_treelist.set_vexpand(True) self.right_panel = RightPanel(is_steam_deck) self.sel_panel = ModSelectionPanel() @@ -2352,14 +2371,9 @@ class Grid(Gtk.Grid): self.bar.add(self.status_right_label) self.update_right_statusbar() - if is_steam_deck is True: - self.attach(self.scrollable_treelist, 0, 0, 4, 1) - self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 4, 1) - self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1) - else: - self.attach(self.scrollable_treelist, 0, 0, 7, 5) - self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 7, 1) - self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1) + self.attach(self.scrollable_treelist, 0, 0, 3, 1) + self.attach_next_to(self.bar, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 3, 1) + self.attach_next_to(self.right_panel, self.scrollable_treelist, Gtk.PositionType.RIGHT, 1, 1) def update_right_statusbar(self): config_vals.clear() @@ -2438,16 +2452,50 @@ class App(Gtk.Application): self.win = OuterWindow(is_steam_deck, is_game_mode) self.win.set_icon_name("dzgui") + accel = Gtk.AccelGroup() accel.connect(Gdk.KEY_q, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE, self._halt_window_subprocess) self.win.add_accel_group(accel) - GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit) + + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self._catch_sigint) Gtk.main() - def _halt_window_subprocess(self, accel_group, window, code, flag): - self.win.halt_proc_and_quit(self, None) + def _catch_sigint(self): + self.win.halt_proc_and_quit(self.win, None) + def _halt_window_subprocess(self, accel_group, window, code, flag): + self.win.halt_proc_and_quit(self.win, None) + + +def save_res_and_quit(window): + if window.props.is_maximized: + Gtk.main_quit() + return + rect = window.get_size() + + def write_json(rect): + data = {"res": { "width": rect.width, "height": rect.height } } + j = json.dumps(data, indent=2) + with open(res_path, "w") as outfile: + outfile.write(j) + logger.info("Wrote initial window size to '%s'" %(res_path)) + + if os.path.isfile(res_path): + with open(res_path, "r") as infile: + try: + data = json.load(infile) + data["res"]["width"] = rect.width + data["res"]["height"] = rect.height + with open(res_path, "w") as outfile: + outfile.write(json.dumps(data, indent=2)) + except json.decoder.JSONDecodeError: + logger.critical("JSON decode error in '%s'" %(res_path)) + write_json(rect) + else: + write_json(rect) + + Gtk.main_quit() class ModSelectionPanel(Gtk.Box): def __init__(self): @@ -2471,7 +2519,6 @@ class ModSelectionPanel(Gtk.Box): button.connect("clicked", self._on_button_clicked) self.pack_start(button, False, True, 0) - def initialize(self): l = len(self.get_children()) last = self.get_children()[l-1] @@ -2483,7 +2530,6 @@ class ModSelectionPanel(Gtk.Box): case "Unhighlight stale": i.set_label("Highlight stale") - def _on_button_clicked(self, button): self.active_button = button label = button.get_label() @@ -2512,7 +2558,6 @@ class ModSelectionPanel(Gtk.Box): path = Gtk.TreePath(i) treeview.get_selection().select_path(path) - def _remove_last_button(self): children = self.get_children() l = len(children) @@ -2616,7 +2661,7 @@ class FilterPanel(Gtk.Box): self.keyword_entry.set_placeholder_text("Filter by keyword") self.keyword_entry.connect("activate", self._on_keyword_enter) self.keyword_entry.connect("key-press-event", self._on_esc_pressed) - + completion = Gtk.EntryCompletion(inline_completion=True) completion.set_text_column(0) completion.set_minimum_key_length(1) @@ -2625,7 +2670,7 @@ class FilterPanel(Gtk.Box): renderer_text = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END) self.maps_combo = Gtk.ComboBox.new_with_model_and_entry(map_store) self.maps_combo.set_entry_text_column(0) - + # instantiate maps completer entry self.maps_entry = self.maps_combo.get_child() self.maps_entry.set_completion(completion) @@ -2637,12 +2682,20 @@ class FilterPanel(Gtk.Box): self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("key-press-event", self._on_esc_pressed) - self.pack_start(self.filters_label, False, False, True) - self.pack_start(self.keyword_entry, False, False, True) - self.pack_start(self.maps_combo, False, False, True) + self.pack_start(self.filters_label, False, False, 0) + self.pack_start(self.keyword_entry, False, False, 0) + self.pack_start(self.maps_combo, False, False, 0) + button_grid = Gtk.Grid() + row = 1 + col = 0 for i, check in enumerate(checks[0:]): - self.pack_start(checks[i], False, False, True) + col = col + 1 + if (col > 3): + row = row + 1 + col = 1 + button_grid.attach(checks[i], col, row, 1, 1) + self.pack_start(button_grid, False, False, 0) def _on_map_entry_keypress(self, entry, event): match event.keyval: