829 lines
35 KiB
Python
829 lines
35 KiB
Python
import wx
|
|
import wx.richtext as rt
|
|
import json
|
|
from collections import defaultdict
|
|
import tempfile
|
|
import os
|
|
import time
|
|
import threading
|
|
from typing import Dict, Any, Optional
|
|
|
|
class NotesDialog(wx.Frame):
|
|
def __init__(self, parent, notes_data=None, pos=None):
|
|
# If no position specified, offset from parent
|
|
if pos is None and parent:
|
|
parent_pos = parent.GetPosition()
|
|
pos = (parent_pos.x + 50, parent_pos.y + 50)
|
|
|
|
super().__init__(parent, title="wxNotes", size=(900, 650), pos=pos,
|
|
style=wx.DEFAULT_FRAME_STYLE)
|
|
|
|
self.parent = parent
|
|
self.theme = getattr(parent, "theme", None)
|
|
self.notes_data = notes_data or defaultdict(dict)
|
|
self.current_note_key = None
|
|
self.updating_title = False
|
|
self.is_closing = False
|
|
self.content_changed = False
|
|
self.last_save_time = time.time()
|
|
self.auto_save_interval = 2 # seconds - reduced for immediate saving
|
|
|
|
self.SetBackgroundColour(self.get_theme_colour("window_bg", wx.SystemSettings().GetColour(wx.SYS_COLOUR_WINDOW)))
|
|
|
|
# Set icon if parent has one
|
|
if parent:
|
|
self.SetIcon(parent.GetIcon())
|
|
|
|
self.create_controls()
|
|
self.load_notes_list()
|
|
|
|
# Bind close event to save before closing
|
|
self.Bind(wx.EVT_CLOSE, self.on_close)
|
|
|
|
# Auto-save timer (save every 2 seconds)
|
|
self.save_timer = wx.Timer(self)
|
|
self.Bind(wx.EVT_TIMER, self.on_auto_save, self.save_timer)
|
|
self.save_timer.Start(self.auto_save_interval * 1000) # Convert to milliseconds
|
|
|
|
# Status update timer (update status more frequently)
|
|
self.status_timer = wx.Timer(self)
|
|
self.Bind(wx.EVT_TIMER, self.on_status_update, self.status_timer)
|
|
self.status_timer.Start(3000) # 3 seconds
|
|
accel_tbl = wx.AcceleratorTable([
|
|
(wx.ACCEL_SHIFT, wx.WXK_ESCAPE, 1003),
|
|
])
|
|
self.SetAcceleratorTable(accel_tbl)
|
|
self.Bind(wx.EVT_MENU, lambda evt: self.close_parent(self.GetParent().GetId()), id=1003)
|
|
|
|
# Initialize status
|
|
self.update_status("Ready")
|
|
|
|
def close_parent(self, pId):
|
|
if self.GetParent().GetId() == pId:
|
|
self.GetParent().Close()
|
|
|
|
def get_theme_colour(self, key, fallback):
|
|
if self.theme and key in self.theme:
|
|
return self.theme[key]
|
|
return fallback
|
|
|
|
def create_controls(self):
|
|
# Create menu bar
|
|
self.create_menu_bar()
|
|
|
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
# Main content area with splitter
|
|
splitter = wx.SplitterWindow(self, style=wx.SP_3D | wx.SP_LIVE_UPDATE)
|
|
|
|
# Left panel - notes list
|
|
left_panel = wx.Panel(splitter)
|
|
left_panel.SetBackgroundColour(self.get_theme_colour("sidebar_bg", left_panel.GetBackgroundColour()))
|
|
left_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
notes_label = wx.StaticText(left_panel, label="Your Notes:")
|
|
font = notes_label.GetFont()
|
|
font.PointSize += 1
|
|
font = font.Bold()
|
|
notes_label.SetFont(font)
|
|
left_sizer.Add(notes_label, 0, wx.ALL, 8)
|
|
|
|
self.notes_list = wx.ListBox(left_panel, style=wx.LB_SINGLE)
|
|
self.notes_list.Bind(wx.EVT_LISTBOX, self.on_note_select)
|
|
self.notes_list.Bind(wx.EVT_LISTBOX_DCLICK, self.on_note_double_click)
|
|
left_sizer.Add(self.notes_list, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
|
|
|
# Note management buttons
|
|
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
self.new_btn = wx.Button(left_panel, label="New Note")
|
|
self.new_btn.Bind(wx.EVT_BUTTON, self.on_new_note)
|
|
btn_sizer.Add(self.new_btn, 1, wx.ALL, 3)
|
|
|
|
self.delete_btn = wx.Button(left_panel, label="Delete")
|
|
self.delete_btn.Bind(wx.EVT_BUTTON, self.on_delete_note)
|
|
btn_sizer.Add(self.delete_btn, 1, wx.ALL, 3)
|
|
|
|
left_sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 8)
|
|
left_panel.SetSizer(left_sizer)
|
|
|
|
# Right panel - editor
|
|
right_panel = wx.Panel(splitter)
|
|
right_panel.SetBackgroundColour(self.get_theme_colour("content_bg", right_panel.GetBackgroundColour()))
|
|
right_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
title_label = wx.StaticText(right_panel, label="Note Title:")
|
|
title_label.SetFont(font)
|
|
right_sizer.Add(title_label, 0, wx.ALL, 8)
|
|
|
|
self.title_ctrl = wx.TextCtrl(right_panel, style=wx.TE_PROCESS_ENTER)
|
|
self.title_ctrl.Bind(wx.EVT_TEXT, self.on_title_change)
|
|
self.title_ctrl.Bind(wx.EVT_KILL_FOCUS, self.on_title_lose_focus)
|
|
title_font = self.title_ctrl.GetFont()
|
|
title_font.PointSize += 2
|
|
self.title_ctrl.SetFont(title_font)
|
|
right_sizer.Add(self.title_ctrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
|
|
|
content_label = wx.StaticText(right_panel, label="Content:")
|
|
content_label.SetFont(font)
|
|
right_sizer.Add(content_label, 0, wx.LEFT | wx.RIGHT, 8)
|
|
|
|
# Formatting toolbar
|
|
self.setup_editor_toolbar(right_panel)
|
|
right_sizer.Add(self.toolbar, 0, wx.EXPAND | wx.ALL, 8)
|
|
|
|
# Rich text editor - FIXED: Make sure editor attribute is created
|
|
self.editor = rt.RichTextCtrl(right_panel,
|
|
style=wx.VSCROLL | wx.HSCROLL | wx.BORDER_SIMPLE)
|
|
self.editor.Bind(wx.EVT_TEXT, self.on_content_change)
|
|
self.editor.Bind(wx.EVT_KILL_FOCUS, self.on_editor_lose_focus)
|
|
|
|
# Set default font
|
|
attr = rt.RichTextAttr()
|
|
attr.SetFontSize(11)
|
|
attr.SetFontFaceName("Segoe UI")
|
|
self.editor.SetBasicStyle(attr)
|
|
|
|
right_sizer.Add(self.editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
|
right_panel.SetSizer(right_sizer)
|
|
|
|
# Split the window
|
|
splitter.SplitVertically(left_panel, right_panel, 250)
|
|
splitter.SetMinimumPaneSize(200)
|
|
|
|
main_sizer.Add(splitter, 1, wx.EXPAND | wx.ALL, 5)
|
|
|
|
# Status bar
|
|
self.status_bar = self.CreateStatusBar(2)
|
|
self.status_bar.SetStatusWidths([-1, 150])
|
|
self.update_status("Ready")
|
|
|
|
self.SetSizer(main_sizer)
|
|
self.enable_editor(False)
|
|
|
|
def create_menu_bar(self):
|
|
menubar = wx.MenuBar()
|
|
|
|
# File menu
|
|
file_menu = wx.Menu()
|
|
export_item = file_menu.Append(wx.ID_ANY, "&Export Notes...\tCtrl+E", "Export all notes to file")
|
|
import_item = file_menu.Append(wx.ID_ANY, "&Import Notes...\tCtrl+I", "Import notes from file")
|
|
file_menu.AppendSeparator()
|
|
export_text_item = file_menu.Append(wx.ID_ANY, "Export Current Note as &Text...\tCtrl+T", "Export current note as plain text")
|
|
file_menu.AppendSeparator()
|
|
exit_item = file_menu.Append(wx.ID_EXIT, "&Close\tSHIFT+ESC", "Close notes window")
|
|
|
|
menubar.Append(file_menu, "&File")
|
|
|
|
# Notes menu
|
|
notes_menu = wx.Menu()
|
|
new_note_item = notes_menu.Append(wx.ID_NEW, "&New Note\tCtrl+N", "Create a new note")
|
|
delete_note_item = notes_menu.Append(wx.ID_DELETE, "&Delete Note\tDel", "Delete current note")
|
|
notes_menu.AppendSeparator()
|
|
rename_note_item = notes_menu.Append(wx.ID_ANY, "&Rename Note\tF2", "Rename current note")
|
|
|
|
menubar.Append(notes_menu, "&Notes")
|
|
|
|
# Edit menu
|
|
edit_menu = wx.Menu()
|
|
undo_item = edit_menu.Append(wx.ID_UNDO, "&Undo\tCtrl+Z", "Undo last action")
|
|
redo_item = edit_menu.Append(wx.ID_REDO, "&Redo\tCtrl+Y", "Redo last action")
|
|
edit_menu.AppendSeparator()
|
|
cut_item = edit_menu.Append(wx.ID_CUT, "Cu&t\tCtrl+X", "Cut selection")
|
|
copy_item = edit_menu.Append(wx.ID_COPY, "&Copy\tCtrl+C", "Copy selection")
|
|
paste_item = edit_menu.Append(wx.ID_PASTE, "&Paste\tCtrl+V", "Paste from clipboard")
|
|
edit_menu.AppendSeparator()
|
|
select_all_item = edit_menu.Append(wx.ID_SELECTALL, "Select &All\tCtrl+A", "Select all text")
|
|
|
|
menubar.Append(edit_menu, "&Edit")
|
|
|
|
# Format menu
|
|
format_menu = wx.Menu()
|
|
bold_item = format_menu.Append(wx.ID_ANY, "&Bold\tCtrl+B", "Bold text")
|
|
italic_item = format_menu.Append(wx.ID_ANY, "&Italic\tCtrl+I", "Italic text")
|
|
underline_item = format_menu.Append(wx.ID_ANY, "&Underline\tCtrl+U", "Underline text")
|
|
format_menu.AppendSeparator()
|
|
font_color_item = format_menu.Append(wx.ID_ANY, "&Text Color...", "Change text color")
|
|
bg_color_item = format_menu.Append(wx.ID_ANY, "&Highlight Color...", "Change highlight color")
|
|
format_menu.AppendSeparator()
|
|
clear_format_item = format_menu.Append(wx.ID_ANY, "&Clear Formatting\tCtrl+Space", "Clear text formatting")
|
|
|
|
menubar.Append(format_menu, "F&ormat")
|
|
|
|
self.SetMenuBar(menubar)
|
|
|
|
# Bind menu events
|
|
self.Bind(wx.EVT_MENU, self.on_save_to_file, export_item)
|
|
self.Bind(wx.EVT_MENU, self.on_load_from_file, import_item)
|
|
self.Bind(wx.EVT_MENU, self.on_export_text, export_text_item)
|
|
self.Bind(wx.EVT_MENU, self.on_close, exit_item)
|
|
self.Bind(wx.EVT_MENU, self.on_new_note, new_note_item)
|
|
self.Bind(wx.EVT_MENU, self.on_delete_note, delete_note_item)
|
|
self.Bind(wx.EVT_MENU, self.on_rename_note, rename_note_item)
|
|
self.Bind(wx.EVT_MENU, self.on_bold, bold_item)
|
|
self.Bind(wx.EVT_MENU, self.on_italic, italic_item)
|
|
self.Bind(wx.EVT_MENU, self.on_underline, underline_item)
|
|
self.Bind(wx.EVT_MENU, self.on_text_color, font_color_item)
|
|
self.Bind(wx.EVT_MENU, self.on_background_color, bg_color_item)
|
|
self.Bind(wx.EVT_MENU, self.on_clear_formatting, clear_format_item)
|
|
self.Bind(wx.EVT_MENU, self.on_undo, undo_item)
|
|
self.Bind(wx.EVT_MENU, self.on_redo, redo_item)
|
|
self.Bind(wx.EVT_MENU, self.on_cut, cut_item)
|
|
self.Bind(wx.EVT_MENU, self.on_copy, copy_item)
|
|
self.Bind(wx.EVT_MENU, self.on_paste, paste_item)
|
|
self.Bind(wx.EVT_MENU, self.on_select_all, select_all_item)
|
|
|
|
def setup_editor_toolbar(self, parent):
|
|
self.toolbar = wx.Panel(parent)
|
|
self.toolbar.SetBackgroundColour(self.get_theme_colour("control_bg", self.toolbar.GetBackgroundColour()))
|
|
toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
# Text formatting buttons
|
|
self.bold_btn = wx.Button(self.toolbar, label="B", size=(35, 30))
|
|
self.bold_btn.SetFont(self.bold_btn.GetFont().Bold())
|
|
self.bold_btn.Bind(wx.EVT_BUTTON, self.on_bold)
|
|
self.bold_btn.SetToolTip("Bold (Ctrl+B)")
|
|
toolbar_sizer.Add(self.bold_btn, 0, wx.ALL, 2)
|
|
|
|
self.italic_btn = wx.Button(self.toolbar, label="I", size=(35, 30))
|
|
font = self.italic_btn.GetFont()
|
|
font.MakeItalic()
|
|
self.italic_btn.SetFont(font)
|
|
self.italic_btn.Bind(wx.EVT_BUTTON, self.on_italic)
|
|
self.italic_btn.SetToolTip("Italic (Ctrl+I)")
|
|
toolbar_sizer.Add(self.italic_btn, 0, wx.ALL, 2)
|
|
|
|
self.underline_btn = wx.Button(self.toolbar, label="U", size=(35, 30))
|
|
self.underline_btn.Bind(wx.EVT_BUTTON, self.on_underline)
|
|
self.underline_btn.SetToolTip("Underline (Ctrl+U)")
|
|
toolbar_sizer.Add(self.underline_btn, 0, wx.ALL, 2)
|
|
|
|
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
|
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
|
|
|
# Font size
|
|
toolbar_sizer.Add(wx.StaticText(self.toolbar, label="Size:"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
|
|
self.font_size_choice = wx.Choice(self.toolbar, choices=['8', '9', '10', '11', '12', '14', '16', '18', '20', '24'])
|
|
self.font_size_choice.SetSelection(3) # Default to 11
|
|
self.font_size_choice.Bind(wx.EVT_CHOICE, self.on_font_size)
|
|
self.font_size_choice.SetToolTip("Font Size")
|
|
toolbar_sizer.Add(self.font_size_choice, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
|
|
|
|
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
|
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
|
|
|
# Alignment buttons
|
|
self.align_left_btn = wx.Button(self.toolbar, label="Left", size=(45, 30))
|
|
self.align_left_btn.Bind(wx.EVT_BUTTON, self.on_align_left)
|
|
self.align_left_btn.SetToolTip("Align Left")
|
|
toolbar_sizer.Add(self.align_left_btn, 0, wx.ALL, 2)
|
|
|
|
self.align_center_btn = wx.Button(self.toolbar, label="Center", size=(55, 30))
|
|
self.align_center_btn.Bind(wx.EVT_BUTTON, self.on_align_center)
|
|
self.align_center_btn.SetToolTip("Align Center")
|
|
toolbar_sizer.Add(self.align_center_btn, 0, wx.ALL, 2)
|
|
|
|
self.align_right_btn = wx.Button(self.toolbar, label="Right", size=(45, 30))
|
|
self.align_right_btn.Bind(wx.EVT_BUTTON, self.on_align_right)
|
|
self.align_right_btn.SetToolTip("Align Right")
|
|
toolbar_sizer.Add(self.align_right_btn, 0, wx.ALL, 2)
|
|
|
|
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
|
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
|
|
|
# Color buttons
|
|
self.text_color_btn = wx.Button(self.toolbar, label="Text Color", size=(80, 30))
|
|
self.text_color_btn.Bind(wx.EVT_BUTTON, self.on_text_color)
|
|
self.text_color_btn.SetToolTip("Text Color")
|
|
toolbar_sizer.Add(self.text_color_btn, 0, wx.ALL, 2)
|
|
|
|
self.bg_color_btn = wx.Button(self.toolbar, label="Highlight", size=(80, 30))
|
|
self.bg_color_btn.Bind(wx.EVT_BUTTON, self.on_background_color)
|
|
self.bg_color_btn.SetToolTip("Highlight Color")
|
|
toolbar_sizer.Add(self.bg_color_btn, 0, wx.ALL, 2)
|
|
|
|
toolbar_sizer.AddStretchSpacer()
|
|
|
|
# Clear formatting
|
|
self.clear_fmt_btn = wx.Button(self.toolbar, label="Clear Format", size=(100, 30))
|
|
self.clear_fmt_btn.Bind(wx.EVT_BUTTON, self.on_clear_formatting)
|
|
self.clear_fmt_btn.SetToolTip("Remove all formatting (Ctrl+Space)")
|
|
toolbar_sizer.Add(self.clear_fmt_btn, 0, wx.ALL, 2)
|
|
|
|
self.toolbar.SetSizer(toolbar_sizer)
|
|
self.toolbar.SetBackgroundColour(wx.Colour(230, 230, 230))
|
|
|
|
def enable_editor(self, enable):
|
|
self.title_ctrl.Enable(enable)
|
|
self.editor.Enable(enable)
|
|
self.delete_btn.Enable(enable)
|
|
|
|
# Enable/disable toolbar buttons
|
|
for child in self.toolbar.GetChildren():
|
|
if isinstance(child, wx.Button) or isinstance(child, wx.Choice):
|
|
child.Enable(enable)
|
|
|
|
def update_status(self, message: str):
|
|
"""Update status text with timestamp"""
|
|
timestamp = wx.DateTime.Now().FormatTime()
|
|
self.status_bar.SetStatusText(f"{timestamp} - {message}", 0)
|
|
|
|
# Update save status in second field
|
|
if self.current_note_key and self.content_changed:
|
|
time_since_change = int(time.time() - self.notes_data[self.current_note_key]['last_modified'])
|
|
self.status_bar.SetStatusText(f"Unsaved changes ({time_since_change}s)", 1)
|
|
elif self.current_note_key:
|
|
time_since_save = int(time.time() - self.last_save_time)
|
|
self.status_bar.SetStatusText(f"Saved {time_since_save}s ago", 1)
|
|
else:
|
|
self.status_bar.SetStatusText("", 1)
|
|
|
|
def load_notes_list(self):
|
|
self.notes_list.Clear()
|
|
sorted_keys = sorted(self.notes_data.keys(),
|
|
key=lambda k: self.notes_data[k].get('last_modified', 0),
|
|
reverse=True)
|
|
for key in sorted_keys:
|
|
display_title = self.notes_data[key].get('title', key)
|
|
self.notes_list.Append(display_title, key)
|
|
|
|
def save_current_note_formatting(self) -> bool:
|
|
"""Save the current note's formatting to XML"""
|
|
if not self.current_note_key or self.is_closing:
|
|
return False
|
|
|
|
try:
|
|
# Save plain text
|
|
self.notes_data[self.current_note_key]['content'] = self.editor.GetValue()
|
|
|
|
# Save XML with formatting using a temporary file
|
|
with tempfile.NamedTemporaryFile(mode='w+', suffix='.xml', delete=False, encoding='utf-8') as tmp:
|
|
tmp_path = tmp.name
|
|
|
|
# Save to temp file
|
|
handler = rt.RichTextXMLHandler()
|
|
buffer = self.editor.GetBuffer()
|
|
|
|
if buffer and handler.SaveFile(buffer, tmp_path):
|
|
# Read back the XML content
|
|
with open(tmp_path, 'r', encoding='utf-8') as f:
|
|
xml_content = f.read()
|
|
self.notes_data[self.current_note_key]['xml_content'] = xml_content
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
# Clean up temp file
|
|
try:
|
|
os.unlink(tmp_path)
|
|
except Exception:
|
|
pass
|
|
|
|
self.content_changed = False
|
|
self.last_save_time = time.time()
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error saving note formatting: {e}")
|
|
return False
|
|
|
|
def auto_save_current_note(self):
|
|
"""Auto-save current note if changes were made"""
|
|
if self.current_note_key and self.content_changed:
|
|
if self.save_current_note_formatting():
|
|
return True
|
|
return False
|
|
|
|
def safe_load_xml_content(self, xml_content: str) -> bool:
|
|
"""Safely load XML content with error handling"""
|
|
try:
|
|
# Create a temporary file for XML loading
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False, encoding='utf-8') as tmp:
|
|
tmp.write(xml_content)
|
|
tmp_path = tmp.name
|
|
|
|
# Clear current buffer first
|
|
self.editor.Clear()
|
|
|
|
# Load from temp file
|
|
handler = rt.RichTextXMLHandler()
|
|
success = handler.LoadFile(self.editor.GetBuffer(), tmp_path)
|
|
|
|
# Clean up temp file
|
|
try:
|
|
os.unlink(tmp_path)
|
|
except Exception:
|
|
pass
|
|
|
|
if success:
|
|
self.editor.Refresh()
|
|
return True
|
|
else:
|
|
# If XML load fails, fall back to plain text
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"Error loading XML content: {e}")
|
|
return False
|
|
|
|
def on_note_select(self, event):
|
|
# Auto-save previous note before switching
|
|
if self.current_note_key:
|
|
self.auto_save_current_note()
|
|
|
|
selection = self.notes_list.GetSelection()
|
|
if selection != wx.NOT_FOUND:
|
|
key = self.notes_list.GetClientData(selection)
|
|
self.current_note_key = key
|
|
note_data = self.notes_data[key]
|
|
|
|
self.updating_title = True
|
|
self.title_ctrl.SetValue(note_data.get('title', ''))
|
|
self.updating_title = False
|
|
|
|
# Load content - check if it's XML format or plain text
|
|
content = note_data.get('content', '')
|
|
xml_content = note_data.get('xml_content', '')
|
|
|
|
# Clear any existing content first
|
|
self.editor.Clear()
|
|
|
|
if xml_content:
|
|
# Try to load rich text from XML
|
|
if not self.safe_load_xml_content(xml_content):
|
|
# Fallback to plain text if XML load fails
|
|
self.editor.SetValue(content)
|
|
elif content:
|
|
# Fallback to plain text
|
|
self.editor.SetValue(content)
|
|
else:
|
|
self.editor.Clear()
|
|
|
|
self.enable_editor(True)
|
|
self.editor.SetFocus()
|
|
self.content_changed = False
|
|
self.update_status(f"Loaded note: {note_data.get('title', 'Untitled')}")
|
|
|
|
def on_note_double_click(self, event):
|
|
"""Rename note on double click"""
|
|
self.on_rename_note(event)
|
|
|
|
def on_new_note(self, event):
|
|
note_id = f"note_{int(time.time() * 1000)}" # More precise timestamp
|
|
title = f"New Note {len(self.notes_data) + 1}"
|
|
self.notes_data[note_id] = {
|
|
'title': title,
|
|
'content': '',
|
|
'xml_content': '',
|
|
'created': time.time(),
|
|
'last_modified': time.time()
|
|
}
|
|
self.load_notes_list()
|
|
|
|
# Select and edit the new note
|
|
for i in range(self.notes_list.GetCount()):
|
|
if self.notes_list.GetClientData(i) == note_id:
|
|
self.notes_list.SetSelection(i)
|
|
self.on_note_select(event)
|
|
self.title_ctrl.SetFocus()
|
|
self.title_ctrl.SelectAll()
|
|
break
|
|
|
|
def on_delete_note(self, event):
|
|
if self.current_note_key:
|
|
note_title = self.notes_data[self.current_note_key].get('title', 'this note')
|
|
result = wx.MessageBox(
|
|
f"Are you sure you want to delete '{note_title}'?",
|
|
"Confirm Delete",
|
|
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION
|
|
)
|
|
if result == wx.YES:
|
|
del self.notes_data[self.current_note_key]
|
|
self.current_note_key = None
|
|
self.load_notes_list()
|
|
self.title_ctrl.SetValue('')
|
|
self.editor.Clear()
|
|
self.enable_editor(False)
|
|
self.update_status("Note deleted")
|
|
|
|
def on_rename_note(self, event):
|
|
"""Rename current note by focusing on title field"""
|
|
if self.current_note_key:
|
|
self.title_ctrl.SetFocus()
|
|
self.title_ctrl.SelectAll()
|
|
|
|
def on_title_change(self, event):
|
|
if self.current_note_key and not self.updating_title:
|
|
new_title = self.title_ctrl.GetValue().strip()
|
|
if new_title:
|
|
self.notes_data[self.current_note_key]['title'] = new_title
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
# Update the display in the list
|
|
for i in range(self.notes_list.GetCount()):
|
|
if self.notes_list.GetClientData(i) == self.current_note_key:
|
|
self.notes_list.SetString(i, new_title)
|
|
break
|
|
|
|
self.content_changed = True
|
|
# Auto-save immediately on title change
|
|
self.auto_save_current_note()
|
|
self.update_status("Title updated")
|
|
|
|
def on_title_lose_focus(self, event):
|
|
"""Auto-save when title loses focus"""
|
|
if self.content_changed:
|
|
self.auto_save_current_note()
|
|
event.Skip()
|
|
|
|
def on_content_change(self, event):
|
|
"""Mark content as changed for auto-save"""
|
|
if self.current_note_key:
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
# Trigger immediate auto-save on content change
|
|
wx.CallLater(1000, self.auto_save_current_note) # Save after 1 second delay
|
|
event.Skip()
|
|
|
|
def on_editor_lose_focus(self, event):
|
|
"""Auto-save when editor loses focus"""
|
|
if self.content_changed:
|
|
self.auto_save_current_note()
|
|
event.Skip()
|
|
|
|
def on_auto_save(self, event):
|
|
"""Auto-save current note on timer"""
|
|
if self.auto_save_current_note():
|
|
self.update_status(f"Auto-saved at {wx.DateTime.Now().FormatTime()}")
|
|
|
|
def on_status_update(self, event):
|
|
"""Update status bar"""
|
|
self.update_status("Ready")
|
|
|
|
# Text formatting methods
|
|
def on_bold(self, event):
|
|
self.editor.ApplyBoldToSelection()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_italic(self, event):
|
|
self.editor.ApplyItalicToSelection()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_underline(self, event):
|
|
self.editor.ApplyUnderlineToSelection()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_font_size(self, event):
|
|
size = int(self.font_size_choice.GetStringSelection())
|
|
attr = rt.RichTextAttr()
|
|
attr.SetFontSize(size)
|
|
|
|
range_obj = self.editor.GetSelectionRange()
|
|
if range_obj.GetLength() > 0:
|
|
self.editor.SetStyle(range_obj, attr)
|
|
else:
|
|
# Apply to next typed text
|
|
self.editor.SetDefaultStyle(attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_align_left(self, event):
|
|
attr = rt.RichTextAttr()
|
|
attr.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
|
|
|
|
# Get current paragraph range
|
|
pos = self.editor.GetInsertionPoint()
|
|
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
|
if para:
|
|
range_obj = para.GetRange()
|
|
self.editor.SetStyle(range_obj, attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_align_center(self, event):
|
|
attr = rt.RichTextAttr()
|
|
attr.SetAlignment(wx.TEXT_ALIGNMENT_CENTRE)
|
|
|
|
pos = self.editor.GetInsertionPoint()
|
|
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
|
if para:
|
|
range_obj = para.GetRange()
|
|
self.editor.SetStyle(range_obj, attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_align_right(self, event):
|
|
attr = rt.RichTextAttr()
|
|
attr.SetAlignment(wx.TEXT_ALIGNMENT_RIGHT)
|
|
|
|
pos = self.editor.GetInsertionPoint()
|
|
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
|
if para:
|
|
range_obj = para.GetRange()
|
|
self.editor.SetStyle(range_obj, attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_text_color(self, event):
|
|
color_data = wx.ColourData()
|
|
color_data.SetChooseFull(True)
|
|
|
|
dlg = wx.ColourDialog(self, color_data)
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
color = dlg.GetColourData().GetColour()
|
|
attr = rt.RichTextAttr()
|
|
attr.SetTextColour(color)
|
|
|
|
range_obj = self.editor.GetSelectionRange()
|
|
if range_obj.GetLength() > 0:
|
|
self.editor.SetStyle(range_obj, attr)
|
|
else:
|
|
self.editor.SetDefaultStyle(attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
dlg.Destroy()
|
|
|
|
def on_background_color(self, event):
|
|
color_data = wx.ColourData()
|
|
color_data.SetChooseFull(True)
|
|
|
|
dlg = wx.ColourDialog(self, color_data)
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
color = dlg.GetColourData().GetColour()
|
|
attr = rt.RichTextAttr()
|
|
attr.SetBackgroundColour(color)
|
|
|
|
range_obj = self.editor.GetSelectionRange()
|
|
if range_obj.GetLength() > 0:
|
|
self.editor.SetStyle(range_obj, attr)
|
|
else:
|
|
self.editor.SetDefaultStyle(attr)
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
dlg.Destroy()
|
|
|
|
def on_clear_formatting(self, event):
|
|
range_obj = self.editor.GetSelectionRange()
|
|
if range_obj.GetLength() > 0:
|
|
# Get the text
|
|
text = self.editor.GetRange(range_obj.GetStart(), range_obj.GetEnd())
|
|
|
|
# Delete the range and reinsert as plain text
|
|
self.editor.Delete(range_obj)
|
|
|
|
# Set basic style
|
|
attr = rt.RichTextAttr()
|
|
attr.SetFontSize(11)
|
|
attr.SetFontFaceName("Segoe UI")
|
|
attr.SetTextColour(wx.BLACK)
|
|
attr.SetBackgroundColour(wx.NullColour)
|
|
|
|
self.editor.BeginStyle(attr)
|
|
self.editor.WriteText(text)
|
|
self.editor.EndStyle()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
# Menu command handlers
|
|
def on_undo(self, event):
|
|
if self.editor.CanUndo():
|
|
self.editor.Undo()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_redo(self, event):
|
|
if self.editor.CanRedo():
|
|
self.editor.Redo()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_cut(self, event):
|
|
self.editor.Cut()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_copy(self, event):
|
|
self.editor.Copy()
|
|
|
|
def on_paste(self, event):
|
|
self.editor.Paste()
|
|
self.content_changed = True
|
|
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
|
|
|
def on_select_all(self, event):
|
|
self.editor.SelectAll()
|
|
|
|
def on_export_text(self, event):
|
|
if not self.current_note_key:
|
|
return
|
|
|
|
note_data = self.notes_data[self.current_note_key]
|
|
title = note_data.get('title', 'note')
|
|
|
|
with wx.FileDialog(self, "Export note as text",
|
|
defaultFile=f"{title}.txt",
|
|
wildcard="Text files (*.txt)|*.txt",
|
|
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
filename = dlg.GetPath()
|
|
try:
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
f.write(f"{title}\n")
|
|
f.write("=" * len(title) + "\n\n")
|
|
f.write(self.editor.GetValue())
|
|
wx.MessageBox("Note exported successfully!", "Success",
|
|
wx.OK | wx.ICON_INFORMATION)
|
|
self.update_status("Note exported to text file")
|
|
except Exception as e:
|
|
wx.MessageBox(f"Error exporting note: {e}", "Error",
|
|
wx.OK | wx.ICON_ERROR)
|
|
|
|
def on_save_to_file(self, event):
|
|
# Save current note first
|
|
if self.current_note_key:
|
|
self.auto_save_current_note()
|
|
|
|
with wx.FileDialog(self, "Export notes to file",
|
|
wildcard="JSON files (*.json)|*.json",
|
|
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
filename = dlg.GetPath()
|
|
try:
|
|
# Convert defaultdict to regular dict for JSON serialization
|
|
notes_dict = {k: dict(v) for k, v in self.notes_data.items()}
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
json.dump(notes_dict, f, indent=2, ensure_ascii=False)
|
|
wx.MessageBox("Notes exported successfully!", "Success",
|
|
wx.OK | wx.ICON_INFORMATION)
|
|
self.update_status("All notes exported to file")
|
|
except Exception as e:
|
|
wx.MessageBox(f"Error saving file: {e}", "Error",
|
|
wx.OK | wx.ICON_ERROR)
|
|
|
|
def on_load_from_file(self, event):
|
|
with wx.FileDialog(self, "Import notes from file",
|
|
wildcard="JSON files (*.json)|*.json",
|
|
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as dlg:
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
filename = dlg.GetPath()
|
|
try:
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
|
loaded_data = json.load(f)
|
|
|
|
# Auto-save current note before importing
|
|
if self.current_note_key:
|
|
self.auto_save_current_note()
|
|
|
|
# Merge with existing notes
|
|
result = wx.MessageBox(
|
|
"Replace existing notes or merge with them?\n\nYes = Replace, No = Merge",
|
|
"Import Options",
|
|
wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
|
|
)
|
|
|
|
if result == wx.YES: # Replace
|
|
self.notes_data.clear()
|
|
self.notes_data.update(loaded_data)
|
|
self.update_status("All notes replaced with imported file")
|
|
elif result == wx.NO: # Merge
|
|
self.notes_data.update(loaded_data)
|
|
self.update_status("Imported notes merged with existing notes")
|
|
else: # Cancel
|
|
return
|
|
|
|
self.load_notes_list()
|
|
self.current_note_key = None
|
|
self.title_ctrl.SetValue('')
|
|
self.editor.Clear()
|
|
self.enable_editor(False)
|
|
wx.MessageBox("Notes imported successfully!", "Success",
|
|
wx.OK | wx.ICON_INFORMATION)
|
|
except Exception as e:
|
|
wx.MessageBox(f"Error loading file: {e}", "Error",
|
|
wx.OK | wx.ICON_ERROR)
|
|
|
|
def on_close(self, event):
|
|
"""Save everything before closing"""
|
|
self.is_closing = True
|
|
|
|
# Stop timers
|
|
self.save_timer.Stop()
|
|
self.status_timer.Stop()
|
|
|
|
# Save current note
|
|
if self.current_note_key:
|
|
self.auto_save_current_note()
|
|
self.update_status("Final auto-save completed")
|
|
|
|
# Notify parent if it has a callback
|
|
if self.parent and hasattr(self.parent, 'on_notes_closed'):
|
|
self.parent.on_notes_closed()
|
|
|
|
self.Destroy()
|
|
|
|
def get_notes_data(self):
|
|
"""Get all notes data"""
|
|
# Save current note before returning
|
|
if self.current_note_key:
|
|
self.auto_save_current_note()
|
|
return self.notes_data |