Upload files to "core"
This commit is contained in:
942
core/app.py
Normal file
942
core/app.py
Normal file
@@ -0,0 +1,942 @@
|
||||
# v3.1.0 app.py
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
QEasingCurve,
|
||||
QPoint,
|
||||
QPropertyAnimation,
|
||||
QRect,
|
||||
QTimer,
|
||||
Qt,
|
||||
pyqtSignal,
|
||||
)
|
||||
|
||||
from PyQt6.QtGui import (
|
||||
QAction,
|
||||
QColor,
|
||||
QFont,
|
||||
QGuiApplication,
|
||||
QIcon,
|
||||
QMouseEvent,
|
||||
QPainter,
|
||||
QPixmap,
|
||||
)
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QFrame,
|
||||
QMenu,
|
||||
QSizeGrip,
|
||||
QSystemTrayIcon,
|
||||
QTextBrowser,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from qframelesswindow import AcrylicWindow
|
||||
|
||||
from config.profiles import THEMES
|
||||
from config.settings import (
|
||||
APP_DIR,
|
||||
SettingsManager,
|
||||
)
|
||||
from ui.editor import LinkTextEdit
|
||||
from ui.styles import build_stylesheet
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
|
||||
STATE_FILE: Final[Path] = (
|
||||
APP_DIR / "sticky_state.json"
|
||||
)
|
||||
|
||||
MARKDOWN_FILE: Final[Path] = (
|
||||
APP_DIR / "sticky.md"
|
||||
)
|
||||
|
||||
|
||||
class WindowState(Enum):
|
||||
IDLE = auto()
|
||||
DRAGGING = auto()
|
||||
|
||||
|
||||
class ActiveState(Enum):
|
||||
ACTIVE = auto()
|
||||
INACTIVE = auto()
|
||||
DRAGGING = auto()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PersistedState:
|
||||
x: int = 100
|
||||
y: int = 100
|
||||
width: int = 260
|
||||
height: int = 300
|
||||
theme_index: int = 0
|
||||
|
||||
|
||||
class DragHandle(QFrame):
|
||||
|
||||
drag_started = pyqtSignal(QPoint)
|
||||
|
||||
drag_moved = pyqtSignal(QPoint)
|
||||
|
||||
drag_finished = pyqtSignal()
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
event: QMouseEvent,
|
||||
) -> None:
|
||||
if (
|
||||
event.button()
|
||||
== Qt.MouseButton.LeftButton
|
||||
):
|
||||
self.drag_started.emit(
|
||||
event.globalPosition().toPoint()
|
||||
)
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(
|
||||
self,
|
||||
event: QMouseEvent,
|
||||
) -> None:
|
||||
if (
|
||||
event.buttons()
|
||||
& Qt.MouseButton.LeftButton
|
||||
):
|
||||
self.drag_moved.emit(
|
||||
event.globalPosition().toPoint()
|
||||
)
|
||||
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(
|
||||
self,
|
||||
event: QMouseEvent,
|
||||
) -> None:
|
||||
self.drag_finished.emit()
|
||||
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class StickyNoteApp(AcrylicWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._closing = False
|
||||
|
||||
self.settings_manager = (
|
||||
SettingsManager.load()
|
||||
)
|
||||
|
||||
self.profile = (
|
||||
self.settings_manager.profile
|
||||
)
|
||||
|
||||
self.window_state = (
|
||||
WindowState.IDLE
|
||||
)
|
||||
|
||||
self.active_state = (
|
||||
ActiveState.ACTIVE
|
||||
)
|
||||
|
||||
self.drag_offset = QPoint()
|
||||
|
||||
self.last_saved_hash = ""
|
||||
|
||||
self.last_preview_hash = ""
|
||||
|
||||
self.startup_ready = False
|
||||
|
||||
self.configure_font()
|
||||
|
||||
self.build_ui()
|
||||
|
||||
self.setup_timers()
|
||||
|
||||
self.configure_window()
|
||||
|
||||
self.build_tray()
|
||||
|
||||
self.restore_state()
|
||||
|
||||
self.apply_style()
|
||||
|
||||
QTimer.singleShot(
|
||||
self.profile["window"][
|
||||
"startup_stabilize_ms"
|
||||
],
|
||||
self.finalize_startup,
|
||||
)
|
||||
|
||||
def configure_font(self) -> None:
|
||||
self.font_object = QFont()
|
||||
|
||||
self.font_object.setFamilies(
|
||||
self.profile["editor"][
|
||||
"font_family"
|
||||
]
|
||||
)
|
||||
|
||||
self.font_object.setPointSize(
|
||||
self.profile["editor"][
|
||||
"font_size"
|
||||
]
|
||||
)
|
||||
|
||||
self.font_object.setHintingPreference(
|
||||
QFont.HintingPreference.PreferFullHinting
|
||||
)
|
||||
|
||||
self.font_object.setStyleStrategy(
|
||||
QFont.StyleStrategy.PreferAntialias
|
||||
| QFont.StyleStrategy.PreferQuality
|
||||
)
|
||||
|
||||
def setup_timers(self) -> None:
|
||||
self.save_timer = QTimer(self)
|
||||
|
||||
self.save_timer.setSingleShot(
|
||||
True
|
||||
)
|
||||
|
||||
self.save_timer.timeout.connect(
|
||||
self.save_state
|
||||
)
|
||||
|
||||
self.preview_timer = QTimer(
|
||||
self
|
||||
)
|
||||
|
||||
self.preview_timer.setSingleShot(
|
||||
True
|
||||
)
|
||||
|
||||
self.preview_timer.timeout.connect(
|
||||
self.show_preview
|
||||
)
|
||||
|
||||
self.opacity_anim = (
|
||||
QPropertyAnimation(
|
||||
self,
|
||||
b"windowOpacity",
|
||||
)
|
||||
)
|
||||
|
||||
self.opacity_anim.setDuration(
|
||||
self.profile["appearance"][
|
||||
"focus_animation_ms"
|
||||
]
|
||||
)
|
||||
|
||||
self.opacity_anim.setEasingCurve(
|
||||
QEasingCurve.Type.OutCubic
|
||||
)
|
||||
|
||||
def configure_window(self) -> None:
|
||||
window = self.profile["window"]
|
||||
|
||||
self.resize(
|
||||
window["width"],
|
||||
window["height"],
|
||||
)
|
||||
|
||||
self.setMinimumSize(
|
||||
window["min_width"],
|
||||
window["min_height"],
|
||||
)
|
||||
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.Tool
|
||||
| Qt.WindowType.WindowStaysOnTopHint
|
||||
| Qt.WindowType.FramelessWindowHint
|
||||
)
|
||||
|
||||
self.setAttribute(
|
||||
Qt.WidgetAttribute.WA_TranslucentBackground,
|
||||
True,
|
||||
)
|
||||
|
||||
self.setAutoFillBackground(False)
|
||||
|
||||
appearance = (
|
||||
self.profile["appearance"]
|
||||
)
|
||||
|
||||
try:
|
||||
if appearance[
|
||||
"enable_mica"
|
||||
]:
|
||||
self.windowEffect.setMicaEffect(
|
||||
self.winId(),
|
||||
True,
|
||||
)
|
||||
|
||||
elif appearance[
|
||||
"enable_acrylic"
|
||||
]:
|
||||
self.windowEffect.setAcrylicEffect(
|
||||
self.winId(),
|
||||
"00000001",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"window_effect_failure"
|
||||
)
|
||||
|
||||
def build_ui(self) -> None:
|
||||
self.root = QWidget(self)
|
||||
|
||||
self.root.setObjectName(
|
||||
"container"
|
||||
)
|
||||
|
||||
self.layout = QVBoxLayout(
|
||||
self.root
|
||||
)
|
||||
|
||||
window = self.profile["window"]
|
||||
|
||||
margin = window[
|
||||
"layout_margin"
|
||||
]
|
||||
|
||||
self.layout.setContentsMargins(
|
||||
margin,
|
||||
margin,
|
||||
margin,
|
||||
margin,
|
||||
)
|
||||
|
||||
self.layout.setSpacing(
|
||||
window["layout_spacing"]
|
||||
)
|
||||
|
||||
self.drag_handle = DragHandle(
|
||||
self.root
|
||||
)
|
||||
|
||||
self.drag_handle.setFixedHeight(
|
||||
window[
|
||||
"drag_handle_height"
|
||||
]
|
||||
)
|
||||
|
||||
self.drag_handle.drag_started.connect(
|
||||
self.start_drag
|
||||
)
|
||||
|
||||
self.drag_handle.drag_moved.connect(
|
||||
self.perform_drag
|
||||
)
|
||||
|
||||
self.drag_handle.drag_finished.connect(
|
||||
self.finish_drag
|
||||
)
|
||||
|
||||
self.editor = LinkTextEdit(
|
||||
self.root
|
||||
)
|
||||
|
||||
self.editor.setObjectName(
|
||||
"editor"
|
||||
)
|
||||
|
||||
self.editor.setFont(
|
||||
self.font_object
|
||||
)
|
||||
|
||||
self.editor.document().setDocumentMargin(
|
||||
0
|
||||
)
|
||||
|
||||
self.editor.textChanged.connect(
|
||||
self.queue_save
|
||||
)
|
||||
|
||||
self.editor.textChanged.connect(
|
||||
self.queue_preview
|
||||
)
|
||||
|
||||
self.preview = QTextBrowser(
|
||||
self.root
|
||||
)
|
||||
|
||||
self.preview.setObjectName(
|
||||
"preview"
|
||||
)
|
||||
|
||||
self.preview.setFont(
|
||||
self.font_object
|
||||
)
|
||||
|
||||
self.preview.hide()
|
||||
|
||||
content = QWidget(self.root)
|
||||
|
||||
content_layout = QVBoxLayout(
|
||||
content
|
||||
)
|
||||
|
||||
content_layout.setContentsMargins(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
content_layout.setSpacing(0)
|
||||
|
||||
content_layout.addWidget(
|
||||
self.editor,
|
||||
1,
|
||||
)
|
||||
|
||||
content_layout.addWidget(
|
||||
self.preview,
|
||||
1,
|
||||
)
|
||||
|
||||
self.resize_grip = QSizeGrip(
|
||||
self.root
|
||||
)
|
||||
|
||||
self.layout.addWidget(
|
||||
self.drag_handle
|
||||
)
|
||||
|
||||
self.layout.addWidget(
|
||||
content,
|
||||
1,
|
||||
)
|
||||
|
||||
self.layout.addWidget(
|
||||
self.resize_grip,
|
||||
0,
|
||||
Qt.AlignmentFlag.AlignBottom
|
||||
| Qt.AlignmentFlag.AlignRight,
|
||||
)
|
||||
|
||||
def build_tray(self) -> None:
|
||||
if not self.profile[
|
||||
"behavior"
|
||||
]["enable_tray"]:
|
||||
return
|
||||
|
||||
self.tray = QSystemTrayIcon(
|
||||
self
|
||||
)
|
||||
|
||||
self.tray.setIcon(
|
||||
self.create_tray_icon()
|
||||
)
|
||||
|
||||
menu = QMenu()
|
||||
|
||||
open_settings = QAction(
|
||||
"Open Settings",
|
||||
self,
|
||||
)
|
||||
|
||||
open_settings.triggered.connect(
|
||||
self.open_settings
|
||||
)
|
||||
|
||||
cycle_theme = QAction(
|
||||
"Cycle Theme",
|
||||
self,
|
||||
)
|
||||
|
||||
cycle_theme.triggered.connect(
|
||||
self.cycle_theme
|
||||
)
|
||||
|
||||
quit_action = QAction(
|
||||
"Quit",
|
||||
self,
|
||||
)
|
||||
|
||||
quit_action.triggered.connect(
|
||||
self.close
|
||||
)
|
||||
|
||||
menu.addAction(open_settings)
|
||||
|
||||
menu.addAction(cycle_theme)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
menu.addAction(quit_action)
|
||||
|
||||
self.tray.setContextMenu(menu)
|
||||
|
||||
self.tray.show()
|
||||
|
||||
def create_tray_icon(self) -> QIcon:
|
||||
pixmap = QPixmap(32, 32)
|
||||
|
||||
pixmap.fill(
|
||||
Qt.GlobalColor.transparent
|
||||
)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
|
||||
painter.setRenderHint(
|
||||
QPainter.RenderHint.Antialiasing,
|
||||
True,
|
||||
)
|
||||
|
||||
painter.setBrush(
|
||||
QColor(
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
220,
|
||||
)
|
||||
)
|
||||
|
||||
painter.setPen(
|
||||
Qt.PenStyle.NoPen
|
||||
)
|
||||
|
||||
painter.drawRoundedRect(
|
||||
6,
|
||||
6,
|
||||
20,
|
||||
20,
|
||||
6,
|
||||
6,
|
||||
)
|
||||
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
|
||||
def finalize_startup(self) -> None:
|
||||
self.startup_ready = True
|
||||
|
||||
self.setWindowOpacity(
|
||||
self.profile["appearance"][
|
||||
"opacity_active"
|
||||
]
|
||||
)
|
||||
|
||||
self.repaint()
|
||||
|
||||
def current_theme(self):
|
||||
theme_name = self.profile[
|
||||
"theme"
|
||||
]
|
||||
|
||||
for theme in THEMES:
|
||||
if theme.name == theme_name:
|
||||
return theme
|
||||
|
||||
return THEMES[0]
|
||||
|
||||
def apply_style(self) -> None:
|
||||
theme = self.current_theme()
|
||||
|
||||
border = {
|
||||
ActiveState.ACTIVE:
|
||||
theme.border_active,
|
||||
ActiveState.INACTIVE:
|
||||
theme.border_inactive,
|
||||
ActiveState.DRAGGING:
|
||||
theme.border_dragging,
|
||||
}[
|
||||
self.active_state
|
||||
]
|
||||
|
||||
background = (
|
||||
theme.background
|
||||
if self.active_state
|
||||
!= ActiveState.INACTIVE
|
||||
else theme.background_inactive
|
||||
)
|
||||
|
||||
self.root.setStyleSheet(
|
||||
build_stylesheet(
|
||||
theme,
|
||||
border,
|
||||
background,
|
||||
self.profile["editor"][
|
||||
"font_size"
|
||||
],
|
||||
self.profile["editor"][
|
||||
"font_family"
|
||||
],
|
||||
self.profile["window"][
|
||||
"layout_margin"
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def start_drag(
|
||||
self,
|
||||
global_pos: QPoint,
|
||||
) -> None:
|
||||
self.window_state = (
|
||||
WindowState.DRAGGING
|
||||
)
|
||||
|
||||
self.active_state = (
|
||||
ActiveState.DRAGGING
|
||||
)
|
||||
|
||||
self.apply_style()
|
||||
|
||||
self.drag_offset = (
|
||||
global_pos
|
||||
- self.frameGeometry().topLeft()
|
||||
)
|
||||
|
||||
self.setWindowOpacity(
|
||||
self.profile["appearance"][
|
||||
"opacity_dragging"
|
||||
]
|
||||
)
|
||||
|
||||
def perform_drag(
|
||||
self,
|
||||
global_pos: QPoint,
|
||||
) -> None:
|
||||
target = (
|
||||
global_pos
|
||||
- self.drag_offset
|
||||
)
|
||||
|
||||
self.move(target)
|
||||
|
||||
def finish_drag(self) -> None:
|
||||
self.window_state = (
|
||||
WindowState.IDLE
|
||||
)
|
||||
|
||||
self.active_state = (
|
||||
ActiveState.ACTIVE
|
||||
)
|
||||
|
||||
self.apply_style()
|
||||
|
||||
self.setWindowOpacity(
|
||||
self.profile["appearance"][
|
||||
"opacity_active"
|
||||
]
|
||||
)
|
||||
|
||||
def queue_save(self) -> None:
|
||||
self.save_timer.start(
|
||||
self.profile["editor"][
|
||||
"save_debounce_ms"
|
||||
]
|
||||
)
|
||||
|
||||
def queue_preview(self) -> None:
|
||||
if not self.profile[
|
||||
"editor"
|
||||
][
|
||||
"enable_markdown_preview"
|
||||
]:
|
||||
return
|
||||
|
||||
self.preview.hide()
|
||||
|
||||
self.editor.show()
|
||||
|
||||
self.preview_timer.start(
|
||||
self.profile["editor"][
|
||||
"preview_delay_ms"
|
||||
]
|
||||
)
|
||||
|
||||
def show_preview(self) -> None:
|
||||
if self.editor.hasFocus():
|
||||
return
|
||||
|
||||
markdown = (
|
||||
self.editor.toPlainText()
|
||||
)
|
||||
|
||||
current_hash = sha256(
|
||||
markdown.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
if (
|
||||
current_hash
|
||||
== self.last_preview_hash
|
||||
):
|
||||
return
|
||||
|
||||
self.last_preview_hash = (
|
||||
current_hash
|
||||
)
|
||||
|
||||
self.preview.setMarkdown(
|
||||
markdown
|
||||
)
|
||||
|
||||
self.editor.hide()
|
||||
|
||||
self.preview.show()
|
||||
|
||||
def save_state(self) -> None:
|
||||
markdown = (
|
||||
self.editor.toPlainText()
|
||||
)
|
||||
|
||||
current_hash = sha256(
|
||||
markdown.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
if current_hash != self.last_saved_hash:
|
||||
MARKDOWN_FILE.write_text(
|
||||
markdown,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
self.last_saved_hash = (
|
||||
current_hash
|
||||
)
|
||||
|
||||
STATE_FILE.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"x": self.x(),
|
||||
"y": self.y(),
|
||||
"width": self.width(),
|
||||
"height": self.height(),
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def restore_state(self) -> None:
|
||||
if MARKDOWN_FILE.exists():
|
||||
self.editor.setPlainText(
|
||||
MARKDOWN_FILE.read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
|
||||
if not STATE_FILE.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
state = json.loads(
|
||||
STATE_FILE.read_text(
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
|
||||
self.resize(
|
||||
state["width"],
|
||||
state["height"],
|
||||
)
|
||||
|
||||
self.move(
|
||||
state["x"],
|
||||
state["y"],
|
||||
)
|
||||
|
||||
self.clamp_to_screen()
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"restore_state_failure"
|
||||
)
|
||||
|
||||
def clamp_to_screen(self) -> None:
|
||||
screen = (
|
||||
QGuiApplication.primaryScreen()
|
||||
)
|
||||
|
||||
if not screen:
|
||||
return
|
||||
|
||||
geometry = (
|
||||
screen.availableGeometry()
|
||||
)
|
||||
|
||||
rect = QRect(
|
||||
self.x(),
|
||||
self.y(),
|
||||
self.width(),
|
||||
self.height(),
|
||||
)
|
||||
|
||||
if not geometry.intersects(rect):
|
||||
self.move(
|
||||
geometry.center()
|
||||
- self.rect().center()
|
||||
)
|
||||
|
||||
def cycle_theme(self) -> None:
|
||||
names = [
|
||||
theme.name
|
||||
for theme in THEMES
|
||||
]
|
||||
|
||||
current = self.profile[
|
||||
"theme"
|
||||
]
|
||||
|
||||
index = names.index(current)
|
||||
|
||||
next_index = (
|
||||
index + 1
|
||||
) % len(names)
|
||||
|
||||
self.profile["theme"] = (
|
||||
names[next_index]
|
||||
)
|
||||
|
||||
self.settings_manager.save()
|
||||
|
||||
self.apply_style()
|
||||
|
||||
def open_settings(self) -> None:
|
||||
path = (
|
||||
APP_DIR / "settings.json"
|
||||
)
|
||||
|
||||
path.touch(exist_ok=True)
|
||||
|
||||
try:
|
||||
if os.name == "nt":
|
||||
os.startfile(str(path))
|
||||
return
|
||||
|
||||
if sys.platform.startswith(
|
||||
"linux"
|
||||
):
|
||||
subprocess.Popen(
|
||||
["xdg-open", str(path)]
|
||||
)
|
||||
return
|
||||
|
||||
if sys.platform == "darwin":
|
||||
subprocess.Popen(
|
||||
["open", str(path)]
|
||||
)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"open_settings_failure"
|
||||
)
|
||||
|
||||
def nativeEvent(
|
||||
self,
|
||||
eventType,
|
||||
message,
|
||||
):
|
||||
if getattr(
|
||||
self,
|
||||
"_closing",
|
||||
False,
|
||||
):
|
||||
return False, 0
|
||||
|
||||
try:
|
||||
return super().nativeEvent(
|
||||
eventType,
|
||||
message,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return False, 0
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
super().resizeEvent(event)
|
||||
|
||||
root = getattr(
|
||||
self,
|
||||
"root",
|
||||
None,
|
||||
)
|
||||
|
||||
if root is None:
|
||||
return
|
||||
|
||||
geometry = self.rect()
|
||||
|
||||
if root.geometry() != geometry:
|
||||
root.setGeometry(
|
||||
geometry
|
||||
)
|
||||
|
||||
def focusInEvent(self, event) -> None:
|
||||
super().focusInEvent(event)
|
||||
|
||||
self.active_state = (
|
||||
ActiveState.ACTIVE
|
||||
)
|
||||
|
||||
self.apply_style()
|
||||
|
||||
self.setWindowOpacity(
|
||||
self.profile["appearance"][
|
||||
"opacity_active"
|
||||
]
|
||||
)
|
||||
|
||||
def focusOutEvent(self, event) -> None:
|
||||
super().focusOutEvent(event)
|
||||
|
||||
if (
|
||||
self.window_state
|
||||
== WindowState.DRAGGING
|
||||
):
|
||||
return
|
||||
|
||||
self.active_state = (
|
||||
ActiveState.INACTIVE
|
||||
)
|
||||
|
||||
self.apply_style()
|
||||
|
||||
self.setWindowOpacity(
|
||||
self.profile["appearance"][
|
||||
"opacity_inactive"
|
||||
]
|
||||
)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
self._closing = True
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.save_state()
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.save_timer.stop()
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.preview_timer.stop()
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self.opacity_anim.stop()
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
if hasattr(self, "tray"):
|
||||
self.tray.hide()
|
||||
|
||||
super().closeEvent(event)
|
||||
Reference in New Issue
Block a user