1
0

main.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import sys
  2. import os
  3. import asyncio
  4. import logging
  5. import time
  6. import signal
  7. from pathlib import Path
  8. from PySide6.QtCore import QUrl, QTimer, QObject, QEvent
  9. from PySide6.QtGui import QGuiApplication, QTouchEvent, QMouseEvent
  10. from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
  11. from qasync import QEventLoop
  12. # Load environment variables from .env file if it exists
  13. from dotenv import load_dotenv
  14. load_dotenv(Path(__file__).parent / ".env")
  15. from backend import Backend
  16. from models.pattern_model import PatternModel
  17. from models.playlist_model import PlaylistModel
  18. from png_cache_manager import ensure_png_cache_startup
  19. # Configure logging
  20. logging.basicConfig(
  21. level=logging.INFO,
  22. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  23. )
  24. logger = logging.getLogger(__name__)
  25. class FirstTouchFilter(QObject):
  26. """
  27. Event filter that ignores the first touch event after inactivity.
  28. Many capacitive touchscreens need the first touch to wake up or calibrate,
  29. and this touch often has incorrect coordinates.
  30. """
  31. def __init__(self, idle_threshold_seconds=2.0):
  32. super().__init__()
  33. self.idle_threshold = idle_threshold_seconds
  34. self.last_touch_time = 0
  35. self.ignore_next_touch = False
  36. logger.info(f"👆 First-touch filter initialized (idle threshold: {idle_threshold_seconds}s)")
  37. def eventFilter(self, obj, event):
  38. """Filter out the first touch after idle period"""
  39. try:
  40. event_type = event.type()
  41. # Handle touch events
  42. if event_type == QEvent.Type.TouchBegin:
  43. current_time = time.time()
  44. time_since_last_touch = current_time - self.last_touch_time
  45. # If it's been more than threshold since last touch, ignore this one
  46. if time_since_last_touch > self.idle_threshold:
  47. logger.debug(f"👆 Ignoring wake-up touch (idle for {time_since_last_touch:.1f}s)")
  48. self.last_touch_time = current_time
  49. return True # Filter out (ignore) this event
  50. self.last_touch_time = current_time
  51. elif event_type in (QEvent.Type.TouchUpdate, QEvent.Type.TouchEnd):
  52. # Update last touch time for any touch activity
  53. self.last_touch_time = time.time()
  54. # Pass through the event
  55. return False
  56. except KeyboardInterrupt:
  57. # Re-raise KeyboardInterrupt to allow clean shutdown
  58. raise
  59. except Exception as e:
  60. logger.error(f"Error in eventFilter: {e}")
  61. return False
  62. async def startup_tasks():
  63. """Run async startup tasks"""
  64. logger.info("🚀 Starting dune-weaver-touch async initialization...")
  65. # Ensure PNG cache is available for all WebP previews
  66. try:
  67. logger.info("🎨 Checking PNG preview cache...")
  68. png_cache_success = await ensure_png_cache_startup()
  69. if png_cache_success:
  70. logger.info("✅ PNG cache check completed successfully")
  71. else:
  72. logger.warning("⚠️ PNG cache check completed with warnings")
  73. except Exception as e:
  74. logger.error(f"❌ PNG cache check failed: {e}")
  75. logger.info("✨ dune-weaver-touch startup tasks completed")
  76. def main():
  77. # Enable virtual keyboard
  78. os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard'
  79. app = QGuiApplication(sys.argv)
  80. # Install first-touch filter to ignore wake-up touches
  81. # Ignores the first touch after 2 seconds of inactivity
  82. first_touch_filter = FirstTouchFilter(idle_threshold_seconds=2.0)
  83. app.installEventFilter(first_touch_filter)
  84. logger.info("✅ First-touch filter installed on application")
  85. # Setup async event loop
  86. loop = QEventLoop(app)
  87. asyncio.set_event_loop(loop)
  88. # Register types
  89. qmlRegisterType(Backend, "DuneWeaver", 1, 0, "Backend")
  90. qmlRegisterType(PatternModel, "DuneWeaver", 1, 0, "PatternModel")
  91. qmlRegisterType(PlaylistModel, "DuneWeaver", 1, 0, "PlaylistModel")
  92. # Load QML
  93. engine = QQmlApplicationEngine()
  94. qml_file = Path(__file__).parent / "qml" / "main.qml"
  95. engine.load(QUrl.fromLocalFile(str(qml_file)))
  96. if not engine.rootObjects():
  97. return -1
  98. # Schedule startup tasks after a brief delay to ensure event loop is running
  99. def schedule_startup():
  100. try:
  101. # Check if we're in an event loop context
  102. current_loop = asyncio.get_running_loop()
  103. current_loop.create_task(startup_tasks())
  104. except RuntimeError:
  105. # No running loop, create task directly
  106. asyncio.create_task(startup_tasks())
  107. # Use QTimer to delay startup tasks
  108. startup_timer = QTimer()
  109. startup_timer.timeout.connect(schedule_startup)
  110. startup_timer.setSingleShot(True)
  111. startup_timer.start(100) # 100ms delay
  112. # Setup signal handlers for clean shutdown
  113. def signal_handler(signum, frame):
  114. logger.info("🛑 Received shutdown signal, exiting...")
  115. loop.stop()
  116. app.quit()
  117. signal.signal(signal.SIGINT, signal_handler)
  118. signal.signal(signal.SIGTERM, signal_handler)
  119. try:
  120. with loop:
  121. loop.run_forever()
  122. except KeyboardInterrupt:
  123. logger.info("🛑 KeyboardInterrupt received, shutting down...")
  124. finally:
  125. loop.close()
  126. return 0
  127. if __name__ == "__main__":
  128. sys.exit(main())