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