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 Window Flag Updates in Qt]
**Инсайт:** Динамическое изменение флагов окна через `setWindowFlags` в PySide6 автоматически скрывает окно. Для предотвращения мигания или исчезновения окна необходимо всегда вызывать метод `show()` сразу после изменения флагов. Настройка `always_on_top` должна быть синхронизирована между SettingsDialog, TrayMenu и ConfigManager для бесшовного UX.
**Действие:** При изменении флагов окна вызывать `self.show()`. Своевременно обновлять состояние QAction в трее и QCheckBox в диалоге настроек при изменении конфигурации из любого источника.
22 changes: 17 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,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

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 @@ -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()
Expand Down Expand Up @@ -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())
Expand Down
17 changes: 17 additions & 0 deletions src/ui/tray_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
# Перезапускаем таймер растяжки с новым интервалом
Expand Down Expand Up @@ -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 "выкл 🔓"))
30 changes: 30 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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.