From 9eb1d2cc88164b0a889332ff716ba541019ee4e4 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 24 May 2026 10:52:44 +0000 Subject: [PATCH] app v1.1 --- core/app.py | 533 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 368 insertions(+), 165 deletions(-) diff --git a/core/app.py b/core/app.py index ca85a00..5846e3f 100644 --- a/core/app.py +++ b/core/app.py @@ -1,18 +1,15 @@ -# v3.1.0 app.py +# v4.2.1 app.py import atexit -import signal -import traceback -import contextlib 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 pathlib import Path -from typing import Final from PyQt6.QtCore import ( QEasingCurve, @@ -49,10 +46,16 @@ from PyQt6.QtWidgets import ( 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 @@ -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): IDLE = auto() DRAGGING = auto() @@ -87,9 +81,8 @@ class ActiveState(Enum): class PersistedState: x: int = 100 y: int = 100 - width: int = 260 - height: int = 300 - theme_index: int = 0 + width: int = 250 + height: int = 260 class DragHandle(QFrame): @@ -141,15 +134,16 @@ class StickyNoteApp(AcrylicWindow): def __init__(self): super().__init__() + self._is_quitting = False self._force_close = False - self._closing = False - self.settings_manager = ( SettingsManager.load() ) + self.ensure_runtime_files() + self.profile = ( self.settings_manager.profile ) @@ -164,6 +158,10 @@ class StickyNoteApp(AcrylicWindow): self.drag_offset = QPoint() + self.drag_origin = QPoint() + + self.pending_drag = False + self.last_saved_hash = "" self.last_preview_hash = "" @@ -172,12 +170,17 @@ class StickyNoteApp(AcrylicWindow): self.configure_font() - self.build_ui() - self.setup_timers() + self.build_ui() + self.configure_window() + QTimer.singleShot( + 80, + self.restore_state, + ) + QTimer.singleShot( 120, self.initialize_effects, @@ -188,11 +191,6 @@ class StickyNoteApp(AcrylicWindow): self.build_tray, ) - QTimer.singleShot( - 80, - self.restore_state, - ) - QTimer.singleShot( 260, self.apply_style, @@ -205,6 +203,34 @@ class StickyNoteApp(AcrylicWindow): 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() @@ -253,6 +279,16 @@ class StickyNoteApp(AcrylicWindow): 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, @@ -270,7 +306,6 @@ class StickyNoteApp(AcrylicWindow): QEasingCurve.Type.OutCubic ) - # v1.0.1 configure_window.py def configure_window(self) -> None: window = self.profile["window"] @@ -289,6 +324,11 @@ class StickyNoteApp(AcrylicWindow): True, ) + self.setWindowFlag( + Qt.WindowType.FramelessWindowHint, + True, + ) + self.setAttribute( Qt.WidgetAttribute.WA_TranslucentBackground, True, @@ -298,19 +338,11 @@ class StickyNoteApp(AcrylicWindow): Qt.WidgetAttribute.WA_NoSystemBackground, True, ) - self.setWindowFlag( - Qt.WindowType.WindowCloseButtonHint, - False, - ) + self.setAutoFillBackground( False ) - QTimer.singleShot( - 120, - self.initialize_effects, - ) - def build_ui(self) -> None: self.root = QWidget(self) @@ -318,48 +350,34 @@ class StickyNoteApp(AcrylicWindow): "container" ) + self.root.setGeometry( + self.rect() + ) + self.layout = QVBoxLayout( self.root ) - - window = self.profile["window"] - - margin = window[ - "layout_margin" - ] self.layout.setContentsMargins( - margin, - margin, - margin, - margin, + 4, + 4, + 4, + 4, ) - self.layout.setSpacing( - window["layout_spacing"] - ) + self.layout.setSpacing(0) + + self.titleBar.hide() + + self.titleBar.setFixedHeight(0) self.drag_handle = DragHandle( self.root ) - self.drag_handle.setFixedHeight( - window[ - "drag_handle_height" - ] - ) + self.drag_handle.hide() - 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.drag_handle.setFixedHeight(0) self.editor = LinkTextEdit( self.root @@ -377,6 +395,37 @@ class StickyNoteApp(AcrylicWindow): 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 ) @@ -406,13 +455,13 @@ class StickyNoteApp(AcrylicWindow): ) content_layout.setContentsMargins( - 0, - 0, - 0, - 0, + 4, + 4, + 4, + 4, ) - content_layout.setSpacing(0) + content_layout.setSpacing(1) content_layout.addWidget( self.editor, @@ -428,10 +477,6 @@ class StickyNoteApp(AcrylicWindow): self.root ) - self.layout.addWidget( - self.drag_handle - ) - self.layout.addWidget( content, 1, @@ -444,12 +489,12 @@ class StickyNoteApp(AcrylicWindow): | Qt.AlignmentFlag.AlignRight, ) - self.titleBar.hide() self.editor.setFocus() - + def build_tray(self) -> None: - if QSystemTrayIcon.isSystemTrayAvailable() is False: + if not QSystemTrayIcon.isSystemTrayAvailable(): return + if not self.profile[ "behavior" ]["enable_tray"]: @@ -603,6 +648,7 @@ class StickyNoteApp(AcrylicWindow): ], ) ) + def initialize_effects(self) -> None: appearance = ( self.profile["appearance"] @@ -631,16 +677,18 @@ class StickyNoteApp(AcrylicWindow): ) self.update() + self.repaint() except Exception: logging.exception( "window_effect_failure" ) - def start_drag( - self, - global_pos: QPoint, - ) -> None: + + def activate_delayed_drag(self) -> None: + if not self.pending_drag: + return + self.window_state = ( WindowState.DRAGGING ) @@ -651,27 +699,84 @@ class StickyNoteApp(AcrylicWindow): 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 + 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() ) - 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: self.window_state = ( @@ -690,7 +795,15 @@ class StickyNoteApp(AcrylicWindow): ] ) + 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" @@ -698,6 +811,12 @@ class StickyNoteApp(AcrylicWindow): ) def queue_preview(self) -> None: + if not hasattr( + self, + "preview_timer", + ): + return + if not self.profile[ "editor" ][ @@ -721,9 +840,11 @@ class StickyNoteApp(AcrylicWindow): or not self.isActiveWindow() ): return + self.preview.setFocusPolicy( Qt.FocusPolicy.NoFocus ) + markdown = ( self.editor.toPlainText() ) @@ -751,49 +872,65 @@ class StickyNoteApp(AcrylicWindow): 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", + try: + APP_DIR.mkdir( + parents=True, + exist_ok=True, ) - self.last_saved_hash = ( + 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, + ), ) - STATE_FILE.write_text( - json.dumps( - { - "x": self.x(), - "y": self.y(), - "width": self.width(), - "height": self.height(), - }, - indent=2, - ), - encoding="utf-8", - ) + except Exception: + logging.exception( + "save_state_failure" + ) def restore_state(self) -> None: - if MARKDOWN_FILE.exists(): - self.editor.setPlainText( + try: + self.ensure_runtime_files() + + markdown = ( MARKDOWN_FILE.read_text( encoding="utf-8", ) ) - if not STATE_FILE.exists(): - return + self.editor.setPlainText( + markdown + ) - try: state = json.loads( STATE_FILE.read_text( encoding="utf-8", @@ -801,13 +938,25 @@ class StickyNoteApp(AcrylicWindow): ) self.resize( - state["width"], - state["height"], + max( + state.get( + "width", + 250, + ), + 200, + ), + max( + state.get( + "height", + 260, + ), + 100, + ), ) self.move( - state["x"], - state["y"], + state.get("x", 100), + state.get("y", 100), ) self.clamp_to_screen() @@ -867,29 +1016,38 @@ class StickyNoteApp(AcrylicWindow): self.apply_style() def open_settings(self) -> None: - path = ( - APP_DIR / "settings.json" + SETTINGS_FILE.touch( + exist_ok=True ) - path.touch(exist_ok=True) - try: if os.name == "nt": - os.startfile(str(path)) + os.startfile( + str(SETTINGS_FILE) + ) + return if sys.platform.startswith( "linux" ): subprocess.Popen( - ["xdg-open", str(path)] + [ + "xdg-open", + str(SETTINGS_FILE), + ] ) + return if sys.platform == "darwin": subprocess.Popen( - ["open", str(path)] + [ + "open", + str(SETTINGS_FILE), + ] ) + return except Exception: @@ -897,18 +1055,21 @@ class StickyNoteApp(AcrylicWindow): "open_settings_failure" ) - def nativeEvent(self, eventType, message): - try: - return super().nativeEvent(eventType, message) - except KeyboardInterrupt: - self.safe_exit() - return False, 0 - except SystemExit: - self.safe_exit() - return False, 0 - except Exception: - traceback.print_exc() - return False, 0 + 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) @@ -919,15 +1080,29 @@ class StickyNoteApp(AcrylicWindow): None, ) - if root is None: - return + if root is not None: + geometry = self.rect() - geometry = self.rect() + if ( + root.geometry() + != geometry + ): + root.setGeometry( + geometry + ) - 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) @@ -966,16 +1141,21 @@ class StickyNoteApp(AcrylicWindow): ) def closeEvent(self, event) -> None: - if self._is_quitting or self._force_close: + 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: @@ -984,11 +1164,20 @@ class StickyNoteApp(AcrylicWindow): def handle_shutdown(*_) -> None: 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: - signal.signal(sig, handle_shutdown) + signal.signal( + sig, + handle_shutdown, + ) + except Exception: pass @@ -1013,24 +1202,38 @@ class StickyNoteApp(AcrylicWindow): traceback.print_exc() try: - if hasattr(self, "save_timer"): + if hasattr( + self, + "save_timer", + ): self.save_timer.stop() - if hasattr(self, "preview_timer"): + if hasattr( + self, + "preview_timer", + ): self.preview_timer.stop() + except Exception: traceback.print_exc() try: - if hasattr(self, "tray"): + 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() @@ -1039,7 +1242,7 @@ class StickyNoteApp(AcrylicWindow): if app is not None: app.quit() - try: - app.quit() - except: - sys.exit(0) \ No newline at end of file + try: + app.quit() + except Exception: + sys.exit(0) \ No newline at end of file