From d6fd7b24944dbbe9f4dd9ab3563cd1ab384c472a Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Mon, 23 Feb 2026 06:22:32 +0900 Subject: [PATCH] feat: add extend() method to ListStore --- dzgui/controllers/mc.py | 4 +- dzgui/model/filtered_model.py | 33 +++-------- dzgui/model/model_factory.py | 94 +++++++++++++++++++++++++++---- dzgui/util/localize.py | 1 + dzgui/views/pages/servers.py | 1 - dzgui/views/trees/tree_servers.py | 3 +- 6 files changed, 94 insertions(+), 42 deletions(-) diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 4c5d663..67830e9 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -315,10 +315,10 @@ class Controller(GObject.GObject): path = self.query_config(Preferences.DEFAULT) mods = get_delimited_mods(Path(path)) + # NOTE: cell renderer highlight toggle for mod in mods: - # NOTE: show highlight color bool mod.append(False) - model.append(mod) + model.extend(mods) self.cleanup_func = StoredFunc(self.load_mods_cleanup, model) diff --git a/dzgui/model/filtered_model.py b/dzgui/model/filtered_model.py index e628594..6cefc27 100644 --- a/dzgui/model/filtered_model.py +++ b/dzgui/model/filtered_model.py @@ -1,11 +1,10 @@ import datetime import re -from dataclasses import dataclass from typing import Optional, TYPE_CHECKING -from warnings import deprecated from dzgui.const.enum import FilterMode +from dzgui.model.model_factory import ModelFactory from dzgui.util import strings import gi @@ -19,22 +18,6 @@ if TYPE_CHECKING: from dzgui.controllers.emitter import Emitter -@dataclass(slots=True, frozen=True) -class ServerColumns: - name: str - _map: str - perspective: str - gametime: str - players: int - _max: int - queue: int - ip: str - qport: int - ping: int - provider: str - modded: bool - - class FilteredModelManager: """ Manages access to cached ListStore resources and @@ -42,7 +25,9 @@ class FilteredModelManager: which share the same column structure. A FilteredModelManager is attached to each ServerTreeView. - Filter methods are not thread-safe in themselves. + + Raw data is cached before being packed into a ListStore, see get_control() + Filtration creates a proxy of the TreeView's model, see get_proxy_model() """ def __init__(self, controller: "Controller") -> None: @@ -117,21 +102,17 @@ class FilteredModelManager: rows = self.filter_toggle_on(filters, *args) # TODO: unimplemented + # just write pings into control model instead # if mode is not FilterMode.INITIAL: # for row in rows: # if row[7] in self.ping_cache: # row[9] = self.ping_cache[row[7]] # NOTE: this ListStore manipulation must remain local to the thread - clone = self.new_model_from_class(ServerColumns) - n_cols = clone.get_n_columns() + clone = ModelFactory().make_server_store() if len(rows) > 0: rows = self.sort_rows(rows) - for i, row in enumerate(rows): - # TODO: consider overriding append() method of Gtk.ListStore - # check Gtk source code - clone.insert_with_values(i, tuple(range(0, n_cols)), row) - # clone.append(row) + clone.extend(rows) self.set_cache(filters, clone, rows) self.set_proxy_model(clone) diff --git a/dzgui/model/model_factory.py b/dzgui/model/model_factory.py index 5e9d687..ed0f250 100644 --- a/dzgui/model/model_factory.py +++ b/dzgui/model/model_factory.py @@ -1,9 +1,9 @@ from dataclasses import dataclass +from typing import Any from dzgui.const.enum import HELP_MENU_ROWS from dzgui.util.redact import redact_log from dzgui.util.strings import delimiter -from dzgui.views.dialogs.generic import ExceptionDialog import gi @@ -11,6 +11,27 @@ gi.require_version("Gtk", "3.0") from gi.repository.Gtk import ListStore # noqa E402 from gi.repository import GObject # noqa E402 +GTYPE_TO_PYTHON = { + GObject.type_from_name(GObject.type_name(ptype)): ptype + for ptype in (int, float, str, bool, object) +} + + +@dataclass(slots=True, frozen=True) +class ServerCols: + name: str + _map: str + perspective: str + gametime: str + players: int + _max: int + queue: int + ip: str + qport: int + ping: int + provider: str + modded: bool + @dataclass(slots=True, frozen=True) class ModCols: @@ -38,7 +59,7 @@ class ServerModCols: @dataclass(slots=True, frozen=True) class MenuCols: - name: str + name: GObject.TYPE_STRING # str hidden: GObject.TYPE_PYOBJECT @@ -46,10 +67,52 @@ class FastInsertListStore(ListStore): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + def _is_same_length(self, lists: list[list[Any]]) -> bool: + first_len = len(lists[0]) + if not all(len(sublist) == first_len for sublist in lists): + return False + return True + + def _is_type_homogeneous(self, lists: list[list[Any]]) -> bool: + transposed = zip(*lists) + + for column in transposed: + first_type = type(column[0]) + if not all(type(item) is first_type for item in column): + return False + return True + + def extend(self, rows: list[list[Any]]) -> None: + """ + Compared to calling append() directly, introduces negligible overhead, + but guarantees type and length equivalence prior to insertion + """ + n_cols = self.get_n_columns() + expected_types = [ + GTYPE_TO_PYTHON[self.get_column_type(i)] for i in range(n_cols) + ] + if not self._is_same_length(rows): + raise ValueError("Sublists are not of uniform length") + if not self._is_type_homogeneous(rows): + raise TypeError("Sublists are not type homogeneous") + if not all(isinstance(a, b) for a, b in zip(rows[0], expected_types)): + raise TypeError("Sublist types are not same as ListStore") + if len(rows[0]) != n_cols: + raise ValueError("Sublist column length is not same as ListStore") + + for row in rows: + self.append(row) + def append(self, row) -> None: + """ + Optimized for speed, but makes no assurances about row homogeneity + and may segfault if types and length are not identical to ListStore. + For this reason, it is recommended to use the extend() method to insert + an entire list of lists + """ total = len(row) i = len(self) - tree_iter = self.insert_with_values(i, tuple(range(0, total)), row) + tree_iter = self.insert_with_values(i, tuple(range(total)), row) return tree_iter @@ -62,7 +125,7 @@ class ModelFactory: with open(path, "r") as f: lines = [line.split(delimiter) for line in f.read().splitlines()] for record in lines: - # NOTE: strip PII and API keys + # NOTE: strips PII and API keys clean = redact_log(record) store.append(clean) return store @@ -73,21 +136,28 @@ class ModelFactory: ) return store - def make_map_store(self) -> ListStore: + def make_map_store(self) -> FastInsertListStore: return ListStore(str) - def make_help_store(self) -> ListStore: + def make_help_store(self) -> FastInsertListStore: store = self.new_model_from_class(MenuCols) - for row in HELP_MENU_ROWS: - label = row.dict["label"] - store.append([label, row]) + rows = [[row.dict["label"], row] for row in HELP_MENU_ROWS] + # for row in rows:HELP_MENU_ROWS: + # print(type(row) is object) + store.extend(rows) + # for row in HELP_MENU_ROWS: + # label = row.dict["label"] + # store.append([label, row]) return store - def make_mod_store(self) -> ListStore: + def make_mod_store(self) -> FastInsertListStore: return self.new_model_from_class(ModCols) - def make_log_store(self) -> ListStore: + def make_log_store(self) -> FastInsertListStore: return self.new_model_from_class(LogCols) - def make_server_mod_store(self) -> ListStore: + def make_server_mod_store(self) -> FastInsertListStore: return self.new_model_from_class(ServerModCols) + + def make_server_store(self) -> FastInsertListStore: + return self.new_model_from_class(ServerCols) diff --git a/dzgui/util/localize.py b/dzgui/util/localize.py index 5fe9fc6..fd4b5c6 100644 --- a/dzgui/util/localize.py +++ b/dzgui/util/localize.py @@ -1,6 +1,7 @@ import os import locale + def number(num: int | float) -> str: spec = "" if type(num) is int: diff --git a/dzgui/views/pages/servers.py b/dzgui/views/pages/servers.py index 52940d7..cd85620 100644 --- a/dzgui/views/pages/servers.py +++ b/dzgui/views/pages/servers.py @@ -36,7 +36,6 @@ class ServerNotebook(Gtk.ScrolledWindow): controller, ServerTab.RECENT, ContextMenuGroup.RECENT ) self.lan = ServerTreeView(controller, ServerTab.LAN, ContextMenuGroup.SCAN_LAN) - self.lan.set_headers_visible(False) tabs = [ (self.browser, server_labels.browser), diff --git a/dzgui/views/trees/tree_servers.py b/dzgui/views/trees/tree_servers.py index cf363c3..20341b4 100644 --- a/dzgui/views/trees/tree_servers.py +++ b/dzgui/views/trees/tree_servers.py @@ -53,7 +53,8 @@ class ServerTreeView(ContextMixin, TreeView): self.map_man = MapManager() self.set_fixed_height_mode(True) - self.set_headers_visible(True) + # NOTE: headers become visible on model load + self.set_headers_visible(False) self.queue_id: int self.handler_id: int