Skip to content

Commit

Permalink
Merge pull request #11 from pilgrimtabby/rework-keyboard
Browse files Browse the repository at this point in the history
v1.0.7 (Rework keyboard)

This PR shifts the code handling keyboard listener/controller backends from ui_controller to a new module called ui_keyboard_controller. This is done to keep keyboard handling in ui_controller abstract, and makes it easier to handle the complexities of using two different backends depending on the platform.

In addition, this PR adds some unit tests for settings.py, splitter.py, and pilgrim_autosplitter.py. This whole project should have unit tests but I have limited time to devote to this right now, so it's not a priority.

It also fixes several bugs, most related to hotkey implementation:

- Hotkeys will no longer be bound to multiple keys when their key codes match (pynput only)
- Hotkeys can no longer be bound if they have a key code but no name (pynput) -- this prevents the user from thinking no key is bound when in reality there is one
- ui._poll is now called at the correct interval even after a settings change (whoops)
- Fix implementation issue that could cause segmentation fault occasionally when binding hotkeys

Other misc. changes/updates:

- Automatically resize the font if hotkey names are larger than the hotkey box
- On MacOS, make calls to caffeinate in separate thread to avoid bogging down main thread
- Typehints are now uniform in style and backwards compatible with Python 3.8+
- Some class attributes and methods have been renamed for brevity and readability
- Pytest added to requirements.txt and requirements-linux.txt
- pilgrim_autosplitter.py rewritten for clarity and neatness
- settings.py refactored to allow using different QSettings in functions for testing purposes
- Some methods in splitter.py refactored and/or split into multiple methods for testing purposes
  • Loading branch information
pilgrimtabby authored Oct 15, 2024
2 parents bfa6a75 + 259e4bb commit 5ad3088
Show file tree
Hide file tree
Showing 18 changed files with 1,148 additions and 388 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
**/_site
**/.jekyll-cache
pilgrim_autosplitter.spec

**/.pytest_cache
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,20 @@ Download the latest Windows build (Pilgrim.Autosplitter.Windows.v1.x.x) from the

If you're familiar with Python:

* Make sure you have Python 3.10+ installed
* Make sure you have Python 3.8+ installed (Python 3.11+ is recommended for an optimal experience)

* Run `python -m pip install -r requirements.txt`

> [!Note]
> If installation hangs up when trying to download PyQt5, try running the following command: `python -m pip install pyqt5 --config-settings --confirm-license= --verbose`.
> Some users (including myself) have experienced a softlock when verifying the PyQt5 license; this should solve that problem.
> In addition, if using Python <=3.10, you may need to manually install PyQt5 using `python -m pip install pyqt5-tools`.
* Open the app with `python pilgrim_autosplitter.py`

If this is your first time using Python:

* Install the latest version of Python (3.12 at this time) by clicking [here](https://www.python.org/downloads/).
* Install the latest stable Python release (3.12 at this time) by clicking [here](https://www.python.org/downloads/).

> [!Important]
> When installing Python using the Python installer, you MUST check the box next to `Add Python to PATH`. If you don't, the next part won't work.
Expand Down Expand Up @@ -100,19 +101,20 @@ Known limitations:

If you're familiar with Python:

* Make sure you have Python 3.10+ installed
* Make sure you have Python 3.8+ installed (Python 3.11+ is recommended for an optimal experience)

* Run `pip3 install -r requirements.txt`

> [!Note]
> If installation hangs up when trying to download PyQt5, try running the following command: `python -m pip install pyqt5 --config-settings --confirm-license= --verbose`.
> Some users (including myself) have experienced a softlock when verifying the PyQt5 license; this should solve that problem.
> In addition, if using Python <=3.10, you may need to manually install PyQt5 using `python -m pip install pyqt5-tools`.
* Open the app with `python3 pilgrim_autosplitter.py`

If this is your first time using Python:

* Install the latest version of Python (3.12 at this time) by clicking [here](https://www.python.org/downloads/).
* Install the latest stable Python release (3.12 at this time) by clicking [here](https://www.python.org/downloads/).

* Download Pilgrim Autosplitter's source code from the [most recent release](https://github.com/pilgrimtabby/pilgrim-autosplitter/releases/latest) (click on `Source code (zip)` or `Source code (tar.gz)`). Extract the files.

Expand All @@ -134,8 +136,9 @@ Known limitations:

### Method 2: Run from source with Python

If you're on Linux, I assume you know what you're doing. Get the latest release [here](https://github.com/pilgrimtabby/pilgrim-autosplitter/releases/latest), use pip to install the dependencies in `requirements-linux.txt`, and run `src/pilgrim_autosplitter.py` as root. Python >=3.10 is required.
If you're on Linux, I assume you know what you're doing. Get the latest release [here](https://github.com/pilgrimtabby/pilgrim-autosplitter/releases/latest), use pip to install the dependencies in `requirements-linux.txt`, and run `src/pilgrim_autosplitter.py` as root. Python >=3.8 is required (Python 3.11+ is recommended for an optimal experience).

> [!Note]
> If installation hangs up when trying to download PyQt5, try running the following command: `python -m pip install pyqt5 --config-settings --confirm-license= --verbose`.
> Some users (including myself) have experienced a softlock when verifying the PyQt5 license; this should solve that problem.
> In addition, if using Python <=3.10, you may need to manually install PyQt5 using `python -m pip install pyqt5-tools`.
1 change: 1 addition & 0 deletions requirements-linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ opencv-python-headless>=4.9.0.80
keyboard>=0.13.5
PyQt5>=5.15.10
PyQt5-sip>=12.13.0
pytest>=8.3.2
requests>=2.32.3
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ opencv-python>=4.9.0.80
pynput>=1.7.6
PyQt5>=5.15.10
PyQt5-sip>=12.13.0
pytest>=8.3.2
requests>=2.32.3
180 changes: 85 additions & 95 deletions src/pilgrim_autosplitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,108 +26,98 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Initialize and run Pilgrim Autosplitter.
"""
"""Initialize and run Pilgrim Autosplitter."""

import os
import platform
import sys

def main():
"""Initialize PilgrimAutosplitter (with logging if not on Windows).
This module is in an unusual format, with all the import statements behind
main() and the class declaration inside of main(), to accomodate the time
required to boot up the executable if this program is run after being built
with PyInstaller. To reassure the user, I've placed print messages before
and after the import statements, which take a very long time (sometimes as
long as 30 seconds).

class PilgrimAutosplitter:
"""Initialize and run Pilgrim Autosplitter.
Import statements are in __init__ because I only expect this class to be
instantiated once per session and I want the print statements in main to
appear before the import statements are run (they can take a long time to
complete, especially using PyInstaller).
Attributes:
pilgrim_autosplitter (QApplication): The application container that
allows QObjects, including the UI, to be initialized.
splitter (Splitter): Backend for capturing and comparing images to
video.
ui_controller (UIController): Backend for updating the UI and handling
user input.
"""

# Initial setup
import os
def __init__(self) -> None:
"""Initialize splitter and controller to run Pilgrim Autosplitter."""
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QApplication

import settings
from splitter.splitter import Splitter
from ui.ui_controller import UIController

program_directory = os.path.dirname(os.path.abspath(__file__))

if platform.system() == "Windows":
# Force title bar to follow system theme
extra_args = ["-platform", "windows:darkmode=1"]
else:
extra_args = []
self.app = QApplication(sys.argv + extra_args)
self.app.setStyle("fusion")
self.app.setApplicationName("Pilgrim Autosplitter")

# Set taskbar icons. Doesn't seem to really do anything, but it's a
# work in progress so I'll leave it for now
if platform.system() == "Windows":
import ctypes

self.app.setWindowIcon(
QIcon(QPixmap(f"{program_directory}/../resources/icon-windows.png"))
)
# Tell Windows this app is its own process so icon shows up
app_id = "pilgrim_tabby.pilgrim_autosplitter.latest"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
# Without the absolute path, the icon only shows up when running
# the program from the same directory /resources is in. This makes
# it show up regardless (at least when ran from source, not build)
else:
self.app.setWindowIcon(
QIcon(QPixmap(f"{program_directory}/../resources/icon-macos.png"))
)

settings.set_program_vals()

self.splitter = Splitter()
if settings.get_bool("START_WITH_VIDEO"):
self.splitter.start()

self.ui_controller = UIController(self.app, self.splitter)

os.system("cls || clear") # This works cross-platform

def main():
"""Initialize PilgrimAutosplitter."""
os.system("cls || clear") # Cross-platform clear screen

print("Welcome to Pilgrim Autosplitter!")
print("You can minimize this window, but DO NOT close it.\n")

print("Importing third-party packages...")

from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QApplication

print("Importing built-in packages...")

import platform
import sys

print("Initializing Pilgrim Autosplitter...")

# Class definition
class PilgrimAutosplitter:
"""Initialize and run Pilgrim Autosplitter.
Attributes:
pilgrim_autosplitter (QApplication): The application container that
allows QObjects, including the UI, to be initialized.
splitter (Splitter): Backend for capturing and comparing images to
video.
ui_controller (UIController): Backend for updating the UI and handling
user input.
"""

def __init__(self) -> None:
"""Initialize splitter and controller to run Pilgrim Autosplitter."""
import settings
from splitter.splitter import Splitter
from ui.ui_controller import UIController

program_directory = os.path.dirname(os.path.abspath(__file__))

if platform.system() == "Windows":
# Force title bar to follow system theme
extra_args = ["-platform", "windows:darkmode=1"]
else:
extra_args = []
self.pilgrim_autosplitter = QApplication(sys.argv + extra_args)
self.pilgrim_autosplitter.setStyle("fusion")
self.pilgrim_autosplitter.setApplicationName("Pilgrim Autosplitter")

# Set taskbar icons. Doesn't seem to really do anything, but it's a
# work in progress so I'll leave it for now
if platform.system() == "Windows":
import ctypes

self.pilgrim_autosplitter.setWindowIcon(
QIcon(QPixmap(f"{program_directory}/../resources/icon-windows.png"))
)
# Tell Windows this app is its own process so icon shows up
app_id = "pilgrim_tabby.pilgrim_autosplitter.latest"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
# Without the absolute path, the icon only shows up when running
# the program from the same directory /resources is in. This makes
# it show up regardless (at least when ran from source, not build)
else:
self.pilgrim_autosplitter.setWindowIcon(
QIcon(QPixmap(f"{program_directory}/../resources/icon-macos.png"))
)

settings.set_defaults()

self.splitter = Splitter()
if settings.get_bool("START_WITH_VIDEO"):
self.splitter.start()

self.ui_controller = UIController(self.pilgrim_autosplitter, self.splitter)

self.pilgrim_autosplitter.exec()

# Prevent segmentation fault or other clumsy errors on exit
# The threads won't persist since they're daemons, but this helps
# make sure they stop BEFORE the main thread ends
self.splitter.safe_exit_all_threads()

# Open application
print("Starting Pilgrim Autosplitter...\n")
PilgrimAutosplitter()
print("You may minimize this window, but DO NOT close it.\n")
print("Loading Pilgrim Autosplitter (this may take a few minutes)...")

pilgrim_autosplitter = PilgrimAutosplitter()

# Close threads safely (these sometimes cause segfaults otherwise), even
# though they are daemons.
# Other app threads don't risk segfaults and are daemons, so leave them
# alone.
pilgrim_autosplitter.app.aboutToQuit.connect(
pilgrim_autosplitter.splitter.safe_exit_all_threads
)

print("Starting...")
pilgrim_autosplitter.app.exec()


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit 5ad3088

Please sign in to comment.