inital commit
This commit is contained in:
commit
c51c099c75
|
@ -0,0 +1,242 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python,autohotkey
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python,autohotkey
|
||||||
|
|
||||||
|
### Autohotkey ###
|
||||||
|
# Windows image file caches
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Operating System Files
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,autohotkey
|
||||||
|
/bin/*.ini
|
||||||
|
/config.ini
|
||||||
|
*.exe
|
||||||
|
/.vs/VSWorkspaceState.json
|
||||||
|
/.vs/keypress/FileContentIndex/52395d4e-3c90-4b4f-aa08-3c78b884c891.vsidx
|
||||||
|
.wsuo
|
||||||
|
/.vs/keypress/v17/DocumentLayout.json
|
||||||
|
/.vs/slnx.sqlite
|
||||||
|
Analysis-00.toc
|
||||||
|
EXE-00.toc
|
||||||
|
JarvisKeyPressUtility.pkg
|
||||||
|
PKG-00.toc
|
||||||
|
PYZ-00.pyz
|
||||||
|
PYZ-00.toc
|
||||||
|
base_library.zip
|
||||||
|
/bin/JarvisKeyPressUtility/warn-JarvisKeyPressUtility.txt
|
||||||
|
/bin/JarvisKeyPressUtility/xref-JarvisKeyPressUtility.html
|
|
@ -0,0 +1,136 @@
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from model.model import KeyPressModel
|
||||||
|
from view.view import KeyPressView
|
||||||
|
# NEW: Import UIController
|
||||||
|
from controller.ui_controller import UIController
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class KeyPressController:
|
||||||
|
def __init__(self, model, view):
|
||||||
|
self.model = model
|
||||||
|
self.view = view # This will be set by main.py after View is instantiated.
|
||||||
|
self.ui_controller = None # Will be instantiated and linked after view is set.
|
||||||
|
logger.info("Controller: Initialized (view and UIController might be None for now).")
|
||||||
|
|
||||||
|
# Register this controller's callback with the Model for AHK stopping events
|
||||||
|
self.model.register_ahk_stopped_callback(self._handle_ahk_external_stop)
|
||||||
|
|
||||||
|
def initial_setup_after_view_is_ready(self):
|
||||||
|
"""
|
||||||
|
Performs controller setup that relies on the view being fully initialized.
|
||||||
|
Called from main.py after controller.view is set.
|
||||||
|
"""
|
||||||
|
if self.view is None:
|
||||||
|
logger.error("Controller: initial_setup_after_view_is_ready called but view is None!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Instantiate UIController and pass itself (main controller) and the view
|
||||||
|
self.ui_controller = UIController(self, self.view)
|
||||||
|
self.ui_controller.initial_ui_setup_after_view_is_ready() # Trigger UIController's setup
|
||||||
|
|
||||||
|
logger.info("Controller: Performing post-view-initialization setup.")
|
||||||
|
# Initial set for key based on UI default
|
||||||
|
self.model.set_key_to_press(self.view.get_key_input())
|
||||||
|
# Initial window refresh and selection handled by ui_controller.refresh_windows_ui()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Methods called by UIController to update Model/Controller state ---
|
||||||
|
def set_key_input_from_ui(self, key_str):
|
||||||
|
"""Receives key input from UI, updates model."""
|
||||||
|
self.model.set_key_to_press(key_str)
|
||||||
|
|
||||||
|
def set_window_selection_from_ui(self, selected_title):
|
||||||
|
"""Receives window selection from UI, updates model based on AHK logic."""
|
||||||
|
if selected_title == "--- Global Input (No Specific Window) ---":
|
||||||
|
# As per user's explicit request: "it is always a specific window", no "GLOBAL" AHK branch.
|
||||||
|
# This selection means AHK script will likely not find a window, thus releasing the key.
|
||||||
|
self.model.set_target_window("INVALID_GLOBAL_TARGET_FOR_AHK") # Use a distinct string for internal model logic
|
||||||
|
self.ui_controller.show_ui_error_message("Warning: 'Global Input' selected. The AutoHotkey script is designed for specific windows and will likely NOT press the key automatically with this selection.")
|
||||||
|
logger.warning("Controller: 'Global Input' selected. AHK logic is non-global; key will not be pressed.")
|
||||||
|
elif selected_title == "--- Select a Window ---":
|
||||||
|
self.model.set_target_window("")
|
||||||
|
logger.info("Controller: No specific window selected. Target is empty.")
|
||||||
|
else:
|
||||||
|
self.model.set_target_window(selected_title)
|
||||||
|
logger.info(f"Controller: Target window set to: '{selected_title}'")
|
||||||
|
|
||||||
|
# --- Methods for UIController to query Model ---
|
||||||
|
def get_available_window_titles(self):
|
||||||
|
"""Provides available window titles (from model) to UIController."""
|
||||||
|
return self.model.get_window_titles()
|
||||||
|
|
||||||
|
# --- Core Application Logic / Event Handlers (Called by UIController/View directly, or internally) ---
|
||||||
|
def _check_start_button_state(self):
|
||||||
|
"""
|
||||||
|
Internal method to check application state and update UI button state.
|
||||||
|
This is called by UIController after relevant inputs change.
|
||||||
|
"""
|
||||||
|
if self.ui_controller: # Ensure UIController is instantiated before calling its methods
|
||||||
|
self.ui_controller._check_start_button_state_ui() # Delegate to UIController for UI update
|
||||||
|
|
||||||
|
def start_key_press(self):
|
||||||
|
"""Handles the 'Start Holding Key' button click event (from View)."""
|
||||||
|
logger.info("Controller: Start key press requested.")
|
||||||
|
|
||||||
|
# Perform validation using UIController's methods to get current UI state
|
||||||
|
key_value = self.view.get_key_input() # Get current key from view
|
||||||
|
selected_window = self.view.get_selected_window_title() # Get current window from view
|
||||||
|
|
||||||
|
if not key_value:
|
||||||
|
self.ui_controller.show_ui_error_message("The 'Key to Press' field cannot be empty. What exactly do you expect me to press?")
|
||||||
|
self.view.set_key_validation_visibility(True)
|
||||||
|
logger.error("Controller: Aborting: Key to press field is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if selected_window == "--- Select a Window ---":
|
||||||
|
self.ui_controller.show_ui_error_message("Please select a specific target window or 'Global Input'.")
|
||||||
|
self.view.set_window_validation_visibility(True)
|
||||||
|
logger.error("Controller: Aborting: No target window selected.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if selected_window == "--- Global Input (No Specific Window) ---":
|
||||||
|
# Show the warning again if the user is attempting to start with 'Global Input'
|
||||||
|
self.ui_controller.show_ui_error_message("Warning: 'Global Input' selected. The current AutoHotkey script is designed to activate a specific window, not provide global key presses. This option will prevent the key from being pressed automatically.")
|
||||||
|
logger.warning("Controller: Attempting to start with 'Global Input'. AHK logic is non-global. Key will not be pressed.")
|
||||||
|
|
||||||
|
# Tell Model to start AHK script. Model handles pynput listener internally.
|
||||||
|
if self.model.start_autohotkey_script():
|
||||||
|
self.ui_controller.set_ui_running_state() # Update UI via UIController
|
||||||
|
logger.info("Controller: Key press operation successfully initiated.")
|
||||||
|
else:
|
||||||
|
self.ui_controller.show_ui_error_message("Failed to start AutoHotkey script. Check application logs for details (e.g., AutoHotkey.exe not found).")
|
||||||
|
self.ui_controller.set_ui_idle_state() # Reset UI via UIController
|
||||||
|
logger.error("Controller: Failed to start AHK script. Aborting.")
|
||||||
|
|
||||||
|
def stop_key_press(self):
|
||||||
|
"""Handles the 'Stop Key Press' button click event (from View)."""
|
||||||
|
logger.info("Controller: Stop key press requested.")
|
||||||
|
self.model.stop_autohotkey_script() # Tell Model to stop AHK
|
||||||
|
self.ui_controller.set_ui_idle_state() # Set UI to idle state via UIController
|
||||||
|
logger.info("Controller: Key press operation stopped.")
|
||||||
|
|
||||||
|
def _handle_ahk_external_stop(self):
|
||||||
|
"""
|
||||||
|
Callback method invoked by the Model when the AHK script stops due to
|
||||||
|
an external event (e.g., AHK error, F6 press, window not found).
|
||||||
|
Resets the UI state via UIController.
|
||||||
|
"""
|
||||||
|
logger.info("Controller: AHK script stopped externally. Requesting UI reset.")
|
||||||
|
# Ensure this is scheduled on the main Tkinter thread if called from a background thread
|
||||||
|
if self.view and self.view.master:
|
||||||
|
self.view.master.after_idle(self.ui_controller.set_ui_idle_state)
|
||||||
|
else:
|
||||||
|
logger.warning("Controller: Cannot schedule UI reset, view or master is unavailable.")
|
||||||
|
|
||||||
|
def on_app_close(self):
|
||||||
|
"""Handles application window close event, ensures AHK cleanup."""
|
||||||
|
logger.info("Controller: Application close requested. Initiating cleanup.")
|
||||||
|
self.model.stop_autohotkey_script()
|
||||||
|
if self.view and self.view.master: # Check if master still exists before destroying
|
||||||
|
self.view.master.destroy()
|
||||||
|
logger.info("Controller: Application closed.")
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class UIController:
|
||||||
|
def __init__(self, controller_main, view):
|
||||||
|
# controller_main is a reference to the main KeyPressController instance
|
||||||
|
self.controller_main = controller_main
|
||||||
|
self.view = view
|
||||||
|
logger.info("UIController: Initialized.")
|
||||||
|
|
||||||
|
def initial_ui_setup_after_view_is_ready(self):
|
||||||
|
"""
|
||||||
|
Performs UI-specific setup that relies on the view being fully initialized.
|
||||||
|
Called from controller.py after view reference is available.
|
||||||
|
"""
|
||||||
|
if self.view is None:
|
||||||
|
logger.error("UIController: initial_ui_setup_after_view_is_ready called but view is None!")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("UIController: Performing post-view-initialization UI setup.")
|
||||||
|
# Any view-dependent setup that was previously in KeyPressController's init_setup
|
||||||
|
# that directly interacts with self.view now lives here.
|
||||||
|
|
||||||
|
# Link View's StringVar traces for key input change
|
||||||
|
self.view.key_to_press_var.trace_add("write", lambda name, index, mode: self._on_key_input_change())
|
||||||
|
|
||||||
|
# Initial refresh of windows list for the UI
|
||||||
|
self.refresh_windows_ui()
|
||||||
|
# Initial check of start button state based on UI inputs
|
||||||
|
self._check_start_button_state_ui()
|
||||||
|
|
||||||
|
|
||||||
|
def _on_key_input_change(self):
|
||||||
|
"""Handles changes in the key input field, informs main controller."""
|
||||||
|
self.controller_main.set_key_input_from_ui(self.view.get_key_input())
|
||||||
|
self._check_start_button_state_ui()
|
||||||
|
|
||||||
|
def refresh_windows_ui(self):
|
||||||
|
"""
|
||||||
|
Refreshes the list of available windows in the View and updates Controller.
|
||||||
|
Calls model via controller_main to get window titles.
|
||||||
|
"""
|
||||||
|
current_selection = self.view.get_selected_window_title()
|
||||||
|
window_titles = self.controller_main.get_available_window_titles() # Get from model via main controller
|
||||||
|
|
||||||
|
display_window_titles = ["--- Select a Window ---"] + window_titles
|
||||||
|
if "--- Global Input (No Specific Window) ---" not in display_window_titles:
|
||||||
|
display_window_titles.insert(1, "--- Global Input (No Specific Window) ---")
|
||||||
|
|
||||||
|
new_selection = current_selection
|
||||||
|
if new_selection not in display_window_titles:
|
||||||
|
new_selection = "--- Select a Window ---"
|
||||||
|
elif current_selection == "--- Global Input (No Specific Window) ---":
|
||||||
|
new_selection = "--- Global Input (No Specific Window) ---"
|
||||||
|
|
||||||
|
self.view.update_window_list(display_window_titles, new_selection)
|
||||||
|
|
||||||
|
# Inform the main controller about the updated window selection
|
||||||
|
self.controller_main.set_window_selection_from_ui(new_selection)
|
||||||
|
|
||||||
|
logger.info(f"UIController: Refreshed window list. Found {len(window_titles)} active windows.")
|
||||||
|
self._check_start_button_state_ui()
|
||||||
|
|
||||||
|
def on_window_selected_ui(self):
|
||||||
|
"""Handles combobox selection event from View, informs main controller."""
|
||||||
|
selected_title = self.view.get_selected_window_title()
|
||||||
|
self.controller_main.set_window_selection_from_ui(selected_title)
|
||||||
|
self._check_start_button_state_ui()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_start_button_state_ui(self):
|
||||||
|
"""
|
||||||
|
Determines if the start button should be enabled based on UI input validation.
|
||||||
|
Updates validation labels in the View directly.
|
||||||
|
"""
|
||||||
|
key_input_present = bool(self.view.get_key_input())
|
||||||
|
self.view.set_key_validation_visibility(not key_input_present)
|
||||||
|
|
||||||
|
selected_option = self.view.get_selected_window_title()
|
||||||
|
|
||||||
|
is_real_window_selected = (selected_option != "--- Select a Window ---" and selected_option in self.controller_main.get_available_window_titles())
|
||||||
|
is_global_input_selected = (selected_option == "--- Global Input (No Specific Window) ---")
|
||||||
|
|
||||||
|
valid_window_selected_for_start = is_real_window_selected or is_global_input_selected
|
||||||
|
|
||||||
|
self.view.set_window_validation_visibility(not valid_window_selected_for_start)
|
||||||
|
|
||||||
|
if key_input_present and valid_window_selected_for_start:
|
||||||
|
# Only enable the button if both are valid
|
||||||
|
self.view.start_button.config(state=tk.NORMAL)
|
||||||
|
logger.debug("UIController: Start button enabled.")
|
||||||
|
else:
|
||||||
|
self.view.start_button.config(state=tk.DISABLED)
|
||||||
|
logger.debug("UIController: Start button disabled (validation error).")
|
||||||
|
|
||||||
|
def show_ui_error_message(self, message):
|
||||||
|
"""Displays a messagebox error to the user via View."""
|
||||||
|
self.view.show_input_error(message)
|
||||||
|
|
||||||
|
def set_ui_running_state(self):
|
||||||
|
"""Updates UI elements to reflect running state."""
|
||||||
|
self.view.set_ui_state_running()
|
||||||
|
|
||||||
|
def set_ui_idle_state(self):
|
||||||
|
"""Updates UI elements to reflect idle state."""
|
||||||
|
self.view.set_ui_state_idle()
|
||||||
|
self._check_start_button_state_ui() # Re-check after returning to idle to ensure proper button state
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import tkinter as tk
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# --- Setup Python Path for MVC Imports ---
|
||||||
|
# Get the absolute path of the directory containing main.py
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# Add the current directory to sys.path to allow importing from model/, view/, controller/
|
||||||
|
sys.path.insert(0, current_dir)
|
||||||
|
|
||||||
|
# Import the main Model class and the message queue from app_logger
|
||||||
|
from model.key_press_model import KeyPressModel # Updated import path
|
||||||
|
from model.app_logger import message_queue, setup_logging_from_config # Updated import for logging setup
|
||||||
|
from view.view import KeyPressView
|
||||||
|
from controller.controller import KeyPressController
|
||||||
|
|
||||||
|
# --- Global Logging Setup will now be handled by setup_logging_from_config from model.app_logger ---
|
||||||
|
# No direct logging setup here, just calling the setup function.
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__) # Logger for the main.py module itself
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Set up logging early using the config file provided by app_logger
|
||||||
|
current_log_file_path = setup_logging_from_config()
|
||||||
|
|
||||||
|
# --- Python Environment Diagnostics (Included for completeness) ---
|
||||||
|
print(f"--- Python Environment Diagnostics ---")
|
||||||
|
print(f"Python Executable: {sys.executable}")
|
||||||
|
print(f"Python Version: {sys.version}")
|
||||||
|
print(f"Python Path (sys.path):")
|
||||||
|
for p in sys.path:
|
||||||
|
print(f" {p}")
|
||||||
|
print(f"--- End Diagnostics ---")
|
||||||
|
|
||||||
|
logger.info("Main: Application starting up.")
|
||||||
|
logger.info(f"Main: Logging all output to file: {current_log_file_path}")
|
||||||
|
|
||||||
|
main_error_occurred = False
|
||||||
|
try:
|
||||||
|
root = tk.Tk()
|
||||||
|
|
||||||
|
# Instantiate Model, View, and Controller
|
||||||
|
model = KeyPressModel()
|
||||||
|
controller = KeyPressController(model, None)
|
||||||
|
view = KeyPressView(root, controller)
|
||||||
|
controller.view = view
|
||||||
|
|
||||||
|
controller.initial_setup_after_view_is_ready()
|
||||||
|
|
||||||
|
root.protocol("WM_DELETE_WINDOW", controller.on_app_close)
|
||||||
|
|
||||||
|
logger.info("Main: GUI initialized. Starting main loop.")
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(f"Main: An unhandled exception occurred in the main application loop: {e}", exc_info=True)
|
||||||
|
main_error_occurred = True
|
||||||
|
finally:
|
||||||
|
logger.info("Main: Application loop ended. Ensuring all background processes are stopped.")
|
||||||
|
|
||||||
|
print("Application closed. Jarvis signing off.")
|
||||||
|
|
||||||
|
if main_error_occurred and not getattr(sys, 'frozen', False):
|
||||||
|
print("\nAn unexpected error occurred. Press Enter to exit the console...")
|
||||||
|
input()
|
|
@ -0,0 +1,305 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from pynput.keyboard import Key, Listener # Directly imported as it's used here
|
||||||
|
from model.constants import STOP_KEY, AHK_LOG_MONITOR_INTERVAL, AHK_TEMPLATE_REL_PATH_FROM_MODEL_DIR, AHK_EXECUTABLE_FALLBACK_PATHS, AUTOHOTKEY_KEY_MAP
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class AHKProcessManager:
|
||||||
|
"""
|
||||||
|
Manages the AutoHotkey script lifecycle: generation, launching,
|
||||||
|
monitoring, stopping, and cleanup of temporary files.
|
||||||
|
Also handles the pynput keyboard listener for global stop key detection.
|
||||||
|
"""
|
||||||
|
def __init__(self, ahk_template_rel_path, ahk_executable_fallback_paths, autohotkey_key_map, stop_key, message_queue):
|
||||||
|
self._ahk_template_rel_path = ahk_template_rel_path
|
||||||
|
self._ahk_executable_fallback_paths = ahk_executable_fallback_paths
|
||||||
|
self._autohotkey_key_map = autohotkey_key_map
|
||||||
|
self._stop_key = stop_key
|
||||||
|
self._message_queue = message_queue # Central message queue for GUI communication
|
||||||
|
|
||||||
|
self._ahk_process = None
|
||||||
|
self._ahk_script_temp_file = None
|
||||||
|
self._ahk_log_temp_file = None
|
||||||
|
self._ahk_log_file_last_read_pos = 0
|
||||||
|
self._keep_running = False # Flag to control monitoring and listener threads
|
||||||
|
|
||||||
|
self._current_target_window_title = ""
|
||||||
|
self._current_key_to_press_str = ''
|
||||||
|
self._ahk_stopped_callback = None # Callback to notify KeyPressModel (and then Controller)
|
||||||
|
|
||||||
|
logger.info("AHKProcessManager: Initialized.")
|
||||||
|
|
||||||
|
def register_ahk_stopped_callback(self, callback_func):
|
||||||
|
"""Registers a callback function to be called when the AHK script stops externally."""
|
||||||
|
self._ahk_stopped_callback = callback_func
|
||||||
|
logger.info("AHKProcessManager: AHK stopped callback registered.")
|
||||||
|
|
||||||
|
def _trigger_ahk_stopped_callback(self):
|
||||||
|
"""Internal method to trigger the registered callback, if any."""
|
||||||
|
if self._ahk_stopped_callback:
|
||||||
|
# The callback is expected to be a method of KeyPressModel,
|
||||||
|
# which then might schedule a call to the Controller on the main thread.
|
||||||
|
self._ahk_stopped_callback()
|
||||||
|
logger.info("AHKProcessManager: AHK stopped callback triggered.")
|
||||||
|
else:
|
||||||
|
logger.warning("AHKProcessManager: AHK stopped, but no callback was registered.")
|
||||||
|
|
||||||
|
def set_target_window(self, title):
|
||||||
|
"""Sets the target window title for AHK operations."""
|
||||||
|
self._current_target_window_title = title
|
||||||
|
|
||||||
|
def set_key_to_press(self, key_str):
|
||||||
|
"""Sets the key string to be pressed by AHK."""
|
||||||
|
self._current_key_to_press_str = key_str
|
||||||
|
|
||||||
|
def _get_autohotkey_key_name(self, user_input_key):
|
||||||
|
"""Converts a user-friendly key input to its AutoHotkey v2 equivalent."""
|
||||||
|
normalized_input = user_input_key.lower().strip()
|
||||||
|
mapped_key = self._autohotkey_key_map.get(normalized_input)
|
||||||
|
if mapped_key:
|
||||||
|
return mapped_key
|
||||||
|
if len(user_input_key) == 1 and user_input_key.isalnum():
|
||||||
|
return user_input_key
|
||||||
|
return user_input_key
|
||||||
|
|
||||||
|
def start_script(self):
|
||||||
|
"""
|
||||||
|
Generates and launches the AutoHotkey script as a subprocess.
|
||||||
|
Monitors its log file and starts pynput listener in separate threads.
|
||||||
|
"""
|
||||||
|
logger.info("AHKProcessManager: start_script: Function entry.")
|
||||||
|
|
||||||
|
if self._ahk_process and self._ahk_process.poll() is None:
|
||||||
|
logger.warning("AHKProcessManager: AutoHotkey script is already running. Exiting function.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
resolved_autohotkey_exe_path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Determine the correct path to template.ahk ---
|
||||||
|
if getattr(sys, 'frozen', False): # Running as a PyInstaller executable
|
||||||
|
template_path = os.path.join(sys._MEIPASS, "resources", "template.ahk")
|
||||||
|
logger.info(f"AHKProcessManager: Running as EXE. Template path: {template_path}")
|
||||||
|
else: # Running as a Python script (from project_root/model/)
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# Construct path relative to ahk_process_manager.py to reach project_root/resources/template.ahk
|
||||||
|
template_path = os.path.join(script_dir, self._ahk_template_rel_path)
|
||||||
|
logger.info(f"AHKProcessManager: Running as script. Template path: {template_path}")
|
||||||
|
|
||||||
|
logger.info(f"AHKProcessManager: Attempting to read AHK template from: {template_path}")
|
||||||
|
if not os.path.exists(template_path):
|
||||||
|
logger.error(f"AHKProcessManager: AHK template file not found at '{template_path}'.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(template_path, 'r') as f:
|
||||||
|
ahk_template_content = f.read()
|
||||||
|
logger.info(f"AHKProcessManager: AHK template loaded successfully from {template_path}.")
|
||||||
|
|
||||||
|
logger.debug("AHKProcessManager: Creating temporary files for AHK script and log.")
|
||||||
|
fd_script, self._ahk_script_temp_file = tempfile.mkstemp(suffix=".ahk", prefix="jarvis_ahk_")
|
||||||
|
fd_log, self._ahk_log_temp_file = tempfile.mkstemp(suffix=".log", prefix="jarvis_ahk_log_")
|
||||||
|
|
||||||
|
os.close(fd_script)
|
||||||
|
os.close(fd_log)
|
||||||
|
|
||||||
|
self._ahk_log_file_last_read_pos = 0
|
||||||
|
|
||||||
|
target_win_ahk = self._current_target_window_title
|
||||||
|
key_to_press_ahk = self._get_autohotkey_key_name(self._current_key_to_press_str)
|
||||||
|
stop_key_ahk_name = self._get_autohotkey_key_name(str(self._stop_key.name))
|
||||||
|
|
||||||
|
logger.info(f"AHKProcessManager: Target window for AHK: '{target_win_ahk}'")
|
||||||
|
logger.info(f"AHKProcessManager: Key to press for AHK: '{key_to_press_ahk}'")
|
||||||
|
logger.info(f"AHKProcessManager: Stop key for AHK: '{stop_key_ahk_name}'")
|
||||||
|
|
||||||
|
ahk_script_content = ahk_template_content.format(
|
||||||
|
TARGET_WINDOW_TITLE_PLACEHOLDER=target_win_ahk,
|
||||||
|
KEY_TO_PRESS_AHK_PLACEHOLDER=key_to_press_ahk,
|
||||||
|
STOP_KEY_AHK_PLACEHOLDER=stop_key_ahk_name,
|
||||||
|
AHK_LOG_FILE_PLACEHOLDER=self._ahk_log_temp_file.replace('\\', '/')
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"AHKProcessManager: Writing AHK script content to: {self._ahk_script_temp_file}")
|
||||||
|
with open(self._ahk_script_temp_file, 'w') as f:
|
||||||
|
f.write(ahk_script_content)
|
||||||
|
logger.debug(f"AHKProcessManager: AutoHotkey script successfully written.")
|
||||||
|
logger.debug(f"AHKProcessManager: AutoHotkey logging will occur in: {self._ahk_log_temp_file}")
|
||||||
|
|
||||||
|
# --- Determine the correct path to AutoHotkey.exe ---
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
bundle_dir = sys._MEIPASS
|
||||||
|
ahk_bundled_locations = [
|
||||||
|
os.path.join(bundle_dir, "AutoHotkey.exe"),
|
||||||
|
os.path.join(bundle_dir, "AutoHotkey", "AutoHotkey.exe"),
|
||||||
|
os.path.join(bundle_dir, "v2", "AutoHotkey.exe"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for loc in ahk_bundled_locations:
|
||||||
|
if os.path.exists(loc):
|
||||||
|
resolved_autohotkey_exe_path = loc
|
||||||
|
logger.debug(f"AHKProcessManager: Found bundled AutoHotkey.exe at: {resolved_autohotkey_exe_path}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if resolved_autohotkey_exe_path is None:
|
||||||
|
logger.warning("AHKProcessManager: Could not find AutoHotkey.exe within the PyInstaller bundle. Falling back to common install paths.")
|
||||||
|
|
||||||
|
if resolved_autohotkey_exe_path is None:
|
||||||
|
for path in self._ahk_executable_fallback_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
resolved_autohotkey_exe_path = path
|
||||||
|
logger.debug(f"AHKProcessManager: Found AutoHotkey.exe at configured path: {resolved_autohotkey_exe_path}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if resolved_autohotkey_exe_path is None:
|
||||||
|
logger.error("AHKProcessManager: AutoHotkey.exe not found in any known location or system PATH. Cannot launch AHK script.")
|
||||||
|
raise FileNotFoundError("AutoHotkey.exe not found. Please ensure it's installed or specify its path.")
|
||||||
|
|
||||||
|
command = [resolved_autohotkey_exe_path, self._ahk_script_temp_file]
|
||||||
|
logger.info(f"AHKProcessManager: Launching AutoHotkey command: {' '.join(command)}")
|
||||||
|
|
||||||
|
creationflags = subprocess.DETACHED_PROCESS if os.name == 'nt' else 0
|
||||||
|
|
||||||
|
self._ahk_process = subprocess.Popen(command, creationflags=creationflags)
|
||||||
|
logger.info(f"AHKProcessManager: AutoHotkey script launched successfully with PID: {self._ahk_process.pid}")
|
||||||
|
|
||||||
|
self._keep_running = True
|
||||||
|
logger.debug("AHKProcessManager: Waiting 0.5 seconds for AHK to initialize.")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
logger.debug("AHKProcessManager: Launching AHK log monitoring thread.")
|
||||||
|
threading.Thread(target=self._monitor_ahk_log, daemon=True).start()
|
||||||
|
logger.debug("AHKProcessManager: AHK log monitoring thread launched.")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.error(f"AHKProcessManager: FileNotFoundError: {e}", exc_info=True)
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: An unexpected error occurred while launching AutoHotkey script: {e}", exc_info=True)
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
logger.info("AHKProcessManager: start_script: Function exit.")
|
||||||
|
|
||||||
|
def stop_script(self):
|
||||||
|
"""
|
||||||
|
Terminates the running AutoHotkey script process and cleans up temporary files.
|
||||||
|
Always attempts to stop the process and clean up, regardless of previous state.
|
||||||
|
"""
|
||||||
|
logger.info("AHKProcessManager: stop_script: Function entry.")
|
||||||
|
self._keep_running = False
|
||||||
|
|
||||||
|
if self._ahk_process and self._ahk_process.poll() is None:
|
||||||
|
logger.info("AHKProcessManager: Terminating AutoHotkey script process.")
|
||||||
|
try:
|
||||||
|
self._ahk_process.terminate()
|
||||||
|
self._ahk_process.wait(timeout=2)
|
||||||
|
if self._ahk_process.poll() is None:
|
||||||
|
self._ahk_process.kill()
|
||||||
|
logger.info("AHKProcessManager: AutoHotkey process forcefully killed.")
|
||||||
|
else:
|
||||||
|
logger.info("AHKProcessManager: AutoHotkey process terminated gracefully.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: Error terminating AutoHotkey process: {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
self._ahk_process = None
|
||||||
|
else:
|
||||||
|
logger.info("AHKProcessManager: AutoHotkey script was not running or already terminated.")
|
||||||
|
|
||||||
|
self._clean_ahk_temp_files()
|
||||||
|
logger.info("AHKProcessManager: stop_script: Function exit.")
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
|
||||||
|
def _monitor_ahk_log(self):
|
||||||
|
"""Continuously reads the AutoHotkey script's log file and puts new lines into the message queue."""
|
||||||
|
logger.info("AHKProcessManager: _monitor_ahk_log: Started monitoring AHK log file.")
|
||||||
|
time.sleep(1)
|
||||||
|
while self._keep_running and self._ahk_log_temp_file and os.path.exists(self._ahk_log_temp_file):
|
||||||
|
try:
|
||||||
|
if self._ahk_process and self._ahk_process.poll() is not None:
|
||||||
|
logger.warning("AHKProcessManager: AHK process unexpectedly terminated. Signaling stop.")
|
||||||
|
self._keep_running = False
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(self._ahk_log_temp_file, 'r') as f:
|
||||||
|
f.seek(self._ahk_log_file_last_read_pos)
|
||||||
|
new_lines = f.readlines()
|
||||||
|
for line in new_lines:
|
||||||
|
self._message_queue.put(f"[AHK] {line.strip()}")
|
||||||
|
self._ahk_log_file_last_read_pos = f.tell()
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning("AHKProcessManager: _monitor_ahk_log: AHK log file not found, it might have been deleted or not created yet. Retrying...")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: _monitor_ahk_log: Error reading AHK log file: {e}", exc_info=True)
|
||||||
|
time.sleep(AHK_LOG_MONITOR_INTERVAL)
|
||||||
|
logger.info("AHKProcessManager: _monitor_ahk_log: Stopped monitoring AHK log file.")
|
||||||
|
if not self._keep_running:
|
||||||
|
self.stop_script()
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_ahk_temp_files(self):
|
||||||
|
"""Removes temporary AutoHotkey script and log files."""
|
||||||
|
logger.info("AHKProcessManager: _clean_ahk_temp_files: Attempting to clean up temporary AHK files.")
|
||||||
|
if self._ahk_script_temp_file and os.path.exists(self._ahk_script_temp_file):
|
||||||
|
try:
|
||||||
|
os.remove(self._ahk_script_temp_file)
|
||||||
|
logger.info(f"AHKProcessManager: Removed temporary AutoHotkey script: {self._ahk_script_temp_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: Failed to remove temporary AHK script file: {e}", exc_info=True)
|
||||||
|
self._ahk_script_temp_file = None
|
||||||
|
|
||||||
|
if self._ahk_log_temp_file and os.path.exists(self._ahk_log_temp_file):
|
||||||
|
try:
|
||||||
|
os.remove(self._ahk_log_temp_file)
|
||||||
|
logger.info(f"AHKProcessManager: Removed temporary AutoHotkey log: {self._ahk_log_temp_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: Failed to remove temporary AHK log file: {e}", exc_info=True)
|
||||||
|
self._ahk_log_temp_file = None
|
||||||
|
|
||||||
|
def _on_pynput_key_listener(self, key):
|
||||||
|
"""Callback for pynput keyboard listener to detect STOP_KEY."""
|
||||||
|
try:
|
||||||
|
if key == self._stop_key:
|
||||||
|
logger.info(f"AHKProcessManager: Stop key pressed. Signaling script to stop.")
|
||||||
|
self._keep_running = False
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
return False
|
||||||
|
except AttributeError: # Handle special keys which don't have a .char attribute (e.g., F-keys)
|
||||||
|
if key == self._stop_key:
|
||||||
|
logger.info(f"AHKProcessManager: '{self._stop_key}' pressed. Signaling script to stop.")
|
||||||
|
self._keep_running = False
|
||||||
|
self._trigger_ahk_stopped_callback()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_pynput_listener(self):
|
||||||
|
"""
|
||||||
|
Starts the pynput keyboard listener thread.
|
||||||
|
This method is a public interface to initiate the internal listener thread.
|
||||||
|
"""
|
||||||
|
logger.info("AHKProcessManager: Public start_pynput_listener called. Initiating internal listener thread.")
|
||||||
|
threading.Thread(target=self._start_pynput_listener_thread_internal, daemon=True).start()
|
||||||
|
|
||||||
|
def _start_pynput_listener_thread_internal(self):
|
||||||
|
"""
|
||||||
|
Internal method to start the pynput keyboard listener.
|
||||||
|
This method is meant to be run in its own daemon thread.
|
||||||
|
"""
|
||||||
|
logger.info("AHKProcessManager: pynput listener thread started. Waiting for stop key press.")
|
||||||
|
try:
|
||||||
|
with Listener(on_press=self._on_pynput_key_listener) as listener:
|
||||||
|
listener.join()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AHKProcessManager: Error in pynput listener thread: {e}", exc_info=True)
|
||||||
|
logger.info("AHKProcessManager: pynput listener thread stopped.")
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import logging
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
# --- Global Message Queue for GUI Communication ---
|
||||||
|
# All log messages and AHK output pass through this queue to the View.
|
||||||
|
message_queue = queue.Queue()
|
||||||
|
|
||||||
|
# --- Custom QueueHandler for UI messages ---
|
||||||
|
class QueueHandler(logging.Handler):
|
||||||
|
"""
|
||||||
|
Custom logging handler to push *only the raw message* to a queue,
|
||||||
|
allowing the GUI to display it directly without additional formatting.
|
||||||
|
"""
|
||||||
|
def __init__(self, message_queue):
|
||||||
|
super().__init__()
|
||||||
|
self.message_queue = message_queue
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
# Use record.getMessage() to get the log message without formatters applied
|
||||||
|
self.message_queue.put(record.getMessage())
|
||||||
|
|
||||||
|
# --- Configuration Constants for Logging ---
|
||||||
|
LOG_FILENAME_BASE = "JarvisKeyPressUtility"
|
||||||
|
CONFIG_FILE_NAME = "config.ini"
|
||||||
|
|
||||||
|
def setup_logging_from_config():
|
||||||
|
"""
|
||||||
|
Sets up logging handlers (console and file) based on settings in config.ini.
|
||||||
|
Creates a default config.ini if it doesn't exist.
|
||||||
|
This function should be called once at application startup.
|
||||||
|
"""
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
|
||||||
|
# Clear existing handlers to prevent duplicates if function is called multiple times
|
||||||
|
if root_logger.handlers:
|
||||||
|
for handler in list(root_logger.handlers):
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
|
||||||
|
# Determine the execution directory for the config and log files
|
||||||
|
if getattr(sys, 'frozen', False): # Running as a PyInstaller executable
|
||||||
|
execution_dir = os.path.dirname(sys.executable)
|
||||||
|
else: # Running as a Python script
|
||||||
|
# When called from main.py, os.path.abspath(__file__) points to model/app_logger.py.
|
||||||
|
# Need to go up two levels to reach project root where config.ini is expected.
|
||||||
|
execution_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
|
|
||||||
|
config_file_path = os.path.join(execution_dir, CONFIG_FILE_NAME)
|
||||||
|
|
||||||
|
# Create default config.ini if it doesn't exist
|
||||||
|
if not os.path.exists(config_file_path):
|
||||||
|
config['Logging'] = {
|
||||||
|
'file_log_level': 'DEBUG', # Default to DEBUG for file
|
||||||
|
'console_log_level': 'INFO' # Default to INFO for console
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(config_file_path, 'w') as f:
|
||||||
|
config.write(f)
|
||||||
|
print(f"Created default '{CONFIG_FILE_NAME}' at '{config_file_path}'")
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error creating default config.ini at {config_file_path}: {e}")
|
||||||
|
# Fallback to default levels if config file cannot be created
|
||||||
|
file_log_level = logging.DEBUG
|
||||||
|
console_log_level = logging.INFO
|
||||||
|
else:
|
||||||
|
config.read(config_file_path)
|
||||||
|
# Get log levels from config, default to INFO if not found or invalid
|
||||||
|
file_log_level_str = config.get('Logging', 'file_log_level', fallback='DEBUG').upper()
|
||||||
|
console_log_level_str = config.get('Logging', 'console_log_level', fallback='INFO').upper()
|
||||||
|
|
||||||
|
file_log_level = getattr(logging, file_log_level_str, logging.INFO)
|
||||||
|
console_log_level = getattr(logging, console_log_level_str, logging.INFO)
|
||||||
|
|
||||||
|
# Set the root logger level to the lowest (most verbose) level needed by any handler
|
||||||
|
root_logger.setLevel(min(file_log_level, console_log_level, logging.WARNING))
|
||||||
|
|
||||||
|
# 1. Console Handler: Outputs to stdout
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
console_handler.setLevel(console_log_level) # Set level for console output
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# 2. File Handler: Outputs to a log file
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
log_file_path = os.path.join(execution_dir, f"{LOG_FILENAME_BASE}_{timestamp}.log")
|
||||||
|
|
||||||
|
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
|
||||||
|
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(funcName)s - %(message)s')
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
file_handler.setLevel(file_log_level) # Set level for file output (e.g., DEBUG for all)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 3. UI Queue Handler (for messages sent to Tkinter GUI)
|
||||||
|
ui_queue_handler = QueueHandler(message_queue)
|
||||||
|
ui_queue_handler.setLevel(logging.WARNING) # UI usually wants WARNING or higher messages by default
|
||||||
|
root_logger.addHandler(ui_queue_handler)
|
||||||
|
|
||||||
|
return log_file_path # Return path for initial log message in main
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
from pynput.keyboard import Key
|
||||||
|
|
||||||
|
# --- Configuration Constants ---
|
||||||
|
STOP_KEY = Key.f6 # The global key to stop the AutoHotkey script listener
|
||||||
|
RETRY_WINDOW_SECONDS = 2 # Not directly used in current AHK template, but kept for context
|
||||||
|
AHK_LOG_MONITOR_INTERVAL = 0.5 # Interval to check AHK log file for new messages
|
||||||
|
|
||||||
|
# Path to the external AutoHotkey template file, now relative from model/ to resources/
|
||||||
|
AHK_TEMPLATE_REL_PATH_FROM_MODEL_DIR = os.path.join(os.pardir, "resources", "template.ahk")
|
||||||
|
|
||||||
|
# List of common AutoHotkey.exe installation paths and fallbacks.
|
||||||
|
AHK_EXECUTABLE_FALLBACK_PATHS = [
|
||||||
|
"C:\\Program Files\\AutoHotkey\\v2\\AutoHotkey.exe", # Common AHK v2 install path
|
||||||
|
"C:\\Program Files\\AutoHotkey\\AutoHotkey.exe", # Common AHK v1 install path
|
||||||
|
"AutoHotkey.exe" # Fallback: relies on system PATH or current working directory
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mapping for common key names to AutoHotkey v2 equivalents.
|
||||||
|
AUTOHOTKEY_KEY_MAP = {
|
||||||
|
'+': 'NumpadAdd', 'num+': 'NumpadAdd', 'numpad+': 'NumpadAdd',
|
||||||
|
'enter': 'Enter', 'return': 'Enter', 'space': 'Space',
|
||||||
|
'esc': 'Escape', 'del': 'Delete', 'Backspace': 'Backspace',
|
||||||
|
'tab': 'Tab', 'shift': 'Shift', 'ctrl': 'Control', 'control': 'Control',
|
||||||
|
'alt': 'Alt', 'up': 'Up', 'down': 'Down', 'left': 'Left', 'right': 'Right',
|
||||||
|
'f1': 'F1', 'f2': 'F2', 'f3': 'F3', 'f4': 'F4', 'f5': 'F5', 'f6': 'F6',
|
||||||
|
'f7': 'F7', 'f8': 'F8', 'f9': 'F9', 'f10': 'F10', 'f11': 'F11', 'f12': 'F12'
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Import components from other model files
|
||||||
|
from model.constants import STOP_KEY, AHK_TEMPLATE_REL_PATH_FROM_MODEL_DIR, AHK_EXECUTABLE_FALLBACK_PATHS, AUTOHOTKEY_KEY_MAP
|
||||||
|
from model.app_logger import message_queue # Assuming app_logger provides the message_queue and handles initial logging setup
|
||||||
|
from model.ahk_process_manager import AHKProcessManager
|
||||||
|
from model.window_manager import WindowManager
|
||||||
|
from pynput.keyboard import Key, Listener # Still needed here for STOP_KEY in on_press_listener callback directly
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class KeyPressModel:
|
||||||
|
def __init__(self):
|
||||||
|
self.current_target_window_title = ""
|
||||||
|
self.current_key_to_press_str = '+'
|
||||||
|
self._ahk_stopped_callback = None # Callback to Controller when AHK script stops externally
|
||||||
|
|
||||||
|
# Instantiate sub-components that manage specific areas of the model's responsibility
|
||||||
|
self.ahk_manager = AHKProcessManager(
|
||||||
|
ahk_template_rel_path=AHK_TEMPLATE_REL_PATH_FROM_MODEL_DIR,
|
||||||
|
ahk_executable_fallback_paths=AHK_EXECUTABLE_FALLBACK_PATHS,
|
||||||
|
autohotkey_key_map=AUTOHOTKEY_KEY_MAP,
|
||||||
|
stop_key=STOP_KEY,
|
||||||
|
message_queue=message_queue # Pass the central message queue
|
||||||
|
)
|
||||||
|
self.window_manager = WindowManager()
|
||||||
|
|
||||||
|
# Register callback for AHK process stopping
|
||||||
|
self.ahk_manager.register_ahk_stopped_callback(self._ahk_process_stopped_internally)
|
||||||
|
|
||||||
|
logger.info("Model: KeyPressModel Initialized. Sub-components loaded.")
|
||||||
|
|
||||||
|
def _ahk_process_stopped_internally(self):
|
||||||
|
"""Internal callback from AHKProcessManager when AHK script stops."""
|
||||||
|
logger.info("Model: AHK process reported termination.")
|
||||||
|
if self._ahk_stopped_callback:
|
||||||
|
self._ahk_stopped_callback()
|
||||||
|
|
||||||
|
def register_ahk_stopped_callback(self, callback_func):
|
||||||
|
"""Registers a callback function to be called when the AHK script stops externally."""
|
||||||
|
self._ahk_stopped_callback = callback_func
|
||||||
|
logger.info("Model: AHK stopped callback registered by Controller.")
|
||||||
|
|
||||||
|
def get_window_titles(self):
|
||||||
|
"""Retrieves a sorted list of active window titles from WindowManager."""
|
||||||
|
return self.window_manager.get_window_titles()
|
||||||
|
|
||||||
|
def set_target_window(self, title):
|
||||||
|
"""Sets the target window title for AHK operations in AHKProcessManager."""
|
||||||
|
self.current_target_window_title = title
|
||||||
|
self.ahk_manager.set_target_window(title)
|
||||||
|
logger.info(f"Model: Target window set to: '{self.current_target_window_title}' in AHK manager.")
|
||||||
|
|
||||||
|
def set_key_to_press(self, key_str):
|
||||||
|
"""Sets the key string to be pressed by AHK in AHKProcessManager."""
|
||||||
|
self.current_key_to_press_str = key_str
|
||||||
|
self.ahk_manager.set_key_to_press(key_str)
|
||||||
|
logger.info(f"Model: Key to press set to: '{self.current_key_to_press_str}' in AHK manager.")
|
||||||
|
|
||||||
|
def get_message_queue(self):
|
||||||
|
"""Returns the central message queue."""
|
||||||
|
return message_queue
|
||||||
|
|
||||||
|
def start_autohotkey_script(self):
|
||||||
|
"""Initiates the AHK script launch via AHKProcessManager."""
|
||||||
|
return self.ahk_manager.start_script()
|
||||||
|
|
||||||
|
def stop_autohotkey_script(self):
|
||||||
|
"""Terminates the AHK script via AHKProcessManager."""
|
||||||
|
self.ahk_manager.stop_script()
|
||||||
|
|
||||||
|
def start_pynput_listener_thread(self):
|
||||||
|
"""Starts the pynput keyboard listener thread via AHKProcessManager."""
|
||||||
|
self.ahk_manager.start_pynput_listener()
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import pygetwindow as gw
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WindowManager:
|
||||||
|
"""
|
||||||
|
Manages interactions related to system windows,
|
||||||
|
primarily retrieving active window titles.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
logger.info("WindowManager: Initialized.")
|
||||||
|
|
||||||
|
def get_window_titles(self):
|
||||||
|
"""
|
||||||
|
Retrieves a sorted list of active window titles.
|
||||||
|
Excludes the utility's own window by its title.
|
||||||
|
"""
|
||||||
|
logger.info("WindowManager: Attempting to retrieve active window list.")
|
||||||
|
try:
|
||||||
|
all_windows = gw.getAllWindows()
|
||||||
|
# Filter out empty titles and the utility's own window by its default title
|
||||||
|
window_titles = sorted(
|
||||||
|
list(set([
|
||||||
|
win.title for win in all_windows
|
||||||
|
if win.title and win.title != "Jarvis Key Press Utility (AHK Integrated)"
|
||||||
|
]))
|
||||||
|
)
|
||||||
|
logger.info(f"WindowManager: Found {len(window_titles)} active windows.")
|
||||||
|
return window_titles
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WindowManager: Failed to retrieve window list: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
; === CONFIGURATION (Filled by Python) ===
|
||||||
|
TargetWindowTitle := "{TARGET_WINDOW_TITLE_PLACEHOLDER}"
|
||||||
|
KeyToPressAHK := "{KEY_TO_PRESS_AHK_PLACEHOLDER}"
|
||||||
|
StopKeyAHK := "{STOP_KEY_AHK_PLACEHOLDER}"
|
||||||
|
AHKLogFile := "{AHK_LOG_FILE_PLACEHOLDER}"
|
||||||
|
|
||||||
|
; === AHK LOGGING FUNCTION ===
|
||||||
|
Log(message) {{
|
||||||
|
FileAppend(message "`n", AHKLogFile)
|
||||||
|
}}
|
||||||
|
|
||||||
|
; === SCRIPT LOGIC ===
|
||||||
|
|
||||||
|
Log("AHK Script started.")
|
||||||
|
; Global state variable to track if the key is currently pressed down
|
||||||
|
isKeyDown := false
|
||||||
|
|
||||||
|
{STOP_KEY_AHK_PLACEHOLDER}::
|
||||||
|
{{
|
||||||
|
Log("Stop key pressed. Releasing key and exiting.")
|
||||||
|
Send("{{KeyToPressAHK Up}}") ; Ensure key is released on manual stop
|
||||||
|
ExitApp
|
||||||
|
}}
|
||||||
|
|
||||||
|
SetTimer(() => SendKeyLoop(), 100)
|
||||||
|
|
||||||
|
SendKeyLoop() {{
|
||||||
|
global isKeyDown
|
||||||
|
|
||||||
|
; Check if window exists and is NOT active, then attempt activation
|
||||||
|
If WinExist(TargetWindowTitle) && not WinActive(TargetWindowTitle) {{
|
||||||
|
Log("Target window '" TargetWindowTitle "' exists but not active. Attempting activation.")
|
||||||
|
WinActivate(TargetWindowTitle)
|
||||||
|
Sleep 50 ; Give a moment for the window to become active
|
||||||
|
if WinActive(TargetWindowTitle) {{
|
||||||
|
Log("Target window '" TargetWindowTitle "' is now active.")
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
; If window does NOT exist
|
||||||
|
Else {{
|
||||||
|
Log("Target window '" TargetWindowTitle "' not found. Stopping script.")
|
||||||
|
If (isKeyDown) {{
|
||||||
|
Send("{{KeyToPressAHK Up}}")
|
||||||
|
isKeyDown := false
|
||||||
|
Log("Key released (Target window not found).")
|
||||||
|
}}
|
||||||
|
ExitApp ; <<< NEW: Exit AHK script if window is not found
|
||||||
|
}}
|
||||||
|
|
||||||
|
; After potential activation attempt, or if already active, check active state to send/release key
|
||||||
|
if WinActive(TargetWindowTitle) {{
|
||||||
|
If (not isKeyDown) {{ ; Only press down if not already down
|
||||||
|
Send("{{KeyToPressAHK Down}}")
|
||||||
|
isKeyDown := true
|
||||||
|
Log("Key pressed down (Target window active).")
|
||||||
|
}}
|
||||||
|
}} Else {{
|
||||||
|
; Window exists but is not active (either initially not active, or activation failed)
|
||||||
|
Log("Target window '" TargetWindowTitle "' exists but failed to activate.")
|
||||||
|
If (isKeyDown) {{
|
||||||
|
Send("{{KeyToPressAHK Up}}")
|
||||||
|
isKeyDown := false
|
||||||
|
Log("Key released (Failed to activate target window).")
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
OnScriptExit(*) {{ ; Defined as a function that accepts any number of parameters
|
||||||
|
global isKeyDown
|
||||||
|
If (isKeyDown) {{ ; Ensure key is released if script exits while it's down
|
||||||
|
Send("{{KeyToPressAHK Up}}")
|
||||||
|
Log("Key released (Script exiting).")
|
||||||
|
}}
|
||||||
|
Log("AHK Script exiting.")
|
||||||
|
ExitApp
|
||||||
|
}}
|
|
@ -0,0 +1,72 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Navigate to the project root from the scripts folder
|
||||||
|
pushd ..
|
||||||
|
REM Now current directory is project_root/
|
||||||
|
|
||||||
|
REM --- Ensure bin directory exists at project root ---
|
||||||
|
echo.
|
||||||
|
echo Ensuring 'bin' directory exists at project root...
|
||||||
|
IF NOT EXIST "bin" (
|
||||||
|
mkdir "bin"
|
||||||
|
echo Created 'bin' directory.
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM --- Cleanup Previous Build Artifacts within bin folder ---
|
||||||
|
echo.
|
||||||
|
echo Cleaning up previous build artifacts in 'bin' folder...
|
||||||
|
IF EXIST "bin\build" (
|
||||||
|
rmdir /s /q "bin\build"
|
||||||
|
echo Removed 'bin\build' directory.
|
||||||
|
)
|
||||||
|
IF EXIST "bin\dist" (
|
||||||
|
rmdir /s /q "bin\dist"
|
||||||
|
echo Removed 'bin\dist' directory.
|
||||||
|
)
|
||||||
|
IF EXIST "bin\JarvisKeyPressUtility.spec" (
|
||||||
|
del /q "bin\JarvisKeyPressUtility.spec"
|
||||||
|
echo Removed 'bin\JarvisKeyPressUtility.spec' file.
|
||||||
|
)
|
||||||
|
echo Cleanup in 'bin' complete.
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Navigate back to the scripts folder where exe_compile.cmd is
|
||||||
|
popd
|
||||||
|
REM Now current directory is project_root/scripts/
|
||||||
|
|
||||||
|
REM This is the PyInstaller command to build your executable.
|
||||||
|
REM Paths are now relative to the current directory (scripts/).
|
||||||
|
pyinstaller --onefile --windowed --name "JarvisKeyPressUtility" ^
|
||||||
|
--add-binary "C:\Program Files\AutoHotkey\v2\AutoHotkey.exe;." ^
|
||||||
|
--add-data "..\resources;resources" ^
|
||||||
|
--uac-admin ..\main.py ^
|
||||||
|
--distpath ..\bin ^
|
||||||
|
--workpath ..\bin ^
|
||||||
|
--specpath ..\bin
|
||||||
|
|
||||||
|
REM Capture the exit code of the PyInstaller command.
|
||||||
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
|
||||||
|
REM Check the exit code. If it's not 0 (indicating a compilation error), pause.
|
||||||
|
IF %EXIT_CODE% NEQ 0 (
|
||||||
|
echo.
|
||||||
|
echo PyInstaller compilation failed!
|
||||||
|
echo PyInstaller exited with code: %EXIT_CODE%
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
) ELSE (
|
||||||
|
REM If compilation was successful, print a message and allow the window to close.
|
||||||
|
echo PyInstaller compilation successful.
|
||||||
|
echo The executable "JarvisKeyPressUtility.exe" should be in the 'bin\dist' folder.
|
||||||
|
|
||||||
|
REM --- Copy template.ahk to bin\dist folder (already handled by --add-data) ---
|
||||||
|
REM This line is now effectively redundant as the entire 'resources' folder
|
||||||
|
REM including 'template.ahk' is bundled by '--add-data "..\resources;resources"'
|
||||||
|
REM PyInstaller places it within the extracted temp folder, accessible via sys._MEIPASS
|
||||||
|
)
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
REM Exit the batch script with the same exit code as PyInstaller.
|
||||||
|
exit /b %EXIT_CODE%
|
|
@ -0,0 +1,105 @@
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# --- Configuration for Code Signing ---
|
||||||
|
# !!! IMPORTANT: Replace these placeholders with your actual details !!!
|
||||||
|
# Path to your .pfx or .p12 certificate file
|
||||||
|
CERTIFICATE_PATH = r"C:\Path\To\Your\my_certificate.pfx"
|
||||||
|
# Password for your certificate file
|
||||||
|
CERTIFICATE_PASSWORD = "YourSecureCertificatePassword"
|
||||||
|
# Path to signtool.exe (usually in Windows SDK bin folder)
|
||||||
|
# You might need to adjust the version number (e.g., 10.0.19041.0) and architecture (x64/x86)
|
||||||
|
SIGNTOOL_PATH = r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe"
|
||||||
|
# Name of your PyInstaller-generated executable (found in the 'dist' folder)
|
||||||
|
EXE_NAME = "JarvisKeyPressUtility.exe"
|
||||||
|
# Timestamp server URL (use one provided by your CA, e.g., DigiCert, Sectigo)
|
||||||
|
TIMESTAMP_SERVER = "http://timestamp.digicert.com"
|
||||||
|
|
||||||
|
# --- Logging Setup ---
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
def run_command(command, cwd=None):
|
||||||
|
"""Executes a shell command and captures its output."""
|
||||||
|
try:
|
||||||
|
logging.info(f"Executing command: {' '.join(command)}")
|
||||||
|
process = subprocess.run(command, capture_output=True, text=True, check=True, cwd=cwd)
|
||||||
|
logging.info("Command completed successfully.")
|
||||||
|
logging.info(f"STDOUT:\n{process.stdout}")
|
||||||
|
if process.stderr:
|
||||||
|
logging.warning(f"STDERR:\n{process.stderr}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logging.error(f"Command failed with exit code {e.returncode}.")
|
||||||
|
logging.error(f"STDOUT:\n{e.stdout}")
|
||||||
|
logging.error(f"STDERR:\n{e.stderr}")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.error(f"Error: Executable not found. Check the path for: {command[0]}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An unexpected error occurred: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sign_executable(exe_path, cert_path, cert_password, signtool_path, timestamp_server):
|
||||||
|
"""
|
||||||
|
Signs the given executable using signtool.exe.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(signtool_path):
|
||||||
|
logging.error(f"SignTool not found at: {signtool_path}. Please ensure Windows SDK is installed and path is correct.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.exists(cert_path):
|
||||||
|
logging.error(f"Certificate file not found at: {cert_path}. Please check the path.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logging.info(f"Attempting to sign executable: {exe_path}")
|
||||||
|
|
||||||
|
# Signtool command components
|
||||||
|
command = [
|
||||||
|
signtool_path,
|
||||||
|
"sign",
|
||||||
|
"/f", cert_path, # Certificate file
|
||||||
|
"/p", cert_password, # Certificate password
|
||||||
|
"/fd", "sha256", # File digest algorithm
|
||||||
|
"/tr", timestamp_server, # Timestamp server URL
|
||||||
|
"/td", "sha256", # Timestamp digest algorithm
|
||||||
|
exe_path # Executable to sign
|
||||||
|
]
|
||||||
|
|
||||||
|
return run_command(command)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Ensure this script is run from the same directory where 'dist' folder is or adjust paths.
|
||||||
|
# Typically, you'd run this script after PyInstaller has finished its build.
|
||||||
|
|
||||||
|
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
dist_folder = os.path.join(current_script_dir, "dist")
|
||||||
|
exe_path_in_dist = os.path.join(dist_folder, EXE_NAME)
|
||||||
|
|
||||||
|
if not os.path.exists(dist_folder):
|
||||||
|
logging.error(f"Error: 'dist' folder not found at '{dist_folder}'. Please run PyInstaller first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not os.path.exists(exe_path_in_dist):
|
||||||
|
# Handle --onefile vs. no --onefile
|
||||||
|
if not os.path.exists(os.path.join(dist_folder, EXE_NAME.replace(".exe", ""), EXE_NAME)):
|
||||||
|
logging.error(f"Error: Executable '{EXE_NAME}' not found in '{dist_folder}' or its subfolder (for non-onefile builds).")
|
||||||
|
logging.info("If you used PyInstaller without --onefile, the EXE might be in a subfolder like 'dist/JarvisKeyPressUtility/JarvisKeyPressUtility.exe'.")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
exe_path_to_sign = os.path.join(dist_folder, EXE_NAME.replace(".exe", ""), EXE_NAME)
|
||||||
|
logging.info(f"Detected non-onefile build. Signing: {exe_path_to_sign}")
|
||||||
|
else:
|
||||||
|
exe_path_to_sign = exe_path_in_dist
|
||||||
|
logging.info(f"Detected onefile build. Signing: {exe_path_to_sign}")
|
||||||
|
|
||||||
|
logging.info("Starting code signing process.")
|
||||||
|
|
||||||
|
success = sign_executable(exe_path_to_sign, CERTIFICATE_PATH, CERTIFICATE_PASSWORD, SIGNTOOL_PATH, TIMESTAMP_SERVER)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logging.info(f"Successfully signed '{EXE_NAME}'. Your AVG should now be less annoyed, theoretically.")
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to sign '{EXE_NAME}'. Check the log for details on the signtool error.")
|
|
@ -0,0 +1,157 @@
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import scrolledtext, Label, Button, Entry, StringVar, ttk, messagebox
|
||||||
|
import datetime
|
||||||
|
import queue
|
||||||
|
|
||||||
|
class KeyPressView(tk.Frame):
|
||||||
|
def __init__(self, master, controller):
|
||||||
|
super().__init__(master)
|
||||||
|
self.master = master
|
||||||
|
self.controller = controller # Store reference to controller
|
||||||
|
|
||||||
|
self.master.title("Jarvis Key Press Utility (AHK Integrated)")
|
||||||
|
self.master.geometry("600x600")
|
||||||
|
self.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.grid_columnconfigure(0, weight=1)
|
||||||
|
self.grid_columnconfigure(1, weight=1)
|
||||||
|
self.grid_rowconfigure(0, weight=0)
|
||||||
|
self.grid_rowconfigure(1, weight=0)
|
||||||
|
self.grid_rowconfigure(2, weight=1)
|
||||||
|
|
||||||
|
# --- Configuration Section ---
|
||||||
|
self.config_frame = tk.LabelFrame(self, text="Configuration", padx=10, pady=10, font=('Inter', 10, 'bold'))
|
||||||
|
self.config_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
|
||||||
|
self.config_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
Label(self.config_frame, text="Key to Press:", font=('Inter', 10)).grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.key_to_press_var = StringVar(value="+")
|
||||||
|
self.key_input_entry = Entry(self.config_frame, textvariable=self.key_to_press_var, width=20, font=('Inter', 10))
|
||||||
|
self.key_input_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
Label(self.config_frame, text="(e.g., '+', 'a', 'enter', 'space', 'f5')", font=('Inter', 8), fg="#586e75").grid(row=1, column=1, padx=5, pady=0, sticky="nw")
|
||||||
|
|
||||||
|
self.key_validation_label = Label(self.config_frame, text="Please enter a key to press.", font=('Inter', 8, 'bold'), fg="red")
|
||||||
|
self.key_validation_label.grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
||||||
|
self.key_validation_label.grid_remove()
|
||||||
|
|
||||||
|
Label(self.config_frame, text="Target Window:", font=('Inter', 10)).grid(row=2, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
|
||||||
|
self.target_window_title_var = StringVar(value="--- Select a Window ---")
|
||||||
|
self.window_combobox = ttk.Combobox(self.config_frame, textvariable=self.target_window_title_var,
|
||||||
|
state="readonly", font=('Inter', 10))
|
||||||
|
self.window_combobox.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
self.window_combobox.bind("<<ComboboxSelected>>", self.controller.on_window_selected) # Bind to controller directly
|
||||||
|
|
||||||
|
self.refresh_button = Button(self.config_frame, text="Refresh Windows", command=self.controller.refresh_windows, font=('Inter', 9))
|
||||||
|
self.refresh_button.grid(row=2, column=2, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.target_window_validation_label = Label(self.config_frame, text="Please select a target window.", font=('Inter', 8, 'bold'), fg="red")
|
||||||
|
self.target_window_validation_label.grid(row=3, column=1, padx=5, pady=0, sticky="nw")
|
||||||
|
self.target_window_validation_label.grid_remove()
|
||||||
|
|
||||||
|
# --- Control Buttons Section ---
|
||||||
|
self.button_frame = tk.Frame(self)
|
||||||
|
self.button_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
|
||||||
|
self.button_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
self.button_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
self.button_frame.grid_columnconfigure(2, weight=1) # For Clear Log Button
|
||||||
|
|
||||||
|
self.start_button = Button(self.button_frame, text="Start Holding Key", command=self.controller.start_key_press, bg="#2aa198", fg="white", font=('Inter', 12, 'bold'))
|
||||||
|
self.start_button.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.stop_button = Button(self.button_frame, text="Stop Key Press (or F6)", command=self.controller.stop_key_press, bg="#dc322f", fg="white", font=('Inter', 12, 'bold'))
|
||||||
|
self.stop_button.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
self.clear_log_button = Button(self.button_frame, text="Clear Log", command=self._clear_log, font=('Inter', 9))
|
||||||
|
self.clear_log_button.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
# --- Log Display Area ---
|
||||||
|
self.log_frame = tk.LabelFrame(self, text="Activity Log (Jarvis's Observations)", padx=10, pady=10, font=('Consolas', 10, 'bold'))
|
||||||
|
self.log_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
|
||||||
|
self.log_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
self.log_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.log_text_area = scrolledtext.ScrolledText(self.log_frame, state='disabled', wrap='word', width=60, height=15, bg="#eee8d5", fg="#586e75", font=('Consolas', 10))
|
||||||
|
self.log_text_area.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
|
# Initial population of window list is now triggered by controller's initial_setup_after_view_is_ready()
|
||||||
|
self.master.after(100, self._poll_message_queue)
|
||||||
|
|
||||||
|
# --- View Update Methods (Called by Controller) ---
|
||||||
|
def update_window_list(self, window_titles, current_selected_title):
|
||||||
|
"""Updates the combobox with new window titles."""
|
||||||
|
self.window_combobox['values'] = window_titles
|
||||||
|
self.target_window_title_var.set(current_selected_title)
|
||||||
|
|
||||||
|
def get_key_input(self):
|
||||||
|
"""Returns the current value of the key input entry."""
|
||||||
|
return self.key_to_press_var.get().strip()
|
||||||
|
|
||||||
|
def get_selected_window_title(self):
|
||||||
|
"""Returns the currently selected window title from the combobox."""
|
||||||
|
return self.target_window_title_var.get()
|
||||||
|
|
||||||
|
def set_ui_state_running(self):
|
||||||
|
"""Sets UI elements to a 'running' state."""
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
self.stop_button.config(state=tk.NORMAL)
|
||||||
|
self.key_input_entry.config(state=tk.DISABLED)
|
||||||
|
self.window_combobox.config(state=tk.DISABLED)
|
||||||
|
self.refresh_button.config(state=tk.DISABLED)
|
||||||
|
# Validation labels are hidden by UIController when changing state
|
||||||
|
|
||||||
|
def set_ui_state_idle(self):
|
||||||
|
"""Sets UI elements to an 'idle' state."""
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
|
self.key_input_entry.config(state=tk.NORMAL)
|
||||||
|
self.window_combobox.config(state="readonly")
|
||||||
|
self.refresh_button.config(state=tk.NORMAL)
|
||||||
|
# Validation labels visibility will be managed by UIController's _check_start_button_state_ui()
|
||||||
|
|
||||||
|
def show_input_error(self, message):
|
||||||
|
"""Displays a messagebox error for input validation."""
|
||||||
|
messagebox.showerror("Input Error", message)
|
||||||
|
|
||||||
|
def set_key_validation_visibility(self, visible):
|
||||||
|
"""Controls visibility of the key validation label."""
|
||||||
|
if visible:
|
||||||
|
self.key_validation_label.grid()
|
||||||
|
else:
|
||||||
|
self.key_validation_label.grid_remove()
|
||||||
|
|
||||||
|
def set_window_validation_visibility(self, visible):
|
||||||
|
"""Controls visibility of the window validation label."""
|
||||||
|
if visible:
|
||||||
|
self.target_window_validation_label.grid()
|
||||||
|
else:
|
||||||
|
self.target_window_validation_label.grid_remove()
|
||||||
|
|
||||||
|
def _on_window_selected_event(self, event):
|
||||||
|
"""Internal handler for combobox selection event, calls controller."""
|
||||||
|
# This now directly calls the main controller, which will then use the UIController
|
||||||
|
self.controller.on_window_selected()
|
||||||
|
|
||||||
|
def _poll_message_queue(self):
|
||||||
|
"""Periodically checks the message queue for log updates."""
|
||||||
|
message_queue = self.controller.get_message_queue()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
record = message_queue.get(block=False)
|
||||||
|
self.log_text_area.configure(state='normal')
|
||||||
|
# Add current timestamp to log entries for better tracking
|
||||||
|
timestamp_str = datetime.datetime.now().strftime('%H:%M:%S')
|
||||||
|
self.log_text_area.insert(tk.END, f"{timestamp_str} - {record}\n")
|
||||||
|
self.log_text_area.see(tk.END)
|
||||||
|
self.log_text_area.configure(state='disabled')
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
self.master.after(100, self._poll_message_queue)
|
||||||
|
|
||||||
|
def _clear_log(self):
|
||||||
|
"""Clears the content of the log text area."""
|
||||||
|
self.log_text_area.configure(state='normal')
|
||||||
|
self.log_text_area.delete(1.0, tk.END)
|
||||||
|
self.log_text_area.configure(state='disabled')
|
||||||
|
|
Loading…
Reference in New Issue