Files
ircdvd/src/NotesDialog.py

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