1616import sys
1717import os
1818import json
19- import cv2
20- import numpy as np
2119
20+ # IMPORTANT: Import PyQt5 BEFORE cv2 to avoid Qt plugin conflicts on Linux
21+ # cv2 bundles its own Qt which can override the system Qt plugin path
2222from PyQt5 .QtWidgets import QApplication , QWidget , QLabel
2323from PyQt5 .QtCore import Qt , QTimer , QPoint , pyqtSignal
2424from PyQt5 .QtGui import QImage , QPixmap , QCursor , QFont
2525
26+ # Now safe to import cv2 (PyQt5 already claimed the Qt plugins)
27+ import cv2
28+ import numpy as np
29+
2630# Pygame for reliable audio playback
2731import pygame
2832pygame .mixer .init ()
2933
30- # Win32 for global key state detection (Windows)
34+ # Platform-specific key state detection
3135import ctypes
32- user32 = ctypes .windll .user32
3336VK_CONTROL = 0x11
3437VK_MENU = 0x12 # Alt key
3538VK_Q = 0x51 # Q key
3639
40+ # Windows uses user32.dll, Linux/macOS use pynput for global key detection
41+ if sys .platform == "win32" :
42+ user32 = ctypes .windll .user32
43+ pynput_keyboard = None
44+ else :
45+ user32 = None
46+ # Linux/macOS: use pynput for global key state detection
47+ # (Qt modifiers only work when app has focus, but our window is click-through)
48+ try :
49+ from pynput import keyboard as pynput_keyboard
50+
51+ # Global key state tracking
52+ _keys_pressed = set ()
53+
54+ def _on_press (key ):
55+ _keys_pressed .add (key )
56+
57+ def _on_release (key ):
58+ _keys_pressed .discard (key )
59+
60+ # Start global keyboard listener in background thread
61+ _keyboard_listener = pynput_keyboard .Listener (on_press = _on_press , on_release = _on_release )
62+ _keyboard_listener .daemon = True
63+ _keyboard_listener .start ()
64+ print ("pynput keyboard listener started for global key detection" )
65+ except ImportError :
66+ print ("WARNING: pynput not installed. Ctrl+drag may not work on Linux." )
67+ print ("Install with: pip install pynput" )
68+ pynput_keyboard = None
69+ _keys_pressed = set ()
70+
3771# Script directory for relative paths
3872SCRIPT_DIR = os .path .dirname (os .path .abspath (__file__ ))
3973CONFIG_FILE = os .path .join (SCRIPT_DIR , "config.json" )
@@ -58,6 +92,7 @@ def __init__(self, parent=None):
5892
5993 # State tracking
6094 self .running = True
95+ self ._q_pressed = False # For Linux/macOS Q key detection
6196 self .idle_frame_idx = 0
6297 self .phrase_frame_idx = 0
6398 self .is_playing_phrase = False
@@ -569,9 +604,34 @@ def _update_tooltip_position(self):
569604
570605 def _check_modifier_keys (self ):
571606 """Check for Ctrl (drag), Alt (resize), Ctrl+Q (quit), and mouse hover."""
572- ctrl_held = (user32 .GetAsyncKeyState (VK_CONTROL ) & 0x8000 ) != 0
573- alt_held = (user32 .GetAsyncKeyState (VK_MENU ) & 0x8000 ) != 0
574- q_held = (user32 .GetAsyncKeyState (VK_Q ) & 0x8000 ) != 0
607+ if user32 :
608+ # Windows: use GetAsyncKeyState for global key state
609+ ctrl_held = (user32 .GetAsyncKeyState (VK_CONTROL ) & 0x8000 ) != 0
610+ alt_held = (user32 .GetAsyncKeyState (VK_MENU ) & 0x8000 ) != 0
611+ q_held = (user32 .GetAsyncKeyState (VK_Q ) & 0x8000 ) != 0
612+ elif pynput_keyboard :
613+ # Linux/macOS: use pynput for global key state detection
614+ # Check if Ctrl, Alt, or Q keys are in the pressed set
615+ ctrl_held = any (
616+ key in _keys_pressed
617+ for key in [pynput_keyboard .Key .ctrl , pynput_keyboard .Key .ctrl_l , pynput_keyboard .Key .ctrl_r ]
618+ )
619+ alt_held = any (
620+ key in _keys_pressed
621+ for key in [pynput_keyboard .Key .alt , pynput_keyboard .Key .alt_l , pynput_keyboard .Key .alt_r , pynput_keyboard .Key .alt_gr ]
622+ )
623+ # Check for Q key (could be KeyCode or character)
624+ q_held = any (
625+ (hasattr (key , 'char' ) and key .char == 'q' ) or
626+ (hasattr (key , 'vk' ) and key .vk == 81 ) # 81 = Q
627+ for key in _keys_pressed
628+ )
629+ else :
630+ # Fallback: use Qt keyboard modifiers (only works when app has focus)
631+ modifiers = QApplication .keyboardModifiers ()
632+ ctrl_held = bool (modifiers & Qt .ControlModifier )
633+ alt_held = bool (modifiers & Qt .AltModifier )
634+ q_held = getattr (self , '_q_pressed' , False )
575635
576636 # Check mouse hover for tooltip
577637 mouse_pos = QCursor .pos ()
@@ -691,6 +751,18 @@ def mouseReleaseEvent(self, event):
691751 self .setCursor (Qt .OpenHandCursor )
692752 self .save_config ()
693753
754+ def keyPressEvent (self , event ):
755+ """Track key presses for Linux/macOS (Q key detection)."""
756+ if event .key () == Qt .Key_Q :
757+ self ._q_pressed = True
758+ super ().keyPressEvent (event )
759+
760+ def keyReleaseEvent (self , event ):
761+ """Track key releases for Linux/macOS."""
762+ if event .key () == Qt .Key_Q :
763+ self ._q_pressed = False
764+ super ().keyReleaseEvent (event )
765+
694766 def save_config (self ):
695767 """Save position and size to config.json."""
696768 try :
0 commit comments