first commit
This commit is contained in:
+780
@@ -0,0 +1,780 @@
|
||||
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 = ""
|
||||
Reference in New Issue
Block a user