Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 273 additions & 53 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ omnitils = "^1.4.4"
dynaconf = {extras = ["yaml"], version = "^3.2.6"}
hexproof = "^0.3.7"
rich = "^13.8.1"
pywin32 = "^310"

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
Expand All @@ -63,6 +64,7 @@ mkdocs-same-dir = "^0.1.2"
mkdocs-git-revision-date-plugin = "^0.3.2"
mkdocstrings = {extras = ["python"], version = "^0.23.0"}
memory-profiler = "^0.61.0"
types-pywin32 = "^310.0.0.20250516"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from src.gui.console import GUIConsole as Console
else:
Console = TerminalConsole
CONSOLE = Console(cfg=CFG, env=ENV)
CONSOLE = Console(cfg=CFG, env=ENV, app=APP)

# Global plugins and templates
PLUGINS = get_all_plugins(con=CON, env=ENV)
Expand Down
1 change: 1 addition & 0 deletions src/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def update_definitions(self):

# BASE - TEMPLATES
self.exit_early = self.file.getboolean('BASE.TEMPLATES', 'Manual.Edit', fallback=False)
self.minimize_photoshop = self.file.getboolean('BASE.TEMPLATES', 'Minimize.Photoshop', fallback=False)
self.import_scryfall_scan = self.file.getboolean('BASE.TEMPLATES', 'Import.Scryfall.Scan', fallback=False)
self.border_color = self.get_option('BASE.TEMPLATES', 'Border.Color', BorderColor)

Expand Down
21 changes: 18 additions & 3 deletions src/console.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""
* Console Module
"""
from __future__ import annotations

# Standard Library
import os
import time
from threading import Lock, Event, Thread
from datetime import datetime as dt
from functools import cached_property
from typing import Optional
from typing import Optional, TYPE_CHECKING

# Third Party Imports
from omnitils.enums import StrConstant
Expand All @@ -17,6 +19,10 @@
# Local Imports
from src._config import AppConfig
from src._state import AppEnvironment, PATH
from src.utils.windows import WindowState

if TYPE_CHECKING:
from src.utils.adobe import PhotoshopHandler

"""
* Enums
Expand Down Expand Up @@ -157,12 +163,14 @@ class TerminalConsole:
def __init__(
self,
cfg: AppConfig,
env: AppEnvironment
env: AppEnvironment,
app: PhotoshopHandler,
):

# Establish global objects
self.cfg: AppConfig = cfg
self.env: AppEnvironment = env
self.app = app

"""
Logger Object Properties
Expand Down Expand Up @@ -332,7 +340,7 @@ def error(
User Prompt Signals
"""

def await_choice(self, thr: Event, msg: Optional[str] = None, end: str = "\n") -> bool:
def await_choice(self, thr: Event, msg: str | None = None, end: str = "\n", show_photoshop: bool = True) -> bool:
"""
Prompt the user to either continue or cancel.
@param thr: Event object representing the status of the render thread.
Expand All @@ -343,6 +351,9 @@ def await_choice(self, thr: Event, msg: Optional[str] = None, end: str = "\n") -
# Clear other await procedures, then begin awaiting a user signal
self.end_await()
self.update(msg=msg or self.message_waiting, end=end)
if self.cfg.minimize_photoshop and show_photoshop:
# Show Photoshop in case it is minimized
self.app.set_window_state(WindowState.SHOWDEFAULT)
response = input("[Y / Enter] Continue — [N] Cancel")

# Signal the choice
Expand All @@ -352,6 +363,10 @@ def await_choice(self, thr: Event, msg: Optional[str] = None, end: str = "\n") -
# Cancel the current thread or continue based on user signal
if thr:
self.cancel_thread(thr) if not choice else self.start_await_cancel(thr)

if choice and self.cfg.minimize_photoshop:
self.app.set_window_state(WindowState.MINIMIZE)

return choice

def signal(self, choice: bool):
Expand Down
6 changes: 6 additions & 0 deletions src/data/config/base.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ desc = """Pause the render process before saving to allow for manual edits."""
type = "bool"
default = 0

[TEMPLATES."Minimize.Photoshop"]
title = "Minimize Photoshop"
desc = """Minimize Photoshop when rendering starts. The rendering might be faster when Photoshop is minimized."""
type = "bool"
default = 0

[TEMPLATES."Import.Scryfall.Scan"]
title = "Import Scryfall Scan"
desc = """Will import the scryfall scan of each card into the document for reference. Useful for ensuring card accuracy when making manual changes."""
Expand Down
20 changes: 18 additions & 2 deletions src/gui/console.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""
CONSOLE MODULES
"""
from __future__ import annotations

# Standard Library Imports
import os
import time
import traceback
from functools import cached_property
from threading import Thread, Event, Lock
from typing import Optional, Any
from typing import Optional, Any, TYPE_CHECKING
from datetime import datetime as dt

# Third Party Imports
Expand All @@ -21,6 +23,10 @@
from src._state import AppEnvironment, PATH
from src.gui._state import get_root_app
from src.gui.utils import HoverButton
from src.utils.windows import WindowState

if TYPE_CHECKING:
from src.utils.adobe import PhotoshopHandler


class GUIConsole(BoxLayout):
Expand All @@ -35,12 +41,14 @@ def __init__(
self,
cfg: AppConfig,
env: AppEnvironment,
app: PhotoshopHandler,
**kwargs
):
# Establish global objects
super().__init__(**kwargs)
self.cfg = cfg
self.env = env
self.app = app

# Test mode uses larger console
if not self.env.TEST_MODE:
Expand Down Expand Up @@ -248,7 +256,7 @@ def error(
* User Prompt Signals
"""

def await_choice(self, thr: Event, msg: Optional[str] = None, end: str = "\n") -> bool:
def await_choice(self, thr: Event, msg: str | None = None, end: str = "\n", show_photoshop: bool = True) -> bool:
"""Prompt the user to either continue or cancel.

Args:
Expand All @@ -263,11 +271,19 @@ def await_choice(self, thr: Event, msg: Optional[str] = None, end: str = "\n") -
self.end_await()
self.update(msg=msg or self.message_waiting, end=end)
self.enable_buttons()
if self.cfg.minimize_photoshop and show_photoshop:
# Show Photoshop in case it is minimized
self.app.set_window_state(WindowState.SHOWDEFAULT)
self.start_await()

# Cancel the current thread or continue based on user signal
if thr:
self.cancel_thread(thr) if not self.running else self.start_await_cancel(thr)

# Minimize Photoshop if the setting for that is active
if self.running and self.cfg.minimize_photoshop:
self.app.set_window_state(WindowState.MINIMIZE)

return self.running

def signal(self, choice: bool) -> None:
Expand Down
4 changes: 4 additions & 0 deletions src/templates/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
PS_EXCEPTIONS,
ReferenceLayer,
try_photoshop)
from src.utils.windows import WindowState

"""
* Template Classes
Expand Down Expand Up @@ -1546,6 +1547,9 @@ def execute(self) -> bool:
):
return False

if CFG.minimize_photoshop:
APP.set_window_state(WindowState.MINIMIZE)

# Pre-process layout data
if not self.run_tasks(
funcs=self.pre_render_methods,
Expand Down
25 changes: 21 additions & 4 deletions src/utils/adobe.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

# Local Imports
from src._state import AppEnvironment
from src.utils.windows import WindowState, get_window_handle_by_process_file_path_suffix, set_window_state

"""
* Types & Definitions
Expand All @@ -52,7 +53,7 @@
OSError
)

PS_ERROR_CODES: dict[int: str] = {
PS_ERROR_CODES: dict[int, str] = {
# --> COMError Messages that contain a message string
# Response: "The message filter indicated that the application is busy."
-2147417846: "Photoshop is currently busy, close any dialog boxes and stop any pending actions.",
Expand Down Expand Up @@ -143,6 +144,15 @@ class PhotoshopHandler(ApplicationHandler):
DIMS_800 = (2176, 2960)
DIMS_600 = (1632, 2220)
_instance = None
_window_handle: int | None = None

@property
def window_handle(self) -> int | None:
if self._window_handle is None:
self._window_handle = get_window_handle_by_process_file_path_suffix(
"Photoshop.exe"
)
return self._window_handle

def __new__(cls, env: Optional[Any] = None) -> 'PhotoshopHandler':
"""Always return the same Photoshop Application instance on successive calls.
Expand Down Expand Up @@ -178,8 +188,15 @@ def refresh_app(self):
except Exception as e:
# Photoshop is either busy or unresponsive
return OSError(get_photoshop_error_message(e))

# Clear window handle as it might have changed
self._window_handle = None
return

def set_window_state(self, state: WindowState) -> None:
if (handle := self.window_handle) is not None:
set_window_state(handle, state)

"""
* Class Methods
"""
Expand Down Expand Up @@ -471,7 +488,7 @@ def bounds_no_effects(self) -> LayerBounds:
"""

@cached_property
def dims(self) -> type[LayerDimensions]:
def dims(self) -> LayerDimensions:
"""LayerDimensions: Returns dimensions of the layer (cached), including:
- bounds (left, right, top, bottom)
- height
Expand All @@ -482,7 +499,7 @@ def dims(self) -> type[LayerDimensions]:
return self.get_dimensions_from_bounds(self.bounds)

@cached_property
def dims_no_effects(self) -> type[LayerDimensions]:
def dims_no_effects(self) -> LayerDimensions:
"""LayerDimensions: Returns dimensions of the layer (cached) without layer effects applied, including:
- bounds (left, right, top, bottom)
- height
Expand All @@ -497,7 +514,7 @@ def dims_no_effects(self) -> type[LayerDimensions]:
"""

@staticmethod
def get_dimensions_from_bounds(bounds) -> type[LayerDimensions]:
def get_dimensions_from_bounds(bounds) -> LayerDimensions:
"""Compute width and height based on a set of bounds given.

Args:
Expand Down
75 changes: 75 additions & 0 deletions src/utils/windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from enum import IntEnum
import pywintypes
import win32api
import win32con
import win32gui
import win32process


# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
class WindowState(IntEnum):
HIDE = win32con.SW_HIDE
NORMAL = win32con.SW_NORMAL
SHOWMINIMIZED = win32con.SW_SHOWMINIMIZED
MAXIMIZE = win32con.SW_MAXIMIZE
SHOWNOACTIVATE = win32con.SW_SHOWNOACTIVATE
SHOW = win32con.SW_SHOW
MINIMIZE = win32con.SW_MINIMIZE
SHOWMINNOACTIVE = win32con.SW_SHOWMINNOACTIVE
SHOWNA = win32con.SW_SHOWNA
RESTORE = win32con.SW_RESTORE
SHOWDEFAULT = win32con.SW_SHOWDEFAULT
FORCEMINIMIZE = win32con.SW_FORCEMINIMIZE


def set_window_state(window_handle: int, state: WindowState) -> None:
win32gui.ShowWindow(window_handle, state)


def get_window_handle_by_process_file_path_suffix(suffix: str) -> int | None:
"""
Tries to look up a window handle that belongs to a prcess that has a module
whose file path ends with suffix.

Returns:
Window handle of a matching window or None if no matching window was found.
"""
extra: list[int] = []
window_handles: list[int] = []

def enum_callback(handle: int, extra: list[int]):
window_handles.append(handle)

win32gui.EnumWindows(enum_callback, extra)
for handle in window_handles:
if (
# Try to skip windows that aren't visible in taskbar
not (
win32gui.GetWindowLong(handle, win32con.GWL_STYLE)
& win32con.WS_EX_APPWINDOW
)
# win32gui.GetParent(handle)
# or not win32gui.GetWindowText(handle)
):
continue

try:
_, process_id = win32process.GetWindowThreadProcessId(handle)
process_handle = win32api.OpenProcess(
win32con.PROCESS_QUERY_INFORMATION + win32con.PROCESS_VM_READ,
False,
process_id,
)
try:
for module_handle in win32process.EnumProcessModules(process_handle):
process_file_path: str = win32process.GetModuleFileNameEx(
process_handle, module_handle
)
if process_file_path.endswith(suffix):
return handle
finally:
win32api.CloseHandle(process_handle)
except pywintypes.error:
pass
except Exception as exc:
print("An exception occurred while getting Photoshop's window handle:", exc)