fixed a bunch of stuff, added a notes programm, which lets you take notes, did some giant refactoring work and did some general designing
This commit is contained in:
811
src/NotesDialog.py
Normal file
811
src/NotesDialog.py
Normal file
@@ -0,0 +1,811 @@
|
||||
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="IRC Notes", size=(900, 650), pos=pos,
|
||||
style=wx.DEFAULT_FRAME_STYLE)
|
||||
|
||||
self.parent = parent
|
||||
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(wx.Colour(245, 245, 245))
|
||||
|
||||
# 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
|
||||
|
||||
# Initialize status
|
||||
self.update_status("Ready")
|
||||
|
||||
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_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_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", "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)
|
||||
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
|
||||
Reference in New Issue
Block a user