From 7fa360992a862fbe4daacf11a847199715c8be85 Mon Sep 17 00:00:00 2001 From: Samuel T Date: Tue, 9 Nov 2021 17:47:07 -0800 Subject: [PATCH] Livesplit integration (#60) * Added LiveSplit Integration Single Source of truth for version number * Fixed freeze on close by using killable QThreads and an os.kill signal Also added the icon to the build command Co-authored-by: KaDiWa4 --- requirements.txt | 2 +- res/about.ui | 2 +- src/AutoSplit.py | 257 ++++++++++++++++++++++++++++--------------- src/about.py | 3 +- src/menu_bar.py | 11 +- src/settings_file.py | 7 +- 6 files changed, 183 insertions(+), 99 deletions(-) diff --git a/requirements.txt b/requirements.txt index b9c87383..249bcf65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # # Usage: pip install -r requirements.txt # -# Creating AutoSplit.exe with PyInstaller: pyinstaller -w -F src\AutoSplit.py +# Creating AutoSplit.exe with PyInstaller: pyinstaller -w -F --icon=src\icon.ico src\AutoSplit.py # # You can find other wheels here: https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyqt4 # If you use 32-bit installation of Python, use the the 2nd URL instead. diff --git a/res/about.ui b/res/about.ui index d247f2c3..f6fe56a6 100644 --- a/res/about.ui +++ b/res/about.ui @@ -65,7 +65,7 @@ - Version: 1.2.0 + Version: diff --git a/src/AutoSplit.py b/src/AutoSplit.py index daed4516..16a10401 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -1,5 +1,7 @@ from PyQt4 import QtGui, QtCore, QtTest +from menu_bar import about, VERSION, viewHelp import sys +import signal import os import win32gui import cv2 @@ -7,16 +9,17 @@ import ctypes.wintypes import ctypes import keyboard -import glob +import threading import numpy as np import design -import about import compare import capture_windows import split_parser + class AutoSplit(QtGui.QMainWindow, design.Ui_MainWindow): + # Importing the functions inside of the class will make them methods of the class from hotkeys import beforeSettingHotkey, afterSettingHotkey, setSplitHotkey, setResetHotkey, setSkipSplitHotkey, setUndoSplitHotkey, setPauseHotkey from error_messages import (splitImageDirectoryError, splitImageDirectoryNotFoundError, imageTypeError, regionError, regionSizeError, splitHotkeyError, customThresholdError, customPauseError, alphaChannelError, alignRegionImageTypeError, alignmentNotMatchedError, @@ -24,7 +27,6 @@ class AutoSplit(QtGui.QMainWindow, design.Ui_MainWindow): invalidSettingsError, oldVersionSettingsFileError, noSettingsFileOnOpenError, tooManySettingsFilesOnOpenError) from settings_file import saveSettings, saveSettingsAs, loadSettings, haveSettingsChanged, getSaveSettingsValues from screen_region import selectRegion, selectWindow, alignRegion - from menu_bar import about, viewHelp myappid = u'mycompany.myproduct.subproduct.version' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) @@ -42,9 +44,12 @@ def __init__(self, parent=None): super(AutoSplit, self).__init__(parent) self.setupUi(self) + # Parse command line args + self.is_auto_controlled = ('--auto-controlled' in sys.argv) + # close all processes when closing window - self.actionView_Help.triggered.connect(self.viewHelp) - self.actionAbout.triggered.connect(self.about) + self.actionView_Help.triggered.connect(viewHelp) + self.actionAbout.triggered.connect(lambda: about(self)) self.actionSave_Settings.triggered.connect(self.saveSettings) self.actionSave_Settings_As.triggered.connect(self.saveSettingsAs) self.actionLoad_Settings.triggered.connect(self.loadSettings) @@ -54,6 +59,57 @@ def __init__(self, parent=None): self.skipsplitButton.setEnabled(False) self.resetButton.setEnabled(False) + if self.is_auto_controlled: + self.setsplithotkeyButton.setEnabled(False) + self.setresethotkeyButton.setEnabled(False) + self.setskipsplithotkeyButton.setEnabled(False) + self.setundosplithotkeyButton.setEnabled(False) + self.setpausehotkeyButton.setEnabled(False) + self.startautosplitterButton.setEnabled(False) + self.splitLineEdit.setEnabled(False) + self.resetLineEdit.setEnabled(False) + self.skipsplitLineEdit.setEnabled(False) + self.undosplitLineEdit.setEnabled(False) + self.pausehotkeyLineEdit.setEnabled(False) + + # Send version and process ID to stdout + print(f"{VERSION}\n{os.getpid()}", flush=True) + + class Worker(QtCore.QObject): + autosplit = None + + def __init__(self, autosplit): + self.autosplit = autosplit + super().__init__() + + def run(self): + while True: + line = input() + # TODO: "AutoSplit Integration" needs to call this and wait instead of outright killing the app. + # TODO: See if we can also get LiveSplit to wait on Exit in "AutoSplit Integration" + # For now this can only used in a Development environment + if line == 'kill': + self.autosplit.closeEvent() + break + elif line == 'start': + self.autosplit.startAutoSplitter() + elif line == 'split' or line == 'skip': + self.autosplit.startSkipSplit() + elif line == 'undo': + self.autosplit.startUndoSplit() + elif line == 'reset': + self.autosplit.startReset() + # TODO: Not yet implemented in AutoSplit Integration + # elif line == 'pause': + # self.startPause() + + # Use and Start the thread that checks for updates from LiveSplit + self.update_auto_control = QtCore.QThread() + worker = Worker(self) + worker.moveToThread(self.update_auto_control) + self.update_auto_control.started.connect(worker.run) + self.update_auto_control.start() + # resize to these width and height so that FPS performance increases self.RESIZE_WIDTH = 320 self.RESIZE_HEIGHT = 240 @@ -118,6 +174,7 @@ def __init__(self, parent=None): self.live_image_function_on_open = True # FUNCTIONS + #TODO add checkbox for going back to image 1 when resetting. def browse(self): # User selects the file with the split images in it. @@ -283,7 +340,7 @@ def checkFPS(self): # undo split button and hotkey connect to here def undoSplit(self): - if self.undosplitButton.isEnabled() == False: + if self.undosplitButton.isEnabled() == False and self.is_auto_controlled == False: return if self.loop_number != 1 and self.groupDummySplitsCheckBox.isChecked() == False: @@ -306,7 +363,7 @@ def undoSplit(self): # skip split button and hotkey connect to here def skipSplit(self): - if self.skipsplitButton.isEnabled() == False: + if self.skipsplitButton.isEnabled() == False and self.is_auto_controlled == False: return if self.loop_number < self.split_image_loop_amount[self.split_image_number] and self.groupDummySplitsCheckBox.isChecked() == False: @@ -335,10 +392,10 @@ def reset(self): # functions for the hotkeys to return to the main thread from signals and start their corresponding functions def startAutoSplitter(self): # if the auto splitter is already running or the button is disabled, don't emit the signal to start it. - if self.startautosplitterButton.text() == 'Running..' or self.startautosplitterButton.isEnabled() == False: + if self.startautosplitterButton.text() == 'Running..' or \ + (self.startautosplitterButton.isEnabled() == False and self.is_auto_controlled == False): return - else: - self.startAutoSplitterSignal.emit() + self.startAutoSplitterSignal.emit() def startReset(self): self.resetSignal.emit() @@ -402,7 +459,7 @@ def autoSplitter(self): return #error out if there is a {p} flag but no pause hotkey set. - if self.pausehotkeyLineEdit.text() == '' and split_parser.flags_from_filename(image) & 0x08 == 0x08: + if self.pausehotkeyLineEdit.text() == '' and split_parser.flags_from_filename(image) & 0x08 == 0x08 and self.is_auto_controlled == False: self.guiChangesOnReset() self.pauseHotkeyError() return @@ -421,7 +478,7 @@ def autoSplitter(self): self.customThresholdError(image) return - if self.splitLineEdit.text() == '': + if self.splitLineEdit.text() == '' and self.is_auto_controlled == False: self.guiChangesOnReset() self.splitHotkeyError() return @@ -444,7 +501,7 @@ def autoSplitter(self): return # If there is no reset hotkey set but a reset image is present, throw an error. - if self.resetLineEdit.text() == '' and self.reset_image is not None: + if self.resetLineEdit.text() == '' and self.reset_image is not None and self.is_auto_controlled == False: self.guiChangesOnReset() self.resetHotkeyError() return @@ -530,7 +587,10 @@ def autoSplitter(self): reset_similarity = self.compareImage(self.reset_image, self.reset_mask, capture) if reset_similarity >= self.reset_image_threshold: - keyboard.send(str(self.resetLineEdit.text())) + if self.is_auto_controlled: + print("reset", flush = True) + else: + keyboard.send(str(self.resetLineEdit.text())) self.reset() # loop goes into here if start auto splitter text is "Start Auto Splitter" @@ -566,17 +626,18 @@ def autoSplitter(self): else: self.highestsimilarityLabel.setText(' ') - # if its the last split image and last loop number, disable the skip split button - if (self.split_image_number == self.number_of_split_images - 1 and self.loop_number == self.split_image_loop_amount[self.split_image_number]) or (self.groupDummySplitsCheckBox.isChecked() == True and self.dummy_splits_array[self.split_image_number:].count(False) <= 1): - self.skipsplitButton.setEnabled(False) - else: - self.skipsplitButton.setEnabled(True) + if self.is_auto_controlled == False: + # if its the last split image and last loop number, disable the skip split button + if (self.split_image_number == self.number_of_split_images - 1 and self.loop_number == self.split_image_loop_amount[self.split_image_number]) or (self.groupDummySplitsCheckBox.isChecked() == True and self.dummy_splits_array[self.split_image_number:].count(False) <= 1): + self.skipsplitButton.setEnabled(False) + else: + self.skipsplitButton.setEnabled(True) - # if its the first split image and first loop, disable the undo split button - if self.split_image_number == 0 and self.loop_number == 1: - self.undosplitButton.setEnabled(False) - else: - self.undosplitButton.setEnabled(True) + # if its the first split image and first loop, disable the undo split button + if self.split_image_number == 0 and self.loop_number == 1: + self.undosplitButton.setEnabled(False) + else: + self.undosplitButton.setEnabled(True) # if the b flag is set, let similarity go above threshold first, then split on similarity below threshold. # if no b flag, just split when similarity goes above threshold. @@ -637,7 +698,10 @@ def autoSplitter(self): reset_similarity = self.compareImage(self.reset_image, self.reset_mask, capture) if reset_similarity >= self.reset_image_threshold: - keyboard.send(str(self.resetLineEdit.text())) + if self.is_auto_controlled: + print("reset", flush = True) + else: + keyboard.send(str(self.resetLineEdit.text())) self.reset() continue @@ -647,9 +711,15 @@ def autoSplitter(self): # if {p} flag hit pause key, otherwise hit split hotkey if (self.flags & 0x08 == 0x08): - keyboard.send(str(self.pausehotkeyLineEdit.text())) + if self.is_auto_controlled: + print("pause", flush = True) + else: + keyboard.send(str(self.pausehotkeyLineEdit.text())) else: - keyboard.send(str(self.splitLineEdit.text())) + if self.is_auto_controlled: + print("split", flush = True) + else: + keyboard.send(str(self.splitLineEdit.text())) # increase loop number if needed, set to 1 if it was the last loop. if self.loop_number < self.split_image_loop_amount[self.split_image_number]: @@ -678,17 +748,18 @@ def autoSplitter(self): self.currentSplitImage.setAlignment(QtCore.Qt.AlignCenter) self.imageloopLabel.setText('Image Loop #: -') - # if its the last split image and last loop number, disable the skip split button - if (self.split_image_number == self.number_of_split_images - 1 and self.loop_number == self.split_image_loop_amount[self.split_image_number]) or (self.groupDummySplitsCheckBox.isChecked() == True and self.dummy_splits_array[self.split_image_number:].count(False) <= 1): - self.skipsplitButton.setEnabled(False) - else: - self.skipsplitButton.setEnabled(True) + if self.is_auto_controlled == False: + # if its the last split image and last loop number, disable the skip split button + if (self.split_image_number == self.number_of_split_images - 1 and self.loop_number == self.split_image_loop_amount[self.split_image_number]) or (self.groupDummySplitsCheckBox.isChecked() == True and self.dummy_splits_array[self.split_image_number:].count(False) <= 1): + self.skipsplitButton.setEnabled(False) + else: + self.skipsplitButton.setEnabled(True) - # if its the first split image and first loop, disable the undo split button - if self.split_image_number == 0 and self.loop_number == 1: - self.undosplitButton.setEnabled(False) - else: - self.undosplitButton.setEnabled(True) + # if its the first split image and first loop, disable the undo split button + if self.split_image_number == 0 and self.loop_number == 1: + self.undosplitButton.setEnabled(False) + else: + self.undosplitButton.setEnabled(True) QtGui.QApplication.processEvents() @@ -733,18 +804,21 @@ def autoSplitter(self): def guiChangesOnStart(self): self.startautosplitterButton.setText('Running..') self.browseButton.setEnabled(False) - self.startautosplitterButton.setEnabled(False) - self.resetButton.setEnabled(True) - self.undosplitButton.setEnabled(True) - self.skipsplitButton.setEnabled(True) - self.setsplithotkeyButton.setEnabled(False) - self.setresethotkeyButton.setEnabled(False) - self.setskipsplithotkeyButton.setEnabled(False) - self.setundosplithotkeyButton.setEnabled(False) - self.setpausehotkeyButton.setEnabled(False) self.custompausetimesCheckBox.setEnabled(False) self.customthresholdsCheckBox.setEnabled(False) self.groupDummySplitsCheckBox.setEnabled(False) + + if self.is_auto_controlled == False: + self.startautosplitterButton.setEnabled(False) + self.resetButton.setEnabled(True) + self.undosplitButton.setEnabled(True) + self.skipsplitButton.setEnabled(True) + self.setsplithotkeyButton.setEnabled(False) + self.setresethotkeyButton.setEnabled(False) + self.setskipsplithotkeyButton.setEnabled(False) + self.setundosplithotkeyButton.setEnabled(False) + self.setpausehotkeyButton.setEnabled(False) + QtGui.QApplication.processEvents() def guiChangesOnReset(self): @@ -755,18 +829,21 @@ def guiChangesOnReset(self): self.livesimilarityLabel.setText(' ') self.highestsimilarityLabel.setText(' ') self.browseButton.setEnabled(True) - self.startautosplitterButton.setEnabled(True) - self.resetButton.setEnabled(False) - self.undosplitButton.setEnabled(False) - self.skipsplitButton.setEnabled(False) - self.setsplithotkeyButton.setEnabled(True) - self.setresethotkeyButton.setEnabled(True) - self.setskipsplithotkeyButton.setEnabled(True) - self.setundosplithotkeyButton.setEnabled(True) - self.setpausehotkeyButton.setEnabled(True) self.custompausetimesCheckBox.setEnabled(True) self.customthresholdsCheckBox.setEnabled(True) self.groupDummySplitsCheckBox.setEnabled(True) + + if self.is_auto_controlled == False: + self.startautosplitterButton.setEnabled(True) + self.resetButton.setEnabled(False) + self.undosplitButton.setEnabled(False) + self.skipsplitButton.setEnabled(False) + self.setsplithotkeyButton.setEnabled(True) + self.setresethotkeyButton.setEnabled(True) + self.setskipsplithotkeyButton.setEnabled(True) + self.setundosplithotkeyButton.setEnabled(True) + self.setpausehotkeyButton.setEnabled(True) + QtGui.QApplication.processEvents() def compareImage(self, image, mask, capture): @@ -920,41 +997,43 @@ def updateSplitImage(self): self.highest_similarity = 0.001 # exit safely when closing the window - def closeEvent(self, event): - if self.haveSettingsChanged(): - #give a different warning if there was never a settings file that was loaded successfully, and save as instead of save. - if self.last_successfully_loaded_settings_file_path == None: - msgBox = QtGui.QMessageBox - warning = msgBox.warning(self, "AutoSplit","Do you want to save changes made to settings file Untitled?", msgBox.Yes | msgBox.No | msgBox.Cancel) - if warning == msgBox.Yes: - self.saveSettingsAs() - sys.exit() - event.accept() - if warning == msgBox.No: - event.accept() - sys.exit() - pass - if warning == msgBox.Cancel: - event.ignore() - return - else: - msgBox = QtGui.QMessageBox - warning = msgBox.warning(self, "AutoSplit", "Do you want to save the changes made to the settings file " + os.path.basename(self.last_successfully_loaded_settings_file_path) + " ?", msgBox.Yes | msgBox.No | msgBox.Cancel) - if warning == msgBox.Yes: - self.saveSettings() - sys.exit() - event.accept() - if warning == msgBox.No: - event.accept() - sys.exit() - pass - if warning == msgBox.Cancel: - event.ignore() - return - else: - event.accept() + def closeEvent(self, event=None): + def exit(): + if event is not None: + event.accept() + if self.is_auto_controlled: + self.update_auto_control.terminate() + # stop main thread (which is probably blocked reading input) via an interrupt signal + # only available for windows in version 3.2 or higher + os.kill(os.getpid(), signal.SIGINT) sys.exit() + # Simulates LiveSplit quitting without asking. See "TODO" at update_auto_control Worker + # This also more gracefully exits LiveSplit + # Users can still manually save their settings + if event is None: + exit() + + if self.haveSettingsChanged(): + # give a different warning if there was never a settings file that was loaded successfully, and save as instead of save. + msgBox = QtGui.QMessageBox + settings_file_name = "Untitled" \ + if self.last_successfully_loaded_settings_file_path is None \ + else os.path.basename(self.last_successfully_loaded_settings_file_path) + warning_message = f"Do you want to save changes made to settings file {settings_file_name}?" + + warning = msgBox.warning(self, "AutoSplit", warning_message, msgBox.Yes | msgBox.No | msgBox.Cancel) + + if warning == msgBox.Yes: + # TODO: Don't close if user cancelled the save + self.saveSettingsAs() + exit() + if warning == msgBox.No: + exit() + if warning == msgBox.Cancel: + event.ignore() + else: + exit() def main(): @@ -967,4 +1046,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/src/about.py b/src/about.py index 93f90bfe..4dd6b430 100644 --- a/src/about.py +++ b/src/about.py @@ -59,7 +59,7 @@ def retranslateUi(self, aboutAutoSplitWidget): aboutAutoSplitWidget.setWindowTitle(_translate("aboutAutoSplitWidget", "About AutoSplit", None)) self.okButton.setText(_translate("aboutAutoSplitWidget", "OK", None)) self.createdbyLabel.setText(_translate("aboutAutoSplitWidget", "

Created by Toufool and Faschz

", None)) - self.versionLabel.setText(_translate("aboutAutoSplitWidget", "Version: 1.5.1", None)) + self.versionLabel.setText(_translate("aboutAutoSplitWidget", "Version: ", None)) self.donatetextLabel.setText(_translate("aboutAutoSplitWidget", "If you enjoy using this program, please\n" " consider donating. Thank you!", None)) self.donatebuttonLabel.setText(_translate("aboutAutoSplitWidget", "

", None)) @@ -75,4 +75,3 @@ def retranslateUi(self, aboutAutoSplitWidget): ui.setupUi(aboutAutoSplitWidget) aboutAutoSplitWidget.show() sys.exit(app.exec_()) - diff --git a/src/menu_bar.py b/src/menu_bar.py index 598f8435..9cd18e5b 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -2,6 +2,10 @@ from PyQt4 import QtGui import about +# AutoSplit Version number +VERSION = "1.5.0" + + # About Window class AboutWidget(QtGui.QWidget, about.Ui_aboutAutoSplitWidget): def __init__(self): @@ -9,11 +13,12 @@ def __init__(self): self.setupUi(self) self.createdbyLabel.setOpenExternalLinks(True) self.donatebuttonLabel.setOpenExternalLinks(True) + self.versionLabel.setText(f"Version: {VERSION}") self.show() -def viewHelp(self): - os.system("start \"\" https://github.com/Toufool/Auto-Split#tutorial") - return + +def viewHelp(): + os.system("start \"\" https://github.com/Toufool/Auto-Split/blob/master/README.md#tutorial") def about(self): diff --git a/src/settings_file.py b/src/settings_file.py index 6e7e270d..3304858c 100644 --- a/src/settings_file.py +++ b/src/settings_file.py @@ -197,9 +197,10 @@ def loadSettings(self): keyboard.remove_hotkey(self.split_hotkey) except AttributeError: pass - self.splitLineEdit.setText(str(self.split_key)) - self.split_hotkey = keyboard.add_hotkey(str(self.split_key), self.startAutoSplitter) - self.old_split_key = self.split_key + if self.is_auto_controlled == False: + self.splitLineEdit.setText(str(self.split_key)) + self.split_hotkey = keyboard.add_hotkey(str(self.split_key), self.startAutoSplitter) + self.old_split_key = self.split_key # pass if the key is an empty string (hotkey was never set) except ValueError: pass