main.py 5.4 KB

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