mirror of
https://github.com/aclist/dztui.git
synced 2026-10-28 21:52:46 +01:00
515 lines
17 KiB
Python
515 lines
17 KiB
Python
from __future__ import annotations
|
|
from enum import Enum
|
|
from typing import Self, TYPE_CHECKING, Union
|
|
|
|
from dzgui.util import css
|
|
from dzgui.const.constants import (
|
|
APPID_DAYZ,
|
|
APPID_DAYZ_EXP,
|
|
APPNAME_DAYZ,
|
|
APPNAME_DAYZ_EXP_HUMAN,
|
|
EDIT_DELETE,
|
|
FOLDER,
|
|
WARNING,
|
|
)
|
|
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
|
from dzgui.managers.offline import OfflineManager
|
|
from dzgui.strings import generic, offline
|
|
from dzgui.views.components.box import HBox, VBox
|
|
from dzgui.views.components.buttons import Icon, IconTextButton
|
|
from dzgui.views.components.eventbox import InfoEventBox
|
|
from dzgui.views.components.frame import HeadingFrame
|
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
|
from dzgui.views.components.misc import ErrorPopover
|
|
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
|
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk, Gdk, GObject # noqa
|
|
|
|
if TYPE_CHECKING:
|
|
from dzgui.controllers.mc import Controller
|
|
from dzgui.controllers.emitter import Emitter
|
|
from dzgui.model.model_factory import FastInsertListStore
|
|
|
|
|
|
class FolderError(Enum):
|
|
NO_VALID_MODS = 1
|
|
NO_VALID_MISSION = 2
|
|
FOLDER_CHANGED = 3
|
|
|
|
|
|
class PageHeading(Gtk.Box):
|
|
def __init__(self, label: str, controller: "Controller") -> None:
|
|
super().__init__(
|
|
halign=Gtk.Align.CENTER, orientation=Gtk.Orientation.HORIZONTAL
|
|
)
|
|
|
|
self.label = Gtk.Label(label=label)
|
|
self.eb = InfoEventBox(offline.heading_disclaimer, controller)
|
|
self.eb.set_icon_yalign(0.7)
|
|
|
|
for el in self.label, self.eb:
|
|
self.add(el)
|
|
|
|
css.add_class(self, "page-heading")
|
|
|
|
|
|
class OfflineErrorPopover(ErrorPopover):
|
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
|
super().__init__(relative_to=relative_to, position=position)
|
|
|
|
def set_error_label(self, error: FolderError, msg: str) -> None:
|
|
match error:
|
|
case FolderError.NO_VALID_MODS:
|
|
prefix = offline.no_mods
|
|
case FolderError.NO_VALID_MISSION:
|
|
prefix = offline.no_mission
|
|
case FolderError.FOLDER_CHANGED:
|
|
prefix = offline.folder_changed
|
|
self.label.set_label(prefix)
|
|
return
|
|
self.label.set_label(f"{prefix}: '{msg}'")
|
|
|
|
|
|
class FolderHBox(HBox):
|
|
def __init__(self, controller: "Controller", btn_label: str, eb_text: str) -> None:
|
|
super().__init__(spacing=10)
|
|
|
|
self.set_margin_start(5)
|
|
self.set_margin_end(10)
|
|
self.set_margin_bottom(5)
|
|
|
|
self.folder = ""
|
|
self.controller = controller
|
|
self.emitter = controller.get_emitter()
|
|
|
|
self.eb = InfoEventBox(eb_text, controller)
|
|
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
|
self.button.set_halign(Gtk.Align.START)
|
|
self.button.set_image_position(Gtk.PositionType.LEFT)
|
|
|
|
self.set_can_focus(True)
|
|
|
|
self.scrolled_label = Gtk.ScrolledWindow(
|
|
propagate_natural_width=True, halign=Gtk.Align.START
|
|
)
|
|
self.label = Gtk.Label()
|
|
self.scrolled_label.add(self.label)
|
|
|
|
self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button)
|
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
|
|
|
self.spinner = Gtk.Spinner()
|
|
self.extend(
|
|
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
|
)
|
|
|
|
self.pop = OfflineErrorPopover(self.button, Gtk.PositionType.RIGHT)
|
|
self.pop.connect("unmap", lambda _: self.grab_focus())
|
|
|
|
self.sidepop = OfflineErrorPopover(self.scrolled_label, Gtk.PositionType.BOTTOM)
|
|
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
|
|
|
self.connect("map", self._on_map)
|
|
self.connect("unmap", self._on_unmap)
|
|
|
|
def start_spinner(self) -> None:
|
|
self.spinner.show()
|
|
self.spinner.start()
|
|
|
|
def stop_spinner(self) -> None:
|
|
self.spinner.hide()
|
|
self.spinner.stop()
|
|
|
|
def _on_unmap(self, widget: Self) -> None:
|
|
self.unset_button.hide()
|
|
self.label.set_label("")
|
|
self.scrolled_label.hide()
|
|
|
|
def _on_map(self, widget: Self) -> None:
|
|
self.spinner.hide()
|
|
self.scrolled_label.show()
|
|
self.unset_button.hide()
|
|
|
|
def _on_unset_clicked(self, button: Gtk.Button) -> None:
|
|
self.folder = ""
|
|
self.label.set_label("")
|
|
self.unset_button.hide()
|
|
# FIXME: rename signal
|
|
self.emitter.emit("custom_mods_unloaded", self)
|
|
|
|
def get_button(self) -> Gtk.Button:
|
|
return self.button
|
|
|
|
def hide_label(self) -> None:
|
|
self.label.hide()
|
|
|
|
def get_folder(self) -> str:
|
|
return self.folder
|
|
|
|
def set_folder(self, folder: str) -> None:
|
|
prefix = offline.folder_prefix
|
|
self.folder = folder
|
|
self.label.set_markup(prefix + folder)
|
|
self.label.show()
|
|
self.unset_button.show()
|
|
|
|
def present_error(self, error: FolderError, msg: str) -> None:
|
|
if error == FolderError.FOLDER_CHANGED:
|
|
self.sidepop.set_error_label(error, msg)
|
|
self.sidepop.popup()
|
|
return
|
|
self.folder = ""
|
|
self.label.set_text("")
|
|
self.unset_button.hide()
|
|
self.pop.set_error_label(error, msg)
|
|
self.pop.popup()
|
|
|
|
|
|
class ModFrame(HeadingFrame):
|
|
def __init__(
|
|
self, parent: OfflineLoader, controller: "Controller", label: str
|
|
) -> None:
|
|
super().__init__(heading=label)
|
|
|
|
self.parent = parent
|
|
self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
self.tree = OfflineModTreeView(controller)
|
|
self.tree.set_model(None)
|
|
self.tree.set_valign(Gtk.Align.FILL)
|
|
|
|
self.scrolled = NoOverlayScrolledWindow()
|
|
self.scrolled.set_margin_end(5)
|
|
self.scrolled.set_vexpand(True)
|
|
self.scrolled.add(self.tree)
|
|
|
|
self.status = Gtk.Label(
|
|
halign=Gtk.Align.START, margin_start=5, margin_top=3, margin_bottom=3
|
|
)
|
|
|
|
self.tree_vbox = VBox()
|
|
self.tree_vbox.extend([self.scrolled, self.status])
|
|
|
|
self.no_mods = HBox(spacing=10)
|
|
no_mods_icon = Icon(WARNING, margin_start=10)
|
|
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
|
|
self.no_mods.extend([no_mods_icon, no_mods_label])
|
|
|
|
self.vbox.pack_end(self.no_mods, expand=True, fill=True, padding=3)
|
|
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
|
|
self.frame.add(self.vbox)
|
|
|
|
sel = self.tree.get_selection()
|
|
sel.connect("changed", self._on_selection_changed)
|
|
|
|
self.connect("map", self._on_map)
|
|
self.connect("unmap", self._on_unmap)
|
|
|
|
def start_empty(self) -> None:
|
|
self.tree_vbox.hide()
|
|
self.no_mods.show()
|
|
|
|
def _on_map(self, widget: Self) -> None:
|
|
self.no_mods.hide()
|
|
self.tree.set_model(None)
|
|
self.tree_vbox.hide()
|
|
|
|
def _on_unmap(self, widget: Self) -> None:
|
|
self.tree.set_model(None)
|
|
self.tree_vbox.hide()
|
|
self.no_mods.hide()
|
|
|
|
def start(self, store: "FastInsertListStore") -> None:
|
|
# TODO: this manipulates mod man store out of band
|
|
self.tree.set_model(store)
|
|
self.tree.mod_man.set_store(store)
|
|
self.show_tree()
|
|
|
|
def get_mods(self) -> list[str]:
|
|
model, treeiters = self.tree.get_selection().get_selected_rows()
|
|
if model is None:
|
|
return []
|
|
if type(self) is CustomModFrame:
|
|
# NOTE: custom mod dirs to be hashed
|
|
return [model[_iter][2] for _iter in treeiters]
|
|
else:
|
|
# NOTE: pre-existing, canonical symlinks to published mods
|
|
return [model[_iter][1] for _iter in treeiters]
|
|
|
|
def show_tree(self) -> None:
|
|
self.tree_vbox.show()
|
|
|
|
def collapse_tree(self) -> None:
|
|
self.tree_vbox.hide()
|
|
|
|
def get_tree(self) -> OfflineModTreeView:
|
|
return self.tree
|
|
|
|
def pack(self, widget: Gtk.Widget) -> None:
|
|
self.vbox.pack_start(widget, expand=False, fill=False, padding=5)
|
|
|
|
def set_model(self, model: Union["FastInsertListStore", None]) -> 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()
|
|
if len(rows) == 0:
|
|
status = generic.selectable_tree
|
|
else:
|
|
status = f"Mods selected: {len(rows)}"
|
|
self.status.set_label(status)
|
|
# TODO: cleaner delegation
|
|
# simply send int value or use emitter
|
|
self.parent.check_button()
|
|
|
|
def set_cursor(self) -> None:
|
|
path = Gtk.TreePath.new_from_indices([0])
|
|
self.tree.set_cursor(path)
|
|
self.tree.get_selection().unselect_all()
|
|
|
|
|
|
class CustomModFrame(ModFrame):
|
|
def __init__(
|
|
self, parent: OfflineLoader, controller: "Controller", heading: str
|
|
) -> None:
|
|
super().__init__(parent, controller, heading)
|
|
|
|
self.tree.set_menu(ContextMenuGroup.MOD_OFFLINE)
|
|
self.parent = parent
|
|
|
|
self.controller = controller
|
|
self.emitter = controller.get_emitter()
|
|
|
|
self.custom_hbox = FolderHBox(
|
|
controller, offline.custom_button, offline.custom_eventbox
|
|
)
|
|
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
|
|
|
|
self.pack(self.custom_hbox)
|
|
|
|
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
|
self.emitter.connect("custom_mods_unloaded", self._on_custom_mods_unloaded)
|
|
|
|
self.connect("map", self._on_map)
|
|
|
|
def _on_map(self, widget: Self) -> None:
|
|
self.hide_tree()
|
|
|
|
def _on_custom_mods_unloaded(self, emitter: "Emitter", widget: FolderHBox) -> None:
|
|
# TODO: kludgy workaround for generic button emitting global signal
|
|
if widget == self.custom_hbox:
|
|
self.hide_tree()
|
|
|
|
def hide_tree(self) -> None:
|
|
self.custom_hbox.hide_label()
|
|
self.tree.set_model(None)
|
|
self.tree_vbox.hide()
|
|
self.no_mods.hide()
|
|
|
|
def present_folder_changed(self, folder: str) -> None:
|
|
self.custom_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
|
|
|
|
def present_error(self, folder: str) -> None:
|
|
self.hide_tree()
|
|
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
|
|
|
|
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
|
|
self.custom_hbox.set_folder(folder)
|
|
self.tree.set_model(store)
|
|
self.tree_vbox.show()
|
|
|
|
def _on_custom_mods_loaded(
|
|
self,
|
|
emitter: "Emitter",
|
|
store: "FastInsertListStore",
|
|
folder: str,
|
|
) -> None:
|
|
self.custom_hbox.stop_spinner()
|
|
if len(store) == 0:
|
|
self.present_error(folder)
|
|
else:
|
|
self.present_tree(store, folder)
|
|
|
|
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
|
|
callback = self.custom_hbox.start_spinner
|
|
# TODO: cleaner delegation
|
|
self.parent.offline_man.find_custom_mods(callback)
|
|
|
|
def get_folder(self) -> str:
|
|
return self.custom_hbox.get_folder()
|
|
|
|
|
|
class MissionFrame(HeadingFrame):
|
|
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
|
super().__init__(heading=offline.mission_frame)
|
|
|
|
self.parent = parent
|
|
self.controller = controller
|
|
self.emitter = controller.get_emitter()
|
|
|
|
self.mission_hbox = FolderHBox(
|
|
controller, offline.mission_button, offline.mission_eventbox
|
|
)
|
|
self.mission_button = self.mission_hbox.get_button()
|
|
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
|
|
|
self.frame.add(self.mission_hbox)
|
|
|
|
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
|
|
|
|
def _on_mission_loaded(
|
|
self, emitter: "Emitter", folder: str, is_valid: bool
|
|
) -> None:
|
|
if is_valid:
|
|
self.mission_hbox.set_folder(folder)
|
|
else:
|
|
self.mission_hbox.present_error(FolderError.NO_VALID_MISSION, folder)
|
|
self.parent.check_button()
|
|
|
|
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
|
self.parent.offline_man.get_mission()
|
|
|
|
def get_mission(self) -> str:
|
|
return self.mission_hbox.get_folder()
|
|
|
|
def present_folder_changed(self, folder: str) -> None:
|
|
self.mission_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
|
|
|
|
|
|
class RadioFrame(HeadingFrame):
|
|
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
|
super().__init__(heading=offline.version)
|
|
|
|
self.controller = controller
|
|
|
|
self.id_map = {APPNAME_DAYZ: APPID_DAYZ, APPNAME_DAYZ_EXP_HUMAN: APPID_DAYZ_EXP}
|
|
self.appid = APPID_DAYZ
|
|
|
|
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)
|
|
|
|
# TODO: cleaner delegation
|
|
has_dayz_exp = parent.offline_man.has_dayz_exp()
|
|
self.dayz.connect("toggled", self._on_radio_toggled)
|
|
if has_dayz_exp is False:
|
|
self.dayz_exp.set_sensitive(False)
|
|
|
|
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
|
|
for el in self.dayz, self.dayz_exp:
|
|
if el.get_active():
|
|
label = el.get_label()
|
|
self.appid = self.id_map[label]
|
|
|
|
def get_appid(self) -> int:
|
|
return self.appid
|
|
|
|
|
|
class OfflineLoader(Gtk.Box):
|
|
def __init__(self, controller: "Controller"):
|
|
super().__init__(
|
|
orientation=Gtk.Orientation.VERTICAL,
|
|
margin_start=10,
|
|
margin_end=10,
|
|
)
|
|
|
|
self.controller = controller
|
|
self.controller.register_widget("offline_loader", self)
|
|
self.emitter = controller.get_emitter()
|
|
self.offline_man = OfflineManager(controller)
|
|
|
|
heading = PageHeading(offline.heading, controller)
|
|
self.add(heading)
|
|
|
|
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
|
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
|
|
|
self.custom_tree = OfflineModTreeView(controller)
|
|
self.mission_frame = MissionFrame(self, controller)
|
|
self.radio_frame = RadioFrame(self, controller)
|
|
|
|
self.scrollable = Gtk.ScrolledWindow(
|
|
vexpand=True, propagate_natural_height=True
|
|
)
|
|
|
|
self.content_box = VBox(spacing=5)
|
|
self.content_box.extend(
|
|
[
|
|
self.local_frame,
|
|
self.custom_frame,
|
|
self.mission_frame,
|
|
self.radio_frame,
|
|
]
|
|
)
|
|
|
|
self.button_box = HBox(spacing=5)
|
|
self.button_box.set_halign(Gtk.Align.END)
|
|
self.button_box.set_margin_top(15)
|
|
self.back = Gtk.Button(label="Back")
|
|
self.ok = Gtk.Button(label="Launch", sensitive=False)
|
|
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.emitter.connect("custom_mods_unloaded", lambda _, __: self.check_button())
|
|
|
|
self.button_box.extend([self.back, self.ok])
|
|
|
|
self.scrollable.add(self.content_box)
|
|
self.add(self.scrollable)
|
|
self.add(self.button_box)
|
|
|
|
def check_button(self) -> None:
|
|
local_mods = self.local_frame.get_mods()
|
|
custom_mods = self.custom_frame.get_mods()
|
|
mission = self.mission_frame.get_mission()
|
|
if len(local_mods) == 0 and len(custom_mods) == 0 and len(mission) == 0:
|
|
self.ok.set_sensitive(False)
|
|
else:
|
|
self.ok.set_sensitive(True)
|
|
|
|
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
self.back.emit("clicked")
|
|
|
|
def populate(self, store: Union["FastInsertListStore", None]) -> None:
|
|
if store is None:
|
|
self.local_frame.start_empty()
|
|
return
|
|
self.local_frame.start(store)
|
|
|
|
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
|
self.controller.open_page(NotebookPage.MODS)
|
|
|
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
|
appid = self.radio_frame.get_appid()
|
|
mission = self.mission_frame.get_mission()
|
|
local_mods = self.local_frame.get_mods()
|
|
custom_folder = self.custom_frame.get_folder()
|
|
custom_mods = self.custom_frame.get_mods()
|
|
|
|
if custom_folder and not self.offline_man.is_custom_folder_valid(custom_folder):
|
|
self.custom_frame.present_folder_changed(mission)
|
|
return
|
|
|
|
if mission and not self.offline_man.is_mission_valid(mission):
|
|
self.mission_frame.present_folder_changed(mission)
|
|
return
|
|
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
|
|
|
def grab_content_area(self) -> None:
|
|
self.local_frame.tree.grab_focus()
|