Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Qt Window Flag Manipulation]
**Инсайт:** Динамическое изменение флагов окна (например, toggling Qt.WindowStaysOnTopHint для опции "Поверх всех окон") через `setWindowFlags` в PySide6 скрывает текущее окно, так как пересоздает его системный дескриптор. Для пользователя это выглядит как неожиданное исчезновение окна.
**Действие:** После вызова `setWindowFlags` всегда явно вызывать метод `show()` для перепоказа окна в его текущем состоянии.
20 changes: 15 additions & 5 deletions src/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -92,6 +92,16 @@ 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 = Qt.FramelessWindowHint | Qt.Tool
if enabled:
flags |= Qt.WindowStaysOnTopHint
self.setWindowFlags(flags)
self.show()

def set_timer_system(self, timer_system):
self.timer_system = timer_system

Expand Down
8 changes: 7 additions & 1 deletion src/ui/settings_dialog.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -61,6 +61,11 @@ def __init__(self, config, parent=None):
self.skin_combo.setCurrentIndex(index)
layout.addWidget(self.skin_combo)

# Поверх всех окон
self.always_on_top_checkbox = QCheckBox("Поверх всех окон")
self.always_on_top_checkbox.setChecked(self.config.get("always_on_top"))
layout.addWidget(self.always_on_top_checkbox)

# Кнопки
btn_layout = QHBoxLayout()
save_btn = QPushButton("Сохранить")
Expand All @@ -80,4 +85,5 @@ def save_settings(self):
self.config.set("pomodoro_work", self.pomodoro_work_spin.value())
self.config.set("pomodoro_break", self.pomodoro_break_spin.value())
self.config.set("skin", self.skin_combo.currentData())
self.config.set("always_on_top", self.always_on_top_checkbox.isChecked())
self.accept()
16 changes: 16 additions & 0 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ 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)
always_on_top = self.window.config.get("always_on_top") if self.window.config else True
self.always_on_top_action.setChecked(always_on_top)
self.always_on_top_action.triggered.connect(self.toggle_always_on_top)
self.menu.addAction(self.always_on_top_action)

self.menu.addSeparator()

# Статистика
Expand Down Expand Up @@ -150,6 +158,10 @@ def stop_pomodoro(self, checked=False):
self.window.timer_system.stop_pomodoro()
self.window.show_message("Таймер остановлен ⏹️")

def toggle_always_on_top(self, checked):
self.window.set_always_on_top(checked)
self.window.show_message("Поверх всех окон: " + ("Вкл" if checked else "Выкл") + " 📌")

def quit_app(self, checked=False):
self.window.close()
QApplication.instance().quit()
Expand Down Expand Up @@ -177,6 +189,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()
# Перезапускаем таймер растяжки с новым интервалом
Expand Down
27 changes: 27 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,32 @@ def test_input_manager_petting_logic(self):

db.close()

def test_always_on_top_logic(self):
config = ConfigManager(self.config_path)
config.set("always_on_top", True)

from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt

app = QApplication.instance() or QApplication([])

from src.ui.main_window import PetWindow
window = PetWindow(config)

# Проверяем начальные флаги
self.assertTrue(bool(window.windowFlags() & Qt.WindowStaysOnTopHint))

# Выключаем поверх всех окон
window.set_always_on_top(False)
self.assertFalse(bool(window.windowFlags() & Qt.WindowStaysOnTopHint))
self.assertFalse(config.get("always_on_top"))

# Включаем обратно
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()
Binary file modified verification/screenshots/stats_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.