mirror of
https://github.com/aclist/dztui.git
synced 2026-08-28 10:47:15 +02:00
Merge branch 'fix/dayz-launcher' of github.com:aclist/dztui into fix/dayz-launcher
This commit is contained in:
commit
3f1aff7cc6
@ -16,8 +16,8 @@ By contrast, wireless routers and enterprise-grade routers may be less likely to
|
|||||||
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for
|
||||||
the server's query port.
|
the server's query port.
|
||||||
|
|
||||||
In addition, packets sent from server responses are expected to be a standard size (see warning below).
|
In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500,
|
||||||
Deviation from this may cause your router to discard incoming responses from the server.
|
it could lead to incoming responses being discarded (see warning below).
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|||||||
@ -151,6 +151,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
|||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
def connect_debug(client: str, addr: str, appid: int, name: str, mods: list[str]) -> str:
|
||||||
|
concat = concat_mods(mods)
|
||||||
|
client_args = concat_bash_args(client)
|
||||||
|
params = [
|
||||||
|
"-applaunch",
|
||||||
|
str(appid),
|
||||||
|
f"-connect={addr}",
|
||||||
|
"-nolauncher",
|
||||||
|
"-nosplash",
|
||||||
|
"-skipintro",
|
||||||
|
f"-name={name}",
|
||||||
|
f"-mod={concat}",
|
||||||
|
]
|
||||||
|
client_args.extend(params)
|
||||||
|
return " ".join(client_args)
|
||||||
|
|
||||||
# TODO: set config to name=user, use official server and no mods,
|
# TODO: set config to name=user, use official server and no mods,
|
||||||
# ensure that formatted string is identical to fixture with same hash
|
# ensure that formatted string is identical to fixture with same hash
|
||||||
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
||||||
|
|||||||
@ -73,12 +73,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
NAME = {
|
NAME = {
|
||||||
"key": "name",
|
"key": "name",
|
||||||
}
|
}
|
||||||
INSTALL = {
|
|
||||||
"key": "auto_install",
|
|
||||||
}
|
|
||||||
DEBUG = {
|
|
||||||
"key": "debug",
|
|
||||||
}
|
|
||||||
DEFAULT = {
|
DEFAULT = {
|
||||||
"key": "default_steam_path",
|
"key": "default_steam_path",
|
||||||
}
|
}
|
||||||
@ -91,9 +85,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
IP_LIST = {
|
IP_LIST = {
|
||||||
"key": "ip_list",
|
"key": "ip_list",
|
||||||
}
|
}
|
||||||
BRANCH = {
|
|
||||||
"key": "branch",
|
|
||||||
}
|
|
||||||
START_TAB = {
|
START_TAB = {
|
||||||
"key": "start_tab",
|
"key": "start_tab",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,9 +114,6 @@ class Controller(GObject.GObject):
|
|||||||
def query_config(self, key: Preferences) -> Any:
|
def query_config(self, key: Preferences) -> Any:
|
||||||
return self.config_man.lookup(key)
|
return self.config_man.lookup(key)
|
||||||
|
|
||||||
def is_auto_install(self) -> bool:
|
|
||||||
return bool(self.query_config(Preferences.INSTALL))
|
|
||||||
|
|
||||||
def suppress_signal(
|
def suppress_signal(
|
||||||
self, owner: Any, child: Any, func_name: str, state: bool
|
self, owner: Any, child: Any, func_name: str, state: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -266,6 +263,7 @@ class Controller(GObject.GObject):
|
|||||||
self.open_page(NotebookPage.LOG)
|
self.open_page(NotebookPage.LOG)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
@ -299,6 +297,7 @@ class Controller(GObject.GObject):
|
|||||||
write_diagnostic(self.prefs.paths.config, file)
|
write_diagnostic(self.prefs.paths.config, file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def update_steam_api_key(self, text: str) -> None:
|
def update_steam_api_key(self, text: str) -> None:
|
||||||
@ -526,6 +525,9 @@ class Controller(GObject.GObject):
|
|||||||
ind = self.config_man.get_start_tab()
|
ind = self.config_man.get_start_tab()
|
||||||
self.get_servers().notebook.set_current_page(ind)
|
self.get_servers().notebook.set_current_page(ind)
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
return self.connection_man.get_debug_args()
|
||||||
|
|
||||||
def update_and_load_to_menu(self) -> None:
|
def update_and_load_to_menu(self) -> None:
|
||||||
self.connection_man.update_and_connect(menu_only=True)
|
self.connection_man.update_and_connect(menu_only=True)
|
||||||
|
|
||||||
|
|||||||
@ -178,8 +178,8 @@ class ConfigManager:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
dialog = ExceptionDialog(self.controller, trace)
|
dialog = ExceptionDialog(self.controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise e
|
|
||||||
|
|
||||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||||
columns = tv.get_columns()
|
columns = tv.get_columns()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts
|
|||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
|
connect_debug,
|
||||||
get_app_allows_downloads,
|
get_app_allows_downloads,
|
||||||
get_app_name,
|
get_app_name,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
@ -45,6 +46,7 @@ from dzgui.strings.dialogs import (
|
|||||||
waiting_for_mods,
|
waiting_for_mods,
|
||||||
waiting_for_directories,
|
waiting_for_directories,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import kb
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings.server_mods import checkmark, resync
|
||||||
from dzgui.util.format import format_mib
|
from dzgui.util.format import format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
@ -193,7 +195,9 @@ class ConnectionManager:
|
|||||||
is_last = self.is_last_server()
|
is_last = self.is_last_server()
|
||||||
|
|
||||||
# TODO: strings
|
# TODO: strings
|
||||||
invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"]
|
invalid_mods = [
|
||||||
|
(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"
|
||||||
|
]
|
||||||
|
|
||||||
prereqs = Prerequisites(
|
prereqs = Prerequisites(
|
||||||
name=info.server_name,
|
name=info.server_name,
|
||||||
@ -298,8 +302,17 @@ class ConnectionManager:
|
|||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
|
return connect_debug(
|
||||||
|
self.client, addr, self.appid, playername, self.remote_mod_ids
|
||||||
|
)
|
||||||
|
|
||||||
def _connect_steam(self, menu_only: bool) -> None:
|
def _connect_steam(self, menu_only: bool) -> None:
|
||||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
playername = self.controller.query_config(Preferences.NAME)
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
|
|||||||
@ -22,9 +22,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage
|
|||||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.format import format_mods
|
from dzgui.util.format import format_mods
|
||||||
from dzgui.util.strings import server_timeout
|
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
|
||||||
|
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
@ -213,10 +211,6 @@ class ModManager:
|
|||||||
func = StoredFunc(self._on_stale_mods_found, stale)
|
func = StoredFunc(self._on_stale_mods_found, stale)
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
|
|||||||
@ -64,4 +64,5 @@ class UpdateManager:
|
|||||||
|
|
||||||
def _on_update_failure(self, msg: str) -> None:
|
def _on_update_failure(self, msg: str) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, msg)
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from dzgui.const.constants import (
|
|||||||
)
|
)
|
||||||
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs, kb
|
||||||
from dzgui.util.strings import server_timeout, dialog
|
from dzgui.util.strings import server_timeout, dialog
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
@ -405,6 +405,7 @@ class ServerModelManager:
|
|||||||
|
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||||
@ -420,6 +421,8 @@ class ServerModelManager:
|
|||||||
# customize statusbar and dialog accordingly
|
# customize statusbar and dialog accordingly
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
|
|||||||
2
dzgui/strings/changelog.py
Normal file
2
dzgui/strings/changelog.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
collapse_all = "Collapse all"
|
||||||
|
expand_all = "Expand all"
|
||||||
@ -20,3 +20,6 @@ ok = "OK"
|
|||||||
|
|
||||||
mission_dialog = "Set a mission folder location"
|
mission_dialog = "Set a mission folder location"
|
||||||
custom_mod_dialog = "Set location to a custom mods folder"
|
custom_mod_dialog = "Set location to a custom mods folder"
|
||||||
|
|
||||||
|
debug_heading = "Debug"
|
||||||
|
debug_secondary = "Launch arguments"
|
||||||
|
|||||||
13
dzgui/strings/kb.py
Normal file
13
dzgui/strings/kb.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration.
|
||||||
|
|
||||||
|
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||||
|
|
||||||
|
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||||
|
|
||||||
|
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||||
|
|
||||||
|
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||||
|
|
||||||
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port.
|
||||||
|
|
||||||
|
In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded."""
|
||||||
@ -2,6 +2,7 @@ update_mods = "Update mods and connect"
|
|||||||
connect = "Connect"
|
connect = "Connect"
|
||||||
connect_last = "Load to menu screen"
|
connect_last = "Load to menu screen"
|
||||||
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
||||||
|
debug = "Debug"
|
||||||
back = "Back"
|
back = "Back"
|
||||||
cancel = "Cancel"
|
cancel = "Cancel"
|
||||||
warnings = "Warnings"
|
warnings = "Warnings"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from typing import Literal, Self, TYPE_CHECKING
|
from typing import Literal, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
||||||
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util import strings
|
from dzgui.util import strings
|
||||||
from dzgui.views.components.buttons import ClipboardButton
|
from dzgui.views.components.buttons import ClipboardButton
|
||||||
|
|
||||||
@ -169,58 +170,30 @@ class QuitDialog(GenericDialog):
|
|||||||
self.controller.save_res_and_quit()
|
self.controller.save_res_and_quit()
|
||||||
|
|
||||||
|
|
||||||
class ExceptionDialog(GenericDialog):
|
class TextBufferDialog(GenericDialog):
|
||||||
"""
|
def __init__(
|
||||||
Error dialog with rich traceback.
|
self,
|
||||||
Usage:
|
controller: "Controller",
|
||||||
try:
|
mtype: Gtk.MessageType,
|
||||||
foo()
|
heading: str,
|
||||||
except Exception:
|
secondary: str,
|
||||||
trace = traceback.format_exc()
|
text: str,
|
||||||
dialog = ExceptionDialog(Controller, trace)
|
):
|
||||||
dialog.run()
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, controller: "Controller", trace: str):
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller=controller,
|
controller=controller,
|
||||||
text=strings.error_heading,
|
text=heading,
|
||||||
mtype=Gtk.MessageType.ERROR,
|
mtype=mtype,
|
||||||
buttons=Gtk.ButtonsType.NONE,
|
buttons=Gtk.ButtonsType.NONE,
|
||||||
secondary=strings.something_wrong,
|
secondary=secondary,
|
||||||
)
|
)
|
||||||
|
self.text = text
|
||||||
self.trace = trace
|
copy_button = ClipboardButton(controller, self.get_text)
|
||||||
# NOTE: box expands to end of content area
|
|
||||||
scrollable = Gtk.ScrolledWindow(
|
|
||||||
propagate_natural_height=True, max_content_height=500
|
|
||||||
)
|
|
||||||
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
# TODO: wrap/truncate long messages
|
|
||||||
textview = Gtk.TextView(
|
|
||||||
wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10
|
|
||||||
)
|
|
||||||
textview.set_buffer(Gtk.TextBuffer(text=self.trace))
|
|
||||||
box.pack_start(textview, EXPAND, FILL, 10)
|
|
||||||
scrollable.add(box)
|
|
||||||
|
|
||||||
content = self.get_content_area()
|
|
||||||
content.set_spacing(0)
|
|
||||||
# TODO: padding around top of content area when traceback is long
|
|
||||||
content.add(scrollable)
|
|
||||||
|
|
||||||
copy_button = ClipboardButton(controller, self.get_trace)
|
|
||||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||||
self.add_button("OK", Gtk.ResponseType.OK)
|
self.add_button("OK", Gtk.ResponseType.OK)
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
self.ok = self.get_widget_for_response(Gtk.ResponseType.OK)
|
|
||||||
if self.ok is not None:
|
|
||||||
self.ok.grab_focus()
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
|
||||||
def get_trace(self) -> str:
|
def get_text(self) -> str:
|
||||||
return self.trace
|
return self.text
|
||||||
|
|
||||||
def _on_response(
|
def _on_response(
|
||||||
self, dialog: Self, response: Gtk.ResponseType
|
self, dialog: Self, response: Gtk.ResponseType
|
||||||
@ -236,3 +209,114 @@ class ExceptionDialog(GenericDialog):
|
|||||||
return None
|
return None
|
||||||
case _:
|
case _:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DebugDialog(TextBufferDialog):
|
||||||
|
def __init__(self, controller: "Controller", debug: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
mtype=Gtk.MessageType.INFO,
|
||||||
|
heading=dialogs.debug_heading,
|
||||||
|
secondary=dialogs.debug_secondary,
|
||||||
|
text=debug,
|
||||||
|
)
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=False,
|
||||||
|
min_content_height=200,
|
||||||
|
max_content_height=200,
|
||||||
|
margin_bottom=10,
|
||||||
|
)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=debug))
|
||||||
|
scrollable.add(textview)
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionDialog(TextBufferDialog):
|
||||||
|
"""
|
||||||
|
Error dialog with rich traceback.
|
||||||
|
Usage:
|
||||||
|
try:
|
||||||
|
foo()
|
||||||
|
except Exception:
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
dialog = ExceptionDialog(Controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller: "Controller", trace: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
heading=strings.error_heading,
|
||||||
|
secondary=strings.something_wrong,
|
||||||
|
text=trace,
|
||||||
|
mtype=Gtk.MessageType.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: box expands to end of content area
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=True, max_content_height=500
|
||||||
|
)
|
||||||
|
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
bottom_margin=10,
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=trace))
|
||||||
|
box.pack_start(textview, EXPAND, FILL, 0)
|
||||||
|
|
||||||
|
self.error_details = Gtk.ScrolledWindow(
|
||||||
|
overlay_scrolling=False,
|
||||||
|
max_content_height=150,
|
||||||
|
propagate_natural_height=False,
|
||||||
|
)
|
||||||
|
details_box = Gtk.Box(
|
||||||
|
hexpand=True,
|
||||||
|
vexpand=True,
|
||||||
|
margin_right=5,
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.details_buffer = Gtk.TextBuffer()
|
||||||
|
details_textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD_CHAR,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
details_textview.set_buffer(self.details_buffer)
|
||||||
|
details_box.pack_start(details_textview, EXPAND, FILL, 10)
|
||||||
|
self.error_details.add(details_box)
|
||||||
|
|
||||||
|
self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15)
|
||||||
|
self.error_notebook.append_page(box, Gtk.Label(label="Error"))
|
||||||
|
self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details"))
|
||||||
|
self.error_notebook.connect("switch-page", self._on_page_changed)
|
||||||
|
scrollable.add(self.error_notebook)
|
||||||
|
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
def _on_page_changed(
|
||||||
|
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
||||||
|
) -> None:
|
||||||
|
if child == self.error_details:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, True)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, False)
|
||||||
|
|
||||||
|
def set_details_buffer(self, text: str) -> None:
|
||||||
|
self.details_buffer.set_text(text)
|
||||||
|
self.error_notebook.set_show_tabs(True)
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
||||||
|
from dzgui.strings import changelog as clog
|
||||||
from dzgui.util.strings import missing_changelog
|
from dzgui.util.strings import missing_changelog
|
||||||
from dzgui.util.format import format_pango
|
from dzgui.util.format import format_pango
|
||||||
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
||||||
@ -31,20 +33,39 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
changelog = missing_changelog
|
changelog = missing_changelog
|
||||||
|
|
||||||
# TODO: should long text be wrapped?
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.box = Gtk.Box(
|
self.box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
||||||
)
|
)
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
|
expand_all = Gtk.Button(
|
||||||
|
label=clog.expand_all, halign=Gtk.Align.START, margin_start=20
|
||||||
|
)
|
||||||
|
expand_all.connect("clicked", self._on_expand_all_clicked)
|
||||||
|
self.box.add(expand_all)
|
||||||
|
|
||||||
|
self.expanded = False
|
||||||
|
self.expanders: list[Gtk.Expander] = []
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("key-press-event", self._on_esc_keypress)
|
self.connect("key-press-event", self._on_esc_keypress)
|
||||||
|
|
||||||
changes = self._parse(changelog)
|
changes = self._parse(changelog)
|
||||||
self._generate_nodes(changes)
|
self._generate_nodes(changes)
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
def _on_expand_all_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
for expander in self.expanders:
|
||||||
|
# NOTE: simply setting set_expanded() does not trigger activate() signal,
|
||||||
|
# so margins are not applied
|
||||||
|
if expander.get_expanded() == self.expanded:
|
||||||
|
continue
|
||||||
|
expander.activate()
|
||||||
|
label = clog.collapse_all if self.expanded else clog.expand_all
|
||||||
|
button.set_label(label)
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
@ -65,9 +86,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
text = "\n".join(changes)
|
text = "\n".join(changes)
|
||||||
formatted = format_pango(text)
|
formatted = format_pango(text)
|
||||||
|
|
||||||
container = Gtk.Box(
|
container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START)
|
||||||
valign=Gtk.Align.START, halign=Gtk.Align.START
|
|
||||||
)
|
|
||||||
label = Gtk.Label()
|
label = Gtk.Label()
|
||||||
label.set_markup(formatted)
|
label.set_markup(formatted)
|
||||||
container.add(label)
|
container.add(label)
|
||||||
@ -81,6 +100,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
expander.add(container)
|
expander.add(container)
|
||||||
expander.connect("activate", self._on_expand, container)
|
expander.connect("activate", self._on_expand, container)
|
||||||
self.box.add(expander)
|
self.box.add(expander)
|
||||||
|
self.expanders.append(expander)
|
||||||
|
|
||||||
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
||||||
"""
|
"""
|
||||||
@ -94,6 +114,7 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
container.set_margin_bottom(15)
|
container.set_margin_bottom(15)
|
||||||
|
|
||||||
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||||
|
|
||||||
release = ""
|
release = ""
|
||||||
releases: list[tuple[str, list[str]]] = []
|
releases: list[tuple[str, list[str]]] = []
|
||||||
release_notes: list[str] = []
|
release_notes: list[str] = []
|
||||||
@ -107,5 +128,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
release = ""
|
release = ""
|
||||||
release = line
|
release = line
|
||||||
continue
|
continue
|
||||||
release_notes.append(line.rstrip())
|
release_notes.append(textwrap.fill(line.rstrip(), width=120))
|
||||||
return releases
|
return releases
|
||||||
|
|||||||
@ -430,6 +430,7 @@ class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
# NOTE: re-check in case file was removed by user between runs
|
# NOTE: re-check in case file was removed by user between runs
|
||||||
if prefs.paths.config.is_file() is False:
|
if prefs.paths.config.is_file() is False:
|
||||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask
|
|||||||
from dzgui.util.localize import number
|
from dzgui.util.localize import number
|
||||||
from dzgui.strings.server_mods import checkmark
|
from dzgui.strings.server_mods import checkmark
|
||||||
from dzgui.strings import preconnect
|
from dzgui.strings import preconnect
|
||||||
|
from dzgui.views.dialogs.generic import DebugDialog
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||||
|
|
||||||
@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
spacing=5,
|
spacing=5,
|
||||||
)
|
)
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||||
|
|
||||||
|
# NOTE: -d flag
|
||||||
|
if self.controller.get_prefs().is_debug:
|
||||||
|
debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START)
|
||||||
|
debug.connect("clicked", self._on_debug_clicked)
|
||||||
|
box.add(debug)
|
||||||
for button in self.back, self.ok, self.connect_last:
|
for button in self.back, self.ok, self.connect_last:
|
||||||
box.add(button)
|
box.add(button)
|
||||||
self.button_box.add(box)
|
self.button_box.add(box)
|
||||||
@ -221,6 +228,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_load_to_menu()
|
self.controller.update_and_load_to_menu()
|
||||||
|
|
||||||
|
def _on_debug_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
args = self.controller.get_debug_args()
|
||||||
|
d = DebugDialog(self.controller, args)
|
||||||
|
d.show_all()
|
||||||
|
d.run()
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_connect()
|
self.controller.update_and_connect()
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,33 @@
|
|||||||
|
import re
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from importlib import resources
|
from pathlib import Path
|
||||||
from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def changelog():
|
def changelog(request) -> None:
|
||||||
path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH)
|
root = request.config.rootpath
|
||||||
return path
|
changelog = Path(root).joinpath("CHANGELOG.md").read_text()
|
||||||
|
return changelog
|
||||||
|
|
||||||
|
|
||||||
def test_headings(changelog):
|
def count_hash(line: str) -> int:
|
||||||
with open(changelog, "r") as f:
|
cnt = 0
|
||||||
lines = f.readlines()
|
for c in line:
|
||||||
for line in lines:
|
if c == "#":
|
||||||
if line.startswith("#"):
|
cnt += 1
|
||||||
pass
|
return cnt
|
||||||
# TODO: use regex
|
|
||||||
pass
|
|
||||||
|
def test_changelog_prefix(changelog) -> None:
|
||||||
|
r = r".*(\[.*\]).*"
|
||||||
|
lines = changelog.splitlines()
|
||||||
|
sort = sorted(lines)
|
||||||
|
match = [line for line in sort if line.startswith("#")]
|
||||||
|
for m in match:
|
||||||
|
if "Changelog" in m:
|
||||||
|
assert count_hash(m) == 1
|
||||||
|
elif re.match(r, m) is not None:
|
||||||
|
assert count_hash(m) == 2
|
||||||
|
else:
|
||||||
|
assert count_hash(m) == 3
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user