From 934ff91f05add1b0bdcd7996924d63f218ebf87b Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:01:18 +0900 Subject: [PATCH] fix: dynamic statusbar WIP --- dzgui/controllers/mc.py | 65 +++++++++++----- dzgui/views/base.py | 23 +++--- dzgui/views/components/statusbar.py | 110 ++++++++++++++++++---------- dzgui/views/pages/help.py | 5 +- dzgui/views/pages/mods.py | 2 + dzgui/views/pages/servers.py | 48 ++++++------ dzgui/views/trees/tree_menu.py | 17 +++-- dzgui/views/trees/tree_mods.py | 20 ++--- dzgui/views/trees/tree_servers.py | 21 ++++-- 9 files changed, 189 insertions(+), 122 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 3870b8e..19412b2 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -212,6 +212,7 @@ class Controller: except Exception as e: logger.critical(e) + logger.info("Normal user exit") if self.mediator.window.props.is_maximized: Gtk.main_quit() return @@ -232,20 +233,30 @@ class Controller: def remove_statusbar(self, context: str) -> None: c = self.mediator.statusbar.statusbar.get_context_id(context) - self.mediator.statusbar.statusbar.remove_all(c) + self.mediator.statusbar.statusbar.pop(c) - def set_statusbar(self, text: str, context: str) -> None: - self.mediator.statusbar.set_text(text, context) + def set_statusbar(self, text: str, context: str) -> int: + msg_id = self.mediator.statusbar.set_text(text, context) + return msg_id def set_statusbar_placeholder(self, text: str) -> None: # TODO: use statusbar stacks instead self.statusbar_placeholder = text def set_statusbar_dist(self, haversine: "Haversine", enum: "ServerTab") -> None: + # NOTE: prevent race condition where page was changed before distance was written + if self.mediator.notebook.get_page_by_enum() is not NotebookPage.SERVERS: + cid = self.mediator.statusbar.statusbar.get_context_id(str(enum)) + self.mediator.statusbar.statusbar.pop(cid) + self.mediator.statusbar.spinner.stop() + return + tv = self.get_active_treeview() if tv.get_enum() != enum: self.mediator.statusbar.spinner.stop() return + + # NOTE: user may have changed km/mi toggle, so recalculate dist: str if haversine is None: dist = "Unknown" @@ -285,7 +296,7 @@ class Controller: mod.append(None) model.append(mod) - self.update_mod_statusbar() + # self.update_mod_statusbar() def toggle_config(self, context: Preferences) -> None: config = self.prefs.paths.config @@ -342,7 +353,6 @@ class Controller: # TODO: consolidate methods with set_page_by_enum match button.context: case ButtonType.EXIT: - logger.info("Normal user exit") self.save_res_and_quit() return case ButtonType.OPTIONS: @@ -352,11 +362,12 @@ class Controller: except Exception: return case ButtonType.MODS: + # TODO: reload using refresh button, rather than on demand self.load_mods() case ButtonType.HELP: - self.mediator.statusbar.refresh(RowType.CHANGELOG) pass case ButtonType.SERVERS: + # TODO: drop after fixing crumbs signal self.mediator.notebook.set_page_by_enum(button.opens) return @@ -364,6 +375,13 @@ class Controller: # TODO: set crumbs by signal self.set_crumbs(button.get_label()) + def get_help_text(self) -> str: + tv = self.mediator.menu + model = self.get_help_store() + tree_iter = tv.get_focused_row_iter() + value = model.get_value(tree_iter, 0) + return value + def open_user_workshop(self, uid: str) -> None: # NOTE: uid may contain leading zeroes, not a real integer client = self.query_config(Preferences.CLIENT) @@ -426,13 +444,10 @@ class Controller: model = self.model_man.get_mod_store() model.remove(it) - def update_mod_statusbar(self) -> None: + def format_mod_statusbar(self) -> None: total_mods, total_size = self.calc_mod_size() msg = format_mods(total_size, total_mods) - # TODO: combine - # meta = self.mediator.statusbar.statusbar.get_context_id("Mods") - # self.mediator.statusbar.statusbar.push(meta, msg) - self.mediator.statusbar.set_text(msg, "Mods") + return msg def calc_mod_size(self) -> tuple[int, int]: model = self.model_man.get_mod_store() @@ -698,12 +713,24 @@ class Controller: context = str(treeview.enum) model = treeview.get_model() status = format_player_count(model) - self.set_statusbar_placeholder(status) + self.set_statusbar_placeholder(status) self.set_statusbar(status, context) - if len(model) >= 1: - self.mediator.statusbar.spinner.start() - # self.mediator.statusbar.spinner.stop() + #if len(model) >= 1: + # self.mediator.statusbar.spinner.start() + + def start_spinner(self) -> None: + # TODO: use signal instead + self.mediator.statusbar.start_spinner() + + def notify_statusbar_of_notebook_change(self) -> None: + enum = self.mediator.notebook.get_page_by_enum() + self.mediator.statusbar.emit("notebook_page_changed", enum) + + def notify_statusbar(self) -> None: + tv = self.get_active_treeview() + enum = tv.get_enum() + self.mediator.statusbar.emit("server_page_changed", enum) def populate_model(self) -> None: treeview = self.get_active_treeview() @@ -722,13 +749,13 @@ class Controller: def present_servers(self) -> None: # TODO: abstract - # TODO: signal for crumbs self.update_server_status() + # TODO: signal for crumbs crumbs = self.mediator.servers.get_cached_label() self.set_crumbs(crumbs) - # TODO: could emit this when treeview gains keyboard focus - # tree = self.get_active_treeview() - # tree.emit("on_distcalc_started") + + tree = self.get_active_treeview() + tree.emit("on_distcalc_started") def toggle_check(self, event: Gdk.EventKey) -> None | Literal[False]: mappings = { diff --git a/dzgui/views/base.py b/dzgui/views/base.py index fa407b6..af8134e 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -186,6 +186,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self.prior_page: NotebookPage self.prior_status: str + self.is_return = False MainController.register_widget("notebook", self) @@ -253,8 +254,8 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self.set_page_by_enum(self.prior_page) self.settings.unblock_text_entry() return + self.is_return = True self.set_page_by_enum(self.prior_page) - MainController.set_statusbar(self.prior_status) def get_page_by_enum(self) -> NotebookPage | None: for k, v in self.indexes.items(): @@ -267,7 +268,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore if cur_page == NotebookPage.KEYS: self.return_prior() else: - self.prior_status = MainController.get_statusbar() + # self.prior_status = MainController.get_statusbar() self.set_page_by_enum(NotebookPage.KEYS) def focus_current(self) -> None: @@ -280,11 +281,7 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore return w = widget.get_children()[0] - try: - w.focus_first_row() - w.grab_focus() - except Exception: - w.grab_focus() + w.grab_focus() def get_page(self) -> Gtk.Widget | None: ind = self.get_current_page() @@ -304,13 +301,19 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self, notebook: "Notebook", page: Gtk.Widget, page_num: int ) -> None: enum = self.get_page_by_enum() + # TODO: crumbs signal if enum is not None: crumbs = enum.dict["crumbs"] - status = enum.dict["statusbar"] MainController.set_crumbs(crumbs) - if status is False: - MainController.set_statusbar("", "") + if self.is_return is True: + MainController.mediator.statusbar.emit( + "notebook_page_returned", self.prior_page + ) + else: + MainController.mediator.statusbar.emit("notebook_page_changed", enum) + + self.is_return = False if enum is NotebookPage.SERVERS: MainController.present_servers() diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py index 43524be..394ab40 100644 --- a/dzgui/views/components/statusbar.py +++ b/dzgui/views/components/statusbar.py @@ -1,16 +1,26 @@ -from typing import TYPE_CHECKING +from warnings import deprecated +from typing import Self, Union, TYPE_CHECKING -from dzgui.const.enum import RowType, Preferences +from dzgui.const.enum import NotebookPage, RowType from dzgui.util import strings import gi + gi.require_version("Gtk", "3.0") -from gi.repository import Gtk # noqa E402 +from gi.repository import Gtk, GObject # noqa E402 if TYPE_CHECKING: + from dzgui.const.enum import ServerTab from dzgui.controllers.mc import Controller + class Statusbar(Gtk.Grid): + __gsignals__ = { + "server_page_changed": (GObject.SignalFlags.RUN_FIRST, None, (object,)), + "notebook_page_changed": (GObject.SignalFlags.RUN_FIRST, None, (object,)), + "notebook_page_returned": (GObject.SignalFlags.RUN_FIRST, None, (object,)), + } + def __init__(self, controller: "Controller") -> None: super().__init__(orientation=Gtk.Orientation.HORIZONTAL) @@ -26,48 +36,79 @@ class Statusbar(Gtk.Grid): self.spinner.start() version = self.controller.get_prefs().version - self.status_right_label = Gtk.Label(label=version, hexpand=True, halign=Gtk.Align.END) + self.status_right_label = Gtk.Label( + label=version, hexpand=True, halign=Gtk.Align.END + ) self.attach(self.statusbar, 0, 0, 3, 1) self.attach_next_to(self.spinner, self.statusbar, Gtk.PositionType.RIGHT, 3, 1) - self.attach_next_to(self.status_right_label, self.spinner, Gtk.PositionType.RIGHT, 3, 1) + self.attach_next_to( + self.status_right_label, self.spinner, Gtk.PositionType.RIGHT, 3, 1 + ) self.set_text(help_text, "Help") self.players = "" + self.connect("server_page_changed", self._on_server_page_changed) + self.connect("notebook_page_changed", self._on_notebook_page_changed) + self.connect("notebook_page_returned", self._on_notebook_page_returned) + + def _on_notebook_page_changed( + self, statusbar: Self, context: "NotebookPage" + ) -> None: + status = context.dict["statusbar"] + bar = "" + if status is False: + self.set_by_context(context, "") + return + + match context: + case NotebookPage.MODS: + bar = self.controller.format_mod_statusbar() + case NotebookPage.HELP: + bar = self.controller.get_help_text() + self.set_by_context(context, bar) + + def _on_notebook_page_returned( + self, statusbar: Self, prior_context: "NotebookPage" + ) -> None: + self.pop(prior_context) + + def _on_server_page_changed(self, statusbar: Self, context: "ServerTab") -> None: + self.pop(context) + + def start_spinner(self) -> None: + self.spinner.start() + + def stop_spinner(self) -> None: + self.spinner.stop() + + def pop(self, context: Union["ServerTab", "NotebookPage"]) -> None: + cid = self.statusbar.get_context_id(str(context)) + self.statusbar.pop(cid) + def get_text(self) -> str: area = self.statusbar.get_message_area() label = area.get_children()[0] return label.get_text() - def set_text(self, string: str, context: str) -> None: - # if string is None: - # return - meta = self.statusbar.get_context_id(context) - #tv = self.controller.get_active_treeview() - #cur_context = tv.get_enum() - #cid = self.get_context_by_enum(cur_context) - #if cid != meta: - # print("requested: ", meta) - # print("current: ", cid) - # return - # TODO: substacks - # get_context_id(ServerTab) + def set_by_context( + self, context: Union[NotebookPage, "ServerTab"], string: str + ) -> None: + meta = self.statusbar.get_context_id(str(context)) self.statusbar.push(meta, string) - #self.set_context(meta) + self.set_cache(string) + def get_cache(self) -> str: + return self.cache - # TODO: type checking - # def get_context_by_enum(self, context: "ServerTab") -> int: - # cid = self.statusbar.get_context_id(str(context)) - # # TODO: substacks - # return cid - # - # def get_context(self) -> int: - # return self.context - # - # def set_context(self, context: int) -> None: - # self.context = context + def set_cache(self, string: str) -> None: + self.cache = string + + def set_text(self, string: str, context: str) -> None: + meta = self.statusbar.get_context_id(context) + self.statusbar.push(meta, string) + self.set_cache(string) def refresh(self, row: "RowType") -> None: if row is None: @@ -76,14 +117,7 @@ class Statusbar(Gtk.Grid): formatted = self.format_metadata(row) self.set_text(formatted, "Help") - def append_distance(self, dist: str) -> None: - # TODO: process strings in controller - if dist == strings.unknown: - dist = f"| Distance: {dist}" - else: - dist = f"| Distance: {dist}" - self.set_text(self.players + dist) - + @deprecated("use controller") def format_metadata(self, row: "RowType") -> str: prefix = row.dict["tooltip"] diff --git a/dzgui/views/pages/help.py b/dzgui/views/pages/help.py index 9fa4aa9..5e4d8a5 100644 --- a/dzgui/views/pages/help.py +++ b/dzgui/views/pages/help.py @@ -2,12 +2,14 @@ from typing import TYPE_CHECKING from dzgui.views.trees.tree_menu import MenuTreeView import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa E402 if TYPE_CHECKING: from dzgui.controllers.mc import Controller + class Help(Gtk.ScrolledWindow): def __init__(self, controller: "Controller") -> None: super().__init__() @@ -15,7 +17,8 @@ class Help(Gtk.ScrolledWindow): self.add(self.treeview) self.controller = controller - self.controller.register_widget("menu", self.treeview) + + self.treeview.focus_first_row() def get_treeview(self) -> MenuTreeView: return self.treeview diff --git a/dzgui/views/pages/mods.py b/dzgui/views/pages/mods.py index 77a76f4..7947a6d 100644 --- a/dzgui/views/pages/mods.py +++ b/dzgui/views/pages/mods.py @@ -2,12 +2,14 @@ from typing import Self, TYPE_CHECKING from dzgui.views.trees.tree_mods import ModTreeView import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk # noqa if TYPE_CHECKING: from dzgui.controllers.mc import Controller + class Mods(Gtk.ScrolledWindow): def __init__(self, controller: "Controller") -> None: super().__init__() diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index ca065e4..617934f 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -7,6 +7,7 @@ from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.util.strings import server_labels import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib # noqa E402 @@ -15,6 +16,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.controllers.mc import Controller + class ServerNotebook(Gtk.ScrolledWindow): def __init__(self, controller: "Controller"): super().__init__() @@ -24,9 +26,13 @@ class ServerNotebook(Gtk.ScrolledWindow): self.controller.register_widget("servers", self) self.notebook = Gtk.Notebook(show_tabs=True) - self.browser = ServerTreeView(controller, ServerTab.BROWSER, ContextMenuGroup.SERVER_BROWSER) + self.browser = ServerTreeView( + controller, ServerTab.BROWSER, ContextMenuGroup.SERVER_BROWSER + ) self.saved = ServerTreeView(controller, ServerTab.SAVED, ContextMenuGroup.SAVED) - self.recent = ServerTreeView(controller, ServerTab.RECENT, ContextMenuGroup.RECENT) + self.recent = ServerTreeView( + controller, ServerTab.RECENT, ContextMenuGroup.RECENT + ) self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) self.browser.set_query_func(self.controller.dump_test_1) @@ -50,6 +56,7 @@ class ServerNotebook(Gtk.ScrolledWindow): self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) + # TODO: strings self.set_crumbs("Server browser") def _on_map(self, widget: Self) -> None: @@ -75,43 +82,28 @@ class ServerNotebook(Gtk.ScrolledWindow): self.controller.set_crumbs(string) self.set_cached_label(string) - def _on_page_changed(self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int) -> None: + def _on_page_changed( + self, notebook: Gtk.Notebook, child: Gtk.Widget, index: int + ) -> None: if self.controller.loaded is False: return - # self.controller.remove_statusbar("Servers") + # TODO :signals + # emit signal to crumbs + # emit signal to statusbar # TODO: abstract label = self.notebook.get_tab_label_text(child) - if label is None: return + # TODO: strings text = label.strip("*") self.notebook.set_tab_label_text(child, text) - self.set_crumbs(text) self.controller.present_servers() self.controller.populate_model() - # TODO: start with lan panel hidden - # FIXME: grid conpan is not set up at init - # TODO: refresh button only usable if LAN has servers? - # or just remove it - #if self.get_active_treeview() is self.lan: - # self.controller.mediator.grid.conpan.lan.set_visible(True) - #else: - # self.controller.mediator.grid.conpan.lan.set_visible(False) - - # TODO: put in controller - # def query_test2(self) -> None: - # # TODO: should trigger first page action after entire UI is loaded - # self.controller.dump_test_1() - # - # def query_test(self) -> None: - # data = (["BAR", "a", "a", "a", 1, 1, 1, "185.207.214.16:2302", 0, 0, "a", False]) - # return data - def set_cached_label(self, label: str) -> None: self.tab_cache = label @@ -146,9 +138,13 @@ class ServerNotebook(Gtk.ScrolledWindow): continue for col in tab.get_columns(): if col.get_title() == title: - self.controller.suppress_signal(tab, col, "_on_col_width_changed", True) + self.controller.suppress_signal( + tab, col, "_on_col_width_changed", True + ) col.set_fixed_width(width) - self.controller.suppress_signal(tab, col, "_on_col_width_changed", False) + self.controller.suppress_signal( + tab, col, "_on_col_width_changed", False + ) def get_tabs(self) -> tuple: return (self.browser, self.saved, self.recent, self.lan) diff --git a/dzgui/views/trees/tree_menu.py b/dzgui/views/trees/tree_menu.py index da9cd9e..953d300 100644 --- a/dzgui/views/trees/tree_menu.py +++ b/dzgui/views/trees/tree_menu.py @@ -6,6 +6,7 @@ from dzgui.util.open_links import open_link_by_rowtype from dzgui.views.trees.tree_base import TreeView import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa @@ -14,11 +15,13 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: from dzgui.controllers.mc import Controller + class MenuTreeView(TreeView): """ Simple Gtk.ListStore representation of main menu options """ + def __init__(self, controller: "Controller") -> None: super().__init__(controller) @@ -34,15 +37,15 @@ class MenuTreeView(TreeView): self.selected_row = self.get_selection() + self.controller.register_widget("menu", self) + self.set_row_separator_func(self._separate) self.connect("generic_row_activated", self._parent_row_activated) self.connect("generic_treesel_changed", self._parent_selection_changed) - def _parent_row_activated(self, - tree: TreeView, - path: Gtk.TreePath, - column: Gtk.TreeViewColumn - ) -> None: + def _parent_row_activated( + self, tree: TreeView, path: Gtk.TreePath, column: Gtk.TreeViewColumn + ) -> None: row_type = self.get_value_at_index(1) match row_type: @@ -73,9 +76,7 @@ class MenuTreeView(TreeView): open_link_by_rowtype(row_type) def _parent_selection_changed( - self, - base_class: TreeView, - sel: Gtk.TreeSelection + self, base_class: TreeView, sel: Gtk.TreeSelection ) -> None: row = self.get_value_at_index(1) if row == "": diff --git a/dzgui/views/trees/tree_mods.py b/dzgui/views/trees/tree_mods.py index b7a39f0..fcff526 100644 --- a/dzgui/views/trees/tree_mods.py +++ b/dzgui/views/trees/tree_mods.py @@ -7,9 +7,10 @@ from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.const.enum import ( ContextMenuGroup, - ) +) import gi + gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa @@ -19,6 +20,7 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) + class ModTreeView(ModsMixin, ContextMixin, TreeView): def __init__(self, controller: "Controller") -> None: super().__init__(controller, menu=ContextMenuGroup.MOD) @@ -32,13 +34,9 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): for i, column_title in enumerate(strings.mod_cols): renderer = Gtk.CellRendererText() - column = Gtk.TreeViewColumn( - column_title, renderer, text=i, foreground=4 - ) + column = Gtk.TreeViewColumn(column_title, renderer, text=i, foreground=4) 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) if column_title == "Mod": column.set_fixed_width(500) else: @@ -57,11 +55,10 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): self.connect("button-press-event", self._on_mods_button_press) self.connect("key-press-event", self._on_mods_keypress) - self.focus_first_row() self.s = self.get_selection().get_selected_rows() def _on_mods_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: - #if event.keyval is Gdk.KEY_space: + # if event.keyval is Gdk.KEY_space: # it = self.get_focused_row_iter() # self.get_selection().select_iter(it) # path = self.get_focused_row_path() @@ -70,10 +67,7 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # TODO: parse keys self.present_menu(widget, event) - def _on_mods_button_press(self, - widget: Gtk.Widget, - event: Gdk.EventButton - ) -> None: + def _on_mods_button_press(self, widget: Gtk.Widget, event: Gdk.EventButton) -> None: if event.button == 3: self.present_menu(widget, event) diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index 3d00fb5..d53519b 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -150,7 +150,8 @@ class ServerTreeView(TreeView): # this can probably be dropped in favor of statusbar cache system # meta = self.controller.mediator.statusbar.statusbar.get_context_id("Mods") # self.controller.mediator.statusbar.statusbar.pop(meta) - self.emit("on_distcalc_started") + pass + # self.emit("on_distcalc_started") def get_enum(self) -> None: return self.enum @@ -215,8 +216,17 @@ class ServerTreeView(TreeView): record = self.get_record() if record is None: return - # TODO: - self.controller.mediator.statusbar.spinner.start() + + cache = self.controller.get_dist_cache() + + if record.ip in cache: + haversine = cache[record.ip] + self.controller.set_statusbar_dist(haversine, None) + return + + if len(self.get_model()) > 1: + self.controller.start_spinner() + self.current_proc = CalcDist( record.ip, self.get_enum(), self.queue, self.controller ) @@ -232,12 +242,10 @@ class ServerTreeView(TreeView): cache = self.controller.get_dist_cache() if latest_result: - # NOTE: if CalcDist was spawned on other tab addr = latest_result[0] haversine = latest_result[1] if addr not in cache: cache[addr] = haversine - # FIXME self.controller.set_statusbar_dist(haversine, self.get_enum()) return True @@ -253,7 +261,6 @@ class ServerTreeView(TreeView): case Gdk.KEY_r: self.refresh_player_count() case Gdk.KEY_f: - # TODO: register filter panel instead of mediating thru right panel self.controller.mediator.filters.keyword_entry.grab_focus() case Gdk.KEY_m: self.controller.mediator.filters.maps_entry.grab_focus() @@ -344,7 +351,7 @@ class ServerTreeView(TreeView): qport = self.get_value_at_index(8) return f"{addr}:{qport}" - def get_record(self) -> dict | None: + def get_record(self) -> Record | None: # TODO: delegate to controller select = self.get_selection() sels = select.get_selected_rows()