diff --git a/src/ui/main_window.py b/src/ui/main_window.py index df1843f..cf53f56 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -15,11 +15,10 @@ def __init__(self, config_manager=None): self.input_manager = None # Настройка прозрачного и безрамочного окна - self.setWindowFlags( - Qt.WindowStaysOnTopHint | - Qt.FramelessWindowHint | - Qt.Tool - ) + flags = Qt.FramelessWindowHint | Qt.Tool + if self.config is None or self.config.get("always_on_top") is True: + 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) @@ -88,6 +87,22 @@ def moveEvent(self, event): def get_cached_pos(self): return self._cached_pos + 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) + # В Qt изменение флагов скрывает окно, поэтому его нужно перепоказать + self.show() + def set_opacity(self, value): """Устанавливает прозрачность окна (0-100)""" self.setWindowOpacity(value / 100.0) diff --git a/src/ui/settings_dialog.py b/src/ui/settings_dialog.py index 17c1a0f..89af19a 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 @@ -50,6 +50,11 @@ def __init__(self, config, parent=None): self.pomodoro_break_spin.setValue(self.config.get("pomodoro_break")) layout.addWidget(self.pomodoro_break_spin) + # Поверх всех окон + 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.skin_combo = QComboBox() @@ -79,5 +84,6 @@ def save_settings(self): 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()) + self.config.set("always_on_top", self.always_on_top_check.isChecked()) self.config.set("skin", self.skin_combo.currentData()) self.accept() diff --git a/src/ui/tray_menu.py b/src/ui/tray_menu.py index c7ca788..d9022fe 100644 --- a/src/ui/tray_menu.py +++ b/src/ui/tray_menu.py @@ -88,6 +88,13 @@ def setup_menu(self): peek_action.triggered.connect(lambda checked=False: self.window.toggle_peek_mode()) self.menu.addAction(peek_action) + # Поверх всех окон + 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() # Статистика @@ -171,12 +178,18 @@ def start_break_timer(self, checked=False): if self.window.input_manager and self.window.input_manager.db: self.window.input_manager.db.log_event("pomodoro_start", "Начата сессия отдыха") + def toggle_always_on_top(self, checked): + self.window.set_always_on_top(checked) + def show_settings(self, checked=False): dialog = SettingsDialog(self.window.config, self.window) if dialog.exec(): # Обновляем скин и прозрачность в реальном времени self.window.animation_manager.set_skin(self.window.config.get("skin")) self.window.set_opacity(self.window.config.get("opacity")) + # Обновляем состояние "Поверх всех окон" в трее + self.always_on_top_action.setChecked(self.window.config.get("always_on_top")) + self.window.set_always_on_top(self.window.config.get("always_on_top")) # Обновляем тексты в меню Pomodoro self.update_pomodoro_menu_texts() # Перезапускаем таймер растяжки с новым интервалом diff --git a/tests/test_utils.py b/tests/test_utils.py index e363433..e4b567d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -86,6 +86,30 @@ def test_timer_system_pomodoro(self): self.assertEqual(ts.pomodoro_state, "idle") self.assertEqual(ts.pomodoro_remaining, 0) + def test_always_on_top_logic(self): + config = ConfigManager(self.config_path) + config.set("always_on_top", True) + + # Test default and toggled state in config + self.assertTrue(config.get("always_on_top")) + config.set("always_on_top", False) + self.assertFalse(config.get("always_on_top")) + + # Test PetWindow dynamic set_always_on_top (using mock since we are running headless without full UI initialization) + mock_window = MagicMock() + mock_window.config = config + + def set_always_on_top_impl(enabled): + config.set("always_on_top", enabled) + + mock_window.set_always_on_top = set_always_on_top_impl + + mock_window.set_always_on_top(True) + self.assertTrue(config.get("always_on_top")) + + mock_window.set_always_on_top(False) + self.assertFalse(config.get("always_on_top")) + def test_input_manager_petting_logic(self): # Мокаем PetWindow, AnimationManager и SoundManager mock_window = MagicMock() diff --git a/verification/screenshots/settings_dialog_always_on_top.png b/verification/screenshots/settings_dialog_always_on_top.png new file mode 100644 index 0000000..ef1fd0e Binary files /dev/null and b/verification/screenshots/settings_dialog_always_on_top.png differ diff --git a/verification/screenshots/stats_history.png b/verification/screenshots/stats_history.png index 9dbf806..d7351a2 100644 Binary files a/verification/screenshots/stats_history.png and b/verification/screenshots/stats_history.png differ