fix: update active maps combo on tab change

This commit is contained in:
aclist 2026-02-15 23:58:48 +09:00
parent 4d0bb2f4f8
commit c3e2f6e4b4
13 changed files with 200 additions and 231 deletions

View File

@ -361,14 +361,14 @@ def details(ip: str, qport: int) -> Details:
return Details(rows, description, True) return Details(rows, description, True)
def ping(iteration: int, row: list) -> Ping: def ping(iteration: int, addr: list, qport: int, ping: int) -> Ping:
addr = row[7] # addr = row[7]
qport = row[8] # qport = row[8]
res = None res = None
if row[9] != 9999: if ping != 9999:
return Ping(addr, iteration, row[9]) return Ping(addr, iteration, ping)
try: try:
ip = addr.split(":")[0] ip = addr.split(":")[0]

View File

@ -115,9 +115,12 @@ class Controller(GObject.GObject):
self.emitter = Emitter() self.emitter = Emitter()
self.emitter.connect("map_selection_changed", self._on_map_selection_changed) self.emitter.connect("map_selection_changed", self._on_map_selection_changed)
self.emitter.connect("check_toggled", self._on_check_toggled) self.emitter.connect("check_toggled", self._on_check_toggled)
self.emitter.connect("servers_loaded", self._on_servers_loaded)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init)
# NOTE: suppress requests until entire UI is loaded # NOTE: suppress requests until entire UI is loaded
self.loaded = False self.loaded = False
self.pending_jobs = 1
def get_emitter(self) -> Emitter: def get_emitter(self) -> Emitter:
return self.emitter return self.emitter
@ -131,7 +134,7 @@ class Controller(GObject.GObject):
GLib.idle_add(self._destroy_on_idle) GLib.idle_add(self._destroy_on_idle)
self = args[0] self = args[0]
self.wait_dialog = WaitDialog(self, dialog_str, True) self.wait_dialog = WaitDialog(self, dialog_str, jobs=self.pending_jobs)
self.wait_dialog.show_all() self.wait_dialog.show_all()
thread = threading.Thread(target=callback) thread = threading.Thread(target=callback)
thread.start() thread.start()
@ -388,48 +391,29 @@ class Controller(GObject.GObject):
params = Servers.params params = Servers.params
serv = [] serv = []
i = 0 i = 0
total = 10
with ThreadPoolExecutor() as executor: with ThreadPoolExecutor() as executor:
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params] futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
# wait(futures)
for future in as_completed(futures): for future in as_completed(futures):
try: try:
i += 1 i += 1
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(i / total)) GLib.idle_add(lambda: self.wait_dialog.increment())
res = future.result(timeout=3) res = future.result(timeout=3)
if res.status != 200 or not res.parsed: if res.status != 200 or not res.parsed:
# TODO: pop warning dialog, create enum around various failure states
# set failure type here
self.cleanup_func = CleanupFunc(self.cleanup_on_failure) self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
return return
j = res.json j = res.json
serv += j["response"]["servers"] serv += j["response"]["servers"]
except Exception as e: except Exception as e:
# TODO: save exception text logger.critical(e)
print(e)
self.cleanup_func = CleanupFunc(self.cleanup_on_failure) self.cleanup_func = CleanupFunc(self.cleanup_on_failure)
""" # NOTE: This step is allowed to fail, since this metadata is incidental
TODO: e.g. class method like 'total'
total of 10 params for this call
@call_on_thread(msg, jobs=10)
if total > 1, pop progress bar
GLib.idle_add(lambda: self.wait_dialog.format_secondary_text("Pass 2"))
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(0.5))
subclass or wrap add fraction and increment, then calculate off of total
This next step is allowed to fail, since this metadata is incidental
"""
res = Servers.query_api(key, APPID_DAYZ_EXP, "") res = Servers.query_api(key, APPID_DAYZ_EXP, "")
if res.status == 200 and res.parsed is True: if res.status == 200 and res.parsed is True:
j = res.json j = res.json
serv += j["response"]["servers"] serv += j["response"]["servers"]
GLib.idle_add( GLib.idle_add(lambda: self.wait_dialog.increment("Unpacking servers"))
lambda: self.wait_dialog.format_secondary_text("Unpacking servers")
)
GLib.idle_add(lambda: self.wait_dialog.prog.set_fraction(10 / total))
# TODO: additional ping column pass, collated
parsed = Servers.parse_json(serv) parsed = Servers.parse_json(serv)
self.push_data_success(parsed, FilterMode.INITIAL) self.push_data_success(parsed, FilterMode.INITIAL)
@ -466,7 +450,6 @@ class Controller(GObject.GObject):
return value return value
def copy_name(self, path: Gtk.TreePath) -> None: def copy_name(self, path: Gtk.TreePath) -> None:
# TODO: column values are deterministic, perhaps use a col name to index map
name = self.get_col_value_by_path_index(path, 0) name = self.get_col_value_by_path_index(path, 0)
if name is None: if name is None:
return return
@ -659,13 +642,13 @@ class Controller(GObject.GObject):
sel.select_path(path) sel.select_path(path)
# TODO: make as method of tree? # TODO: make as method of tree?
# FIXME: move cursor when finished
def uncolorize_mods(self) -> None: def uncolorize_mods(self) -> None:
model = self.get_mod_store() model = self.get_mod_store()
for mod in model: for mod in model:
it = mod.iter it = mod.iter
path = model.get_path(it) path = model.get_path(it)
model[path][4] = None model[path][4] = None
self.mediator.modtreeview.set_cursor(0)
def dump_test_2(self) -> None: def dump_test_2(self) -> None:
import time import time
@ -833,8 +816,10 @@ class Controller(GObject.GObject):
return self.get_active_treeview().get_map_man() return self.get_active_treeview().get_map_man()
def cleanup_on_success(self) -> None: def cleanup_on_success(self) -> None:
self.pending_jobs = 1
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
treeview.set_loaded(True) treeview.set_loaded(True)
treeview.set_model(self.to_insert) treeview.set_model(self.to_insert)
map_man = treeview.get_map_man() map_man = treeview.get_map_man()
@ -842,6 +827,7 @@ class Controller(GObject.GObject):
# model insertion after thread closes # model insertion after thread closes
# cf. servers_loaded signal # cf. servers_loaded signal
# TODO: servers_loaded vs servers_reloaded
context = self.get_active_context() context = self.get_active_context()
self.emitter.emit("servers_loaded", context) self.emitter.emit("servers_loaded", context)
@ -856,27 +842,29 @@ class Controller(GObject.GObject):
def cleanup_on_failure(self) -> None: def cleanup_on_failure(self) -> None:
treeview = self.get_active_treeview() treeview = self.get_active_treeview()
treeview.set_model(None)
treeview.set_loaded(True) treeview.set_loaded(True)
map_man = treeview.get_map_man() map_man = treeview.get_map_man()
# TODO: what if refresh action occurred, and the old model is still valid?
# TODO: disable map, keyword, and filter widgets if model is None # TODO: disable map, keyword, and filter widgets if model is None
# -> signal driven (servers_empty) # -> signal driven (servers_empty)
map_man.set_unique_maps(None) # TODO: what if refresh action occurred and failed, and the old model is still valid?
# skip the step below if refresh action failed
# do not wipe control model in this case
# e.g. if treeview.is_refresh():
# revert old model
# wipe refresh state to False
context = self.get_active_context() treeview.set_model(None)
# TODO: distinguish signals, e.g. "servers_failed_to_load"
self.emitter.emit("servers_loaded", context)
treeview.grab_focus() treeview.grab_focus()
dialog = ExceptionDialog(self, "API TIMEOUT")
dialog.run()
# def push_data_failure(self) -> None: map_man.set_unique_maps(None)
# #treeview = self.get_active_treeview() context = self.get_active_context()
# #treeview.set_loaded(True)
# # TODO: wipe control model on failure or keep old results? # TODO: distinguish signals, e.g. "servers_failed_to_load"
# # manager = treeview.get_filter_man() # may trigger different behavior
# self.cleanup_func = CleanupFunc(self.cleanup_on_failure) self.emitter.emit("servers_loaded", context)
dialog = ExceptionDialog(self, strings.api_warn_msg)
dialog.run()
def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None: def push_data_success(self, data: tuple, mode: Optional[FilterMode]) -> None:
# FIXME: set outside of thread # FIXME: set outside of thread
@ -901,6 +889,7 @@ class Controller(GObject.GObject):
self.cleanup_func = CleanupFunc(self.cleanup_on_success) self.cleanup_func = CleanupFunc(self.cleanup_on_success)
def highlight_stale_cleanup(self, stale_mods: list) -> None: def highlight_stale_cleanup(self, stale_mods: list) -> None:
"""Manipulates attached ListStore in the main event loop"""
model = self.get_mod_store() model = self.get_mod_store()
for mod in model: for mod in model:
it = mod.iter it = mod.iter
@ -911,6 +900,7 @@ class Controller(GObject.GObject):
@call_on_thread(strings.dialog.working) @call_on_thread(strings.dialog.working)
def highlight_stale(self) -> None: def highlight_stale(self) -> None:
# TODO: set progress bar for number of mods
stale = find_stale_mods(self.prefs.paths.config) stale = find_stale_mods(self.prefs.paths.config)
self.cleanup_func = CleanupFunc(self.highlight_stale_cleanup, stale) self.cleanup_func = CleanupFunc(self.highlight_stale_cleanup, stale)
@ -995,6 +985,7 @@ class Controller(GObject.GObject):
@call_on_thread(strings.dialog.fetching) @call_on_thread(strings.dialog.fetching)
def run_query_func(self, func: Callable) -> None: def run_query_func(self, func: Callable) -> None:
# TODO: use CleanupFunc -> StoredFunc
func() func()
@call_on_thread(strings.dialog.filtering) @call_on_thread(strings.dialog.filtering)
@ -1010,6 +1001,7 @@ class Controller(GObject.GObject):
# TODO: call filter_man methods directly # TODO: call filter_man methods directly
def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None: def refilter_model(self, mode: FilterMode, label: Optional[str] = None) -> None:
tv = self.get_active_treeview() tv = self.get_active_treeview()
#tv.freeze_child_notify()
filter_man = tv.get_filter_man() filter_man = tv.get_filter_man()
if filter_man.get_control() is None: if filter_man.get_control() is None:
return return
@ -1022,7 +1014,7 @@ class Controller(GObject.GObject):
self.emitter.emit("servers_loaded", treeview.get_enum()) self.emitter.emit("servers_loaded", treeview.get_enum())
return return
func = treeview.get_query_func() func, jobs = treeview.get_query_func()
if func is None: if func is None:
self.emitter.emit("servers_loaded", treeview.get_enum()) self.emitter.emit("servers_loaded", treeview.get_enum())
treeview.set_model(None) treeview.set_model(None)
@ -1031,8 +1023,10 @@ class Controller(GObject.GObject):
# TODO: clear ephemeral model if necessary # TODO: clear ephemeral model if necessary
# manager = treeview.get_filter_man() # manager = treeview.get_filter_man()
# manager.clear_model() # manager.clear_model()
#treeview.fancy_col.set_cell_data_func(treeview.fancy_rend, None)
self.first_iteration = True self.first_iteration = True
self.pending_jobs = jobs
self.run_query_func(func) self.run_query_func(func)
def get_favorite(self) -> tuple[str, str] | tuple[None, None]: def get_favorite(self) -> tuple[str, str] | tuple[None, None]:
@ -1080,6 +1074,27 @@ class Controller(GObject.GObject):
map_man = self.get_map_man() map_man = self.get_map_man()
return map_man.get_prior_map() return map_man.get_prior_map()
def get_active_map(self) -> None:
return self.get_map_man().get_active_map()
def set_active_map(self, ind: int) -> None:
self.get_map_man().set_active_map(ind)
def _on_servers_loaded_init(self, emitter: "Emitter") -> None:
# FIXME: wipe maps store when changing tabs if model is none
tv = self.get_active_treeview()
if tv.loaded is False:
return
store = self.get_map_store()
self.emitter.emit("load_maps", store)
def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None:
# NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None
state = self.has_server_model()
tv = self.get_active_treeview()
tv.set_headers_visible(state)
tv.set_headers_clickable(state)
def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None: def _on_check_toggled(self, emitter: Emitter, label: str, state: bool) -> None:
map_man = self.get_map_man() map_man = self.get_map_man()
map_man.set_filter(label, state) map_man.set_filter(label, state)

View File

@ -1,6 +1,7 @@
import datetime
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from warnings import deprecated from warnings import deprecated
from dzgui.const.enum import FilterMode from dzgui.const.enum import FilterMode
@ -114,16 +115,18 @@ class FilteredModelManager:
case FilterMode.TOGGLE_ON: case FilterMode.TOGGLE_ON:
rows = self.filter_toggle_on(filters, *args) rows = self.filter_toggle_on(filters, *args)
if mode is not FilterMode.INITIAL: # if mode is not FilterMode.INITIAL:
for row in rows: # for row in rows:
if row[7] in self.ping_cache: # if row[7] in self.ping_cache:
row[9] = self.ping_cache[row[7]] # row[9] = self.ping_cache[row[7]]
clone = self.new_model_from_class(ServerColumns) clone = self.new_model_from_class(ServerColumns)
if len(rows) > 0: if len(rows) > 0:
# TODO: considering exposing an option to not sort rows
rows = self.sort_rows(rows) rows = self.sort_rows(rows)
for row in rows: for row in rows:
clone.append(row) clone.append(row)
# else: # else:
# print("nothing to filter, sending none") # print("nothing to filter, sending none")
# clone = None # clone = None
@ -256,7 +259,7 @@ class FilteredModelManager:
def convert_model_to_list(self, model: ListStore) -> list: def convert_model_to_list(self, model: ListStore) -> list:
return [[el for el in row] for row in model] return [[el for el in row] for row in model]
def set_filtered(self, rows: list | None) -> None: def set_filtered(self, rows: Optional[list]) -> None:
if rows is None: if rows is None:
rows = [] rows = []
self.filtered = rows self.filtered = rows

View File

@ -31,12 +31,19 @@ class MapManager:
strings.filter_unofficial: True, strings.filter_unofficial: True,
strings.filter_modded: True, strings.filter_modded: True,
} }
self.active_map = 0
self.filters: list self.filters: list
self.enabled_filters = dict(self.default_filters) self.enabled_filters = dict(self.default_filters)
def reinit_filters(self) -> None: def reinit_filters(self) -> None:
self.enabled_filters = dict(self.default_filters) self.enabled_filters = dict(self.default_filters)
def get_active_map(self) -> int:
return self.active_map
def set_active_map(self, ind: int) -> None:
self.active_map = ind
def get_default_filters(self) -> dict: def get_default_filters(self) -> dict:
"""Deep copy of defaults""" """Deep copy of defaults"""
return dict(self.default_filters) return dict(self.default_filters)

View File

@ -99,10 +99,8 @@ steam_missing = "Local Steam installation is not set, possibly malformed config
build_corrupted = ( build_corrupted = (
"Steam settings or DayZ installation may be corrupted. Try restarting Steam." "Steam settings or DayZ installation may be corrupted. Try restarting Steam."
) )
api_warn_msg = """No servers returned. Possible causes: api_warn_msg = """No servers returned. Please wait and try again.
no servers in favorites/history, local network issue, or API key on cooldown. If this issue persists, your API key may be defunct or your network is blocking requests.
Return to the main menu, wait 30s, and try again.
If this issue persists, your API key may be defunct.
""" """
server_timeout = "Timed out when querying server, check IP or try again later" server_timeout = "Timed out when querying server, check IP or try again later"
server_error = ( server_error = (
@ -167,7 +165,7 @@ ping_tooltip = (
) )
# statusbar_helptext = "Select a row to see its detailed description" # statusbar_helptext = "Select a row to see its detailed description"
#statusbar_helptext = "No server metadata to list." # statusbar_helptext = "No server metadata to list."
# use e.g. filters.1pp # use e.g. filters.1pp
filter_1pp = "1PP" filter_1pp = "1PP"

View File

@ -153,7 +153,6 @@ class FilterPanel(Gtk.Box):
"request_maps_focus", lambda _: self.maps_entry.grab_focus() "request_maps_focus", lambda _: self.maps_entry.grab_focus()
) )
self.emitter.connect("check_button_pressed", self.toggle_check_by_key) self.emitter.connect("check_button_pressed", self.toggle_check_by_key)
# self.emitter.connect("servers_loaded", self._on_servers_loaded)
self.emitter.connect("load_maps", self._on_maps_loaded) self.emitter.connect("load_maps", self._on_maps_loaded)
# TODO: break into MapsCombo class # TODO: break into MapsCombo class
@ -177,6 +176,9 @@ class FilterPanel(Gtk.Box):
self.maps_combo.connect("changed", self._on_map_changed) self.maps_combo.connect("changed", self._on_map_changed)
self.maps_combo.connect("key-press-event", self._on_combo_keypress) self.maps_combo.connect("key-press-event", self._on_combo_keypress)
# FIXME: should be a property of treeview's meta manager
self.active_map = 0
for el in ( for el in (
self.filters_label, self.filters_label,
self.keyword_entry, self.keyword_entry,
@ -194,10 +196,12 @@ class FilterPanel(Gtk.Box):
if text == row[0]: if text == row[0]:
self.maps_combo.set_active(i) self.maps_combo.set_active(i)
self._on_map_changed(self.maps_combo) self._on_map_changed(self.maps_combo)
self.controller.set_active_map(i)
def _on_maps_loaded(self, emitter: "Emitter", store: Gtk.ListStore) -> None: def _on_maps_loaded(self, emitter: "Emitter", store: Gtk.ListStore) -> None:
self.maps_combo.set_model(store) self.maps_combo.set_model(store)
self.maps_combo.set_active(0) ind = self.controller.get_active_map()
self.maps_combo.set_active(ind)
self.button_grid.reload_filters() self.button_grid.reload_filters()
# TODO: move into metamanager # TODO: move into metamanager
@ -259,6 +263,8 @@ class FilterPanel(Gtk.Box):
store = self.controller.get_map_store() store = self.controller.get_map_store()
if len(text) >= completion.get_minimum_key_length(): if len(text) >= completion.get_minimum_key_length():
completion.set_model(store) completion.set_model(store)
ind = self.get_active_combo()
self.controller.set_active_map(ind)
def restore_focus_to_treeview(self) -> Literal[False]: def restore_focus_to_treeview(self) -> Literal[False]:
view = self.controller.get_active_treeview() view = self.controller.get_active_treeview()

View File

@ -44,9 +44,10 @@ class RightPanel(Gtk.Box):
state = self.controller.has_server_model() state = self.controller.has_server_model()
for widget in (self.refresh_button, self.filters_vbox): for widget in (self.refresh_button, self.filters_vbox):
widget.set_sensitive(state) widget.set_sensitive(state)
# FIXME: may wish to reload these in place to show new data # FIXME: may wish to reload these in place to show new data
if context in (ServerTab.RECENT, ServerTab.SAVED): # if context in (ServerTab.RECENT, ServerTab.SAVED):
self.refresh_button.set_sensitive(False) # self.refresh_button.set_sensitive(False)
# TODO: reference for ping pass # TODO: reference for ping pass
# def _on_ping_clicked(self, button: Gtk.Button) -> None: # def _on_ping_clicked(self, button: Gtk.Button) -> None:

View File

@ -1,83 +0,0 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING
from dzgui.const.enum import Popup
class EntryDialog(GenericDialog):
def __init__(self, text: str, mode: Popup, link: str, button_type=None):
super().__init__(text, mode)
"""
Wraps Gtk.Entry in a dialog and provides basic response handling.
Returns user input as a string or None.
The Entry widget itself can be manipulated via the get_entry() method.
"""
self.dialog = GenericDialog(text, mode)
self.dialogBox = self.dialog.get_content_area()
self.dialog.set_default_response(Gtk.ResponseType.OK)
self.dialog.set_size_request(500, 0)
self.user_entry = Gtk.Entry()
set_surrounding_margins(self.user_entry, 20)
self.user_entry.set_margin_top(0)
self.user_entry.set_size_request(250, 0)
self.user_entry.set_activates_default(True)
self.dialogBox.pack_start(self.user_entry, NO_EXPAND, NO_FILL, NO_PADDING)
if link:
button = Gtk.Button(label=link)
button.set_margin_start(60)
button.set_margin_end(60)
button.connect("clicked", self._on_button_clicked, button_type)
self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, NO_PADDING)
self.ok = self.dialog.action_area.get_children()[1]
self.ok.set_sensitive(False)
self.user_entry.connect("insert-text", self._on_text_typed)
self.user_entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted
)
def _is_valid_text(self, text: str) -> bool:
if text.isspace():
return False
if len(text) == 0:
return False
return True
def _on_text_deleted(
self, buffer: Gtk.EntryBuffer, position: int, chars: int
) -> None:
text = buffer.get_text()
state = self._is_valid_text(text)
self.ok.set_sensitive(state)
def _on_text_typed(
self, entry: Gtk.Entry, text: str, length: int, pos: int
) -> None:
state = self._is_valid_text(text)
self.ok.set_sensitive(state)
def _on_button_clicked(self, button: Gtk.Button, enum: RowType) -> None:
result = open_links.open_link_by_rowtype(enum)
if result is False:
AppNav.window.spawn_dialog(strings.something_wrong, Popup.NOTIFY)
def get_entry(self) -> Gtk.Entry:
return self.user_entry
def get_input(self) -> str | None:
self.dialog.show_all()
response = self.dialog.run()
text = self.user_entry.get_text()
self.dialog.destroy()
if (response == Gtk.ResponseType.OK) and (text != ""):
return text
else:
return None

View File

@ -101,9 +101,7 @@ class NotifyDialog(GenericDialog):
class WaitDialog(GenericDialog): class WaitDialog(GenericDialog):
def __init__( def __init__(self, controller: "Controller", secondary: str, jobs: int = 1):
self, controller: "Controller", secondary: str, show_progress: bool = False
):
super().__init__( super().__init__(
controller=controller, controller=controller,
text=strings.wait, text=strings.wait,
@ -112,16 +110,26 @@ class WaitDialog(GenericDialog):
secondary=secondary, secondary=secondary,
) )
self.jobs = jobs
self.cur_job = 1
self.connect("delete-event", self._on_dialog_delete) self.connect("delete-event", self._on_dialog_delete)
content = self.get_content_area() content = self.get_content_area()
spinner = Gtk.Spinner() spinner = Gtk.Spinner()
self.prog = Gtk.ProgressBar() self.prog = Gtk.ProgressBar()
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0) content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
if show_progress: if self.jobs > 1:
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0) content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
else: else:
spinner.start() spinner.start()
def increment(self, msg: str = "") -> None:
if msg != "":
self.format_secondary_text(msg)
fraction = self.cur_job / self.jobs
self.prog.set_fraction(fraction)
self.cur_job += 1
def _on_dialog_delete( def _on_dialog_delete(
self, response_id: Gtk.ResponseType, event: Gdk.Event self, response_id: Gtk.ResponseType, event: Gdk.Event
) -> Literal[True]: ) -> Literal[True]:

View File

@ -1,14 +1,13 @@
import textwrap import textwrap
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
from dzgui.const.enum import Popup, Preferences
from dzgui.const.constants import NO_EXPAND, NO_FILL from dzgui.const.constants import NO_EXPAND, NO_FILL
from dzgui.util.open_links import open_user_workshop
from dzgui.util.strings import notice from dzgui.util.strings import notice
from dzgui.views.dialogs.generic import GenericDialog from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.components.buttons import SteamTextButton, WebButton from dzgui.views.components.buttons import SteamTextButton
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402 from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
@ -18,9 +17,17 @@ if TYPE_CHECKING:
class WorkshopLinkDialog(GenericDialog): class WorkshopLinkDialog(GenericDialog):
def __init__(self, controller: "Controller", text: str, button_label: str, uid: str): def __init__(
self, controller: "Controller", text: str, button_label: str, uid: str
):
text = textwrap.dedent(text).replace("\n", " ") text = textwrap.dedent(text).replace("\n", " ")
super().__init__(controller, text=notice, mtype=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, secondary=text) super().__init__(
controller,
text=notice,
mtype=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.OK,
secondary=text,
)
self.controller = controller self.controller = controller
self.dialogBox = self.get_content_area() self.dialogBox = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK) self.set_default_response(Gtk.ResponseType.OK)
@ -43,12 +50,10 @@ class WorkshopLinkDialog(GenericDialog):
def _on_button_clicked(self, button: Gtk.Button, uid: str) -> None: def _on_button_clicked(self, button: Gtk.Button, uid: str) -> None:
# TODO: currently checks user id on instantiation # TODO: currently checks user id on instantiation
self.controller.open_user_workshop(uid) self.controller.open_user_workshop(uid)
#client = self.controller.query_config(Preferences.CLIENT) # client = self.controller.query_config(Preferences.CLIENT)
#open_user_workshop(uid, client) # open_user_workshop(uid, client)
def _on_dialog_response( def _on_dialog_response(self, dialog: Self, resp: Gtk.ResponseType) -> None:
self, dialog: Self, resp: Gtk.ResponseType
) -> None:
match resp: match resp:
case Gtk.ResponseType.DELETE_EVENT: case Gtk.ResponseType.DELETE_EVENT:
return return

View File

@ -20,27 +20,21 @@ class ContextMixin(TreeView):
) -> None: ) -> None:
# FIXME: double click causes issue # FIXME: double click causes issue
# split into present by click and present by key # split into present by click and present by key
if self.is_selection_empty(): if self.is_selection_empty():
return False return False
if event.type is Gdk.EventType.BUTTON_PRESS: match event.type:
if event.button != 3: case Gdk.EventType.BUTTON_PRESS:
return False if event.button != 3:
try: return False
pathinfo = self.get_path_at_pos(int(event.x), int(event.y)) self._process_button_event(event)
if pathinfo is None: case Gdk.EventType.KEY_PRESS:
return True if event.state is not Gdk.ModifierType.CONTROL_MASK:
(path, col, cellx, celly) = pathinfo return False
if path is None: if event.keyval is not Gdk.KEY_l:
return True return False
self.set_cursor(path, col, False) case _:
except AttributeError:
pass
if event.type is Gdk.EventType.KEY_PRESS:
if event.state is not Gdk.ModifierType.CONTROL_MASK:
return False
if event.keyval is not Gdk.KEY_l:
return False return False
group = self.menu group = self.menu
@ -64,6 +58,18 @@ class ContextMixin(TreeView):
self.context_menu.select_first(False) self.context_menu.select_first(False)
def _process_button_event(self, event: Gdk.EventButton) -> None:
try:
pathinfo = self.get_path_at_pos(int(event.x), int(event.y))
if pathinfo is None:
return True
(path, col, cellx, celly) = pathinfo
if path is None:
return True
self.set_cursor(path, col, False)
except AttributeError:
pass
def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None: def _on_menu_click(self, widget: Gtk.MenuItem, enum: ContextMenu) -> None:
""" """
Local mods page allows multi selection, so ensure that only focused row Local mods page allows multi selection, so ensure that only focused row

View File

@ -37,8 +37,9 @@ class ServerNotebook(Gtk.ScrolledWindow):
self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN)
self.recent.set_query_func(self.controller.dump_test_2) self.recent.set_query_func(self.controller.dump_test_2)
# TODO: number of servers will not be known a priori until reading file during query phase
self.lan.set_query_func(self.controller.dump_test_2) self.lan.set_query_func(self.controller.dump_test_2)
self.browser.set_query_func(self.controller.dump_api) self.browser.set_query_func(self.controller.dump_api, jobs=11)
tabs = [ tabs = [
(self.browser, server_labels.browser), (self.browser, server_labels.browser),

View File

@ -7,12 +7,14 @@ from warnings import deprecated
from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.context_mixin import ContextMixin
from dzgui.const.enum import ContextMenuGroup, ServerTab from dzgui.const.enum import ContextMenuGroup, ServerTab
from dzgui.api.servers import Record from dzgui.api.servers import Record
from dzgui.api.servers import ping as Ping
from dzgui.model.map_model import MapManager from dzgui.model.map_model import MapManager
from dzgui.model.filtered_model import FilteredModelManager from dzgui.model.filtered_model import FilteredModelManager
from dzgui.util.dist import CalcDist from dzgui.util.dist import CalcDist
from dzgui.util import strings from dzgui.util import strings
from typing import Callable, Literal, TYPE_CHECKING from typing import Callable, Literal, TYPE_CHECKING
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import dzgui.util._json as JSON # noqa import dzgui.util._json as JSON # noqa
@ -56,6 +58,9 @@ class ServerTreeView(ContextMixin, TreeView):
self.queue_id: int self.queue_id: int
self.handler_id: int self.handler_id: int
self.query_func_jobs = 1
self.seen_cache = []
self.current_proc = None self.current_proc = None
self.queue = multiprocessing.Queue() self.queue = multiprocessing.Queue()
@ -95,17 +100,21 @@ class ServerTreeView(ContextMixin, TreeView):
else: else:
w = width_map[column_title] w = width_map[column_title]
column.set_fixed_width(w) column.set_fixed_width(w)
#if column_title == "Name": #if column_title == "Ping":
#self.fancy_col = column
#self.fancy_rend = renderer
#column.set_cell_data_func(renderer, self._get_ping)
# if column_title == "Name":
# column.set_fixed_width(800) # column.set_fixed_width(800)
#if column_title == "Map": # if column_title == "Map":
# column.set_fixed_width(300) # column.set_fixed_width(300)
# TODO: standardize widths based on column title and longest content # TODO: standardize widths based on column title and longest content
#if column_title == "Name": # if column_title == "Name":
# column.set_fixed_width(500) # column.set_fixed_width(500)
#if column_title == "Map": # if column_title == "Map":
# column.set_fixed_width(200) # column.set_fixed_width(200)
#if column_title == "IP": # if column_title == "IP":
# column.set_fixed_width(240) # column.set_fixed_width(240)
column.connect("notify::fixed-width", self._on_col_width_changed) column.connect("notify::fixed-width", self._on_col_width_changed)
@ -119,55 +128,47 @@ class ServerTreeView(ContextMixin, TreeView):
self.connect("map", self._on_map) self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
self.emitter.connect("servers_loaded_init", self._on_servers_loaded_init) def _get_ping(
self.emitter.connect("servers_loaded", self._on_servers_loaded) self,
column: Gtk.TreeViewColumn,
cell: Gtk.CellRendererText,
model: Gtk.TreeModel,
it: Gtk.TreeIter,
data: Any,
):
def ping_server(model, _iter, ip: str, qport: int, ping: int):
#res = Ping(0, model[_iter])
res = Ping(0, ip, qport, ping)
ping = res.ping
print(res)
GLib.idle_add(lambda: model.set(_iter, ping_column, ping))
def _on_servers_loaded(self, emitter: "Emitter", tab: "ServerTab") -> None: addr_column = 7
# FIXME: gets called on every tab, should be localized only to that context qport_column = 8
# hook signal from main controller ping_column = 9
state = self.controller.has_server_model() # TODO: unpack model before entering thread
# NOTE: workaround for GTK bug where fullscreen causes headers to vanish when model is None # test 40k servers with unpacked model
self.set_headers_visible(state) addr = model.get_value(it, addr_column).split(":")[0]
self.set_headers_clickable(state) qport = model.get_value(it, qport_column)
ping = model.get_value(it, ping_column)
ip = f"{addr}:{qport}"
def _on_servers_loaded_init(self, emitter: "Emitter") -> None: if ip in self.seen_cache:
if self.loaded is False:
return return
store = self.map_man.get_map_store() self.seen_cache.append(ip)
self.emitter.emit("load_maps", store)
#def _get_ping( thread = threading.Thread(
# self, daemon=True,
# column: Gtk.TreeViewColumn, target=ping_server,
# cell: Gtk.CellRendererText, args=(
# model: Gtk.TreeModel, model,
# it: Gtk.TreeIter, it,
# data: Any, ip,
#): qport,
# def ping(model, it, ip: str): ping
# # TODO: use a2s to ping server ),
# # a2s.info -> "ping" key )
# # Servers.ping() -> this accepts a whole row thread.start()
# # try, if failure just abort
# self.res = ip.replace(":", "%")
# num = 11111
# GLib.idle_add(lambda: model.set(it, 9, num))
# addr = model.get_value(it, 7).split(":")[0]
# qport = model.get_value(it, 8)
# ip = f"{addr}:{qport}"
# thread = threading.Thread(
# daemon=True,
# target=ping,
# args=(
# model,
# it,
# ip,
# ),
# )
# thread.start()
# pass
def start_timeout(self) -> None: def start_timeout(self) -> None:
self.queue_id = GLib.timeout_add(QUEUE_CHECK_DELAY, self._check_result_queue) self.queue_id = GLib.timeout_add(QUEUE_CHECK_DELAY, self._check_result_queue)
@ -178,7 +179,7 @@ class ServerTreeView(ContextMixin, TreeView):
def get_filter_man(self) -> FilteredModelManager: def get_filter_man(self) -> FilteredModelManager:
return self.filter_man return self.filter_man
#def shrink_to_fit(self) -> None: # def shrink_to_fit(self) -> None:
# cols = self.get_columns() # cols = self.get_columns()
# # TODO: run on only one treeview and propagate results # # TODO: run on only one treeview and propagate results
# # TODO: does not shrink name, map, ip fields to fit # # TODO: does not shrink name, map, ip fields to fit
@ -208,8 +209,8 @@ class ServerTreeView(ContextMixin, TreeView):
if self.get_enum() is ServerTab.LAN: if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", True) self.emitter.emit("lan_tab_toggled", True)
# FIXME: only if the tab is active
store = self.map_man.get_map_store() store = self.map_man.get_map_store()
# FIXME: if model is none, wipe maps
self.emitter.emit("load_maps", store) self.emitter.emit("load_maps", store)
self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc) self.handler_id = self.emitter.connect("statusbar_loaded", self.start_distcalc)
self.start_timeout() self.start_timeout()
@ -222,11 +223,12 @@ class ServerTreeView(ContextMixin, TreeView):
if self.get_enum() is ServerTab.LAN: if self.get_enum() is ServerTab.LAN:
self.emitter.emit("lan_tab_toggled", False) self.emitter.emit("lan_tab_toggled", False)
def set_query_func(self, func: Callable) -> None: def set_query_func(self, func: Callable, jobs: int = 1) -> None:
self.query_func = func self.query_func = func
self.query_func_jobs = jobs
def get_query_func(self) -> Callable | None: def get_query_func(self) -> Callable | None:
return self.query_func return self.query_func, self.query_func_jobs
def _on_col_width_changed( def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt