diff --git a/.jules/bolt.md b/.jules/bolt.md index d72fd7b..7d8a7fa 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -18,3 +18,7 @@ ## 2026-07-06 - [Exploit Mitigation for Pet Interaction] **Инсайт:** Простая проверка пересечения координат курсора с областью питомца позволяет тривиально накручивать (фармить) очки поглаживания пассивным удержанием курсора над спрайтом. Требование минимальной длины мазка (stroke length) и кулдаун-таймер полностью убирают этот эксплойт. **Действие:** В `InputManager` отслеживать вектор поглаживания `stroke_dist_sq >= 900` (минимум 30px мазок мыши) и кулдаун в 500мс между признаниями действия успешным. + +## 2026-07-06 - [Dynamic Window Flag Updates in Qt] +**Инсайт:** Динамическое изменение флагов окна через `setWindowFlags` в PySide6 автоматически скрывает окно. Для предотвращения мигания или исчезновения окна необходимо всегда вызывать метод `show()` сразу после изменения флагов. Настройка `always_on_top` должна быть синхронизирована между SettingsDialog, TrayMenu и ConfigManager для бесшовного UX. +**Действие:** При изменении флагов окна вызывать `self.show()`. Своевременно обновлять состояние QAction в трее и QCheckBox в диалоге настроек при изменении конфигурации из любого источника. diff --git a/src/ui/main_window.py b/src/ui/main_window.py index df1843f..ff9cf10 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -15,11 +15,11 @@ def __init__(self, config_manager=None): self.input_manager = None # Настройка прозрачного и безрамочного окна - self.setWindowFlags( - Qt.WindowStaysOnTopHint | - Qt.FramelessWindowHint | - Qt.Tool - ) + always_on_top = self.config.get("always_on_top") if self.config else True + flags = Qt.FramelessWindowHint | Qt.Tool + if always_on_top: + flags |= Qt.WindowStaysOnTopHint + self.setWindowFlags(flags) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowOpacity(self.config.get("opacity") / 100.0 if self.config else 1.0) @@ -92,6 +92,18 @@ def set_opacity(self, value): """Устанавливает прозрачность окна (0-100)""" self.setWindowOpacity(value / 100.0) + def set_always_on_top(self, enabled): + """Включает или выключает режим 'Поверх всех окон'""" + if self.config: + self.config.set("always_on_top", enabled) + flags = self.windowFlags() + if enabled: + flags |= Qt.WindowStaysOnTopHint + else: + flags &= ~Qt.WindowStaysOnTopHint + self.setWindowFlags(flags) + self.show() + def set_timer_system(self, timer_system): self.timer_system = timer_system diff --git a/src/ui/settings_dialog.py b/src/ui/settings_dialog.py index 17c1a0f..42f44e7 100644 --- a/src/ui/settings_dialog.py +++ b/src/ui/settings_dialog.py @@ -1,4 +1,4 @@ -from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSlider, QComboBox, QPushButton, QSpinBox +from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSlider, QComboBox, QPushButton, QSpinBox, QCheckBox from PySide6.QtCore import Qt from src.utils.bonding_utils import CAT_SKINS @@ -30,6 +30,11 @@ def __init__(self, config, parent=None): self.opacity_slider.setValue(self.config.get("opacity")) layout.addWidget(self.opacity_slider) + # Поверх всех окон + self.always_on_top_check = QCheckBox("Поверх всех окон") + self.always_on_top_check.setChecked(self.config.get("always_on_top")) + layout.addWidget(self.always_on_top_check) + # Интервал растяжки layout.addWidget(QLabel("Интервал растяжки (мин):")) self.stretch_spin = QSpinBox() @@ -76,6 +81,7 @@ def save_settings(self): self.config.set("username", self.name_edit.text()) self.config.set("volume", self.volume_slider.value()) self.config.set("opacity", self.opacity_slider.value()) + self.config.set("always_on_top", self.always_on_top_check.isChecked()) self.config.set("stretch_interval", self.stretch_spin.value()) self.config.set("pomodoro_work", self.pomodoro_work_spin.value()) self.config.set("pomodoro_break", self.pomodoro_break_spin.value()) diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index c7ca788..959c9db 100644 --- a/src/ui/tray_menu.py +++ b/src/ui/tray_menu.py @@ -90,6 +90,15 @@ def setup_menu(self): self.menu.addSeparator() + # Поверх всех окон + self.always_on_top_action = QAction("Поверх всех окон", self) + self.always_on_top_action.setCheckable(True) + self.always_on_top_action.setChecked(self.window.config.get("always_on_top") if self.window.config else True) + self.always_on_top_action.triggered.connect(self.toggle_always_on_top) + self.menu.addAction(self.always_on_top_action) + + self.menu.addSeparator() + # Статистика stats_action = QAction("Статистика", self) stats_action.triggered.connect(self.show_stats) @@ -177,6 +186,10 @@ def show_settings(self, checked=False): # Обновляем скин и прозрачность в реальном времени self.window.animation_manager.set_skin(self.window.config.get("skin")) self.window.set_opacity(self.window.config.get("opacity")) + # Обновляем поверх всех окон + always_on_top = self.window.config.get("always_on_top") if self.window.config else True + self.window.set_always_on_top(always_on_top) + self.always_on_top_action.setChecked(always_on_top) # Обновляем тексты в меню Pomodoro self.update_pomodoro_menu_texts() # Перезапускаем таймер растяжки с новым интервалом @@ -208,3 +221,7 @@ def toggle_laser(self, checked): self.show_message("Мини-игра", "Лазерная указка активирована! 🔴") else: self.show_message("Мини-игра", "Лазерная указка выключена.") + + def toggle_always_on_top(self, checked): + self.window.set_always_on_top(checked) + self.window.show_message("Поверх окон: " + ("вкл 🔝" if checked else "выкл 🔓")) diff --git a/tests/test_utils.py b/tests/test_utils.py index e363433..85b9a21 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -122,5 +122,35 @@ def test_input_manager_petting_logic(self): db.close() + def test_pet_window_always_on_top(self): + # We need a QApplication to instantiate QWidgets/QMainWindow + from PySide6.QtWidgets import QApplication + from src.ui.main_window import PetWindow + from PySide6.QtCore import Qt + + app = QApplication.instance() + if not app: + os.environ["QT_QPA_PLATFORM"] = "offscreen" + app = QApplication(sys.argv) + + config = ConfigManager(self.config_path) + + # 1. Test starting as True (default) + config.set("always_on_top", True) + window = PetWindow(config) + self.assertTrue(bool(window.windowFlags() & Qt.WindowStaysOnTopHint)) + + # 2. Test toggling to False + window.set_always_on_top(False) + self.assertFalse(bool(window.windowFlags() & Qt.WindowStaysOnTopHint)) + self.assertFalse(config.get("always_on_top")) + + # 3. Test toggling back to True + window.set_always_on_top(True) + self.assertTrue(bool(window.windowFlags() & Qt.WindowStaysOnTopHint)) + self.assertTrue(config.get("always_on_top")) + + window.close() + if __name__ == '__main__': unittest.main() diff --git a/verification/screenshots/stats_history.png b/verification/screenshots/stats_history.png index 9dbf806..2171ec2 100644 Binary files a/verification/screenshots/stats_history.png and b/verification/screenshots/stats_history.png differ