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()
)