781 lines
33 KiB
Python
781 lines
33 KiB
Python
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 = ""
|