1248 lines
25 KiB
Python
1248 lines
25 KiB
Python
# v4.2.1 app.py
|
|
import atexit
|
|
import json
|
|
import logging
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
from dataclasses import dataclass
|
|
from enum import Enum, auto
|
|
from hashlib import sha256
|
|
|
|
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 (
|
|
QApplication,
|
|
QFrame,
|
|
QMenu,
|
|
QSizeGrip,
|
|
QSystemTrayIcon,
|
|
QTextBrowser,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from qframelesswindow import AcrylicWindow
|
|
|
|
from config.profiles import THEMES
|
|
|
|
from config.settings import (
|
|
APP_DIR,
|
|
MARKDOWN_FILE,
|
|
SETTINGS_FILE,
|
|
STATE_FILE,
|
|
SettingsManager,
|
|
atomic_write,
|
|
)
|
|
|
|
from ui.editor import LinkTextEdit
|
|
from ui.styles import build_stylesheet
|
|
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
)
|
|
|
|
|
|
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 = 250
|
|
height: int = 260
|
|
|
|
|
|
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._is_quitting = False
|
|
self._force_close = False
|
|
|
|
self.settings_manager = (
|
|
SettingsManager.load()
|
|
)
|
|
|
|
self.ensure_runtime_files()
|
|
|
|
self.profile = (
|
|
self.settings_manager.profile
|
|
)
|
|
|
|
self.window_state = (
|
|
WindowState.IDLE
|
|
)
|
|
|
|
self.active_state = (
|
|
ActiveState.ACTIVE
|
|
)
|
|
|
|
self.drag_offset = QPoint()
|
|
|
|
self.drag_origin = QPoint()
|
|
|
|
self.pending_drag = False
|
|
|
|
self.last_saved_hash = ""
|
|
|
|
self.last_preview_hash = ""
|
|
|
|
self.startup_ready = False
|
|
|
|
self.configure_font()
|
|
|
|
self.setup_timers()
|
|
|
|
self.build_ui()
|
|
|
|
self.configure_window()
|
|
|
|
QTimer.singleShot(
|
|
80,
|
|
self.restore_state,
|
|
)
|
|
|
|
QTimer.singleShot(
|
|
120,
|
|
self.initialize_effects,
|
|
)
|
|
|
|
QTimer.singleShot(
|
|
220,
|
|
self.build_tray,
|
|
)
|
|
|
|
QTimer.singleShot(
|
|
260,
|
|
self.apply_style,
|
|
)
|
|
|
|
QTimer.singleShot(
|
|
320,
|
|
self.finalize_startup,
|
|
)
|
|
|
|
self.register_shutdown_hooks()
|
|
|
|
def ensure_runtime_files(self) -> None:
|
|
APP_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
if not SETTINGS_FILE.exists():
|
|
self.settings_manager.save()
|
|
|
|
if not MARKDOWN_FILE.exists():
|
|
atomic_write(
|
|
MARKDOWN_FILE,
|
|
"",
|
|
)
|
|
|
|
if not STATE_FILE.exists():
|
|
atomic_write(
|
|
STATE_FILE,
|
|
json.dumps(
|
|
{
|
|
"x": 100,
|
|
"y": 100,
|
|
"width": 250,
|
|
"height": 260,
|
|
},
|
|
indent=2,
|
|
),
|
|
)
|
|
|
|
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.drag_timer = QTimer(self)
|
|
|
|
self.drag_timer.setSingleShot(
|
|
True
|
|
)
|
|
|
|
self.drag_timer.timeout.connect(
|
|
self.activate_delayed_drag
|
|
)
|
|
|
|
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.setWindowFlag(
|
|
Qt.WindowType.WindowStaysOnTopHint,
|
|
True,
|
|
)
|
|
|
|
self.setWindowFlag(
|
|
Qt.WindowType.FramelessWindowHint,
|
|
True,
|
|
)
|
|
|
|
self.setAttribute(
|
|
Qt.WidgetAttribute.WA_TranslucentBackground,
|
|
True,
|
|
)
|
|
|
|
self.setAttribute(
|
|
Qt.WidgetAttribute.WA_NoSystemBackground,
|
|
True,
|
|
)
|
|
|
|
self.setAutoFillBackground(
|
|
False
|
|
)
|
|
|
|
def build_ui(self) -> None:
|
|
self.root = QWidget(self)
|
|
|
|
self.root.setObjectName(
|
|
"container"
|
|
)
|
|
|
|
self.root.setGeometry(
|
|
self.rect()
|
|
)
|
|
|
|
self.layout = QVBoxLayout(
|
|
self.root
|
|
)
|
|
|
|
self.layout.setContentsMargins(
|
|
4,
|
|
4,
|
|
4,
|
|
4,
|
|
)
|
|
|
|
self.layout.setSpacing(0)
|
|
|
|
self.titleBar.hide()
|
|
|
|
self.titleBar.setFixedHeight(0)
|
|
|
|
self.drag_handle = DragHandle(
|
|
self.root
|
|
)
|
|
|
|
self.drag_handle.hide()
|
|
|
|
self.drag_handle.setFixedHeight(0)
|
|
|
|
self.editor = LinkTextEdit(
|
|
self.root
|
|
)
|
|
|
|
self.editor.setObjectName(
|
|
"editor"
|
|
)
|
|
|
|
self.editor.setFont(
|
|
self.font_object
|
|
)
|
|
|
|
self.editor.document().setDocumentMargin(
|
|
0
|
|
)
|
|
|
|
self.editor.setViewportMargins(
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
)
|
|
|
|
self.editor.setAcceptRichText(
|
|
False
|
|
)
|
|
|
|
self.editor.setMouseTracking(
|
|
True
|
|
)
|
|
|
|
self.editor.setStyleSheet("""
|
|
QTextEdit {
|
|
margin: 0px;
|
|
padding: 0px;
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
QTextEdit > QWidget {
|
|
margin: 0px;
|
|
padding: 0px;
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
""")
|
|
|
|
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(
|
|
4,
|
|
4,
|
|
4,
|
|
4,
|
|
)
|
|
|
|
content_layout.setSpacing(1)
|
|
|
|
content_layout.addWidget(
|
|
self.editor,
|
|
1,
|
|
)
|
|
|
|
content_layout.addWidget(
|
|
self.preview,
|
|
1,
|
|
)
|
|
|
|
self.resize_grip = QSizeGrip(
|
|
self.root
|
|
)
|
|
|
|
self.layout.addWidget(
|
|
content,
|
|
1,
|
|
)
|
|
|
|
self.layout.addWidget(
|
|
self.resize_grip,
|
|
0,
|
|
Qt.AlignmentFlag.AlignBottom
|
|
| Qt.AlignmentFlag.AlignRight,
|
|
)
|
|
|
|
self.editor.setFocus()
|
|
|
|
def build_tray(self) -> None:
|
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
|
return
|
|
|
|
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.safe_exit
|
|
)
|
|
|
|
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 initialize_effects(self) -> None:
|
|
appearance = (
|
|
self.profile["appearance"]
|
|
)
|
|
|
|
try:
|
|
hwnd = int(self.winId())
|
|
|
|
if not hwnd:
|
|
return
|
|
|
|
if appearance[
|
|
"enable_mica"
|
|
]:
|
|
self.windowEffect.setMicaEffect(
|
|
hwnd,
|
|
True,
|
|
)
|
|
|
|
elif appearance[
|
|
"enable_acrylic"
|
|
]:
|
|
self.windowEffect.setAcrylicEffect(
|
|
hwnd,
|
|
"00000001",
|
|
)
|
|
|
|
self.update()
|
|
|
|
self.repaint()
|
|
|
|
except Exception:
|
|
logging.exception(
|
|
"window_effect_failure"
|
|
)
|
|
|
|
def activate_delayed_drag(self) -> None:
|
|
if not self.pending_drag:
|
|
return
|
|
|
|
self.window_state = (
|
|
WindowState.DRAGGING
|
|
)
|
|
|
|
self.active_state = (
|
|
ActiveState.DRAGGING
|
|
)
|
|
|
|
self.apply_style()
|
|
|
|
self.setWindowOpacity(
|
|
self.profile["appearance"][
|
|
"opacity_dragging"
|
|
]
|
|
)
|
|
|
|
def mousePressEvent(self, event) -> None:
|
|
if (
|
|
event.button()
|
|
== Qt.MouseButton.LeftButton
|
|
):
|
|
self.pending_drag = True
|
|
|
|
self.drag_origin = (
|
|
event.globalPosition()
|
|
.toPoint()
|
|
)
|
|
|
|
self.drag_offset = (
|
|
self.drag_origin
|
|
- self.frameGeometry().topLeft()
|
|
)
|
|
|
|
if hasattr(
|
|
self,
|
|
"drag_timer",
|
|
):
|
|
self.drag_timer.start(180)
|
|
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseMoveEvent(self, event) -> None:
|
|
if not (
|
|
event.buttons()
|
|
& Qt.MouseButton.LeftButton
|
|
):
|
|
return
|
|
|
|
global_pos = (
|
|
event.globalPosition()
|
|
.toPoint()
|
|
)
|
|
|
|
if (
|
|
self.pending_drag
|
|
and (
|
|
global_pos
|
|
- self.drag_origin
|
|
).manhattanLength()
|
|
> 6
|
|
):
|
|
if (
|
|
self.window_state
|
|
== WindowState.DRAGGING
|
|
):
|
|
self.move(
|
|
global_pos
|
|
- self.drag_offset
|
|
)
|
|
|
|
super().mouseMoveEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event) -> None:
|
|
if hasattr(
|
|
self,
|
|
"drag_timer",
|
|
):
|
|
self.drag_timer.stop()
|
|
|
|
self.pending_drag = False
|
|
|
|
if (
|
|
self.window_state
|
|
== WindowState.DRAGGING
|
|
):
|
|
self.finish_drag()
|
|
|
|
super().mouseReleaseEvent(event)
|
|
|
|
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"
|
|
]
|
|
)
|
|
|
|
self.queue_save()
|
|
|
|
def queue_save(self) -> None:
|
|
if not hasattr(
|
|
self,
|
|
"save_timer",
|
|
):
|
|
return
|
|
|
|
self.save_timer.start(
|
|
self.profile["editor"][
|
|
"save_debounce_ms"
|
|
]
|
|
)
|
|
|
|
def queue_preview(self) -> None:
|
|
if not hasattr(
|
|
self,
|
|
"preview_timer",
|
|
):
|
|
return
|
|
|
|
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()
|
|
or not self.isActiveWindow()
|
|
):
|
|
return
|
|
|
|
self.preview.setFocusPolicy(
|
|
Qt.FocusPolicy.NoFocus
|
|
)
|
|
|
|
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:
|
|
try:
|
|
APP_DIR.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
markdown = (
|
|
self.editor.toPlainText()
|
|
)
|
|
|
|
current_hash = sha256(
|
|
markdown.encode("utf-8")
|
|
).hexdigest()
|
|
|
|
if (
|
|
current_hash
|
|
!= self.last_saved_hash
|
|
):
|
|
atomic_write(
|
|
MARKDOWN_FILE,
|
|
markdown,
|
|
)
|
|
|
|
self.last_saved_hash = (
|
|
current_hash
|
|
)
|
|
|
|
atomic_write(
|
|
STATE_FILE,
|
|
json.dumps(
|
|
{
|
|
"x": self.x(),
|
|
"y": self.y(),
|
|
"width": self.width(),
|
|
"height": self.height(),
|
|
},
|
|
indent=2,
|
|
),
|
|
)
|
|
|
|
except Exception:
|
|
logging.exception(
|
|
"save_state_failure"
|
|
)
|
|
|
|
def restore_state(self) -> None:
|
|
try:
|
|
self.ensure_runtime_files()
|
|
|
|
markdown = (
|
|
MARKDOWN_FILE.read_text(
|
|
encoding="utf-8",
|
|
)
|
|
)
|
|
|
|
self.editor.setPlainText(
|
|
markdown
|
|
)
|
|
|
|
state = json.loads(
|
|
STATE_FILE.read_text(
|
|
encoding="utf-8",
|
|
)
|
|
)
|
|
|
|
self.resize(
|
|
max(
|
|
state.get(
|
|
"width",
|
|
250,
|
|
),
|
|
200,
|
|
),
|
|
max(
|
|
state.get(
|
|
"height",
|
|
260,
|
|
),
|
|
100,
|
|
),
|
|
)
|
|
|
|
self.move(
|
|
state.get("x", 100),
|
|
state.get("y", 100),
|
|
)
|
|
|
|
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:
|
|
SETTINGS_FILE.touch(
|
|
exist_ok=True
|
|
)
|
|
|
|
try:
|
|
if os.name == "nt":
|
|
os.startfile(
|
|
str(SETTINGS_FILE)
|
|
)
|
|
|
|
return
|
|
|
|
if sys.platform.startswith(
|
|
"linux"
|
|
):
|
|
subprocess.Popen(
|
|
[
|
|
"xdg-open",
|
|
str(SETTINGS_FILE),
|
|
]
|
|
)
|
|
|
|
return
|
|
|
|
if sys.platform == "darwin":
|
|
subprocess.Popen(
|
|
[
|
|
"open",
|
|
str(SETTINGS_FILE),
|
|
]
|
|
)
|
|
|
|
return
|
|
|
|
except Exception:
|
|
logging.exception(
|
|
"open_settings_failure"
|
|
)
|
|
|
|
def moveEvent(self, event) -> None:
|
|
super().moveEvent(event)
|
|
|
|
if (
|
|
getattr(
|
|
self,
|
|
"startup_ready",
|
|
False,
|
|
)
|
|
and hasattr(
|
|
self,
|
|
"save_timer",
|
|
)
|
|
):
|
|
self.queue_save()
|
|
|
|
def resizeEvent(self, event) -> None:
|
|
super().resizeEvent(event)
|
|
|
|
root = getattr(
|
|
self,
|
|
"root",
|
|
None,
|
|
)
|
|
|
|
if root is not None:
|
|
geometry = self.rect()
|
|
|
|
if (
|
|
root.geometry()
|
|
!= geometry
|
|
):
|
|
root.setGeometry(
|
|
geometry
|
|
)
|
|
|
|
if (
|
|
getattr(
|
|
self,
|
|
"startup_ready",
|
|
False,
|
|
)
|
|
and hasattr(
|
|
self,
|
|
"save_timer",
|
|
)
|
|
):
|
|
self.queue_save()
|
|
|
|
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:
|
|
if (
|
|
self._is_quitting
|
|
or self._force_close
|
|
):
|
|
try:
|
|
self.save_state()
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
event.accept()
|
|
|
|
return
|
|
|
|
event.ignore()
|
|
|
|
self.hide()
|
|
|
|
def register_shutdown_hooks(self) -> None:
|
|
app = QApplication.instance()
|
|
|
|
def handle_shutdown(*_) -> None:
|
|
self.safe_exit()
|
|
|
|
atexit.register(
|
|
handle_shutdown
|
|
)
|
|
|
|
for sig in (
|
|
signal.SIGINT,
|
|
signal.SIGTERM,
|
|
):
|
|
try:
|
|
signal.signal(
|
|
sig,
|
|
handle_shutdown,
|
|
)
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
if app is not None:
|
|
app.aboutToQuit.connect(
|
|
lambda: setattr(
|
|
self,
|
|
"_is_quitting",
|
|
True,
|
|
)
|
|
)
|
|
|
|
def safe_exit(self) -> None:
|
|
if self._is_quitting:
|
|
return
|
|
|
|
self._is_quitting = True
|
|
|
|
try:
|
|
self.save_state()
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
try:
|
|
if hasattr(
|
|
self,
|
|
"save_timer",
|
|
):
|
|
self.save_timer.stop()
|
|
|
|
if hasattr(
|
|
self,
|
|
"preview_timer",
|
|
):
|
|
self.preview_timer.stop()
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
try:
|
|
if hasattr(
|
|
self,
|
|
"tray",
|
|
):
|
|
self.tray.hide()
|
|
|
|
self.tray.deleteLater()
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
try:
|
|
self.hide()
|
|
|
|
self.deleteLater()
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
app = QApplication.instance()
|
|
|
|
if app is not None:
|
|
app.quit()
|
|
|
|
try:
|
|
app.quit()
|
|
except Exception:
|
|
sys.exit(0) |