|
|
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 = ""
|
|
|
|