This commit is contained in:
admin
2026-05-24 10:52:44 +00:00
parent 9a64fecd3d
commit 9eb1d2cc88

View File

@@ -1,18 +1,15 @@
# v3.1.0 app.py # v4.2.1 app.py
import atexit import atexit
import signal
import traceback
import contextlib
import json import json
import logging import logging
import os import os
import signal
import subprocess import subprocess
import sys import sys
import traceback
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from hashlib import sha256 from hashlib import sha256
from pathlib import Path
from typing import Final
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QEasingCurve, QEasingCurve,
@@ -49,10 +46,16 @@ from PyQt6.QtWidgets import (
from qframelesswindow import AcrylicWindow from qframelesswindow import AcrylicWindow
from config.profiles import THEMES from config.profiles import THEMES
from config.settings import ( from config.settings import (
APP_DIR, APP_DIR,
MARKDOWN_FILE,
SETTINGS_FILE,
STATE_FILE,
SettingsManager, SettingsManager,
atomic_write,
) )
from ui.editor import LinkTextEdit from ui.editor import LinkTextEdit
from ui.styles import build_stylesheet from ui.styles import build_stylesheet
@@ -63,15 +66,6 @@ logging.basicConfig(
) )
STATE_FILE: Final[Path] = (
APP_DIR / "sticky_state.json"
)
MARKDOWN_FILE: Final[Path] = (
APP_DIR / "sticky.md"
)
class WindowState(Enum): class WindowState(Enum):
IDLE = auto() IDLE = auto()
DRAGGING = auto() DRAGGING = auto()
@@ -87,9 +81,8 @@ class ActiveState(Enum):
class PersistedState: class PersistedState:
x: int = 100 x: int = 100
y: int = 100 y: int = 100
width: int = 260 width: int = 250
height: int = 300 height: int = 260
theme_index: int = 0
class DragHandle(QFrame): class DragHandle(QFrame):
@@ -141,15 +134,16 @@ class StickyNoteApp(AcrylicWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._is_quitting = False self._is_quitting = False
self._force_close = False self._force_close = False
self._closing = False
self.settings_manager = ( self.settings_manager = (
SettingsManager.load() SettingsManager.load()
) )
self.ensure_runtime_files()
self.profile = ( self.profile = (
self.settings_manager.profile self.settings_manager.profile
) )
@@ -164,6 +158,10 @@ class StickyNoteApp(AcrylicWindow):
self.drag_offset = QPoint() self.drag_offset = QPoint()
self.drag_origin = QPoint()
self.pending_drag = False
self.last_saved_hash = "" self.last_saved_hash = ""
self.last_preview_hash = "" self.last_preview_hash = ""
@@ -172,12 +170,17 @@ class StickyNoteApp(AcrylicWindow):
self.configure_font() self.configure_font()
self.build_ui()
self.setup_timers() self.setup_timers()
self.build_ui()
self.configure_window() self.configure_window()
QTimer.singleShot(
80,
self.restore_state,
)
QTimer.singleShot( QTimer.singleShot(
120, 120,
self.initialize_effects, self.initialize_effects,
@@ -188,11 +191,6 @@ class StickyNoteApp(AcrylicWindow):
self.build_tray, self.build_tray,
) )
QTimer.singleShot(
80,
self.restore_state,
)
QTimer.singleShot( QTimer.singleShot(
260, 260,
self.apply_style, self.apply_style,
@@ -205,6 +203,34 @@ class StickyNoteApp(AcrylicWindow):
self.register_shutdown_hooks() 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: def configure_font(self) -> None:
self.font_object = QFont() self.font_object = QFont()
@@ -253,6 +279,16 @@ class StickyNoteApp(AcrylicWindow):
self.show_preview 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 = ( self.opacity_anim = (
QPropertyAnimation( QPropertyAnimation(
self, self,
@@ -270,7 +306,6 @@ class StickyNoteApp(AcrylicWindow):
QEasingCurve.Type.OutCubic QEasingCurve.Type.OutCubic
) )
# v1.0.1 configure_window.py
def configure_window(self) -> None: def configure_window(self) -> None:
window = self.profile["window"] window = self.profile["window"]
@@ -289,6 +324,11 @@ class StickyNoteApp(AcrylicWindow):
True, True,
) )
self.setWindowFlag(
Qt.WindowType.FramelessWindowHint,
True,
)
self.setAttribute( self.setAttribute(
Qt.WidgetAttribute.WA_TranslucentBackground, Qt.WidgetAttribute.WA_TranslucentBackground,
True, True,
@@ -298,19 +338,11 @@ class StickyNoteApp(AcrylicWindow):
Qt.WidgetAttribute.WA_NoSystemBackground, Qt.WidgetAttribute.WA_NoSystemBackground,
True, True,
) )
self.setWindowFlag(
Qt.WindowType.WindowCloseButtonHint,
False,
)
self.setAutoFillBackground( self.setAutoFillBackground(
False False
) )
QTimer.singleShot(
120,
self.initialize_effects,
)
def build_ui(self) -> None: def build_ui(self) -> None:
self.root = QWidget(self) self.root = QWidget(self)
@@ -318,48 +350,34 @@ class StickyNoteApp(AcrylicWindow):
"container" "container"
) )
self.root.setGeometry(
self.rect()
)
self.layout = QVBoxLayout( self.layout = QVBoxLayout(
self.root self.root
) )
window = self.profile["window"]
margin = window[
"layout_margin"
]
self.layout.setContentsMargins( self.layout.setContentsMargins(
margin, 4,
margin, 4,
margin, 4,
margin, 4,
) )
self.layout.setSpacing( self.layout.setSpacing(0)
window["layout_spacing"]
) self.titleBar.hide()
self.titleBar.setFixedHeight(0)
self.drag_handle = DragHandle( self.drag_handle = DragHandle(
self.root self.root
) )
self.drag_handle.setFixedHeight( self.drag_handle.hide()
window[
"drag_handle_height"
]
)
self.drag_handle.drag_started.connect( self.drag_handle.setFixedHeight(0)
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.editor = LinkTextEdit(
self.root self.root
@@ -377,6 +395,37 @@ class StickyNoteApp(AcrylicWindow):
0 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.editor.textChanged.connect(
self.queue_save self.queue_save
) )
@@ -406,13 +455,13 @@ class StickyNoteApp(AcrylicWindow):
) )
content_layout.setContentsMargins( content_layout.setContentsMargins(
0, 4,
0, 4,
0, 4,
0, 4,
) )
content_layout.setSpacing(0) content_layout.setSpacing(1)
content_layout.addWidget( content_layout.addWidget(
self.editor, self.editor,
@@ -428,10 +477,6 @@ class StickyNoteApp(AcrylicWindow):
self.root self.root
) )
self.layout.addWidget(
self.drag_handle
)
self.layout.addWidget( self.layout.addWidget(
content, content,
1, 1,
@@ -444,12 +489,12 @@ class StickyNoteApp(AcrylicWindow):
| Qt.AlignmentFlag.AlignRight, | Qt.AlignmentFlag.AlignRight,
) )
self.titleBar.hide()
self.editor.setFocus() self.editor.setFocus()
def build_tray(self) -> None: def build_tray(self) -> None:
if QSystemTrayIcon.isSystemTrayAvailable() is False: if not QSystemTrayIcon.isSystemTrayAvailable():
return return
if not self.profile[ if not self.profile[
"behavior" "behavior"
]["enable_tray"]: ]["enable_tray"]:
@@ -603,6 +648,7 @@ class StickyNoteApp(AcrylicWindow):
], ],
) )
) )
def initialize_effects(self) -> None: def initialize_effects(self) -> None:
appearance = ( appearance = (
self.profile["appearance"] self.profile["appearance"]
@@ -631,16 +677,18 @@ class StickyNoteApp(AcrylicWindow):
) )
self.update() self.update()
self.repaint() self.repaint()
except Exception: except Exception:
logging.exception( logging.exception(
"window_effect_failure" "window_effect_failure"
) )
def start_drag(
self, def activate_delayed_drag(self) -> None:
global_pos: QPoint, if not self.pending_drag:
) -> None: return
self.window_state = ( self.window_state = (
WindowState.DRAGGING WindowState.DRAGGING
) )
@@ -651,27 +699,84 @@ class StickyNoteApp(AcrylicWindow):
self.apply_style() self.apply_style()
self.drag_offset = (
global_pos
- self.frameGeometry().topLeft()
)
self.setWindowOpacity( self.setWindowOpacity(
self.profile["appearance"][ self.profile["appearance"][
"opacity_dragging" "opacity_dragging"
] ]
) )
def perform_drag( def mousePressEvent(self, event) -> None:
self, if (
global_pos: QPoint, event.button()
) -> None: == Qt.MouseButton.LeftButton
target = ( ):
global_pos self.pending_drag = True
- self.drag_offset
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()
) )
self.move(target) 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: def finish_drag(self) -> None:
self.window_state = ( self.window_state = (
@@ -690,7 +795,15 @@ class StickyNoteApp(AcrylicWindow):
] ]
) )
self.queue_save()
def queue_save(self) -> None: def queue_save(self) -> None:
if not hasattr(
self,
"save_timer",
):
return
self.save_timer.start( self.save_timer.start(
self.profile["editor"][ self.profile["editor"][
"save_debounce_ms" "save_debounce_ms"
@@ -698,6 +811,12 @@ class StickyNoteApp(AcrylicWindow):
) )
def queue_preview(self) -> None: def queue_preview(self) -> None:
if not hasattr(
self,
"preview_timer",
):
return
if not self.profile[ if not self.profile[
"editor" "editor"
][ ][
@@ -721,9 +840,11 @@ class StickyNoteApp(AcrylicWindow):
or not self.isActiveWindow() or not self.isActiveWindow()
): ):
return return
self.preview.setFocusPolicy( self.preview.setFocusPolicy(
Qt.FocusPolicy.NoFocus Qt.FocusPolicy.NoFocus
) )
markdown = ( markdown = (
self.editor.toPlainText() self.editor.toPlainText()
) )
@@ -751,49 +872,65 @@ class StickyNoteApp(AcrylicWindow):
self.preview.show() self.preview.show()
def save_state(self) -> None: def save_state(self) -> None:
markdown = ( try:
self.editor.toPlainText() APP_DIR.mkdir(
) parents=True,
exist_ok=True,
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 = ( markdown = (
self.editor.toPlainText()
)
current_hash = sha256(
markdown.encode("utf-8")
).hexdigest()
if (
current_hash 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,
),
) )
STATE_FILE.write_text( except Exception:
json.dumps( logging.exception(
{ "save_state_failure"
"x": self.x(), )
"y": self.y(),
"width": self.width(),
"height": self.height(),
},
indent=2,
),
encoding="utf-8",
)
def restore_state(self) -> None: def restore_state(self) -> None:
if MARKDOWN_FILE.exists(): try:
self.editor.setPlainText( self.ensure_runtime_files()
markdown = (
MARKDOWN_FILE.read_text( MARKDOWN_FILE.read_text(
encoding="utf-8", encoding="utf-8",
) )
) )
if not STATE_FILE.exists(): self.editor.setPlainText(
return markdown
)
try:
state = json.loads( state = json.loads(
STATE_FILE.read_text( STATE_FILE.read_text(
encoding="utf-8", encoding="utf-8",
@@ -801,13 +938,25 @@ class StickyNoteApp(AcrylicWindow):
) )
self.resize( self.resize(
state["width"], max(
state["height"], state.get(
"width",
250,
),
200,
),
max(
state.get(
"height",
260,
),
100,
),
) )
self.move( self.move(
state["x"], state.get("x", 100),
state["y"], state.get("y", 100),
) )
self.clamp_to_screen() self.clamp_to_screen()
@@ -867,29 +1016,38 @@ class StickyNoteApp(AcrylicWindow):
self.apply_style() self.apply_style()
def open_settings(self) -> None: def open_settings(self) -> None:
path = ( SETTINGS_FILE.touch(
APP_DIR / "settings.json" exist_ok=True
) )
path.touch(exist_ok=True)
try: try:
if os.name == "nt": if os.name == "nt":
os.startfile(str(path)) os.startfile(
str(SETTINGS_FILE)
)
return return
if sys.platform.startswith( if sys.platform.startswith(
"linux" "linux"
): ):
subprocess.Popen( subprocess.Popen(
["xdg-open", str(path)] [
"xdg-open",
str(SETTINGS_FILE),
]
) )
return return
if sys.platform == "darwin": if sys.platform == "darwin":
subprocess.Popen( subprocess.Popen(
["open", str(path)] [
"open",
str(SETTINGS_FILE),
]
) )
return return
except Exception: except Exception:
@@ -897,18 +1055,21 @@ class StickyNoteApp(AcrylicWindow):
"open_settings_failure" "open_settings_failure"
) )
def nativeEvent(self, eventType, message): def moveEvent(self, event) -> None:
try: super().moveEvent(event)
return super().nativeEvent(eventType, message)
except KeyboardInterrupt: if (
self.safe_exit() getattr(
return False, 0 self,
except SystemExit: "startup_ready",
self.safe_exit() False,
return False, 0 )
except Exception: and hasattr(
traceback.print_exc() self,
return False, 0 "save_timer",
)
):
self.queue_save()
def resizeEvent(self, event) -> None: def resizeEvent(self, event) -> None:
super().resizeEvent(event) super().resizeEvent(event)
@@ -919,15 +1080,29 @@ class StickyNoteApp(AcrylicWindow):
None, None,
) )
if root is None: if root is not None:
return geometry = self.rect()
geometry = self.rect() if (
root.geometry()
!= geometry
):
root.setGeometry(
geometry
)
if root.geometry() != geometry: if (
root.setGeometry( getattr(
geometry self,
"startup_ready",
False,
) )
and hasattr(
self,
"save_timer",
)
):
self.queue_save()
def focusInEvent(self, event) -> None: def focusInEvent(self, event) -> None:
super().focusInEvent(event) super().focusInEvent(event)
@@ -966,16 +1141,21 @@ class StickyNoteApp(AcrylicWindow):
) )
def closeEvent(self, event) -> None: def closeEvent(self, event) -> None:
if self._is_quitting or self._force_close: if (
self._is_quitting
or self._force_close
):
try: try:
self.save_state() self.save_state()
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
event.accept() event.accept()
return return
event.ignore() event.ignore()
self.hide() self.hide()
def register_shutdown_hooks(self) -> None: def register_shutdown_hooks(self) -> None:
@@ -984,11 +1164,20 @@ class StickyNoteApp(AcrylicWindow):
def handle_shutdown(*_) -> None: def handle_shutdown(*_) -> None:
self.safe_exit() self.safe_exit()
atexit.register(handle_shutdown) atexit.register(
handle_shutdown
)
for sig in (signal.SIGINT, signal.SIGTERM): for sig in (
signal.SIGINT,
signal.SIGTERM,
):
try: try:
signal.signal(sig, handle_shutdown) signal.signal(
sig,
handle_shutdown,
)
except Exception: except Exception:
pass pass
@@ -1013,24 +1202,38 @@ class StickyNoteApp(AcrylicWindow):
traceback.print_exc() traceback.print_exc()
try: try:
if hasattr(self, "save_timer"): if hasattr(
self,
"save_timer",
):
self.save_timer.stop() self.save_timer.stop()
if hasattr(self, "preview_timer"): if hasattr(
self,
"preview_timer",
):
self.preview_timer.stop() self.preview_timer.stop()
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
try: try:
if hasattr(self, "tray"): if hasattr(
self,
"tray",
):
self.tray.hide() self.tray.hide()
self.tray.deleteLater() self.tray.deleteLater()
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
try: try:
self.hide() self.hide()
self.deleteLater() self.deleteLater()
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
@@ -1040,6 +1243,6 @@ class StickyNoteApp(AcrylicWindow):
app.quit() app.quit()
try: try:
app.quit() app.quit()
except: except Exception:
sys.exit(0) sys.exit(0)