diff --git a/src-python/config.py b/src-python/config.py index 3a3e7c3b..d5e600ac 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -734,6 +734,10 @@ class Config: SELECTED_TRANSLATION_COMPUTE_DEVICE = ValidatedProperty('SELECTED_TRANSLATION_COMPUTE_DEVICE', _compute_device_validator) SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = ValidatedProperty('SELECTED_TRANSCRIPTION_COMPUTE_DEVICE', _compute_device_validator) + # -- Clipboard control --- + ENABLE_COPY_TO_CLIPBOARD = ManagedProperty('ENABLE_COPY_TO_CLIPBOARD', type_=bool) + ENABLE_PASTE_FROM_CLIPBOARD = ManagedProperty('ENABLE_PASTE_FROM_CLIPBOARD', type_=bool) + def init_config(self): # Read Only self._VERSION = "3.3.1" @@ -997,6 +1001,8 @@ class Config: self._WEBSOCKET_SERVER = False self._WEBSOCKET_HOST = "127.0.0.1" self._WEBSOCKET_PORT = 2231 + self._ENABLE_COPY_TO_CLIPBOARD = False + self._ENABLE_PASTE_FROM_CLIPBOARD = False def load_config(self): if os_path.isfile(self.PATH_CONFIG) is not False: diff --git a/src-python/controller.py b/src-python/controller.py index 06df12bf..09c3585f 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -396,6 +396,12 @@ class Controller: ) model.updateOverlayLargeLog(overlay_image) + if config.ENABLE_COPY_TO_CLIPBOARD is True: + clipboard_message = self.messageFormatter("SEND", translation, message) + model.setCopyToClipboard(clipboard_message) + if config.ENABLE_PASTE_FROM_CLIPBOARD is True: + model.setPasteFromClipboard() + if model.checkWebSocketServerAlive() is True: model.websocketSendMessage( { @@ -728,6 +734,12 @@ class Controller: ) model.updateOverlayLargeLog(overlay_image) + if config.ENABLE_COPY_TO_CLIPBOARD is True: + clipboard_message = self.messageFormatter("SEND", translation, message) + model.setCopyToClipboard(clipboard_message) + if config.ENABLE_PASTE_FROM_CLIPBOARD is True: + model.setPasteFromClipboard() + if model.checkWebSocketServerAlive() is True: model.websocketSendMessage( { diff --git a/src-python/model.py b/src-python/model.py index 58f947dc..1a74ee62 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -32,6 +32,7 @@ from models.overlay.overlay import Overlay from models.overlay.overlay_image import OverlayImage from models.watchdog.watchdog import Watchdog from models.websocket.websocket_server import WebSocketServer +from models.clipboard.clipboard import Clipboard from utils import errorLogging, setupLogger class threadFnc(Thread): @@ -138,6 +139,7 @@ class Model: # default no-op callbacks for energy check functions self.check_mic_energy_fnc: Callable[[float], None] = lambda v: None self.check_speaker_energy_fnc: Callable[[float], None] = lambda v: None + self.clipboard = Clipboard() self._inited = True def ensure_initialized(self) -> None: @@ -1201,4 +1203,27 @@ class Model: errorLogging() return False + def setCopyToClipboard(self, text:str) -> bool: + self.ensure_initialized() + try: + if isinstance(self.clipboard, Clipboard): + self.clipboard.copy(text) + return True + else: + return False + except Exception: + errorLogging() + return False + + def setPasteFromClipboard(self) -> bool: + self.ensure_initialized() + try: + if isinstance(self.clipboard, Clipboard): + return self.clipboard.paste() + else: + return False + except Exception: + errorLogging() + return False + model = Model() \ No newline at end of file diff --git a/src-python/models/clipboard/clipboard.py b/src-python/models/clipboard/clipboard.py index 67b55192..c284edc6 100644 --- a/src-python/models/clipboard/clipboard.py +++ b/src-python/models/clipboard/clipboard.py @@ -1,6 +1,8 @@ import sys import time +import os from subprocess import Popen, PIPE +from psutil import process_iter import openvr try: @@ -9,6 +11,10 @@ except ImportError: def printLog(data, *args, **kwargs): print(data, *args, **kwargs) +def checkSteamvrRunning() -> bool: + _proc_name = "vrmonitor.exe" if os.name == "nt" else "vrmonitor" + return _proc_name in (p.name() for p in process_iter()) + # Windows-specific imports via ctypes will be used when focusing windows if sys.platform == 'win32': import ctypes @@ -117,11 +123,10 @@ def paste_via_pyautogui(countdown: int = 0) -> bool: printLog('pyautogui not installed. Install with: pip install pyautogui') return False - printLog(f'Give focus to the target input. Pasting in {countdown} seconds...') for i in range(countdown, 0, -1): print(i, end=' ', flush=True) time.sleep(1) - printLog('\nSending Ctrl+V...') + try: # pyautogui.hotkey is a safe cross-platform way to send keys pyautogui.hotkey('ctrl', 'v') @@ -132,8 +137,8 @@ def paste_via_pyautogui(countdown: int = 0) -> bool: class Clipboard: - def __init__(self, vr_mode: bool = True): - if not vr_mode: + def __init__(self): + if not checkSteamvrRunning(): self.app_name = None else: openvr.init(openvr.VRApplication_Background) @@ -156,6 +161,7 @@ class Clipboard: self.app_name = name break openvr.shutdown() + printLog(f"Clipboard initialized. SteamVR App Name: {self.app_name}") def copy(self, message: str) -> bool: """Copy `message` to clipboard. @@ -179,18 +185,17 @@ class Clipboard: True if paste command was sent, False otherwise. """ - if window_name is None: - window_name = self.app_name + window_name = window_name if window_name is not None else self.app_name - # focus target window (Windows only) — window_name is required - focused = False - if sys.platform == 'win32': - if not window_name: - printLog('paste: window_name is required on Windows') - return False + # If window_name is provided, attempt to focus it (Windows only). + # If window_name is None, skip focusing and paste into the currently focused window. + if window_name is not None and sys.platform == 'win32': + printLog(f"paste: attempting to focus window matching '{window_name}'") + focused = False # try title substring match first wins = find_windows_by_title_substring(window_name) + printLog(f"paste: found {wins} windows matching title substring '{window_name}'") for hwnd in wins: if focus_window(hwnd): focused = True @@ -199,6 +204,7 @@ class Clipboard: # if not found by title, try treating window_name as process name if not focused: wins = find_windows_by_process_name(window_name) + printLog(f"paste: found {wins} windows matching process name '{window_name}'") for hwnd in wins: if focus_window(hwnd): focused = True @@ -218,4 +224,4 @@ class Clipboard: if __name__ == '__main__': clipboard = Clipboard() clipboard.copy("Sample text to copy to clipboard.") - clipboard.paste(window_name=None, countdown=0) \ No newline at end of file + clipboard.paste(window_name=None, countdown=3) \ No newline at end of file