diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index 3ae8ffc..82e774d 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -54,7 +54,7 @@ if TYPE_CHECKING: from dzgui.views.pages.servers import ServerNotebook from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_menu import MenuTreeView - from dzgui.views.trees.tree_mods import ModTreeView + from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView from dzgui.views.trees.tree_servers import ServerTreeView @@ -221,19 +221,29 @@ class Controller(GObject.GObject): open_user_workshop(uid, client) def load_mods(self) -> None: - self.mod_man = ModManager(self) + self.mediator.modtreeview.load_mods() def uncolorize_mods(self) -> None: - self.mod_man.uncolorize_mods() + mod_man = self.mediator.modtreeview.get_mod_man() + mod_man.uncolorize_mods() def highlight_stale(self) -> None: - self.mod_man.highlight_stale() + mod_man = self.mediator.modtreeview.get_mod_man() + mod_man.highlight_stale() def toggle_mod_selection(self, state: bool) -> None: - self.mod_man.toggle_mod_selection(state) + mod_man = self.mediator.modtreeview.get_mod_man() + mod_man.toggle_mod_selection(state) - def delete_mods(self) -> None: - self.mod_man.delete_mods() + def delete_mods( + self, treeview: Union["ModTreeView", "OfflineModTreeView"] = None + ) -> None: + if treeview is None: + view = self.mediator.modtreeview + else: + view = treeview + mod_man = view.get_mod_man() + mod_man.delete_mods() def get_mod_store(self) -> Gtk.TreeModel | None: return self.mediator.modtreeview.get_model() @@ -260,7 +270,8 @@ class Controller(GObject.GObject): dialog.run() def select_colorized(self) -> None: - self.mod_man.select_colorized() + mod_man = self.mediator.modtreeview.get_mod_man() + mod_man.select_colorized() def get_all_tree_filters(self) -> dict[str, dict[str, bool]]: servers = self.get_servers() @@ -293,7 +304,9 @@ class Controller(GObject.GObject): def set_custom_folder(self) -> Union["Path", None]: picker = FolderPicker(self.mediator.window) - return picker.pick_folder() + folder = picker.pick_folder() + picker.destroy() + return folder def update_api_key(self, key: Preferences, text: str) -> None: self.config_man.update_api_key(key, text) @@ -541,6 +554,8 @@ class Controller(GObject.GObject): return True return False - def open_offline(self, mods: Gtk.TreeModel | None) -> None: + def open_offline(self) -> None: self.open_page(NotebookPage.OFFLINE) - self.mediator.offline_loader.populate(mods) + mod_man = self.mediator.modtreeview.get_mod_man() + store = mod_man.store + self.mediator.offline_loader.populate(store) diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 33aea9a..7701b1c 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -8,7 +8,7 @@ from dzgui.util.clip import copy_clipboard from dzgui.util.open_links import open_workshop_page from dzgui.views.dialogs.note import NoteDialog -from dzgui.views.trees.tree_mods import ModTreeView +from dzgui.views.trees.tree_mods import ModTreeView, OfflineModTreeView from dzgui.views.trees.tree_log import LogTreeView from dzgui.views.trees.tree_servers import ServerTreeView from dzgui.views.trees.tree_server_mods import ServerModTreeView @@ -27,7 +27,13 @@ if TYPE_CHECKING: class ContextMenuManager: def __init__( self, - treeview: LogTreeView | ModTreeView | ServerTreeView | ServerModTreeView, + treeview: ( + LogTreeView + | ModTreeView + | ServerTreeView + | ServerModTreeView + | OfflineModTreeView + ), controller: "Controller", ) -> None: self.controller = controller @@ -72,10 +78,10 @@ class ContextMenuManager: case ContextMenu.SHOW_MODS: self.controller.get_modlist(record) - if isinstance(self.treeview, ModTreeView): + if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): match action: case ContextMenu.DELETE_MOD: - self.controller.delete_mods() + self.controller.delete_mods(self.treeview) case ContextMenu.OPEN_WORKSHOP: self.open_mod_page() diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index a19f7c0..cc50181 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -2,7 +2,7 @@ import logging import shutil from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING from dzgui.api.mods import ( get_delimited_mods, @@ -40,20 +40,20 @@ class ModManager: this manager is instantiated each time the Mods page is opened """ - def __init__(self, controller: "Controller") -> None: + def __init__(self, tree: Gtk.TreeView, controller: "Controller") -> None: + self.treeview = tree self.controller = controller + self.emitter = controller.get_emitter() self.prefs = controller.get_prefs() self.path = controller.query_config(Preferences.DEFAULT) - self.treeview = self.controller.get_modtreeview() self.store: FastInsertListStore self.thread_man = ThreadingManager(controller) - self._get_mods() @call_on_thread(dialogs.fetching_mods) - def _get_mods(self) -> None: + def load_mods(self) -> None: mods = get_delimited_mods(self.path) if len(mods) < 1: msg = self.format_mod_statusbar() @@ -66,8 +66,6 @@ class ModManager: func = StoredFunc(self._on_mods_loaded) self.thread_man.set_cleanup_func(func) - # def _on_mods_loaded(self, mods: list[list[Any]]) -> None: - def _on_mods_loaded(self) -> None: self.treeview.set_model(self.store) msg = self.format_mod_statusbar() diff --git a/dzgui/model/model_factory.py b/dzgui/model/model_factory.py index ed1ccdc..ad255cf 100644 --- a/dzgui/model/model_factory.py +++ b/dzgui/model/model_factory.py @@ -165,3 +165,6 @@ class ModelFactory: def make_server_store(self) -> FastInsertListStore: return self.new_model_from_class(ServerCols) + + def convert_model_to_list(self, model: "FastInsertListStore") -> list: + return [[el for el in row] for row in model] diff --git a/dzgui/strings/offline.py b/dzgui/strings/offline.py index 1b21f79..631b979 100644 --- a/dzgui/strings/offline.py +++ b/dzgui/strings/offline.py @@ -1 +1,11 @@ heading = "Play offline" + +mission_button = "Select mission folder" +mission_frame = "Mission" + +version = "DayZ Version" + +custom_frame = "Custom mods" +custom_button = "Set custom mod folder" + +local_frame = "Installed mods" diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index cbff3d7..3d1a0e4 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -64,7 +64,7 @@ connect = "Connect" # Columns server_mod_cols = ["Mod", "ID", "Installed"] -mod_cols = ["Mod", "Symlink", "Dir", "Size (MiB)", "Color"] +mod_cols = ["Mod", "Symlink", "ID", "Size (MiB)", "Color"] log_cols = ["Timestamp", "Flag", "Traceback", "Message"] browser_cols = [ "Name", diff --git a/dzgui/views/base.py b/dzgui/views/base.py index d91c795..7f9ae36 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -23,6 +23,7 @@ from dzgui.views.pages.help import Help from dzgui.views.pages.keys import Keybindings from dzgui.views.pages.log import Log from dzgui.views.pages.mods import Mods + from dzgui.views.pages.offline import OfflineLoader from dzgui.views.pages.options import Options from dzgui.views.pages.preconnect import PreConnectionAssistant diff --git a/dzgui/views/components/frame.py b/dzgui/views/components/frame.py index c782d5f..a7193ad 100644 --- a/dzgui/views/components/frame.py +++ b/dzgui/views/components/frame.py @@ -6,16 +6,23 @@ gi.require_version("Gtk", "3.0") from gi.repository import Gtk # noqa +# TODO: subclass Gtk.Frame directly class HeadingFrame(Gtk.Box): - def __init__(self, widget: Gtk.Widget, heading: str) -> None: + def __init__(self, heading: str = "") -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL) - label = Gtk.Label(label=heading) - label.set_halign(Gtk.Align.START) - css.add_class(label, "settings-subheading") + self.label = Gtk.Label(label=heading) + self.label.set_halign(Gtk.Align.START) + css.add_class(self.label, "settings-subheading") - frame = Gtk.Frame(hexpand=True) - frame.add(widget) - frame.set_label_widget(label) + self.frame = Gtk.Frame(hexpand=True) + self.frame.set_label_widget(self.label) - self.add(frame) + self.add(self.frame) + + @classmethod + def new_with_widget_and_label(cls, widget: Gtk.Widget, label: str) -> None: + n = cls() + n.frame.add(widget) + n.label.set_label(label) + return n diff --git a/dzgui/views/mixins/context_mixin.py b/dzgui/views/mixins/context_mixin.py index f8c4b83..78728de 100644 --- a/dzgui/views/mixins/context_mixin.py +++ b/dzgui/views/mixins/context_mixin.py @@ -44,6 +44,8 @@ class ContextMixin(TreeView): self.context_menu.connect("key-press-event", self._on_key) for row in group.value: + if row is None: + return item = self._process_dynamic_row(row) self.context_menu.append(item) diff --git a/dzgui/views/pages/mods.py b/dzgui/views/pages/mods.py index 4218b3f..ee4946b 100644 --- a/dzgui/views/pages/mods.py +++ b/dzgui/views/pages/mods.py @@ -11,12 +11,14 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller -class Mods(NoOverlayScrolledWindow): +class Mods(Gtk.Box): def __init__(self, controller: "Controller") -> None: - super().__init__() + super().__init__(orientation=Gtk.Orientation.VERTICAL) + self.box = NoOverlayScrolledWindow() self.tree = ModTreeView(controller) self.tree.set_vexpand(True) + self.box.add(self.tree) self.controller = controller self.controller.register_widget("modtreeview", self.tree) @@ -30,18 +32,14 @@ class Mods(NoOverlayScrolledWindow): ) self.offline_button.connect("clicked", self._on_offline_clicked) - self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.box.add(self.offline_button) - self.box.add(self.tree) - + self.add(self.offline_button) self.add(self.box) self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) def _on_offline_clicked(self, button: Gtk.Button) -> None: - self.controller.open_offline(self.tree.get_model()) - # TODO: add offline page + self.controller.open_offline() def _on_unmap(self, widget: Self) -> None: self.emitter.emit("mod_page_toggled", False) diff --git a/dzgui/views/pages/offline.py b/dzgui/views/pages/offline.py index e3ffc7a..07b230e 100644 --- a/dzgui/views/pages/offline.py +++ b/dzgui/views/pages/offline.py @@ -1,9 +1,14 @@ -from typing import Self, TYPE_CHECKING +from pathlib import Path +from typing import Self, Sequence, TYPE_CHECKING -from dzgui.strings import offline from dzgui.util import css +import dzgui.api.pefile as PeFile +from dzgui.const.constants import APPID_DAYZ_EXP, APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN +from dzgui.const.enum import NotebookPage, Preferences +from dzgui.strings import offline +from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.components.frame import HeadingFrame -from dzgui.views.trees.tree_mods import ModTreeView +from dzgui.views.trees.tree_mods import OfflineModTreeView import gi @@ -13,9 +18,144 @@ from gi.repository import Gtk, Gdk # noqa if TYPE_CHECKING: from dzgui.controllers.mc import Controller + from dzgui.model.model_factory import FastInsertListStore -# FIXME: grab focus of outer page -# TODO: if any custom mods, symlink them before connecting proceeding + +class GenericBox(Gtk.Box): + def __init__(self, orientation: Gtk.Orientation) -> None: + super().__init__(orientation=orientation) + + def extend(self, els: Sequence[Gtk.Widget]) -> None: + for el in els: + self.add(el) + + +class HBox(GenericBox): + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + + +class VBox(GenericBox): + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + +class PageHeading(Gtk.Label): + def __init__(self, label: str) -> None: + super().__init__(label=label, halign=Gtk.Align.CENTER) + + css.add_class(self, "page-heading") + + +class FolderHBox(HBox): + def __init__(self, btn_label: str) -> None: + super().__init__() + + self.set_margin_start(10) + self.set_margin_bottom(10) + self.set_spacing(5) + + # TODO: use IconButton with folder-symbolic + self.button = Gtk.Button(label=btn_label, halign=Gtk.Align.START) + self.label = Gtk.Label() + self.extend([self.button, self.label]) + + def get_button(self) -> Gtk.Button: + return self.button + + def set_label(self, label: str) -> None: + self.label.set_label(label) + + +class ModFrame(HeadingFrame): + def __init__(self, controller: "Controller", label: str) -> None: + super().__init__(heading=label) + + self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + + self.tree = OfflineModTreeView(controller) + self.tree.set_model(None) + + self.scrolled = NoOverlayScrolledWindow() + self.scrolled.set_size_request(600, 400) + self.scrolled.add(self.tree) + self.status = Gtk.Label( + halign=Gtk.Align.START, margin_start=5, margin_top=3, margin_bottom=3 + ) + + self.vbox.pack_end(self.status, expand=False, fill=False, padding=0) + self.vbox.pack_end(self.scrolled, expand=False, fill=False, padding=0) + self.frame.add(self.vbox) + + sel = self.tree.get_selection() + sel.connect("changed", self._on_selection_changed) + + def collapse_tree(self) -> None: + self.scrolled.hide() + + def get_tree(self) -> OfflineModTreeView: + return self.tree + + def pack_start(self, widget: Gtk.Widget) -> None: + self.vbox.pack_start(widget, expand=False, fill=False, padding=5) + + def set_model(self, model: "FastInsertListStore") -> None: + self.tree.set_model(model) + self.tree.mod_man.store = model + self.set_cursor() + + def _on_selection_changed(self, sel: Gtk.TreeSelection) -> None: + model, rows = sel.get_selected_rows() + status = f"Mods selected: {len(rows)}" + self.status.set_label(status) + + def set_cursor(self) -> None: + path = Gtk.TreePath.new_from_indices([0]) + self.tree.set_cursor(path) + + +class CustomModFrame(ModFrame): + def __init__(self, controller: "Controller", heading: str) -> None: + super().__init__(controller, heading) + + self.controller = controller + self.custom_hbox = FolderHBox(offline.custom_button) + self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked) + + self.pack_start(self.custom_hbox) + + def _on_custom_button_clicked(self, button: Gtk.Button) -> None: + # TODO: recycle for mission folder + # TODO: propagate results back to parent + folder = self.controller.set_custom_folder() + if folder is not None: + # TODO: CustomModManager + self.custom_hbox.set_label(str(folder)) + + +class RadioFrame(HeadingFrame): + def __init__(self, controller: "Controller") -> None: + super().__init__(heading=offline.version) + + self.controller = controller + + self.dayz = Gtk.RadioButton.new_with_label(None, APPNAME_DAYZ) + self.dayz_exp = Gtk.RadioButton.new_with_label_from_widget( + self.dayz, APPNAME_DAYZ_EXP_HUMAN + ) + + self.radio_box = HBox() + self.radio_box.extend([self.dayz, self.dayz_exp]) + self.radio_box.set_margin_start(10) + self.radio_box.set_margin_bottom(5) + + self.frame.add(self.radio_box) + + default_steam_path = self.controller.query_config(Preferences.DEFAULT) + steam_path = Path(default_steam_path) + dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP) + if dayz_exp is None: + self.dayz_exp.set_sensitive(False) class OfflineLoader(Gtk.Box): @@ -26,127 +166,80 @@ class OfflineLoader(Gtk.Box): margin_end=10, ) - # TODO: wrap entire page in scrollable + # TODO: wrap entire page in scrollable, see preconnect dialog + # TODO: spacing between inner and outer scrollbars self.controller = controller self.controller.register_widget("offline_loader", self) - label = Gtk.Label(label=offline.heading) - label.set_halign(Gtk.Align.CENTER) - css.add_class(label, "page-heading") - self.add(label) + self.add(PageHeading(offline.heading)) - # TODO: wrap trees in scrollable - # TODO: suppress trees if there are no mods - self.local_mods = ModTreeView(controller) - self.scr = Gtk.ScrolledWindow() - self.scr.add(self.local_mods) - self.scr.set_size_request(600, 400) - # TODO: strings - self.local_frame = HeadingFrame(self.scr, "Installed mods") - - # TODO: update selected count statusbar when changed - sel = self.local_mods.get_selection() - sel.connect("changed", self._on_selection_changed) - - # TODO: abstract into class - # TODO: folder label # TODO: descriptive text here explaining how this area works - # TODO: use statusbar msg from format.format_mods - # TODO: strings - self.custom_button = Gtk.Button( - label="Set custom mod folder", halign=Gtk.Align.START + self.local_frame = ModFrame(controller, offline.local_frame) + self.custom_frame = CustomModFrame(controller, offline.custom_frame) + + self.mission_hbox = FolderHBox(offline.mission_button) + self.mission_frame = HeadingFrame.new_with_widget_and_label( + self.mission_hbox, offline.mission_frame ) - self.custom_button.connect("clicked", self._on_custom_button_clicked) - self.custom_mods = Gtk.TreeView() - self.custom_mods_box = Gtk.Box( - orientation=Gtk.Orientation.VERTICAL, margin_start=10, margin_bottom=10 - ) - self.custom_mods_box.add(self.custom_button) - self.custom_mods_box.add(self.custom_mods) - # TODO: strings - self.custom_frame = HeadingFrame(self.custom_mods_box, "Custom mods") - - self.mission_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, margin_start=10, margin_bottom=10 - ) - # TODO: strings - self.mission_button = Gtk.Button(label="Select mission folder") - self.mission_label = Gtk.Label() - - self.mission_box.add(self.mission_button) - self.mission_box.add(self.mission_label) - - # TODO: strings - self.mission_frame = HeadingFrame(self.mission_box, "Mission") - - self.radio_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL, margin_start=10, margin_bottom=10 - ) - # TODO: strings - # TODO: disable if not available, use PeFile - self.dayz = Gtk.RadioButton.new_with_label(None, "DayZ") - self.dayz_exp = Gtk.RadioButton.new_with_label_from_widget( - self.dayz, "DayZ Experimental" - ) - self.radio_box.add(self.dayz) - self.radio_box.add(self.dayz_exp) - self.radio_frame = HeadingFrame(self.radio_box, "DayZ Version") + self.radio_frame = RadioFrame(controller) + # HeadingFrame.new_with_widget_and_label(self.radio_box, offline.version) + # TODO: abstraction self.scrollable = Gtk.ScrolledWindow( vexpand=True, propagate_natural_height=True ) # TODO: use same button anchoring logic as preconnect dialog - self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - self.box.add(self.local_frame) - self.box.add(self.custom_frame) - self.box.add(self.mission_frame) - self.box.add(self.radio_frame) - self.scrollable.add(self.box) + self.content_box = VBox() + self.content_box.set_spacing(5) + self.content_box.extend( + [ + self.local_frame, + self.custom_frame, + self.mission_frame, + self.radio_frame, + ] + ) + self.scrollable.add(self.content_box) self.add(self.scrollable) # TODO: use ModelFactory - self.custom_mods = Gtk.TreeView() + # TODO: suppress symlink column + self.custom_tree = OfflineModTreeView(controller) - # TODO: shared ConnectBox class + # TODO: share ConnectBox class with preconnect dialog + self.button_box = HBox() self.back = Gtk.Button() self.ok = Gtk.Button() self.back.connect("clicked", self._on_back_clicked) self.ok.connect("clicked", self._on_ok_clicked) self.connect("key-press-event", self._on_keypress) + self.button_box.extend([self.back, self.ok]) + def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None: + # TODO: consider dropping this if event.keyval == Gdk.KEY_Escape: self.back.emit("clicked") - def _on_selection_changed(self, sel: Gtk.TreeSelection) -> None: - model, rows = sel.get_selected_rows() - # TODO: update sub-statusbar with selected mods - print(len(rows)) - - def populate(self, mods: Gtk.TreeModel | None) -> None: - self.local_mods.set_model(mods) - # TODO: push existing model - # TODO: set sensitivity of radio buttons - pass - - def _on_custom_button_clicked(self, button: Gtk.Button) -> None: - # TODO: recycle this for both buttons - folder = self.controller.set_custom_folder() - if folder is not None: - # TODO: parse mods from here - # TODO: offline mod manager would be cleaner - print(folder) + def populate(self, store: "FastInsertListStore") -> None: + self.local_frame.set_model(store) + # TODO: toggle if empty model, show warning label + # TODO: suppress trees if there are no mods + self.custom_frame.collapse_tree() def _on_back_clicked(self, button: Gtk.Button) -> None: - from dzgui.const.enum import NotebookPage - + # FIXME: returns to server page and not parent mod page + # consider using a toggle button instead, and suppressing statusbar self.controller.open_page(NotebookPage.SERVERS) def _on_ok_clicked(self, button: Gtk.Button) -> None: + """ + - collect symlinks to selected mods + cf. rebuild_symlinks() + - create symlinks for custom mods + - collect appid + - collect mission folder + """ pass - """ - create symlinks if there are custom mods - cf. rebuild_symlinks() - """ diff --git a/dzgui/views/pages/options.py b/dzgui/views/pages/options.py index e38d6ea..29b5130 100644 --- a/dzgui/views/pages/options.py +++ b/dzgui/views/pages/options.py @@ -187,12 +187,14 @@ class Options(Gtk.Box): if prefs.is_debug: grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) - for frame in [ - HeadingFrame(api_box, strings.options.api_keys), - HeadingFrame(prefs_grid, strings.options.prefs), - HeadingFrame(mods_grid, strings.options.mods), - HeadingFrame(version_grid, strings.options.version), + for pair in [ + (api_box, strings.options.api_keys), + (prefs_grid, strings.options.prefs), + (mods_grid, strings.options.mods), + (version_grid, strings.options.version), ]: + + frame = HeadingFrame.new_with_widget_and_label(*pair) grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT) row += 1 diff --git a/dzgui/views/pages/preconnect.py b/dzgui/views/pages/preconnect.py index 90dbcf6..59789a8 100644 --- a/dzgui/views/pages/preconnect.py +++ b/dzgui/views/pages/preconnect.py @@ -166,7 +166,9 @@ class PreConnectionAssistant(Gtk.Box): self.mods_placeholder = Placeholder("This server has no mods.") self.tree_box.add(self.mods_placeholder) - self.tree_frame = HeadingFrame(self.tree_box, preconnect.mods) + self.tree_frame = HeadingFrame.new_with_widget_and_label( + self.tree_box, preconnect.mods + ) # TODO: abstract into components self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) @@ -175,7 +177,9 @@ class PreConnectionAssistant(Gtk.Box): self.warning_placeholder = Placeholder("No warnings.") self.warning_box.add(self.warning_tree) self.warning_box.add(self.warning_placeholder) - self.warning_frame = HeadingFrame(self.warning_box, preconnect.warnings) + self.warning_frame = HeadingFrame.new_with_widget_and_label( + self.warning_box, preconnect.warnings + ) self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.error_tree = MaskedTree(ERROR) @@ -183,7 +187,9 @@ class PreConnectionAssistant(Gtk.Box): self.error_placeholder = Placeholder("No errors.") self.error_box.add(self.error_tree) self.error_box.add(self.error_placeholder) - self.error_frame = HeadingFrame(self.error_box, preconnect.errors) + self.error_frame = HeadingFrame.new_with_widget_and_label( + self.error_box, preconnect.errors + ) self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.box.add(self.title) diff --git a/dzgui/views/trees/tree_base.py b/dzgui/views/trees/tree_base.py index ac0733a..fbb3a1f 100644 --- a/dzgui/views/trees/tree_base.py +++ b/dzgui/views/trees/tree_base.py @@ -4,7 +4,7 @@ from typing import Any, Optional, TYPE_CHECKING from warnings import deprecated from dzgui.const.constants import APP_NAME, SEPARATOR -from dzgui.util.keys import is_navkey +from dzgui.util.keys import is_ctrl_mask, is_navkey from dzgui.views.mixins.cursor_mixin import CursorMixin import gi @@ -85,10 +85,17 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore model, rows = sel.get_selected_rows() return [model[row] for row in rows] - def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None: + def _on_keypress(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> bool: + from dzgui.views.trees.tree_servers import ServerTreeView + + # NOTE: suppress popup search and delegate binding to server view (cf. request_keyword_focus) + if not isinstance(treeview, ServerTreeView): + if is_ctrl_mask(event) and event.keyval == Gdk.KEY_f: + return True + if is_navkey(event.keyval): if self.get_model() is None: - return + return True if self.sel_blocked is False: self.sel_blocked = True self.controller.suppress_signal( @@ -98,7 +105,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore True, ) self._vim_nav(event) - return + return False def _on_key_release(self, treeview: Gtk.TreeView, event: Gdk.EventKey) -> None: """ diff --git a/dzgui/views/trees/tree_mods.py b/dzgui/views/trees/tree_mods.py index 1d74f84..17ab196 100644 --- a/dzgui/views/trees/tree_mods.py +++ b/dzgui/views/trees/tree_mods.py @@ -3,8 +3,8 @@ from typing import Any, TYPE_CHECKING from dzgui.const.constants import APP_NAME, HEX_RED from dzgui.const.enum import ContextMenuGroup +from dzgui.managers.mods import ModManager from dzgui.util import strings, localize -from dzgui.views.mixins.cursor_mixin import CursorMixin from dzgui.views.mixins.context_mixin import ContextMixin from dzgui.views.mixins.mods_mixin import ModsMixin from dzgui.views.trees.tree_base import TreeView @@ -25,7 +25,10 @@ logger = logging.getLogger(APP_NAME) class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore def __init__(self, controller: "Controller") -> None: super().__init__(controller, menu=ContextMenuGroup.MOD) + self.controller = controller + self.mod_man = ModManager(self, controller) + emitter = self.controller.get_emitter() emitter.connect("mods_updated", self._on_mods_updated) emitter.connect("mods_highlighted", self._on_mods_highlighted) @@ -57,6 +60,12 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore self.connect("button-press-event", self.present_menu) self.connect("key-press-event", self.present_menu) + def get_mod_man(self) -> ModManager: + return self.mod_man + + def load_mods(self) -> None: + self.mod_man.load_mods() + def _on_mods_highlighted(self, emitter: "Emitter") -> None: self.get_selection().unselect_all() @@ -106,3 +115,10 @@ class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore val = model[it][3] formatted = localize.number(val) cell.set_property("text", formatted) + + def set_menu(self, context: ContextMenuGroup) -> None: + self.menu = context + +class OfflineModTreeView(ModTreeView): + def __init__(self, controller: "Controller") -> None: + super().__init__(controller)