feat: dzgui 7 alpha

This commit is contained in:
aclist committed 2025-12-08 17:32:25 +09:00
1 parent 1acdbd58c5
commit 2e5b771137
150 files changed
+10359

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
import textwrap
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
class EarlyAlertDialog(Gtk.MessageDialog):
def __init__(self, string):
# TODO: strings
super().__init__(
title="DZGUI - Dialog",
flags=0,
text="ERROR",
transient_for=None,
buttons=Gtk.ButtonsType.OK
)
ok = self.action_area.get_children()[0]
ok.set_label("OK")
ok.connect("clicked", Gtk.main_quit)
msg = textwrap.fill(string, 50)
self.format_secondary_text(msg)
self.action_area.set_margin_bottom(20)
self.outer = self.get_content_area()
self.outer.set_margin_start(30)
self.outer.set_margin_end(30)
self.set_default_size(250, 100)
self.connect("delete-event", Gtk.main_quit)
self.connect("destroy", Gtk.main_quit)
self.show_all()
Gtk.main()
+80
View File
@@ -0,0 +1,80 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from dzgui.const.enum import Popup
class EntryDialog(GenericDialog):
def __init__(self, text: str, mode: Popup, link: str, button_type=None):
super().__init__(text, mode)
"""
Wraps Gtk.Entry in a dialog and provides basic response handling.
Returns user input as a string or None.
The Entry widget itself can be manipulated via the get_entry() method.
"""
self.dialog = GenericDialog(text, mode)
self.dialogBox = self.dialog.get_content_area()
self.dialog.set_default_response(Gtk.ResponseType.OK)
self.dialog.set_size_request(500, 0)
self.user_entry = Gtk.Entry()
set_surrounding_margins(self.user_entry, 20)
self.user_entry.set_margin_top(0)
self.user_entry.set_size_request(250, 0)
self.user_entry.set_activates_default(True)
self.dialogBox.pack_start(self.user_entry, NO_EXPAND, NO_FILL, 0)
if link:
button = Gtk.Button(label=link)
button.set_margin_start(60)
button.set_margin_end(60)
button.connect("clicked", self._on_button_clicked, button_type)
self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, 0)
self.ok = self.dialog.action_area.get_children()[1]
self.ok.set_sensitive(False)
self.user_entry.connect("insert-text", self._on_text_typed)
self.user_entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted
)
def _is_valid_text(self, text: str) -> bool:
if text.isspace():
return False
if len(text) == 0:
return False
return True
def _on_text_deleted(
self, buffer: Gtk.EntryBuffer, position: int, chars: int
) -> None:
text = buffer.get_text()
state = self._is_valid_text(text)
self.ok.set_sensitive(state)
def _on_text_typed(
self, entry: Gtk.Entry, text: str, length: int, pos: int
) -> None:
state = self._is_valid_text(text)
self.ok.set_sensitive(state)
def _on_button_clicked(self, button: Gtk.Button, enum: RowType) -> None:
result = open_links.open_link_by_rowtype(enum)
if result is False:
AppNav.window.spawn_dialog(strings.something_wrong, Popup.NOTIFY)
def get_entry(self) -> Gtk.Entry:
return self.user_entry
def get_input(self) -> str | None:
self.dialog.show_all()
response = self.dialog.run()
text = self.user_entry.get_text()
self.dialog.destroy()
if (response == Gtk.ResponseType.OK) and (text != ""):
return text
else:
return None
+34
View File
@@ -0,0 +1,34 @@
from pathlib import Path
import gi # noqa E402
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, GObject
from dzgui.util.strings import picker
class FilePicker(Gtk.FileChooserDialog):
def __init__(self, parent: Gtk.Window) -> None:
super().__init__(
title=picker.title,
action=Gtk.FileChooserAction.SAVE,
parent=parent,
resizable=True,
)
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
self.add_buttons("_Save", Gtk.ResponseType.OK)
self.set_default_response(Gtk.ResponseType.OK)
self.set_current_name(picker.placeholder)
def pick_file(self) -> Path | None:
res = self.run()
if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self.destroy()
return None
if res == Gtk.ResponseType.OK:
file = self.get_filename()
if file is not None:
self.destroy()
return Path(file)
return None
+104
View File
@@ -0,0 +1,104 @@
import textwrap
from typing import Any, Literal, TYPE_CHECKING
from dzgui.const.constants import NO_EXPAND, NO_FILL
from dzgui.const.enum import Popup, ButtonType, NotebookPage
from dzgui.util import strings
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class GenericDialog(Gtk.MessageDialog):
def __init__(self, controller: "Controller", text: str, mode: Popup):
match mode:
case Popup.WAIT:
dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.NONE
header_text = strings.wait
case Popup.NOTIFY | Popup.RETURN | Popup.QUIT:
dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.OK
header_text = strings.notice
case Popup.CONFIRM:
dialog_type = Gtk.MessageType.QUESTION
button_type = Gtk.ButtonsType.OK_CANCEL
header_text = strings.confirmation
case Popup.ENTRY:
dialog_type = Gtk.MessageType.QUESTION
button_type = Gtk.ButtonsType.OK_CANCEL
header_text = strings.input_required
case Popup.MODLIST:
dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.OK
header_text = strings.modlist
case Popup.DETAILS:
dialog_type = Gtk.MessageType.INFO
button_type = Gtk.ButtonsType.OK
header_text = strings.server_details
# NOTE: steam deck prints <2> if dialog title is same as window title
Gtk.MessageDialog.__init__(
self,
transient_for=controller.mediator.window,
message_type=dialog_type,
buttons=button_type,
text=header_text,
secondary_text=textwrap.fill(text, 50),
title=strings.dialog_header,
modal=True,
)
self.controller = controller
if mode == Popup.WAIT:
dialogBox = self.get_content_area()
spinner = Gtk.Spinner()
dialogBox.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
spinner.start()
self.connect("delete-event", self._on_dialog_delete)
if mode == Popup.RETURN:
button_label = strings.main_menu
ok = self.action_area.get_children()[0]
ok.set_label(button_label)
ok.connect("clicked", self._return_to_main_menu)
self.connect("delete-event", self._return_to_main_menu)
if mode == Popup.QUIT:
button_label = strings.exit_app
ok = self.action_area.get_children()[0]
ok.set_label(button_label)
ok.connect("clicked", self._quit)
self.connect("delete-event", self._quit)
self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(500, 0)
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.action_area.set_layout(Gtk.ButtonBoxStyle.CENTER)
self.action_area.set_margin_bottom(20)
self.outer = self.get_content_area()
self.outer.set_margin_start(30)
self.outer.set_margin_end(30)
def _quit(self, *args: Any) -> None:
self.controller.save_res_and_quit()
def _on_dialog_delete(
self, response_id: Gtk.ResponseType, event: Gdk.Event
) -> Literal[True]:
"""
Prevent manual dialog destruction
"""
return True
def _return_to_main_menu(self, widget: Gtk.Widget) -> None:
self.controller.open_page(NotebookPage.MAIN)
def update_label(self, text: str) -> None:
self.format_secondary_text(text)
+152
View File
@@ -0,0 +1,152 @@
from typing import Self
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from dzgui.util.import strings
class LanDialog(Gtk.MessageDialog):
"""
Performs integer validation on the provided port
and blocks if out of range. Returns None if user cancels
"""
def __init__(self) -> None:
super().__init__(
transient_for=AppNav.window, #FIXME
flags=0,
message_type=Gtk.MessageType.INFO,
buttons=Gtk.ButtonsType.OK_CANCEL,
text=strings.scan_servers,
secondary_text=strings.select_port,
title=strings.dialog_header,
modal=True,
)
self.set_size_request(500, 0)
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
buttons = [
(strings.default_port, Port.DEFAULT),
(strings.custom_port, Port.CUSTOM),
]
self.button_box = Gtk.Box()
self.button_box.set_orientation(Gtk.Orientation.VERTICAL)
self.button_box.active_button = None
for k, v in buttons:
button = Gtk.RadioButton(label=k)
button.port = v
button.connect("toggled", self._on_button_toggled)
self.button_box.add(button)
if v == Port.DEFAULT:
self.button_box.active_button = button
else:
button.join_group(self.button_box.active_button)
self.entry = Gtk.Entry()
self.button_box.add(self.entry)
self.entry.set_no_show_all(True)
# TODO: strings
self.warn_label = Gtk.Label(label="Invalid port")
self.warn_label.set_no_show_all(True)
self.button_box.add(self.warn_label)
content = self.get_content_area()
content.pack_start(self.button_box, NO_EXPAND, NO_FILL, 0)
content.set_margin_start(30)
content.set_margin_end(30)
content.show_all()
self.action_area.set_layout(Gtk.ButtonBoxStyle.CENTER)
self.action_area.set_margin_bottom(20)
self.port = None
self.ok = self.action_area.get_children()[1]
self.connect("response", self._on_dialog_response)
self.connect("key-press-event", self._on_keypress)
self.connect("delete-event", self.restore_context)
self.entry.connect("insert-text", self._on_text_typed)
self.entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted
)
def _validate(self, text: str) -> None:
state = ip.is_valid_port(text)
self.ok.set_sensitive(not state)
self.warn_label.set_visible(state)
# TODO: can put in validation method
if len(text) == 0:
self.warn_label.set_visible(False)
def _on_text_deleted(
self, buffer: Gtk.EntryBuffer, position: int, chars: int
) -> None:
text = buffer.get_text()
self._validate(text)
def _on_text_typed(
self, entry: Gtk.Entry, text: str, length: int, pos: int) -> None:
self._validate(entry.get_text() + text)
def restore_context(self, *args: Any) -> None:
context = WindowContext.MAIN_MENU
AppNav.treeview.set_view(context)
def _on_keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None:
if event.keyval == Gdk.KEY_Return:
if self.ok.is_sensitive():
self.response(Gtk.ResponseType.OK)
if self.button_box.get_children()[0].is_focus():
self.response(Gtk.ResponseType.OK)
else:
self.restore_context()
if event.keyval == Gdk.KEY_Up:
self.ok.set_sensitive(True)
self.entry.set_text("")
self.button_box.get_children()[0].grab_focus()
def _on_dialog_response(
self, dialog: Self, response: Gtk.ResponseType
) -> None:
cancel_events = [
Gtk.ResponseType.CLOSE,
Gtk.ResponseType.CANCEL,
Gtk.ResponseType.DELETE_EVENT,
]
if response in cancel_events:
self.restore_context()
return
string = self.entry.get_text()
port = self.button_box.active_button.port
match port:
case Port.DEFAULT:
self.port = UDP_PORT
case Port.CUSTOM:
if ip.is_valid_port(string):
self.stop_emission_by_name("response")
else:
self.port = int(string)
def get_selected_port(self) -> int:
return self.port
def _on_button_toggled(self, button: Gtk.Button) -> None:
if button.get_active():
self.button_box.active_button = button
match button.port:
case Port.DEFAULT:
self.entry.set_visible(False)
case Port.CUSTOM:
self.entry.set_visible(True)
self.entry.grab_focus()
self.ok.set_sensitive(False)
+54
View File
@@ -0,0 +1,54 @@
import textwrap
from typing import Self, TYPE_CHECKING
from dzgui.const.enum import Popup, Preferences
from dzgui.const.constants import NO_EXPAND, NO_FILL
from dzgui.util.open_links import open_user_workshop
from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.components.web_button import WebButton
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
if TYPE_CHECKING:
from dzgui.controllers.mc import Controller
class WorkshopLinkDialog(GenericDialog):
def __init__(self, controller: "Controller", text: str, button_label: str, uid: str):
super().__init__(controller, text, Popup.NOTIFY)
text = textwrap.dedent(text)
self.dialogBox = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(500, 0)
"""
If the user's ID was successfully parsed from loginusers,
a clickable link to their workshop subscriptions is added
"""
if uid is not None:
button = WebButton(label=button_label)
button.set_margin_start(60)
button.set_margin_end(60)
button.connect("clicked", self._on_button_clicked, uid)
self.dialogBox.pack_end(button, NO_EXPAND, NO_FILL, 0)
self.show_all()
self.connect("response", self._on_dialog_response)
def _on_button_clicked(self, button: Gtk.Button, uid: str) -> None:
# TODO: currently checks user id on instantiation
client = self.controller.query_config(Preferences.CLIENT)
open_user_workshop(uid, client)
def _on_dialog_response(
self, dialog: Self, resp: Gtk.ResponseType
) -> None:
match resp:
case Gtk.ResponseType.DELETE_EVENT:
return
case Gtk.ResponseType.OK:
self.destroy()
+102
View File
@@ -0,0 +1,102 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
from dzgui.const.enum import Popup
class ServerDetailsDialog(GenericDialog):
def __init__(self, server_name: str, ip: str, qport: int):
super().__init__(server_name, Popup.DETAILS)
dialog_box = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(800, 700)
self.ip = ip.split(":")[0]
self.qport = qport
self.store = Gtk.ListStore(str, str, Pango.Weight)
self.view = Gtk.TreeView(
enable_search=False,
search_column=-1,
headers_visible=False,
fixed_height_mode=True,
)
self.view.connect("row-activated", self._on_row_activated)
for i, column_title in enumerate(["Item", "Details"]):
renderer = Gtk.CellRendererText(xalign=0)
if i == 0:
column = Gtk.TreeViewColumn(
column_title, renderer, text=i, weight=2
)
else:
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
if i != 2:
self.view.append_column(column)
column.set_sort_column_id(i)
column.set_expand(True)
scrollable_tree = Gtk.ScrolledWindow()
scrollable_tree.add(self.view)
scrollable_tree.set_size_request(700, 200)
scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START)
css.add_class(desc, "details-heading")
box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER
)
self.description = Gtk.Label(
justify=Gtk.Justification.CENTER, wrap=True
)
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
sep.set_margin_bottom(10)
for el in desc, sep, self.description:
box.add(el)
scrollable_message.add(box)
dialog_box.pack_start(scrollable_tree, EXPAND, FILL, 0)
dialog_box.pack_start(scrollable_message, EXPAND, FILL, 0)
self.wait_dialog = GenericDialog(strings.details, Popup.WAIT)
self.wait_dialog.show_all()
thread = threading.Thread(
target=self._background, args=(self.wait_dialog, ip, qport)
)
thread.start()
def _on_row_activated(
self,
treeview: Gtk.TreeView,
tree_iter: Gtk.TreeIter,
col: Gtk.TreeViewColumn,
) -> None:
self.destroy()
def _load(self) -> None:
if self.wait_dialog:
self.wait_dialog.destroy()
if self.success is False:
AppNav.window.spawn_dialog(strings.server_error, Popup.NOTIFY)
return
self.show_all()
self.run()
self.destroy()
def _background(
self, dialog: "GenericDialog", ip: str, qport: int
) -> None:
response = Servers.details(self.ip, self.qport)
if response.success:
for row in response.data:
self.store.append(row + [Pango.Weight.BOLD])
self.view.set_model(self.store)
text = response.description
text = format_hyperlinks(text)
self.description.set_markup(text)
self.success = response.success
GLib.idle_add(self._load)
+94
View File
@@ -0,0 +1,94 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
class ModDialog(GenericDialog):
def __init__(self, record: str):
# TODO: center secondary text
msg = strings.workshop
super().__init__(textwrap.dedent(msg), Popup.MODLIST)
dialogBox = self.get_content_area()
self.set_default_response(Gtk.ResponseType.OK)
self.set_size_request(800, 500)
self.scrollable = Gtk.ScrolledWindow()
self.view = Gtk.TreeView(
enable_search=False, search_column=-1, fixed_height_mode=True
)
self.scrollable.add(self.view)
set_surrounding_margins(self.scrollable, 20)
self.view.connect("row-activated", self._on_row_activated)
for i, column_title in enumerate(strings.server_mod_cols):
renderer = Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
self.view.append_column(column)
column.set_sort_column_id(i)
match column_title:
case strings.mod:
column.set_fixed_width(350)
case strings.id:
column.set_fixed_width(200)
case _:
pass
dialogBox.pack_end(self.scrollable, EXPAND, FILL, 0)
wait_dialog = GenericDialog(strings.modlist, Popup.WAIT)
wait_dialog.show_all()
thread = threading.Thread(
target=self._background, args=(wait_dialog, record)
)
thread.start()
# TODO: why are we passing record here?
def _background(self, dialog: "GenericDialog", record: str) -> None:
def _load():
dialog.destroy()
# TODO: natively implement
#if data.returncode == 1:
# AppNav.window.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()
if not record:
return
# TODO: thread
try:
# TODO: get default_steam_path from config
modlist = get_server_modlist(record)
mod_count = self._parse_modlist_rows(modlist)
except Exception:
# TODO: needs to pop error dialog
mod_count = 0
modlist_store.clear()
self.view.set_model(modlist_store)
GLib.idle_add(_load)
def _parse_modlist_rows(self, rows: list) -> bool | int:
for row in rows:
modlist_store.append(row)
return len(rows)
def _on_row_activated(
self,
treeview: Gtk.TreeView,
tree_iter: Gtk.TreeIter,
col: Gtk.TreeViewColumn,
) -> None:
select = treeview.get_selection()
sels = select.get_selected_rows()
(model, pathlist) = sels
if len(pathlist) < 1:
return
path = pathlist[0]
tree_iter = model.get_iter(path)
mod_id = model.get_value(tree_iter, 1)
call_bash_func("open_workshop_page", mod_id)
+18
View File
@@ -0,0 +1,18 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class Watcher(Gtk.Dialog):
def __init__():
super().__init__()
pass
def bump_progress(self):
self.set_secondary_text("FOO")
#w = Watcher()
#thread = threading()
## compute here
#GLib.idle_add(self._bump_progress)
## raise window:
#Gdk.present_with_time(Gdk.CURRENT_TIME)