from PySide6.QtCore import Qt, QPoint, QRect, QTimer, QBuffer from PySide6.QtGui import (QKeySequence, QShortcut, QAction, QPainter, QFont, QScreen, QIcon) from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QSystemTrayIcon, QMenu) import sys, io, os, signal, time, platform from PIL import Image from dataclasses import dataclass from typing import List, Optional from config import ADD_OVERLAY, FONT_FILE, FONT_SIZE from logging_config import logger def qpixmap_to_bytes(qpixmap): qimage = qpixmap.toImage() buffer = QBuffer() buffer.open(QBuffer.ReadWrite) qimage.save(buffer, "PNG") return qimage @dataclass class TextEntry: text: str x: int y: int font: QFont = QFont('Arial', FONT_SIZE) visible: bool = True text_color: Qt.GlobalColor = Qt.GlobalColor.red background_color: Optional[Qt.GlobalColor] = None padding: int = 1 class TranslationOverlay(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Translation Overlay") self.is_passthrough = True self.text_entries: List[TextEntry] = [] self.setup_window_attributes() self.setup_shortcuts() self.closeEvent = lambda event: QApplication.quit() self.default_font = QFont('Arial', FONT_SIZE) self.text_entries_copy: List[TextEntry] = [] self.next_text_entries: List[TextEntry] = [] #self.show_background = True self.background_opacity = 0.5 # self.setup_tray() def prepare_for_capture(self): """Preserve current state and clear overlay""" if ADD_OVERLAY: self.text_entries_copy = self.text_entries.copy() self.clear_all_text() self.update() def restore_after_capture(self): """Restore overlay state after capture""" if ADD_OVERLAY: logger.debug(f'Text entries copy during initial phase of restore_after_capture: {self.text_entries_copy}') self.text_entries = self.text_entries_copy.copy() logger.debug(f"Restored text entries: {self.text_entries}") self.update() def add_next_text_at_position_no_update(self, x: int, y: int, text: str, font: Optional[QFont] = None, text_color: Qt.GlobalColor = Qt.GlobalColor.red): """Add new text without triggering update""" entry = TextEntry( text=text, x=x, y=y, font=font or self.default_font, text_color=text_color ) self.next_text_entries.append(entry) def update_translation(self, ocr_output, translation): # Update your overlay with new translations here # You'll need to implement the logic to display the translations self.clear_all_text() self.text_entries = self.next_text_entries.copy() self.next_text_entries.clear() self.update() def capture_behind(self, x=None, y=None, width=None, height=None): """ Capture the screen area behind the overlay. If no coordinates provided, captures the area under the window. """ # Temporarily hide the window self.hide() # Get screen screen = QScreen.grabWindow( self.screen(), 0, x if x is not None else self.x(), y if y is not None else self.y(), width if width is not None else self.width(), height if height is not None else self.height() ) # Show the window again self.show() screen_bytes = qpixmap_to_bytes(screen) return screen_bytes def clear_all_text(self): """Clear all text entries""" self.text_entries.clear() self.update() def setup_window_attributes(self): # Set window flags for overlay behavior self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) # Set attributes for transparency self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # Make the window cover the entire screen self.setGeometry(QApplication.primaryScreen().geometry()) # Special handling for Wayland if platform.system() == "Linux": if "WAYLAND_DISPLAY" in os.environ: self.setAttribute(Qt.WidgetAttribute.WA_X11NetWmWindowTypeCombo) self.setAttribute(Qt.WidgetAttribute.WA_DontCreateNativeAncestors) def setup_shortcuts(self): # Toggle visibility (Alt+Shift+T) self.toggle_visibility_shortcut = QShortcut(QKeySequence("Alt+Shift+T"), self) self.toggle_visibility_shortcut.activated.connect(self.toggle_visibility) # Toggle passthrough mode (Alt+Shift+P) self.toggle_passthrough_shortcut = QShortcut(QKeySequence("Alt+Shift+P"), self) self.toggle_passthrough_shortcut.activated.connect(self.toggle_passthrough) # Quick hide (Escape) self.hide_shortcut = QShortcut(QKeySequence("Esc"), self) self.hide_shortcut.activated.connect(self.hide) # Clear all text (Alt+Shift+C) self.clear_shortcut = QShortcut(QKeySequence("Alt+Shift+C"), self) self.clear_shortcut.activated.connect(self.clear_all_text) # Toggle background self.toggle_background_shortcut = QShortcut(QKeySequence("Alt+Shift+B"), self) self.toggle_background_shortcut.activated.connect(self.toggle_background) def toggle_visibility(self): if self.isVisible(): self.hide() else: self.show() def toggle_passthrough(self): self.is_passthrough = not self.is_passthrough if self.is_passthrough: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) if platform.system() == "Linux" and "WAYLAND_DISPLAY" not in os.environ: self.setWindowFlags(self.windowFlags() | Qt.WindowType.X11BypassWindowManagerHint) else: self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) if platform.system() == "Linux" and "WAYLAND_DISPLAY" not in os.environ: self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.X11BypassWindowManagerHint) self.hide() self.show() def toggle_background(self): """Toggle background visibility""" self.show_background = not self.show_background self.update() def set_background_opacity(self, opacity: float): """Set background opacity (0.0 to 1.0)""" self.background_opacity = max(0.0, min(1.0, opacity)) self.update() def add_text_at_position(self, x: int, y: int, text: str): """Add new text at specific coordinates""" entry = TextEntry(text, x, y) self.text_entries.append(entry) self.update() def update_text_at_position(self, x: int, y: int, text: str): """Update text at specific coordinates, or add if none exists""" # Look for existing text entry near these coordinates (within 5 pixels) for entry in self.text_entries: if abs(entry.x - x) <= 1 and abs(entry.y - y) <= 1: entry.text = text self.update() return # If no existing entry found, add new one self.add_text_at_position(x, y, text) def setup_tray(self): self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon.fromTheme("applications-system")) tray_menu = QMenu() toggle_action = tray_menu.addAction("Show/Hide Overlay") toggle_action.triggered.connect(self.toggle_visibility) toggle_passthrough = tray_menu.addAction("Toggle Passthrough") toggle_passthrough.triggered.connect(self.toggle_passthrough) # Add background toggle to tray menu toggle_background = tray_menu.addAction("Toggle Background") toggle_background.triggered.connect(self.toggle_background) clear_action = tray_menu.addAction("Clear All Text") clear_action.triggered.connect(self.clear_all_text) tray_menu.addSeparator() quit_action = tray_menu.addAction("Quit") quit_action.triggered.connect(self.clean_exit) self.tray_icon.setToolTip("Translation Overlay") self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() self.tray_icon.activated.connect(self.tray_activated) def remove_text_at_position(self, x: int, y: int): """Remove text entry near specified coordinates""" self.text_entries = [ entry for entry in self.text_entries if abs(entry.x - x) > 1 or abs(entry.y - y) > 1 ] self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # Draw each text entry for entry in self.text_entries: if not entry.visible: continue # Set the font for this specific entry painter.setFont(entry.font) text_metrics = painter.fontMetrics() # Get the bounding rectangles for text text_bounds = text_metrics.boundingRect( entry.text ) total_width = text_bounds.width() total_height = text_bounds.height() # Create rectangles for text placement text_rect = QRect(entry.x, entry.y, total_width, total_height) # Calculate background rectangle that encompasses both texts if entry.background_color is not None: bg_rect = QRect(entry.x - entry.padding, entry.y - entry.padding, total_width + (2 * entry.padding), total_height + (2 * entry.padding)) painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(entry.background_color) painter.drawRect(bg_rect) # Draw the texts painter.setPen(entry.text_color) painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft, entry.text) def handle_exit(signum, frame): QApplication.quit() def start_overlay(): app = QApplication(sys.argv) # Enable Wayland support if available if platform.system() == "Linux" and "WAYLAND_DISPLAY" in os.environ: app.setProperty("platform", "wayland") overlay = TranslationOverlay() overlay.show() signal.signal(signal.SIGINT, handle_exit) # Handle Ctrl+C (KeyboardInterrupt) signal.signal(signal.SIGTERM, handle_exit) return (app, overlay) # sys.exit(app.exec()) if ADD_OVERLAY: app, overlay = start_overlay() if __name__ == "__main__": ADD_OVERLAY = True if not ADD_OVERLAY: app, overlay = start_overlay() overlay.add_text_at_position(600, 100, "Hello World I AM A BIG FAAT FOROGGGGGGGGGG") capture = overlay.capture_behind() capture.save("capture.png") sys.exit(app.exec())