import sys import time import os from PyQt6.QtWidgets import (QMainWindow, QVBoxLayout, QWidget, QSystemTrayIcon, QMenu, QApplication, QHBoxLayout, QPushButton) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QPoint from PyQt6.QtGui import QFont, QPainter, QIcon, QAction, QPixmap, QDragEnterEvent, QDropEvent, QMouseEvent import uiautomation as auto from ai_worker import AIWorker from drag_drop_handler import DragDropHandler from icon_window import IconWindow from main_interface import MainInterface import qtawesome as qta import pyperclip import keyboard class TransparentWindow(QMainWindow): text_to_insert = pyqtSignal(str) def __init__(self): super().__init__() self.is_visible = False # Видимость окна (скрыто/показано) self.is_expanded = False # Ширина окна (узкое/широкое) self.hook_manager = None self.dragging = False self.drag_offset = QPoint() self.drag_n_drop_active = False self.setup_ui() self.setup_tray() self.setup_focus_monitor() self.hide() # Изначально окно скрыто self.last_focus_state = False self.last_active_timestamp = time.time() # Инициализация AI worker self.ai_worker = AIWorker() self.ai_worker.text_processed.connect(self.on_text_processed) self.ai_worker.image_processed.connect(self.on_image_processed) self.ai_worker.processing_finished.connect(self.on_processing_finished) self.ai_worker.start() # Инициализация drag-drop handler self.drag_drop_handler = DragDropHandler(self) # Переменные состояния self.current_complaints_text = "" self.current_history_text = "" self.current_image_path = "" self.is_processing = False self.pending_text_type = None # Настройка drag-and-drop self.setAcceptDrops(True) def setup_ui(self): """Настраивает основной интерфейс окна""" # Устанавливаем прозрачность и убираем рамку self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # Получаем размеры экрана screen_geometry = QApplication.primaryScreen().geometry() screen_width = screen_geometry.width() screen_height = screen_geometry.height() # Размеры окон self.collapsed_width = 70 self.expanded_width = 200 self.window_height = 300 # Позиционируем основное окно слева по центру main_x_position = 10 main_y_position = (screen_height - self.window_height) // 2 # Устанавливаем начальную позицию основного окна (свернутое состояние) self.setGeometry(main_x_position, main_y_position, self.collapsed_width, self.window_height) # Создаем центральный виджет self.central_widget = QWidget() self.central_widget.setObjectName("CentralWidget") self.central_widget.setAcceptDrops(True) self.setCentralWidget(self.central_widget) self.central_widget.mousePressEvent = self.mouse_press_event self.central_widget.mouseMoveEvent = self.mouse_move_event self.central_widget.mouseReleaseEvent = self.mouse_release_event # Основной layout self.main_layout = QVBoxLayout(self.central_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(0) self.top_layout = QHBoxLayout() self.top_layout.setContentsMargins(4, 4, 4, 0) self.top_layout.setSpacing(0) self.top_layout.addStretch() icn = qta.icon("ph.gear-six-bold", color=('#FFFFFF', 255)) button_1 = QPushButton(icn, "") button_1.setStyleSheet(""" QPushButton { color: #FFFFFF; background-color: rgba(0, 0, 0, 0); padding: 4px 4px; border-radius: 5px; font-size: 14px; font-weight: 650; margin: 0px 3px; text-align: left; max-width: 16px; } QPushButton:hover { background-color: rgba(0, 0, 0, 100); } QPushButton:pressed { background-color: rgba(0, 0, 0, 220); } QPushButton:disabled { color: #888888; } """) icn = qta.icon("ph.push-pin-bold", color=('#FFFFFF', 255)) button_2 = QPushButton(icn, "") button_2.setStyleSheet(""" QPushButton { color: #FFFFFF; background-color: rgba(0, 0, 0, 0); padding: 4px 4px; border-radius: 5px; font-size: 14px; font-weight: 650; margin: 0px 3px; text-align: left; max-width: 16px; } QPushButton:hover { background-color: rgba(0, 0, 0, 100); } QPushButton:pressed { background-color: rgba(0, 0, 0, 220); } QPushButton:disabled { color: #888888; } """) self.top_layout.addWidget(button_1) self.top_layout.addWidget(button_2) self.main_layout.addLayout(self.top_layout) # Создаем и показываем иконку self.icon_window = IconWindow(self) self.icon_window.icon_clicked.connect(self.toggle_window_visibility) self.icon_window.show_icon() # .addLayout(self.central_widget) # Создаем основной интерфейс self.main_interface = MainInterface(self) self.main_interface.button_clicked.connect(self.on_button_click) self.main_interface.hide() # Изначально скрыт self.main_layout.addWidget(self.main_interface) self.central_widget.setStyleSheet(""" #CentralWidget { background: qlineargradient( x1:0, y1:0, x2:0, y2:1, stop:0 rgba(30, 30, 30, 220), stop:1 rgba(40, 40, 40, 240) ); border-radius: 15px; } """) self.text_to_insert.connect(self.insert_text) def mouse_press_event(self, event: QMouseEvent): """Обработчик нажатия мыши на иконку""" if event.button() == Qt.MouseButton.LeftButton: self.dragging = True self.drag_offset = event.position().toPoint() event.accept() def mouse_move_event(self, event: QMouseEvent): """Обработчик движения мыши для иконки""" if self.dragging and event.buttons() & Qt.MouseButton.LeftButton: global_pos = self.central_widget.mapToGlobal(event.position().toPoint()) new_pos = global_pos - self.drag_offset + QPoint(-10,-10) self.move(new_pos) event.accept() def mouse_release_event(self, event: QMouseEvent): """Обработчик отпускания мыши для иконки""" if event.button() == Qt.MouseButton.LeftButton and self.dragging: # Решение конфликта событий mouse_press_event и clicked self.dragging = False event.accept() def toggle_window_visibility(self): """Переключает видимость окна (скрыть/показать)""" if self.is_visible: self.hide_window() else: self.show_window() def show_window(self): """Показывает окно в узком состоянии""" if self.is_visible: return self.is_visible = True self.show() self.raise_() # Показываем основной интерфейс self.main_interface.show() # Устанавливаем узкую ширину и показываем compact drop область # self.resize(self.collapsed_width, self.window_height) # self.main_interface.hide_buttons() # self.main_interface.show_compact_drop_area() self.expand_for_text_field() def hide_window(self): """Полностью скрывает окно""" if not self.is_visible: return self.is_visible = False self.hide() self.main_interface.hide_all_drop_areas() def expand_for_drag_drop(self, mime_data): """Расширяет окно для drag-n-drop""" if not self.is_visible: self.show_window() if not self.is_expanded: self.animate_window_width(self.expanded_width) self.is_expanded = True # Показываем соответствующие drop области (без кнопок) self.main_interface.hide_buttons() self.main_interface.hide_all_drop_areas() if mime_data.hasUrls(): urls = mime_data.urls() if urls and urls[0].isLocalFile(): file_path = urls[0].toLocalFile() if self.drag_drop_handler.is_image_file(file_path): self.main_interface.show_image_drop_interface() else: self.main_interface.show_text_drop_interface() else: urls = mime_data.urls() file_path = urls[0] self.main_interface.show_image_drop_interface() elif mime_data.hasText(): self.main_interface.show_text_drop_interface() def expand_for_text_field(self): """Расширяет окно для работы с текстовым полем""" if not self.is_visible: self.show_window() if not self.is_expanded: self.animate_window_width(self.expanded_width) self.is_expanded = True # Показываем кнопки (без drop областей) self.main_interface.show_buttons() self.main_interface.hide_all_drop_areas() def collapse_to_compact(self): """Сворачивает окно до компактного состояния""" if not self.is_expanded: return self.animate_window_width(self.collapsed_width) self.is_expanded = False # Показываем compact drop область self.main_interface.hide_buttons() self.main_interface.show_compact_drop_area() def animate_window_width(self, target_width, callback=None): """Анимирует изменение ширины окна""" # self.resize(target_width, self.window_height) if callback: callback() def show_drag_drop_interface(self, mime_data): """Показывает интерфейс для drag-n-drop""" self.expand_for_drag_drop(mime_data) def handle_dropped_image(self, image_path): """Обрабатывает перетащенное изображение""" try: self.current_image_path = image_path print("⚠️ NEW Image. URL:", image_path) # Запускаем обработку изображения self.set_processing_state(True) self.main_interface.buttons[2].setText(" ЛОР-Статус (обработка...)") self.main_interface.buttons[3].setText(" Заключение ИИ (обработка...)") self.ai_worker.process_image(image_path) self.main_interface.show_compact_drop_area() except Exception as e: self.show_error(f"Не удалось загрузить изображение: {str(e)}") self.set_processing_state(False) self.main_interface.show_compact_drop_area() def handle_dropped_text(self, text, drop_type, y_position): """Обрабатывает перетащенный текст""" if not text.strip() and drop_type == 'file_drop': try: text = "Текст из перетащенного файла" except: text = "Не удалось прочитать файл" if y_position < self.window_height/2: # Обрабатываем как жалобы self.start_text_processing(text, 'complaints') else: # Обрабатываем как жалобы self.start_text_processing(text, 'history') self.main_interface.show_compact_drop_area() def start_text_processing(self, text, processing_type): """Запускает обработку текста""" self.set_processing_state(True) if processing_type == 'complaints': self.main_interface.buttons[0].setText(" Обработка...") self.pending_text_type = 'complaints' else: self.main_interface.buttons[1].setText(" Обработка...") self.pending_text_type = 'history' self.ai_worker.process_text(text, processing_type) def set_processing_state(self, processing): """Устанавливает состояние обработки""" self.is_processing = processing self.main_interface.set_buttons_enabled(not processing) self.main_interface.set_processing_state(processing) def on_text_processed(self, result, processing_type): """Обработчик завершения обработки текста""" if processing_type == 'complaints': self.current_complaints_text = result self.main_interface.buttons[0].setText(" Вставить жалобы (ИИ)") else: self.current_history_text = result self.main_interface.buttons[1].setText(" Вставить анамнез (ИИ)") def on_image_processed(self, result): """Обработчик завершения обработки изображения""" self.current_image_path = result self.main_interface.buttons[2].setText(" ЛОР-Статус (ИИ)") self.main_interface.buttons[3].setText(" Заключение ИИ (готово)") def on_processing_finished(self, processing_type): """Обработчик завершения любой обработки""" self.set_processing_state(False) # Drag and Drop events def dragEnterEvent(self, event: QDragEnterEvent): """Обработчик события перетаскивания в окно""" self.drag_drop_handler.handle_drag_enter(event) self.drag_n_drop_active = True def dragLeaveEvent(self, event): """Обработчик события выхода перетаскивания из окна""" if self.is_visible and not self.is_processing: self.collapse_to_compact() self.drag_n_drop_active = False def dropEvent(self, event: QDropEvent): """Обработчик события отпускания перетаскиваемого объекта""" self.drag_drop_handler.handle_drop(event) self.drag_n_drop_active = False def setup_tray(self): """Настраиваем системный трей""" self.tray_icon = QSystemTrayIcon(self) pixmap = QPixmap(16, 16) pixmap.fill(Qt.GlobalColor.blue) self.tray_icon.setIcon(QIcon(pixmap)) self.tray_icon.setToolTip("Input Helper\nАвтоматическая панель для ввода текста") tray_menu = QMenu() show_action = QAction("Показать панель", self) show_action.triggered.connect(self.show_window) hide_action = QAction("Скрыть панель", self) hide_action.triggered.connect(self.hide_window) quit_action = QAction("Выход", self) quit_action.triggered.connect(self.quit_application) tray_menu.addAction(show_action) tray_menu.addAction(hide_action) tray_menu.addSeparator() tray_menu.addAction(quit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.activated.connect(self.tray_icon_activated) self.tray_icon.show() def is_editable_field(self): try: focused = auto.GetFocusedControl() if not focused: return False control_type = focused.ControlType if control_type in [auto.ControlType.EditControl, auto.ControlType.ComboBoxControl, auto.ControlType.TextControl]: return True return False except Exception as e: return False def setup_focus_monitor(self): """Мониторинг фокуса ввода через таймер""" self.focus_timer = QTimer() self.focus_timer.timeout.connect(self.check_focused_control) self.focus_timer.start(200) def check_focused_control(self): """Проверяет, есть ли фокус на редактируемом поле""" try: if self.drag_n_drop_active: return 0 current_focus_state = self.is_editable_field() if current_focus_state: self.last_active_timestamp = time.time() if not self.last_focus_state: # При фокусе на текстовом поле - расширяем окно и показываем кнопки self.expand_for_text_field() else: if self.geometry().width() < self.expanded_width: self.animate_window_width(self.expanded_width) self.is_expanded = True self.main_interface.show_buttons() self.main_interface.hide_all_drop_areas() else: if time.time() - self.last_active_timestamp > 5: self.last_active_timestamp = time.time() if not self.last_focus_state: # При фокусе на текстовом поле - расширяем окно и показываем кнопки self.expand_for_text_field() else: if self.geometry().width() < self.expanded_width: self.animate_window_width(self.expanded_width) self.is_expanded = True self.main_interface.show_buttons() self.main_interface.hide_all_drop_areas() self.last_focus_state = current_focus_state except Exception: pass def on_button_click(self, index): """Обработчик клика по кнопке""" if index == len(self.main_interface.buttons) - 1: # Последняя кнопка - скрыть self.restart_form() elif index == 0: # Кнопка жалоб self.handle_complaints_button() elif index == 1: # Кнопка анамнеза self.handle_history_button() elif index == 2: # ЛОР-Статус if "(ИИ)" in self.main_interface.buttons[2].text() and self.current_image_path: self.insert_text(self.current_image_path) else: self.insert_text("""Отоскопия (эндоскопия уха): Правое ухо: Наружный слуховой проход широкий, кожа розовая, чистая. Барабанная перепонка перламутрово-серого цвета, опознавательные знаки четко дифференцируются. Левое ухо: Визуализируется обтурирующая масса в костном отделе наружного слухового прохода.""") elif index == 3: # Заключение ИИ if "(готово)" in self.main_interface.buttons[3].text() and self.current_image_path: self.insert_text(self.current_image_path) else: self.insert_text("H68.0 Воспаление слуховой (евстахиевой) трубы") def handle_complaints_button(self): """Обрабатывает нажатие кнопки жалоб""" button = self.main_interface.buttons[0] text = button.text() if "Копировать" in text: self.copy_current_text('complaints') elif "Вставить" in text and self.current_complaints_text: self.insert_text(self.current_complaints_text) def handle_history_button(self): """Обрабатывает нажатие кнопки анамнеза""" button = self.main_interface.buttons[1] text = button.text() if "Копировать" in text: self.copy_current_text('history') elif "Вставить" in text and self.current_history_text: self.insert_text(self.current_history_text) def copy_current_text(self, processing_type): """Копирует текст из активного поля и запускает обработку""" try: self.hide_window() self.is_visible = False if processing_type == 'complaints': self.main_interface.buttons[0].setText(" Обработка...") else: self.main_interface.buttons[1].setText(" Обработка...") self.set_processing_state(True) QTimer.singleShot(50, lambda: self._do_copy_and_process(processing_type)) except Exception as e: print(f"Ошибка при копировании: {e}") self.set_processing_state(False) # def _do_copy_and_process(self, processing_type): # """Копирует текст и запускает обработку""" # try: # # original_text = pyperclip.paste() # # keyboard.press_and_release('ctrl+a') # time.sleep(0.3) # keyboard.press_and_release('ctrl+c') # time.sleep(0.3) # keyboard.press_and_release('end') # time.sleep(0.3) # # copied_text = pyperclip.paste() # # pyperclip.copy(original_text) # # print(copied_text) # if copied_text: #and copied_text != original_text # self.ai_worker.process_text(copied_text, processing_type) # else: # if processing_type == 'complaints': # self.main_interface.buttons[0].setText(" Копировать жалобы") # else: # self.main_interface.buttons[1].setText(" Копировать анамнез") # print("Не удалось скопировать текст из активного поля") # self.show_error("Не удалось скопировать текст из активного поля") # self.set_processing_state(False) # # QTimer.singleShot(100, self.show_window) # # except Exception as e: # print(f"Ошибка при работе с pyperclip: {e}") # self.set_processing_state(False) # QTimer.singleShot(100, self.show_window) def _do_copy_and_process(self, processing_type): """Копирует выделенный текст из любого окна (включая Chrome) через UI Automation""" try: copied_text = None if auto is not None: try: selected_text = self._get_selected_text_uia() if selected_text: copied_text = selected_text except Exception as e: print(f"UIA сбой: {e}") copied_text = None print("Auto:", copied_text) # Fallback: если UIA недоступен или не сработал — используем SendInput if not copied_text: copied_text = self._copy_via_sendinput() print("SendInput:", copied_text) # Обработка результата if copied_text: self.ai_worker.process_text(copied_text, processing_type) else: if processing_type == 'complaints': self.main_interface.buttons[0].setText(" Копировать жалобы") else: self.main_interface.buttons[1].setText(" Копировать анамнез") print("Не удалось скопировать текст из активного поля") self.show_error("Не удалось скопировать текст из активного поля") self.set_processing_state(False) QTimer.singleShot(100, self.show_window) except Exception as e: print(f"Ошибка при работе с pyperclip: {e}") self.set_processing_state(False) QTimer.singleShot(100, self.show_window) def _get_selected_text_uia(self): """Получает весь текст из активного редактируемого поля через UI Automation""" try: focused = auto.GetFocusedControl() if not focused: return None # Игнорируем известные "пустышки" if focused.ClassName == "IHWindowClass" and "Input Capture" in (focused.Name or ""): # RDP: невозможно получить текст локально return None # Пытаемся получить TextPattern (даже у ComboBoxControl в Chrome он есть!) text_pattern = focused.GetTextPattern() if text_pattern is not None: try: full_range = text_pattern.DocumentRange if full_range: full_text = full_range.GetText(-1) if isinstance(full_text, str): return full_text.strip() except Exception as e: print(f"Ошибка при получении DocumentRange: {e}") # Fallback: если TextPattern недоступен, пробуем Value (осторожно!) if hasattr(focused, 'LegacyIAccessible'): acc = focused.LegacyIAccessible value = getattr(acc, 'Value', None) if isinstance(value, str) and value.strip(): # Дополнительная проверка: если Name похож на placeholder — не доверяем Value # Но в большинстве нативных приложений Value = реальный текст return value.strip() except Exception as e: print(f"UIA: ошибка получения текста: {e}") return None def _copy_via_sendinput(self): try: # Сохраняем оригинальный буфер original_clipboard = pyperclip.paste() # Очищаем буфер перед копированием (чтобы точно знать, что скопировалось) pyperclip.copy("") time.sleep(0.15) # Выделяем всё и копируем keyboard.press_and_release('ctrl+a', do_press=True, do_release=False) time.sleep(0.2) keyboard.press_and_release('ctrl+a', do_press=False, do_release=True) time.sleep(0.2) keyboard.press_and_release('ctrl+c', do_press=True, do_release=False) time.sleep(0.2) keyboard.press_and_release('ctrl+c', do_press=False, do_release=True) time.sleep(0.4) # Достаточно для большинства случаев # # Перемещаем курсор в конец (опционально) # keyboard.press_and_release('end') time.sleep(0.15) # Получаем результат copied_text = pyperclip.paste().strip() # Восстанавливаем оригинальный буфер pyperclip.copy(original_clipboard) if copied_text: return copied_text else: return None except Exception as e: print(f"Ошибка при копировании: {e}") return None def insert_text(self, text): """Вставляет текст в активное поле ввода""" try: self.hide_window() self.is_visible = False if text: QTimer.singleShot(50, lambda: self._do_insert_pyperclip(text)) except Exception as e: print(f"Ошибка при вставке: {e}") def _do_insert_pyperclip(self, text): """Вставка текста с использованием pyperclip""" try: import pyperclip original_text = pyperclip.paste() pyperclip.copy(text) QTimer.singleShot(30, lambda: self._paste_and_restore(original_text)) except Exception as e: print(f"Ошибка при работе с pyperclip: {e}") QTimer.singleShot(100, self.show_window) def _paste_and_restore(self, original_text): """Вставляет текст и восстанавливает буфер""" try: keyboard.press_and_release('ctrl+v') QTimer.singleShot(100, lambda: pyperclip.copy(original_text)) QTimer.singleShot(100, self.show_window) except Exception as e: print(f"Ошибка при вставке: {e}") QTimer.singleShot(100, self.show_window) def show_error(self, message): """Показывает сообщение об ошибке""" print(f"Ошибка: {message}") def tray_icon_activated(self, reason): """Обработчик активации иконки в трее""" if reason == QSystemTrayIcon.ActivationReason.DoubleClick: if self.is_visible: self.hide_window() else: self.show_window() def quit_application(self): """Корректный выход из приложения""" if self.hook_manager: try: self.hook_manager.UnhookMouse() except: pass if self.ai_worker.isRunning(): self.ai_worker.stop() self.tray_icon.hide() QApplication.quit() def paintEvent(self, event): """Отрисовывает тень вокруг окна""" painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) shadow_margin = 8 shadow_rect = self.rect().adjusted( shadow_margin, shadow_margin, -shadow_margin, -shadow_margin ) painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(Qt.GlobalColor.black) painter.setOpacity(0.2) painter.drawRoundedRect(shadow_rect, 15, 15) painter.end() super().paintEvent(event) def closeEvent(self, event): """Обработчик закрытия окна""" event.ignore() self.hide_window() def restart_form(self): self.main_interface.buttons[0].setText(" Копировать жалобы") self.main_interface.buttons[1].setText(" Копировать анамнез") self.main_interface.buttons[2].setText(" ЛОР-Статус") self.main_interface.buttons[3].setText(" Заключение ИИ") self.current_complaints_text = "" self.current_history_text = "" self.current_image_path = ""