From 3c82b71f76635cde38e984c962f9eedf2d044d4c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:32:32 +0900 Subject: [PATCH] feat: live input validation --- dzgui/controllers/model.py | 3 -- dzgui/util/css.py | 9 ++++- dzgui/views/base.py | 17 ++++----- dzgui/views/components/connect_panel.py | 47 +++++++++++++++++++++++-- dzgui/views/components/filter_panel.py | 12 ++----- dzgui/views/components/right_panel.py | 15 ++++---- dzgui/views/trees/tree_server_mods.py | 4 +-- dzgui/views/trees/tree_servers.py | 2 ++ 8 files changed, 71 insertions(+), 38 deletions(-) diff --git a/dzgui/controllers/model.py b/dzgui/controllers/model.py index 3372bf5..8dae7e4 100644 --- a/dzgui/controllers/model.py +++ b/dzgui/controllers/model.py @@ -151,11 +151,9 @@ class ModelManager: if prior_map == "All maps": rows = self.filter_map(filters) else: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) case FilterMode.KEYWORD: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) case FilterMode.TOGGLE_OFF: @@ -164,7 +162,6 @@ class ModelManager: rows = self.filtered case FilterMode.TOGGLE_ON: - AppNav.right_panel.ping.set_sensitive(True) rows = self.filter_toggle_on(filters, *args) if mode is not FilterMode.INITIAL: diff --git a/dzgui/util/css.py b/dzgui/util/css.py index 90411ad..3c18249 100644 --- a/dzgui/util/css.py +++ b/dzgui/util/css.py @@ -11,11 +11,18 @@ def add_class(widget: Gtk.Widget, label: str) -> None: context = widget.get_style_context() context.add_class(label) -# TODO: def remove_class() + +def remove_class(widget: Gtk.Widget, label: str) -> None: + context = widget.get_style_context() + context.remove_class(label) + def load_css() -> None: # TODO: consider storing this in a data file css = """ + .invalid-entry { + border-color: red; + } .frame { border: 0px; } diff --git a/dzgui/views/base.py b/dzgui/views/base.py index 12d3301..5ef5a4c 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -80,14 +80,9 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # return rows # #def process_tree_option(choice: RowType) -> None: -# context = AppNav.treeview.view -# command = choice -# cmd_string = command.dict["label"] -# logger.info(f"Parsing tree option '{command}' for the context '{context}'") -# # # server tables # if command == RowType.RESOLVE_IP: -# record = AppNav.treeview.get_record() +# record = treeview.get_record() # wait_msg = command.dict["wait_msg"] # show_wait_dialog = True # @@ -103,12 +98,12 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # if command == RowType.QUICK_CONNECT: # record = MainController.query_config(Preferences.FAV_SRV) # if record == "": -# AppNav.window.spawn_dialog("No favorite server currently set", Popup.NOTIFY) +# spawn_dialog("No favorite server currently set", Popup.NOTIFY) # return # # record = str_to_record(record) # thread_new_with_dialog( -# AppNav.treeview.prepare_connection, +# treeview.prepare_connection, # parse_shell_output, # "Querying server", # command, @@ -131,7 +126,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) #def connect_by_ip(enum: RowType, response: str) -> None: # def _prep(response: str) -> None: # record = Servers.validate_ip(response) -# proc = AppNav.treeview.prepare_connection(record) +# proc = treeview.prepare_connection(record) # return proc # # thread_new_with_dialog( @@ -155,7 +150,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # logger.critical(e) # # raise dialog # return -# # proc = AppNav.treeview.prepare_connection(record) +# # proc = treeview.prepare_connection(record) # # return proc # # #thread_new_with_dialog( @@ -172,7 +167,7 @@ warnings.filterwarnings("ignore", ".*g_value_get_int", Warning) # if enum == RowType.CONN_BY_ID: # key = MainController.query_config(Preferences.BM) # if len(key) == 0: -# AppNav.window.spawn_dialog( +# spawn_dialog( # "No Battlemetrics API key is set; see Options", Popup.NOTIFY # ) # return diff --git a/dzgui/views/components/connect_panel.py b/dzgui/views/components/connect_panel.py index 97f132a..0ccdd1a 100644 --- a/dzgui/views/components/connect_panel.py +++ b/dzgui/views/components/connect_panel.py @@ -1,12 +1,14 @@ from typing import TYPE_CHECKING +from dzgui.api.servers import validate_ip +from dzgui.util.css import add_class, remove_class from dzgui.util.strings import connect_panel from dzgui.views.components.buttons import WebButton from dzgui.views.components.labels import BoldLabel import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk, Gdk # noqa E402 if TYPE_CHECKING: from dzgui.controllers.mc import Controller @@ -17,6 +19,7 @@ class ConnectPanel(Gtk.Frame): COLS = 1 ROWS = 1 + self.classname = "invalid-entry" self.controller = controller self.entry = Gtk.Entry( @@ -24,14 +27,16 @@ class ConnectPanel(Gtk.Frame): hexpand=True, tooltip_text=connect_panel.entry_tooltip ) + self.entry.connect("key-press-event", self._on_entry_keypress) + self.entry.connect("changed", self._on_text_changed) # TODO: get ip as well? user_fav = self.controller.get_favorite_label() server_name = user_fav if user_fav is not None else connect_panel.no_fav + self.fav_label = Gtk.Label(label=server_name, halign=Gtk.Align.START) scrollable_label = Gtk.ScrolledWindow() - label = Gtk.Label(label=server_name, halign=Gtk.Align.START) - scrollable_label.add(label) + scrollable_label.add(self.fav_label) self.fav_button = Gtk.Button(label=connect_panel.connect, tooltip_text=connect_panel.connect_tooltip @@ -70,3 +75,39 @@ class ConnectPanel(Gtk.Frame): self.grid.attach_next_to(el, sibling, pos, h_span, v_span) self.add(self.grid) + + def mark_valid(self) -> None: + self.conn_server.set_sensitive(True) + self.add_server.set_sensitive(True) + remove_class(self.entry, self.classname) + + def mark_invalid(self) -> None: + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) + add_class(self.entry, self.classname) + + def _on_text_changed(self, entry: Gtk.Entry) -> None: + text = entry.get_text() + if len(text) < 1: + self.conn_server.set_sensitive(False) + self.add_server.set_sensitive(False) + remove_class(entry, self.classname) + return + try: + validate_ip(text) + self.mark_valid() + except Exception as e: + if text.isdigit(): + self.mark_valid() + else: + self.mark_invalid() + + def set_fav_label(self, text: str) -> None: + # TODO: called by controller when changing fav + self.fav_label.set_text(text) + + def _on_entry_keypress(self, entry: Gtk.Entry, event: Gdk.EventKey) -> None: + if event.keyval == Gdk.KEY_Escape: + # NOTE: unselect text + entry.select_region(0, 0) + self.controller.grab_active_treeview() diff --git a/dzgui/views/components/filter_panel.py b/dzgui/views/components/filter_panel.py index 8afa0d1..ed6c1ef 100644 --- a/dzgui/views/components/filter_panel.py +++ b/dzgui/views/components/filter_panel.py @@ -108,9 +108,8 @@ class FilterPanel(Gtk.Box): def set_unique_maps(self, maps: list) -> None: if len(maps) < 1: return - # FIXME: clear typehints - u_maps = set([row[1] for row in maps]) # type: ignore - u_maps = sorted(u_maps) # type: ignore + u_maps = set([row[1] for row in maps]) + u_maps = sorted(u_maps) for m in u_maps: self.controller.append_map([m]) self.maps_hr.append(m) @@ -124,13 +123,6 @@ class FilterPanel(Gtk.Box): filters.append(k) return tuple(filters) - # used on personal/local server lists - def enable_all_filters(self) -> None: - for check in self.checks: - check.set_active(True) - for k in self.enabled_filters: - self.enabled_filters[k] = True - def reinit_panel(self) -> None: self.keyword_entry.set_text("") self.keyword_filter = "" diff --git a/dzgui/views/components/right_panel.py b/dzgui/views/components/right_panel.py index fb72e42..e5601f6 100644 --- a/dzgui/views/components/right_panel.py +++ b/dzgui/views/components/right_panel.py @@ -52,10 +52,9 @@ class RightPanel(Gtk.Box): def _on_refresh_clicked(self, button: RefreshButton) -> None: self.controller.refresh_tree() + # TODO: reference #def _on_ping_clicked(self, button: Gtk.Button) -> None: - # # TODO # block_signals() - # def _update_pings(): # # TODO # rows = ModelManager.get_filtered() @@ -71,17 +70,17 @@ class RightPanel(Gtk.Box): # temp_model[path][9] = res.ping # ModelManager.ping_cache[res.addr] = res.ping # # TODO: drop/rewrite - # self.AppNav.treeview.set_model(temp_model) - # self.AppNav.treeview.wait_dialog.destroy() - # self.AppNav.treeview.grab_focus() + # treeview.set_model(temp_model) + # treeview.wait_dialog.destroy() + # treeview.grab_focus() # # TODO: # unblock_signals() # temp_model = self.AppNav.treeview.get_model() - # self.AppNav.treeview.set_model(None) - # self.AppNav.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) - # self.AppNav.treeview.wait_dialog.show_all() + # treeview.set_model(None) + # treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT) + # treeview.wait_dialog.show_all() # thread = threading.Thread(target=_update_pings, args=()) # thread.start() diff --git a/dzgui/views/trees/tree_server_mods.py b/dzgui/views/trees/tree_server_mods.py index e84f0fc..df44134 100644 --- a/dzgui/views/trees/tree_server_mods.py +++ b/dzgui/views/trees/tree_server_mods.py @@ -76,14 +76,14 @@ class ModDialog(GenericDialog): # dialog.destroy() # # TODO: natively implemented # #if data.returncode == 1: - # # AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY) + # # spawn_dialog(strings.server_error, Popup.NOTIFY) # # return # self.show_all() # self.set_markup(f"Modlist ({mod_count} mods)") # self.run() # self.destroy() - # record = AppNav.treeview.get_record() + # record = treeview.get_record() # if not record: # return diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index a686165..47edc6a 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -184,6 +184,8 @@ class ServerTreeView(TreeView): self.controller.mediator.grid.right_panel.filters_vbox.keyword_entry.grab_focus() case Gdk.KEY_m: self.controller.mediator.grid.right_panel.filters_vbox.maps_entry.grab_focus() + case Gdk.KEY_i: + self.controller.mediator.grid.conpan.entry.grab_focus() else: match event.keyval: case Gdk.KEY_l | Gdk.KEY_Right: