diff --git a/.gitignore b/.gitignore index 535baa6..a8cdde9 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,4 @@ $RECYCLE.BIN/ # Project settings file src/**/settings.json +src/**/server_settings.json diff --git a/application/app_server.py b/application/app_server.py index dd18926..70516ce 100644 --- a/application/app_server.py +++ b/application/app_server.py @@ -1,28 +1,69 @@ +from PySide6.QtWidgets import QApplication, QMainWindow +import waitress + +from flask import cli + from splitguides.server import app, get_notes, settings -import flask.cli as cli +from splitguides.ui.server_settings_ui import ServerSettingsDialog -# Stop flask from giving users an unhelpful warning. + +# Suppress some of flask's messages cli.show_server_banner = lambda *x: None def launch(): - get_notes() # Sets internal 'notes' and 'notefile' variables + # Create a base application and main window for the dialogs to use as parent + qt_app = QApplication() + main_window = QMainWindow() + + settings_dialog = ServerSettingsDialog( + parent=main_window, + settings=settings, + ) + + result = settings_dialog.exec() + if result == 0: # Rejected, close without launching server + print("Settings cancelled, closing application.") + qt_app.quit() + return + + success = get_notes(main_window) # Sets internal 'notes' and 'notefile' variables + if not success: + print("No notes file selected, closing application.") + qt_app.quit() + return print( "This server version of SplitGuides allows you view notes via a browser window " "and should work across a local network.\n" - "It is not intended to be used over the internet and as such is not based on a " - "production server." + "This uses a development server and is not intended " + "to be used over the internet." ) print( - f"Connect a browser to http://{settings.server_hostname}:{settings.server_port}/ " + f"Connect a browser to " + f"http://{settings.server_hostname}:{settings.server_port}/ " f"in order to view the notes." ) - print(f"This hostname and port can be changed in {settings.output_file} if needed.") + print("Press ctrl+c to close the server.") + + try: + # app.run( + # threaded=True, + # host=settings.server_hostname, + # port=settings.server_port + # ) - app.run(threaded=True, host=settings.server_hostname, port=settings.server_port) + waitress.serve( + app, + host=settings.server_hostname, + port=settings.server_port + ) + except KeyboardInterrupt: + print("Interrupt received, closing application.") + finally: + qt_app.quit() if __name__ == "__main__": diff --git a/readme.md b/readme.md index c205f95..d29eb54 100644 --- a/readme.md +++ b/readme.md @@ -11,15 +11,20 @@ Includes a server version for rendering notes in browsers on another device ## Install/Setup ## 1. Under the Livesplit layout editor add 'LiveSplit Server' (listed under 'control') + * Take note of the port number here, this is the value needed for + 'Livesplit Server Port' in settings. + * Livesplit Server Hostname can be the local ip given. If SplitGuides is running + on the same machine as Livesplit 'localhost' (the default) should also work. 2. Download SplitGuides from the [**releases page**](https://github.com/DavidCEllis/SplitGuides/releases) 3. Extract anywhere and run *splitguides.exe* ## Usage ## 1. Connect with livesplit by starting the livesplit server component selecting - 'Control' and 'Start Server' + 'Control' and 'Start Server' in livesplit. 2. Right click in the splitguides window and select 'Open Notes' and find the text file containing the notes you wish to use. +3. Some configuration is available from the settings dialog. Plain text formatting works the same way as SplitNotes. Notes made for that should function fine in SplitGuides. @@ -31,65 +36,91 @@ inserted in between lines. 1. Comment lines still use square brackets. 2. By default splits will break on newlines, multiple newlines are ignored in this case. -3. The rendering is done as HTML so HTML formatting can be used. + * If a split separator is given, newlines are left as in the input to the + markdown/html processors. -## splitguides_server.exe ## +## SplitGuides Server ## -Now included is a server version which launches a (local) webhost so you can view the notes -on another device on your local network. Launch splitguides_server.exe to start the service. +Included is a separate server version which launches a (local) webhost so you can view +the notes on another device on your local network. -If the hostname and port defaults aren't usable you can set them by editing server_hostname -and server_port in settings.json. There is no dialog for editing these settings yet. +Launch **splitguides_server.exe** to start the service. A settings dialog will appear +so you can customise this version separately from the desktop version. After asking +for the notes file the server will launch serving those notes and will update +automatically as you split. + +If the hostname and port defaults aren't usable you can edit them +in the settings dialog. + +This version is intended for people doing runs on a single monitor so the notes can be +displayed on another device (a tablet or phone for example). Just connect to the host +and port given in a web browser. -### Example Notes ### +## Configuration ## + +Configuration Options: -#### Source #### +* Livesplit server hostname and port +* Display previous/next splits +* Split separator (leave blank for empty line separator) +* Font Size +* Text and Background Colour +* Alternative template HTML and CSS files + * Jinja2 templating is used for the HTML, use the included file as a guide + * Allows for further customising of the appearance if desired +* Hotkeys to offset the notes from the splits (not available in splitnotes server) + * This allows for some adjustment if the notes have ended up in the wrong place + relative to the splits. +* Server hostname and port (server only) + * This should be your local machine name on the network and an open port to + connect to from the device you wish to use to display the notes. + +## Example Notes ## + +### Source ### ```markdown -## High Hedge ## -### Friendly Arm Inn ### -* *East* -* *Pick up the ring* -* **Peldvale** - -### Peldvale ### -* *South* -* **High Hedge** - -### High Hedge ### -* Rest and Spin -* *South to Shop* -* Thalantyr (1, 1) -* Shop: - * Sell the wand - * Identify the ring - * Sell the ring - * 3x Potion of Explosions - * Potion of Magic Blocking - * Protection from Magic - * Identify - * Shield - * Mirror Image - * 3x Invisibility -* *South* -* Go to Wilderness Map -/split +# Dark Souls Remastered - SL1 NWW # +## Asylum ## +* Start Pyro + MK +* Keep Hilt +* Get Axe/Estus/Flame +* Asylum Skip +* Pick up a 200 soul +* Dupe the soul 99x +* Leave + +## Laurentius ## +* Pick up the firebombs +* Grab the 200 soul +* Ladder glitch +* Buy + * Max wooden arrows + * 4 blooming moss + * 2 throwing knifes + * Max (25) homeward bones +* Free Laurentius +* Darksign + +## GCS ## +* Dupe souls with the arrows +* Ascend flame to +15 +* Buy flash sweat, combustion, fire orb +* Valley Run +* Get DCS/RTSR +* Upwarp +* Get GCS ``` -#### Result #### +### Result ### -![Image of splitguides rendering](resources/demo_notes_md.png) +On Desktop: -## Configuration ## +![Image of splitguides rendering](resources/splits_example.png) -The settings page offers some customisation and connection settings including: +Via Splitnotes Server on Tablet: - * Server hostname and port - * Show previous/next N splits - * Custom split separator - * Base font size - * Default text and background colour - * HTML (Jinja2) template and CSS files to use for rendering +![Image of splitguides server - yes this is an old iPad](resources/splitguides_server_example.jpg) ## Dependencies ## * pyside6 - QT Gui Bindings @@ -98,7 +129,9 @@ The settings page offers some customisation and connection settings including: * flask - Handling the server version * markdown - Converting markdown to html for rendering * keyboard - Global hotkeys to advance/reverse note offset to splits +* waitress - wsgi server for splitguides server --- -Inspired by (but otherwise unassociated with) the original splitnotes: https://github.com/joeloskarsson/SplitNotes +Inspired by (but otherwise unassociated with) the original splitnotes: +https://github.com/joeloskarsson/SplitNotes diff --git a/resources/demo_notes_md.png b/resources/demo_notes_md.png deleted file mode 100644 index 6511279..0000000 Binary files a/resources/demo_notes_md.png and /dev/null differ diff --git a/resources/splitguides_server_example.jpg b/resources/splitguides_server_example.jpg new file mode 100644 index 0000000..095f4f1 Binary files /dev/null and b/resources/splitguides_server_example.jpg differ diff --git a/resources/splits_example.png b/resources/splits_example.png new file mode 100644 index 0000000..73feb6b Binary files /dev/null and b/resources/splits_example.png differ diff --git a/setup.py b/setup.py index afa71f0..2a67044 100644 --- a/setup.py +++ b/setup.py @@ -25,11 +25,12 @@ install_requires=[ "pyside6", "jinja2", - "bleach[css]", + "bleach[css]==6.0", # Each upgrade to bleach has broken something so pin it. "flask", "markdown", "keyboard", "prefab-classes", + "waitress", ], tests_require=test_requirements, extras_require={ diff --git a/src/splitguides/hotkeys/keyboard_fixer.py b/src/splitguides/hotkeys/keyboard_fixer.py index fe368ae..bc23bc6 100644 --- a/src/splitguides/hotkeys/keyboard_fixer.py +++ b/src/splitguides/hotkeys/keyboard_fixer.py @@ -29,7 +29,7 @@ def read_hotkey(suppress=True): """ Modified read_hotkey function to correctly support numpad keys. - The original function returns just the names, this returns a ([scancodes], name) tuple. + The original function returns just the names, this returns a Hotkey object. The scancodes can then be stored while the name can be displayed. """ diff --git a/src/splitguides/server/split_server.py b/src/splitguides/server/split_server.py index d627590..725f77e 100644 --- a/src/splitguides/server/split_server.py +++ b/src/splitguides/server/split_server.py @@ -5,27 +5,24 @@ from pathlib import Path from flask import Flask, render_template, Response -from PySide6.QtWidgets import QApplication, QFileDialog +from PySide6.QtWidgets import QFileDialog -from ..settings import Settings +from ..settings import ServerSettings from ..livesplit_client import get_client from ..note_parser import Notes KEEP_ALIVE = 10 -settings = Settings.load() - -template_folder = str(Path(__file__).parent / "templates") -static_folder = str(Path(__file__).parent / "static") +settings = ServerSettings.load() app = Flask( "splitguides", - template_folder=settings.server_template_folder, - static_folder=settings.server_static_folder, + template_folder=settings.html_template_folder, + static_folder=settings.css_folder, ) -notefile = None -notes = None +notefile: None | Path = None +notes: None | Notes = None app.secret_key = "".join( secrets.choice(string.printable) for _ in range(random.randint(30, 40)) @@ -40,7 +37,7 @@ def notes_page(): :return: """ global notefile - return render_template(settings.server_html_template_file, notefile=notefile.stem) + return render_template(settings.html_template_file, notefile=notefile.stem) @app.route("/splits") @@ -82,12 +79,12 @@ def event_stream(): last_update = now current_note_index = new_index split_text = notes.render_splits( - new_index - settings.server_previous_splits, - new_index + settings.server_next_splits + 1, + new_index - settings.previous_splits, + new_index + settings.next_splits + 1, ) if len(split_text) > 0: # Remove newlines from the notes as they break the send - data = split_text[0].replace("\n", "") + data = "".join(split_text).replace("\n", "") yield f"data: {data}\n\n" else: yield f"data: End of Notes.\n\n" @@ -106,21 +103,23 @@ def event_stream(): return Response(event_stream(), mimetype="text/event-stream") -def get_notes(): +def get_notes(parent): global notes, notefile - temp_app = QApplication() + # noinspection PyTypeChecker filepath, _ = QFileDialog.getOpenFileName( - None, + parent, "Open Notes", settings.notes_folder, "Note Files (*.txt *.md);;All Files (*.*)", ) - temp_app.quit() - - notefile = Path(filepath) - notes = Notes.from_file(notefile, settings.split_separator) + if filepath: + notefile = Path(filepath) + notes = Notes.from_file(notefile, settings.split_separator) - settings.notes_folder = str(notefile.parent) - settings.save() + settings.notes_folder = str(notefile.parent) + settings.save() + return True + else: + return False diff --git a/src/splitguides/settings.py b/src/splitguides/settings.py index c7d6205..38fd001 100644 --- a/src/splitguides/settings.py +++ b/src/splitguides/settings.py @@ -3,12 +3,14 @@ import sys import json +from abc import ABCMeta from pathlib import Path +from typing import ClassVar -from prefab_classes import prefab, attribute +from prefab_classes import prefab from prefab_classes.funcs import to_json -from .hotkeys import hotkey_or_none +from .hotkeys import hotkey_or_none, Hotkey if getattr(sys, "frozen", False): # pragma: nocover # Application is .exe, use visible files @@ -17,14 +19,16 @@ # Running as .py - use standard folder structure base_path = Path(__file__).parent -settings_file = Path(base_path / "settings.json") +desktop_settings_file = Path(base_path / "settings.json") +server_settings_file = Path(base_path / "server_settings.json") + default_template_folder = Path(base_path / "templates") default_static_folder = Path(base_path / "static") user_path = str(Path(os.path.expanduser("~")) / "Documents") try: local_hostname = socket.gethostname() -except Exception: +except OSError: local_hostname = "127.0.0.1" print( "Could not get local network hostname, using 127.0.0.1. " @@ -33,66 +37,48 @@ @prefab -class Settings: - """ - Global persistent settings handler - """ - # What file to use - output_file = attribute(default=settings_file) +class BaseSettings(metaclass=ABCMeta): + # Settings file to use + SETTINGS_FILE: ClassVar[None | Path] = None + output_file: None | Path = None # Settings save file # Networking Settings - hostname = attribute(default="localhost") - port = attribute(default=16834) + hostname: str = "localhost" + port: int = 16834 + # Parser Settings - split_separator = attribute(default="") - # User Preferences - previous_splits = attribute(default=0) - next_splits = attribute(default=2) - font_size = attribute(default=20) - font_color = attribute(default="#000000") - background_color = attribute(default="#f1f8ff") - # Templating - html_template_folder = attribute(default=default_template_folder) - html_template_file = attribute(default="desktop.html") - css_folder = attribute(default=default_static_folder) - css_file = attribute(default="desktop.css") - # Window Settings - on_top = attribute(default=False) - width = attribute(default=800) - height = attribute(default=800) - notes_folder = attribute(default=user_path) - # Hotkey Settings - hotkeys_enabled = attribute(default=False) + split_separator: str = "" - increase_offset_hotkey = attribute(default=None) - decrease_offset_hotkey = attribute(default=None) + # Display Settings + previous_splits: int = 0 + next_splits: int = 2 + font_size: int | float = 20.0 + font_color: str = "#000000" + background_color: str = "#f1f8ff" - # Server Settings - server_previous_splits = attribute(default=0) - server_next_splits = attribute(default=0) - server_hostname = attribute(default=local_hostname) - server_port = attribute(default=14250) + html_template_folder: Path = default_template_folder + css_folder: Path = default_static_folder + html_template_file: None | str = None + css_file: None | str = None - server_template_folder = attribute(default=default_template_folder) - server_html_template_file = attribute(default="server.html") - server_static_folder = attribute(default=default_static_folder) - server_css_file = attribute(default="server.css") + notes_folder: Path = user_path + + # Hotkey Settings + hotkeys_enabled: bool = False + increase_offset_hotkey: None | Hotkey = None + decrease_offset_hotkey: None | Hotkey = None def __prefab_post_init__( - self, - output_file, - html_template_folder, - css_folder, - increase_offset_hotkey, - decrease_offset_hotkey, - server_template_folder, - server_static_folder, + self, + output_file, + html_template_folder, + css_folder, + increase_offset_hotkey, + decrease_offset_hotkey, ): self.output_file = Path(output_file) self.html_template_folder = Path(html_template_folder) self.css_folder = Path(css_folder) - self.server_template_folder = Path(server_template_folder) - self.server_static_folder = Path(server_static_folder) self.increase_offset_hotkey = hotkey_or_none(increase_offset_hotkey) self.decrease_offset_hotkey = hotkey_or_none(decrease_offset_hotkey) @@ -101,6 +87,7 @@ def save(self): """ Save settings as JSON """ + def path_to_json(o): if isinstance(o, Path): return str(o) @@ -111,14 +98,15 @@ def path_to_json(o): json_str = to_json( self, - excludes=("output_file", ), + excludes=("output_file",), default=path_to_json, indent=2, ) self.output_file.write_text(json_str) + # noinspection PyArgumentList @classmethod - def load(cls, input_filename=settings_file): + def load(cls, input_filename: None | str | Path = None): """ Load settings from a file, if the file does not exist just use defaults @@ -126,6 +114,9 @@ def load(cls, input_filename=settings_file): :param input_filename: Saved settings file. :return: """ + if input_filename is None: + input_filename = cls.SETTINGS_FILE + input_path = Path(input_filename) if input_path.exists(): new_settings = json.loads(input_path.read_text()) @@ -137,22 +128,19 @@ def load(cls, input_filename=settings_file): # This will happen if the executable folder is moved # Absolute path ends up getting used because otherwise launching # from an external folder doesn't work - if not Path(loaded_settings.full_template_path).exists(): - loaded_settings.html_template_folder = default_template_folder - loaded_settings.html_template_file = "desktop.html" - if not Path(loaded_settings.full_css_path).exists(): - loaded_settings.css_folder = default_static_folder - loaded_settings.css_file = "desktop.css" - if not Path(loaded_settings.server_template_folder).exists(): - loaded_settings.server_template_folder = default_template_folder - loaded_settings.server_html_template_file = "server.html" - if not Path(loaded_settings.server_static_folder).exists(): - loaded_settings.server_static_folder = default_static_folder - loaded_settings.server_css_file = "server.css" + loaded_settings.fix_template_paths() return loaded_settings else: - return Settings(output_file=input_filename) + return cls(output_file=input_filename) + + def fix_template_paths(self): + if not self.full_template_path.exists(): + self.html_template_folder = default_template_folder + self.html_template_file = self.default_template_filename + if not self.full_css_path.exists(): + self.css_folder = default_static_folder + self.css_file = self.default_css_filename @property def full_template_path(self): @@ -161,3 +149,44 @@ def full_template_path(self): @property def full_css_path(self): return self.css_folder / self.css_file + + +@prefab +class DesktopSettings(BaseSettings): + """ + Global persistent settings handler + """ + + # Class variables (untyped) + default_template_filename: ClassVar[str] = "desktop.html" + default_css_filename: ClassVar[str] = "desktop.css" + + # What file to use + SETTINGS_FILE: ClassVar[Path] = desktop_settings_file + output_file: Path = desktop_settings_file + + # Override Defaults + html_template_file: str = "desktop.html" + css_file: str = "desktop.css" + + # Window Settings + on_top: bool = False + width: int = 800 + height: int = 800 + + +@prefab +class ServerSettings(BaseSettings): + # Class variables (untyped) + default_template_filename: ClassVar[str] = "server.html" + default_css_filename: ClassVar[str] = "server.css" + + SETTINGS_FILE: ClassVar[Path] = server_settings_file + output_file: Path = server_settings_file + + # Override defaults + html_template_file: str = "server.html" + css_file: str = "server.css" + + server_hostname: str = local_hostname + server_port: int = 8000 diff --git a/src/splitguides/ui/layouts/__init__.py b/src/splitguides/ui/layouts/__init__.py index 59d2a3a..40c59e8 100644 --- a/src/splitguides/ui/layouts/__init__.py +++ b/src/splitguides/ui/layouts/__init__.py @@ -1,6 +1,7 @@ try: from .build.main_window import Ui_MainWindow from .build.settings import Ui_Settings + from .build.server_settings import Ui_ServerSettings except ImportError: from .build_ui import build_ui @@ -9,6 +10,7 @@ try: from .build.main_window import Ui_MainWindow from .build.settings import Ui_Settings + from .build.server_settings import Ui_ServerSettings except ImportError: raise FileNotFoundError( "Dialog files could not be found, Ui files need to be rebuilt." diff --git a/src/splitguides/ui/layouts/server_settings.ui b/src/splitguides/ui/layouts/server_settings.ui new file mode 100644 index 0000000..6347010 --- /dev/null +++ b/src/splitguides/ui/layouts/server_settings.ui @@ -0,0 +1,394 @@ + + + ServerSettings + + + + 0 + 0 + 400 + 535 + + + + SplitGuides Server Settings + + + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 10 + + + + Livesplit Server Hostname: + + + + + + + + 10 + + + + localhost + + + + + + + + 10 + + + + Livesplit Server Port: + + + + + + + + 10 + + + + 16834 + + + + + + + Qt::Horizontal + + + + + + + Show Previous Splits: + + + + + + + 0 + + + + + + + + 10 + + + + Show Next Splits: + + + + + + + + 10 + + + + 2 + + + + + + + + 10 + + + + Split Separator: + + + + + + + + 10 + + + + Leave blank for empty line as separator + + + Empty line + + + + + + + Qt::Horizontal + + + + + + + Font Size: + + + + + + + + + + Text Colour: + + + + + + + + + + + + Select + + + + + + + + + Background Colour: + + + + + + + + + + + + Select + + + + + + + + + Qt::Horizontal + + + + + + + HTML Template: + + + + + + + + + false + + + + + + + ... + + + + + + + + + CSS: + + + + + + + + + false + + + + + + + ... + + + + + + + + + Qt::Horizontal + + + + + + + Next Split Hotkey: + + + + + + + + + false + + + + + + + Select + + + + + + + + + Previous Split Hotkey: + + + + + + + + + false + + + + + + + Select + + + + + + + + + Note Server Hostname: + + + + + + + + + + Note Server Port: + + + + + + + + + + Qt::Horizontal + + + + + + + + + + 10 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + hostname_edit + port_edit + previous_edit + advance_edit + separator_edit + fontsize_edit + textcolor_edit + textcolor_button + bgcolor_edit + bgcolor_button + htmltemplate_edit + htmltemplate_button + css_edit + css_button + nextsplitkey_edit + nextsplitkey_button + previoussplitkey_edit + previoussplitkey_button + noteserverhost_edit + noteserverport_edit + + + + diff --git a/src/splitguides/ui/layouts/settings.ui b/src/splitguides/ui/layouts/settings.ui index d1cbed6..cb4df10 100644 --- a/src/splitguides/ui/layouts/settings.ui +++ b/src/splitguides/ui/layouts/settings.ui @@ -10,7 +10,7 @@ 0 0 400 - 444 + 478 @@ -100,7 +100,7 @@ - + Qt::Horizontal @@ -172,7 +172,7 @@ - + Qt::Horizontal @@ -281,28 +281,28 @@ - + Qt::Horizontal - + Qt::Horizontal - + Next Split Hotkey: - + Previous Split Hotkey: @@ -371,32 +371,12 @@ accepted() Settings accept() - - - 248 - 254 - - - 157 - 274 - - buttonBox rejected() Settings reject() - - - 316 - 260 - - - 286 - 274 - - diff --git a/src/splitguides/ui/main.pyproject b/src/splitguides/ui/main.pyproject index 255c2f9..37b6e4d 100644 --- a/src/splitguides/ui/main.pyproject +++ b/src/splitguides/ui/main.pyproject @@ -1,3 +1,3 @@ { - "files": ["settings.py","layouts/main_window.ui","main_window.py","layouts/settings.ui","layouts/build_ui.py"] + "files": ["settings.py","layouts/main_window.ui","layouts/settings.ui","layouts/server_settings.ui","layouts/build_ui.py"] } diff --git a/src/splitguides/ui/main_window.py b/src/splitguides/ui/main_window.py index e44d22a..b82dad1 100644 --- a/src/splitguides/ui/main_window.py +++ b/src/splitguides/ui/main_window.py @@ -3,13 +3,13 @@ from pathlib import Path from concurrent.futures import ThreadPoolExecutor -from jinja2 import Environment, FileSystemLoader +from jinja2 import Environment, FileSystemLoader, Template from PySide6 import QtCore -from PySide6.QtGui import QCursor, QIcon +from PySide6.QtGui import QCursor, QIcon, QAction from PySide6.QtWidgets import QMainWindow, QFileDialog, QMenu, QErrorMessage from .custom_elements import ExtLinkWebEnginePage -from ..settings import Settings +from ..settings import DesktopSettings from .settings_ui import SettingsDialog from .layouts import Ui_MainWindow from ..note_parser import Notes @@ -38,22 +38,23 @@ def __init__(self): self.ui.statusbar.showMessage("Not connected to server.") # Get settings - self.settings = Settings.load() + self.settings = DesktopSettings.load() # Window size self.resize(self.settings.width, self.settings.height) # Always on Top - self.menu_on_top = None + self.menu_on_top: None | QAction = None + # noinspection PyUnresolvedReferences self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, self.settings.on_top) # Setup notes variables - self.notefile = None - self.notes = None + self.notefile: None | str = None + self.notes: None | Notes = None # Right Click Menu - self.rc_menu = None - self.hotkeys_toggle = None + self.rc_menu: None | QMenu = None + self.hotkeys_toggle: None | QAction = None self.build_menu() self.setup_actions() @@ -63,7 +64,7 @@ def __init__(self): autoescape=False, ) - self.template = None + self.template: None | Template = None self.load_template() self.css = "" @@ -83,9 +84,7 @@ def __init__(self): try: self.enable_hotkeys() except AttributeError: - QErrorMessage(parent=self).showMessage( - "Could not enable hotkeys." - ) + QErrorMessage(parent=self).showMessage("Could not enable hotkeys.") self.disable_hotkeys() self.settings.hotkeys_enabled = False self.hotkeys_toggle.setChecked(False) @@ -96,6 +95,7 @@ def toggle_on_top(self): """Toggle window always on top, update settings and window flag to match.""" self.settings.on_top = not self.settings.on_top self.menu_on_top.setChecked(self.settings.on_top) + # noinspection PyUnresolvedReferences self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, self.settings.on_top) self.show() @@ -110,9 +110,7 @@ def toggle_hotkey_enable(self): self.settings.hotkeys_enabled = True self.hotkeys_toggle.setChecked(True) except AttributeError: - QErrorMessage(parent=self).showMessage( - "Could not enable hotkeys." - ) + QErrorMessage(parent=self).showMessage("Could not enable hotkeys.") self.settings.hotkeys_enabled = False self.hotkeys_toggle.setChecked(False) @@ -173,6 +171,7 @@ def resizeEvent(self, event): def setup_actions(self): """Setup the browser element with custom options""" # Replace the context menu with the app context menu + # noinspection PyUnresolvedReferences self.ui.notes.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.ui.notes.customContextMenuRequested.connect(self.show_menu) # Allow links to open in an external browser @@ -260,7 +259,6 @@ def update_notes(self, idx, refresh=False): idx = max(idx, 0) if self.notes and (idx != self.split_index or refresh): - start = idx - self.settings.previous_splits end = idx + self.settings.next_splits + 1 @@ -347,12 +345,14 @@ def update_status(self, message): def ls_connect(self): self.update_status( - f"Trying to connect to Livesplit. | Split Offset: {self.main_window.split_offset}" + f"Trying to connect to Livesplit. | " + f"Split Offset: {self.main_window.split_offset}" ) self.connected = self.client.connect() if self.connected: self.update_status( - f"Connected to Livesplit. | Split Offset: {self.main_window.split_offset}" + f"Connected to Livesplit. | " + f"Split Offset: {self.main_window.split_offset}" ) def loop_update_split(self): diff --git a/src/splitguides/ui/server_settings_ui.py b/src/splitguides/ui/server_settings_ui.py new file mode 100644 index 0000000..f690d70 --- /dev/null +++ b/src/splitguides/ui/server_settings_ui.py @@ -0,0 +1,183 @@ +import sys + +from pathlib import Path + +from PySide6 import QtCore +from PySide6.QtWidgets import QDialog, QColorDialog, QFileDialog +from PySide6.QtCore import QRegularExpression +from PySide6.QtGui import ( + QIntValidator, + QDoubleValidator, + QRegularExpressionValidator, + QColor, +) + +from ..settings import ServerSettings +from .layouts import Ui_ServerSettings + + +# Get correct paths +if getattr(sys, "frozen", False): # pragma: nocover + base_path = Path(sys.executable).parent + icon_file = str(base_path / "logo_alpha.png") +else: + base_path = Path(__file__).parent + icon_file = str(base_path.parents[2] / "resources" / "logo_alpha.png") + + +class ServerSettingsDialog(QDialog): + def __init__( + self, + parent, + settings: ServerSettings, + ): + super().__init__(parent=parent) + + self.ui = Ui_ServerSettings() + self.ui.setupUi(self) + + self.settings = settings + + # noinspection PyUnresolvedReferences + self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) + + # self.hotkey_manager = hotkey_manager + self.nextsplitkey = None + self.previoussplitkey = None + + self.temp_html_path = self.settings.full_template_path + self.temp_css_path = self.settings.full_css_path + + self.setup_validators() + self.fill_settings() + + self.ui.textcolor_button.clicked.connect(self.font_color_dialog) + self.ui.bgcolor_button.clicked.connect(self.bg_color_dialog) + self.ui.htmltemplate_button.clicked.connect(self.html_template_dialog) + self.ui.css_button.clicked.connect(self.css_dialog) + + # Next and previous split keys are currently non-functioning + # self.ui.nextsplitkey_button.clicked.connect(self.get_increase_hotkey) + # self.ui.previoussplitkey_button.clicked.connect(self.get_decrease_hotkey) + # self.pool = ThreadPoolExecutor(max_workers=1) + self.ui.nextsplitkey_button.setDisabled(True) + self.ui.previoussplitkey_button.setDisabled(True) + self.ui.nextsplitkey_label.hide() + self.ui.nextsplitkey_edit.hide() + self.ui.nextsplitkey_button.hide() + self.ui.previoussplitkey_label.hide() + self.ui.previoussplitkey_edit.hide() + self.ui.previoussplitkey_button.hide() + self.ui.divider_4.hide() + self.adjustSize() + + self.ui.confirm_cancel_box.accepted.connect(self.accept) + self.ui.confirm_cancel_box.rejected.connect(self.reject) + + def setup_validators(self): + color_re = QRegularExpression(r"#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})") + color_validator = QRegularExpressionValidator(color_re, None) + self.ui.port_edit.setValidator(QIntValidator(1024, 65535, None)) + # 255 splits seems like a lot + self.ui.previous_edit.setValidator(QIntValidator(0, 255, None)) + self.ui.advance_edit.setValidator(QIntValidator(0, 255, None)) + # I don't know why you'd set a font size of 10k but sure why not + self.ui.fontsize_edit.setValidator(QDoubleValidator(0.0, 10000.0, 2, None)) + self.ui.textcolor_edit.setValidator(color_validator) + self.ui.bgcolor_edit.setValidator(color_validator) + + def fill_settings(self): + self.ui.hostname_edit.setText(self.settings.hostname) + self.ui.port_edit.setText(str(self.settings.port)) + self.ui.previous_edit.setText(str(self.settings.previous_splits)) + self.ui.advance_edit.setText(str(self.settings.next_splits)) + self.ui.separator_edit.setText(self.settings.split_separator) + self.ui.fontsize_edit.setText(str(self.settings.font_size)) + self.ui.textcolor_edit.setText(self.settings.font_color) + self.ui.bgcolor_edit.setText(self.settings.background_color) + self.ui.htmltemplate_edit.setText(str(self.settings.html_template_file)) + self.ui.css_edit.setText(str(self.settings.css_file)) + + if self.settings.increase_offset_hotkey: + self.ui.nextsplitkey_edit.setText(self.settings.increase_offset_hotkey.name) + if self.settings.decrease_offset_hotkey: + self.ui.previoussplitkey_edit.setText( + self.settings.decrease_offset_hotkey.name + ) + self.nextsplitkey = self.settings.increase_offset_hotkey + self.previoussplitkey = self.settings.decrease_offset_hotkey + + self.ui.noteserverhost_edit.setText(self.settings.server_hostname) + self.ui.noteserverport_edit.setText(str(self.settings.server_port)) + + def store_settings(self): + self.settings.hostname = self.ui.hostname_edit.text() + self.settings.port = int(self.ui.port_edit.text()) + self.settings.previous_splits = int(self.ui.previous_edit.text()) + self.settings.next_splits = int(self.ui.advance_edit.text()) + self.settings.split_separator = self.ui.separator_edit.text() + self.settings.font_size = float(self.ui.fontsize_edit.text()) + self.settings.font_color = self.ui.textcolor_edit.text() + self.settings.background_color = self.ui.bgcolor_edit.text() + + self.settings.increase_offset_hotkey = self.nextsplitkey + self.settings.decrease_offset_hotkey = self.previoussplitkey + + # Paths get stored in temporary variables + self.settings.html_template_folder = Path(self.temp_html_path).parent + self.settings.html_template_file = Path(self.temp_html_path).name + + self.settings.css_folder = Path(self.temp_css_path).parent + self.settings.css_file = Path(self.temp_css_path).name + + self.settings.server_hostname = self.ui.noteserverhost_edit.text() + self.settings.server_port = int(self.ui.noteserverport_edit.text()) + + def font_color_dialog(self): + """ + Pop up a color dialog for the text color. + """ + color = QColorDialog.getColor(QColor(self.settings.font_color), parent=self) + if color.isValid(): + self.ui.textcolor_edit.setText(color.name()) + + def bg_color_dialog(self): + """ + Pop up a color dialog for the background color. + """ + color = QColorDialog.getColor( + QColor(self.settings.background_color), parent=self + ) + if color.isValid(): + self.ui.bgcolor_edit.setText(color.name()) + + def html_template_dialog(self): + htmlfile, _ = QFileDialog.getOpenFileName( + self, + "Select Template File", + str(self.settings.html_template_folder), + "html templates (*.html);;All Files (*.*)", + ) + + if htmlfile: + self.temp_html_path = htmlfile + self.ui.htmltemplate_edit.setText(Path(htmlfile).name) + + def css_dialog(self): + cssfile, _ = QFileDialog.getOpenFileName( + self, + "Select Template File", + str(self.settings.css_folder), + "css files (*.css);;All Files (*.*)", + ) + + if cssfile: + self.temp_css_path = cssfile + self.ui.css_edit.setText(Path(cssfile).name) + + def accept(self): + """If the dialog is accepted save the settings""" + # Normal cleanup + super().accept() + # Store the settings in the settings object + self.store_settings() diff --git a/src/splitguides/ui/settings_ui.py b/src/splitguides/ui/settings_ui.py index bbadb0a..40b2185 100644 --- a/src/splitguides/ui/settings_ui.py +++ b/src/splitguides/ui/settings_ui.py @@ -4,14 +4,23 @@ from PySide6.QtWidgets import QDialog, QColorDialog, QFileDialog from PySide6.QtCore import QRegularExpression, Slot -from PySide6.QtGui import QIntValidator, QRegularExpressionValidator, QColor - +from PySide6.QtGui import ( + QIntValidator, + QDoubleValidator, + QRegularExpressionValidator, + QColor, +) + +from ..settings import DesktopSettings +from .hotkey_manager import HotkeyManager from .layouts import Ui_Settings from ..hotkeys import Hotkey class SettingsDialog(QDialog): - def __init__(self, parent, settings, hotkey_manager): + def __init__( + self, parent, settings: DesktopSettings, hotkey_manager: HotkeyManager + ): super().__init__(parent=parent) self.ui = Ui_Settings() self.ui.setupUi(self) @@ -45,7 +54,7 @@ def setup_validators(self): self.ui.previous_edit.setValidator(QIntValidator(0, 255, None)) self.ui.advance_edit.setValidator(QIntValidator(0, 255, None)) # I don't know why you'd set a font size of 10k but sure why not - self.ui.fontsize_edit.setValidator(QIntValidator(0, 10000, None)) + self.ui.fontsize_edit.setValidator(QDoubleValidator(0.0, 10000.0, 2, None)) self.ui.textcolor_edit.setValidator(color_validator) self.ui.bgcolor_edit.setValidator(color_validator) @@ -76,7 +85,7 @@ def store_settings(self): self.settings.previous_splits = int(self.ui.previous_edit.text()) self.settings.next_splits = int(self.ui.advance_edit.text()) self.settings.split_separator = self.ui.separator_edit.text() - self.settings.font_size = int(self.ui.fontsize_edit.text()) + self.settings.font_size = float(self.ui.fontsize_edit.text()) self.settings.font_color = self.ui.textcolor_edit.text() self.settings.background_color = self.ui.bgcolor_edit.text() @@ -137,9 +146,7 @@ def get_increase_hotkey(self): # First set the buttons dialog and disable the interface self.ui.nextsplitkey_button.setText("Listening...") self.setEnabled(False) - fn = lambda: self.hotkey_manager.select_input( - self.return_increase_hotkey - ) + fn = lambda: self.hotkey_manager.select_input(self.return_increase_hotkey) self.pool.submit(fn) @Slot(str) @@ -164,9 +171,7 @@ def return_increase_hotkey(self, hotkey=None): self.previoussplitkey = None # Disconnect the hotkey signal from this function - self.hotkey_manager.hotkey_signal.disconnect( - self.return_increase_hotkey - ) + self.hotkey_manager.hotkey_signal.disconnect(self.return_increase_hotkey) self.setEnabled(True) @@ -174,9 +179,7 @@ def get_decrease_hotkey(self): """Get a hotkey to use to decrease the split offset""" self.ui.previoussplitkey_button.setText("Listening...") self.setEnabled(False) - fn = lambda: self.hotkey_manager.select_input( - self.return_decrease_hotkey - ) + fn = lambda: self.hotkey_manager.select_input(self.return_decrease_hotkey) self.pool.submit(fn) @Slot(str) @@ -200,9 +203,7 @@ def return_decrease_hotkey(self, hotkey=None): self.nextsplitkey = None # Disconnect the hotkey signal from this function - self.hotkey_manager.hotkey_signal.disconnect( - self.return_decrease_hotkey - ) + self.hotkey_manager.hotkey_signal.disconnect(self.return_decrease_hotkey) self.setEnabled(True) diff --git a/tests/conftest.py b/tests/conftest.py index 9eba909..45f029e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest -from splitguides.settings import settings_file +from splitguides.settings import desktop_settings_file src_folder = Path("./src").resolve() sys.path.insert(0, str(src_folder)) @@ -11,6 +11,6 @@ @pytest.fixture(scope="function") def clear_settings(): - settings_file.unlink(missing_ok=True) + desktop_settings_file.unlink(missing_ok=True) yield - settings_file.unlink(missing_ok=True) + desktop_settings_file.unlink(missing_ok=True) diff --git a/tests/test_ui/test_settings.py b/tests/test_ui/test_settings.py index 0538421..7c1783b 100644 --- a/tests/test_ui/test_settings.py +++ b/tests/test_ui/test_settings.py @@ -7,7 +7,7 @@ from prefab_classes.funcs import as_dict -from splitguides.settings import Settings +from splitguides.settings import DesktopSettings from splitguides.settings import default_static_folder, default_template_folder from splitguides.ui.settings_ui import SettingsDialog @@ -22,7 +22,7 @@ @pytest.fixture def settings_ui(qtbot): - settings = Settings.load() + settings = DesktopSettings.load() fake_hotkey_manager = MagicMock() @@ -48,6 +48,7 @@ def settings_ui(qtbot): qtbot.keyClicks(settings_dialog.ui.separator_edit, "/split") qtbot.mouseDClick(settings_dialog.ui.fontsize_edit, Qt.LeftButton) + qtbot.mouseClick(settings_dialog.ui.fontsize_edit, Qt.LeftButton) qtbot.keyClicks(settings_dialog.ui.fontsize_edit, "25") qtbot.mouseDClick(settings_dialog.ui.textcolor_edit, Qt.LeftButton) @@ -64,7 +65,7 @@ class TestSettings: def test_settings_with_file(self): """Check settings are read and updated from a settings file""" with patch.object(Path, "exists", return_value=True): - s = Settings.load(test_settings) + s = DesktopSettings.load(test_settings) assert s.hostname == "fakehost" assert s.port == 12345 @@ -83,7 +84,7 @@ def test_settings_with_file(self): def test_default_paths(self): """Test if the paths listed in the settings file do not exist that defaults are used""" - s = Settings.load(test_settings) + s = DesktopSettings.load(test_settings) # Check they are not what is listed assert s.full_template_path != Path("fake/html/folder/fakehtml.html") @@ -93,13 +94,13 @@ def test_default_paths(self): assert s.full_css_path == default_static_folder / "desktop.css" def test_save_load(self): - s = Settings.load(test_settings) + s = DesktopSettings.load(test_settings) # Change the output file s.output_file = temp_settings s.save() - s2 = Settings.load(temp_settings) + s2 = DesktopSettings.load(temp_settings) for key in as_dict(s): assert as_dict(s)[key] == as_dict(s2)[key], key @@ -136,7 +137,7 @@ def test_settings_ui_cancel(self, qtbot, settings_ui): assert result == 0 - default_settings = Settings() + default_settings = DesktopSettings() assert settings.hostname == default_settings.hostname assert settings.port == default_settings.port @@ -151,7 +152,7 @@ def test_settings_ui_colorpicker_font(self, qtbot): """ Test font color picker """ - settings = Settings.load() + settings = DesktopSettings.load() settings_dialog = SettingsDialog( parent=None, settings=settings, hotkey_manager=MagicMock() @@ -178,7 +179,7 @@ def test_settings_ui_colorpicker_bg(self, qtbot): """ Test BG color picker """ - settings = Settings.load() + settings = DesktopSettings.load() settings_dialog = SettingsDialog( parent=None, settings=settings, hotkey_manager=MagicMock() )