A simple notes system added for the panel, I38 notes.
This commit is contained in:
parent
dc8f832840
commit
f41741866a
3
i38.sh
3
i38.sh
@ -591,6 +591,9 @@ mode "panel" {
|
||||
# System information bound to s
|
||||
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
|
||||
|
||||
# Simple notes system bound to n
|
||||
bindsym n exec --no-startup-id ${i3Path}/scripts/notes.py, mode "default"
|
||||
|
||||
# Exit panel mode without any action
|
||||
bindsym Escape mode "default"
|
||||
bindsym Control+g mode "default"
|
||||
|
823
scripts/notes.py
Executable file
823
scripts/notes.py
Executable file
@ -0,0 +1,823 @@
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gio, Gdk
|
||||
import os
|
||||
import datetime
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
class JotApp(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id="com.example.jot",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
||||
|
||||
# Set up data structures
|
||||
self.dbPath = self.get_db_path()
|
||||
self.conn = None
|
||||
self.init_database()
|
||||
|
||||
# Initialize settings
|
||||
self.expirationDays = self.get_setting("expirationDays", 0)
|
||||
self.confirmDelete = self.get_setting("confirmDelete", 1)
|
||||
|
||||
def do_activate(self):
|
||||
# Create main window when app is activated
|
||||
self.window = Gtk.ApplicationWindow(application=self, title="I38 Notes")
|
||||
self.window.set_default_size(500, 400)
|
||||
|
||||
# Connect the delete-event signal (for window close button)
|
||||
self.window.connect("delete-event", self.on_window_close)
|
||||
|
||||
# Set up keyboard shortcuts
|
||||
self.setup_actions()
|
||||
|
||||
# Build the main interface
|
||||
self.build_ui()
|
||||
|
||||
# Check for expired notes
|
||||
self.check_expirations()
|
||||
|
||||
self.window.show_all()
|
||||
|
||||
def on_window_close(self, window, event):
|
||||
"""Handle window close event"""
|
||||
# Close the database connection before quitting
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.quit()
|
||||
return True
|
||||
|
||||
def get_db_path(self):
|
||||
"""Get path to the SQLite database"""
|
||||
configHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
||||
configDir = os.path.join(configHome, 'stormux', 'I38')
|
||||
os.makedirs(configDir, exist_ok=True)
|
||||
return os.path.join(configDir, 'notes.sqlite')
|
||||
|
||||
def init_database(self):
|
||||
"""Initialize the SQLite database"""
|
||||
try:
|
||||
self.conn = sqlite3.connect(self.dbPath)
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Create notes table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires TIMESTAMP NULL,
|
||||
locked BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Create settings table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
# Initialize default settings if they don't exist
|
||||
self.init_default_settings()
|
||||
|
||||
# Check if we need to migrate from old format
|
||||
self.migrate_if_needed()
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
def init_default_settings(self):
|
||||
"""Initialize default settings if they don't exist"""
|
||||
defaultSettings = {
|
||||
"expirationDays": "0",
|
||||
"confirmDelete": "1" # 1 = enabled, 0 = disabled
|
||||
}
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
for key, value in defaultSettings.items():
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
|
||||
(key, value)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def migrate_if_needed(self):
|
||||
"""Check if we need to migrate from old format"""
|
||||
# Check for old config directory
|
||||
oldConfigHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
||||
oldConfigDir = os.path.join(oldConfigHome, 'jot')
|
||||
oldNotesFile = os.path.join(oldConfigDir, 'notes')
|
||||
|
||||
if os.path.exists(oldNotesFile):
|
||||
try:
|
||||
# Check if we already have notes in the database
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM notes")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
# Only migrate if database is empty
|
||||
if count == 0:
|
||||
with open(oldNotesFile, 'r') as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(': ', 1)
|
||||
if len(parts) == 2:
|
||||
noteNum, noteText = parts
|
||||
cursor.execute(
|
||||
"INSERT INTO notes (text, locked) VALUES (?, ?)",
|
||||
(noteText, 0)
|
||||
)
|
||||
|
||||
self.conn.commit()
|
||||
print(f"Migrated notes from {oldNotesFile}")
|
||||
except Exception as e:
|
||||
print(f"Migration error: {e}")
|
||||
|
||||
def get_setting(self, key, default=None):
|
||||
"""Get a setting from the database"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
else:
|
||||
# Set default if not exists
|
||||
self.save_setting(key, default)
|
||||
return default
|
||||
except sqlite3.Error:
|
||||
return default
|
||||
|
||||
def save_setting(self, key, value):
|
||||
"""Save a setting to the database"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
|
||||
(key, value)
|
||||
)
|
||||
self.conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Settings error: {e}")
|
||||
|
||||
def check_expirations(self):
|
||||
"""Check and remove expired notes"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Find expired notes that aren't locked
|
||||
cursor.execute(
|
||||
"SELECT id, text FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
|
||||
(now,)
|
||||
)
|
||||
expired = cursor.fetchall()
|
||||
|
||||
if expired:
|
||||
# Delete expired notes
|
||||
cursor.execute(
|
||||
"DELETE FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
|
||||
(now,)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
expiredCount = len(expired)
|
||||
if expiredCount > 0:
|
||||
self.show_status_message(f"Removed {expiredCount} expired notes")
|
||||
# Refresh notes list
|
||||
self.populate_notes()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Expiration check error: {e}")
|
||||
|
||||
def build_ui(self):
|
||||
"""Build the main user interface with tabs"""
|
||||
# Create notebook (tabbed interface)
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
|
||||
|
||||
# Make tabs keyboard navigable
|
||||
self.notebook.set_can_focus(True)
|
||||
|
||||
self.window.add(self.notebook)
|
||||
|
||||
# Build notes tab
|
||||
notesTab = self.build_notes_tab()
|
||||
notesTabLabel = Gtk.Label(label="Notes")
|
||||
self.notebook.append_page(notesTab, notesTabLabel)
|
||||
self.notebook.set_tab_reorderable(notesTab, False)
|
||||
|
||||
# Build settings tab
|
||||
settingsTab = self.build_settings_tab()
|
||||
settingsTabLabel = Gtk.Label(label="Settings")
|
||||
self.notebook.append_page(settingsTab, settingsTabLabel)
|
||||
self.notebook.set_tab_reorderable(settingsTab, False)
|
||||
|
||||
# Connect tab change signal
|
||||
self.notebook.connect("switch-page", self.on_tab_switched)
|
||||
|
||||
def build_notes_tab(self):
|
||||
"""Build the notes tab"""
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
vbox.set_border_width(10)
|
||||
|
||||
# Notes list with scrolling
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
# Create list store and view
|
||||
self.notesStore = Gtk.ListStore(int, str, bool, str) # id, text, locked, expiry
|
||||
self.notesView = Gtk.TreeView(model=self.notesStore)
|
||||
self.notesView.set_activate_on_single_click(False)
|
||||
self.notesView.connect("row-activated", self.on_row_activated)
|
||||
|
||||
# Improve keyboard navigation in the tree view
|
||||
self.notesView.set_can_focus(True)
|
||||
self.notesView.set_headers_clickable(True)
|
||||
self.notesView.set_enable_search(True)
|
||||
self.notesView.set_search_column(1) # Search by note text
|
||||
|
||||
# Add columns with renderers
|
||||
self.add_columns()
|
||||
|
||||
# Populate the list
|
||||
self.populate_notes()
|
||||
|
||||
scrolled.add(self.notesView)
|
||||
vbox.pack_start(scrolled, True, True, 0)
|
||||
|
||||
# Action buttons
|
||||
actionBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Copy button
|
||||
copyButton = Gtk.Button.new_with_label("Copy to Clipboard")
|
||||
copyButton.connect("clicked", self.on_copy_clicked)
|
||||
copyButton.set_can_focus(True)
|
||||
actionBox.pack_start(copyButton, False, False, 0)
|
||||
|
||||
# Delete button
|
||||
deleteButton = Gtk.Button.new_with_label("Delete Note")
|
||||
deleteButton.connect("clicked", self.on_delete_button_clicked)
|
||||
deleteButton.set_can_focus(True)
|
||||
actionBox.pack_start(deleteButton, False, False, 0)
|
||||
|
||||
vbox.pack_start(actionBox, False, False, 0)
|
||||
|
||||
# Entry for adding new notes
|
||||
entryBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
self.newNoteEntry = Gtk.Entry()
|
||||
self.newNoteEntry.set_placeholder_text("Type a new note and press Enter")
|
||||
self.newNoteEntry.connect("activate", self.on_entry_activate)
|
||||
self.newNoteEntry.set_can_focus(True)
|
||||
entryBox.pack_start(self.newNoteEntry, True, True, 0)
|
||||
|
||||
# Add button
|
||||
addButton = Gtk.Button.new_with_label("Add Note")
|
||||
addButton.connect("clicked", self.on_add_clicked)
|
||||
addButton.set_can_focus(True)
|
||||
entryBox.pack_start(addButton, False, False, 0)
|
||||
|
||||
vbox.pack_start(entryBox, False, False, 0)
|
||||
|
||||
# Status bar
|
||||
self.statusbar = Gtk.Statusbar()
|
||||
self.statusbarCtx = self.statusbar.get_context_id("jot")
|
||||
vbox.pack_start(self.statusbar, False, False, 0)
|
||||
|
||||
return vbox
|
||||
|
||||
def build_settings_tab(self):
|
||||
"""Build the settings tab"""
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
vbox.set_border_width(20)
|
||||
|
||||
# Create a frame for expiration settings
|
||||
expiryFrame = Gtk.Frame(label="Note Expiration")
|
||||
vbox.pack_start(expiryFrame, False, False, 0)
|
||||
|
||||
# Container for frame content
|
||||
expiryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
expiryBox.set_border_width(10)
|
||||
expiryFrame.add(expiryBox)
|
||||
|
||||
# Note expiration setting
|
||||
# First radio button for "Never expire"
|
||||
self.neverExpireRadio = Gtk.RadioButton.new_with_label_from_widget(None, "Never expire notes")
|
||||
self.neverExpireRadio.set_can_focus(True)
|
||||
expiryBox.pack_start(self.neverExpireRadio, False, False, 0)
|
||||
|
||||
# Container for expiration days selection
|
||||
expireDaysBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Radio button for custom expiration
|
||||
self.customExpireRadio = Gtk.RadioButton.new_with_label_from_widget(
|
||||
self.neverExpireRadio,
|
||||
"Expire notes after"
|
||||
)
|
||||
self.customExpireRadio.set_can_focus(True)
|
||||
expireDaysBox.pack_start(self.customExpireRadio, False, False, 0)
|
||||
|
||||
# Spin button for days
|
||||
adjustment = Gtk.Adjustment(
|
||||
value=max(1, int(self.expirationDays)) if int(self.expirationDays) > 0 else 7,
|
||||
lower=1,
|
||||
upper=30,
|
||||
step_increment=1
|
||||
)
|
||||
self.daysSpinButton = Gtk.SpinButton()
|
||||
self.daysSpinButton.set_adjustment(adjustment)
|
||||
self.daysSpinButton.set_can_focus(True)
|
||||
expireDaysBox.pack_start(self.daysSpinButton, False, False, 0)
|
||||
|
||||
# Label for "days"
|
||||
daysLabel = Gtk.Label(label="days")
|
||||
expireDaysBox.pack_start(daysLabel, False, False, 0)
|
||||
|
||||
expiryBox.pack_start(expireDaysBox, False, False, 0)
|
||||
|
||||
# Create a frame for confirmation settings
|
||||
confirmFrame = Gtk.Frame(label="Confirmations")
|
||||
vbox.pack_start(confirmFrame, False, False, 10)
|
||||
|
||||
# Container for confirmation settings
|
||||
confirmBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
confirmBox.set_border_width(10)
|
||||
confirmFrame.add(confirmBox)
|
||||
|
||||
# Delete confirmation checkbox
|
||||
self.confirmDeleteCheck = Gtk.CheckButton(label="Confirm before deleting notes")
|
||||
self.confirmDeleteCheck.set_active(bool(int(self.confirmDelete)))
|
||||
self.confirmDeleteCheck.set_can_focus(True)
|
||||
confirmBox.pack_start(self.confirmDeleteCheck, False, False, 0)
|
||||
|
||||
# Set the active radio button based on current setting
|
||||
if int(self.expirationDays) > 0:
|
||||
self.customExpireRadio.set_active(True)
|
||||
else:
|
||||
self.neverExpireRadio.set_active(True)
|
||||
|
||||
# Connect signals
|
||||
self.neverExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
|
||||
self.customExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
|
||||
|
||||
# Enable/disable the spin button based on the selected radio
|
||||
self.on_expiry_radio_toggled(None)
|
||||
|
||||
# Save button
|
||||
saveButton = Gtk.Button.new_with_label("Save Settings")
|
||||
saveButton.connect("clicked", self.on_save_settings)
|
||||
saveButton.set_can_focus(True)
|
||||
vbox.pack_start(saveButton, False, False, 10)
|
||||
|
||||
return vbox
|
||||
|
||||
def add_columns(self):
|
||||
"""Add columns to the TreeView"""
|
||||
# ID Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("ID", renderer, text=0)
|
||||
column.set_sort_column_id(0)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Note Text Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
renderer.set_property("ellipsize", True)
|
||||
column = Gtk.TreeViewColumn("Note", renderer, text=1)
|
||||
column.set_expand(True)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Locked Column
|
||||
renderer = Gtk.CellRendererToggle()
|
||||
renderer.connect("toggled", self.on_locked_toggled)
|
||||
column = Gtk.TreeViewColumn("Locked", renderer, active=2)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Expiration Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Expires", renderer, text=3)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
def populate_notes(self):
|
||||
"""Populate the list store with notes from the database"""
|
||||
self.notesStore.clear()
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id, text, locked, expires FROM notes ORDER BY id"
|
||||
)
|
||||
notes = cursor.fetchall()
|
||||
|
||||
for note in notes:
|
||||
noteId, text, locked, expires = note
|
||||
|
||||
# Format expiry date if it exists
|
||||
expiryText = ""
|
||||
if expires:
|
||||
try:
|
||||
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
|
||||
expiryText = expiryDate.strftime("%Y-%m-%d")
|
||||
except:
|
||||
expiryText = "Invalid date"
|
||||
|
||||
self.notesStore.append([
|
||||
noteId, # Database ID
|
||||
text, # Note text
|
||||
bool(locked), # Locked status
|
||||
expiryText # Expiry date
|
||||
])
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error populating notes: {e}")
|
||||
|
||||
def setup_actions(self):
|
||||
"""Set up keyboard shortcuts"""
|
||||
# Delete selected note
|
||||
deleteAction = Gio.SimpleAction.new("delete", None)
|
||||
deleteAction.connect("activate", self.on_delete_clicked)
|
||||
self.add_action(deleteAction)
|
||||
self.set_accels_for_action("app.delete", ["Delete"])
|
||||
|
||||
# Toggle lock on selected note
|
||||
lockAction = Gio.SimpleAction.new("lock", None)
|
||||
lockAction.connect("activate", self.on_lock_toggled)
|
||||
self.add_action(lockAction)
|
||||
self.set_accels_for_action("app.lock", ["l"])
|
||||
|
||||
# Copy note to clipboard
|
||||
copyAction = Gio.SimpleAction.new("copy", None)
|
||||
copyAction.connect("activate", self.on_copy_clicked)
|
||||
self.add_action(copyAction)
|
||||
self.set_accels_for_action("app.copy", ["<Control>c"])
|
||||
|
||||
# Switch to notes tab
|
||||
notesTabAction = Gio.SimpleAction.new("notes_tab", None)
|
||||
notesTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(0))
|
||||
self.add_action(notesTabAction)
|
||||
self.set_accels_for_action("app.notes_tab", ["<Alt>1"])
|
||||
|
||||
# Switch to settings tab
|
||||
settingsTabAction = Gio.SimpleAction.new("settings_tab", None)
|
||||
settingsTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(1))
|
||||
self.add_action(settingsTabAction)
|
||||
self.set_accels_for_action("app.settings_tab", ["<Alt>2"])
|
||||
|
||||
# Quit application
|
||||
quitAction = Gio.SimpleAction.new("quit", None)
|
||||
quitAction.connect("activate", lambda a, p: self.quit())
|
||||
self.add_action(quitAction)
|
||||
self.set_accels_for_action("app.quit", ["Escape"])
|
||||
|
||||
def show_status_message(self, message):
|
||||
"""Show a message in the statusbar"""
|
||||
self.statusbar.push(self.statusbarCtx, message)
|
||||
# Auto-remove after 5 seconds
|
||||
GLib.timeout_add_seconds(5, self.statusbar.pop, self.statusbarCtx)
|
||||
|
||||
def on_tab_switched(self, notebook, page, page_num):
|
||||
"""Handler for tab switching"""
|
||||
# Reset status bar on tab switch
|
||||
self.statusbar.pop(self.statusbarCtx)
|
||||
|
||||
# Set focus appropriately
|
||||
if page_num == 0: # Notes tab
|
||||
self.newNoteEntry.grab_focus()
|
||||
elif page_num == 1: # Settings tab
|
||||
if self.neverExpireRadio.get_active():
|
||||
self.neverExpireRadio.grab_focus()
|
||||
else:
|
||||
self.customExpireRadio.grab_focus()
|
||||
|
||||
def on_expiry_radio_toggled(self, widget):
|
||||
"""Handler for expiry radio button toggles"""
|
||||
# Enable/disable spin button based on which radio is active
|
||||
self.daysSpinButton.set_sensitive(self.customExpireRadio.get_active())
|
||||
|
||||
def on_save_settings(self, button):
|
||||
"""Handler for Save Settings button"""
|
||||
if self.neverExpireRadio.get_active():
|
||||
self.expirationDays = 0
|
||||
else:
|
||||
self.expirationDays = self.daysSpinButton.get_value_as_int()
|
||||
|
||||
# Get delete confirmation setting
|
||||
self.confirmDelete = 1 if self.confirmDeleteCheck.get_active() else 0
|
||||
|
||||
# Save to database
|
||||
self.save_setting("expirationDays", self.expirationDays)
|
||||
self.save_setting("confirmDelete", self.confirmDelete)
|
||||
|
||||
# Apply expiration to notes if needed
|
||||
if self.expirationDays > 0:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
now = datetime.datetime.now()
|
||||
expiryDate = now + datetime.timedelta(days=self.expirationDays)
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Set expiration for notes that aren't locked and don't have expiration
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE locked = 0 AND expires IS NULL",
|
||||
(expiryStr,)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
# Refresh the notes list
|
||||
self.populate_notes()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error updating expirations: {e}")
|
||||
|
||||
self.show_status_message("Settings saved")
|
||||
|
||||
# Switch back to notes tab
|
||||
self.notebook.set_current_page(0)
|
||||
|
||||
def on_entry_activate(self, entry):
|
||||
"""Handle Enter key in the entry field"""
|
||||
self.add_new_note(entry.get_text())
|
||||
entry.set_text("")
|
||||
|
||||
def on_add_clicked(self, button):
|
||||
"""Handle Add Note button click"""
|
||||
self.add_new_note(self.newNoteEntry.get_text())
|
||||
self.newNoteEntry.set_text("")
|
||||
|
||||
def add_new_note(self, text):
|
||||
"""Add a new note to the database"""
|
||||
if not text.strip():
|
||||
self.show_status_message("Note text cannot be empty")
|
||||
return
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Set expiration if enabled
|
||||
expires = None
|
||||
if int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expires = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Insert the new note
|
||||
cursor.execute(
|
||||
"INSERT INTO notes (text, expires, locked) VALUES (?, ?, ?)",
|
||||
(text, expires, 0)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
# Refresh the notes list
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note added")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error adding note: {e}")
|
||||
|
||||
def on_row_activated(self, view, path, column):
|
||||
"""Handle double-click on a note - edit the note"""
|
||||
model = view.get_model()
|
||||
noteId = model[path][0]
|
||||
|
||||
# Get the note from the database
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT text, locked, expires FROM notes WHERE id = ?", (noteId,))
|
||||
note = cursor.fetchone()
|
||||
|
||||
if note:
|
||||
self.edit_note_dialog(noteId, note)
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error retrieving note: {e}")
|
||||
|
||||
def on_locked_toggled(self, renderer, path):
|
||||
"""Handle toggling the locked state from the view"""
|
||||
model = self.notesView.get_model()
|
||||
noteId = model[path][0]
|
||||
currentLocked = model[path][2]
|
||||
newLocked = not currentLocked
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Update locked status
|
||||
cursor.execute(
|
||||
"UPDATE notes SET locked = ? WHERE id = ?",
|
||||
(1 if newLocked else 0, noteId)
|
||||
)
|
||||
|
||||
# If unlocking and expiration is enabled, set expiration
|
||||
if not newLocked and int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE id = ?",
|
||||
(expiryStr, noteId)
|
||||
)
|
||||
|
||||
# Update the expiry text in the model
|
||||
model[path][3] = expiryDate.strftime("%Y-%m-%d")
|
||||
|
||||
self.show_status_message("Note unlocked - expiration set")
|
||||
elif newLocked:
|
||||
self.show_status_message("Note locked - will not expire")
|
||||
else:
|
||||
self.show_status_message("Note unlocked")
|
||||
|
||||
# Update the model
|
||||
model[path][2] = newLocked
|
||||
|
||||
self.conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error updating note: {e}")
|
||||
|
||||
def on_lock_toggled(self, action, parameter):
|
||||
"""Handle keyboard shortcut to toggle lock"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
currentLocked = model[treeiter][2]
|
||||
|
||||
# Simulate clicking the toggle
|
||||
path = model.get_path(treeiter)
|
||||
self.on_locked_toggled(None, path)
|
||||
|
||||
def on_copy_clicked(self, action=None, parameter=None):
|
||||
"""Copy the selected note to clipboard"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
# Get the clipboard
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(noteText, -1)
|
||||
|
||||
self.show_status_message("Note copied to clipboard")
|
||||
else:
|
||||
self.show_status_message("No note selected")
|
||||
|
||||
def on_delete_button_clicked(self, button):
|
||||
"""Handle Delete button click"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
if int(self.confirmDelete):
|
||||
self.confirm_delete_note(noteId, noteText)
|
||||
else:
|
||||
self.delete_note(noteId)
|
||||
|
||||
def on_delete_clicked(self, action, parameter):
|
||||
"""Handle Delete key to remove a note"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
if int(self.confirmDelete):
|
||||
self.confirm_delete_note(noteId, noteText)
|
||||
else:
|
||||
self.delete_note(noteId)
|
||||
|
||||
def delete_note(self, noteId):
|
||||
"""Delete a note by ID without confirmation"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM notes WHERE id = ?", (noteId,))
|
||||
self.conn.commit()
|
||||
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note deleted")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error deleting note: {e}")
|
||||
|
||||
def confirm_delete_note(self, noteId, noteText):
|
||||
"""Show confirmation dialog before deleting a note"""
|
||||
if len(noteText) > 30:
|
||||
noteText = noteText[:30] + "..."
|
||||
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self.window,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
buttons=Gtk.ButtonsType.YES_NO,
|
||||
text=f"Delete note: {noteText}?"
|
||||
)
|
||||
dialog.set_default_response(Gtk.ResponseType.NO)
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.YES:
|
||||
self.delete_note(noteId)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def edit_note_dialog(self, noteId, note):
|
||||
"""Show dialog to edit a note"""
|
||||
text, locked, expires = note
|
||||
|
||||
dialog = Gtk.Dialog(
|
||||
title="Edit Note",
|
||||
parent=self.window,
|
||||
flags=0,
|
||||
buttons=(
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE, Gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
dialog.set_default_size(400, 200)
|
||||
|
||||
# Make the dialog accessible
|
||||
dialog.set_role("dialog")
|
||||
dialog.set_property("has-tooltip", True)
|
||||
|
||||
# Create a text view for the note
|
||||
entryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
entryBox.set_border_width(10)
|
||||
|
||||
label = Gtk.Label(label="Edit note text:")
|
||||
label.set_halign(Gtk.Align.START)
|
||||
entryBox.pack_start(label, False, False, 0)
|
||||
|
||||
# Scrolled window for text view
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
scrolled.set_shadow_type(Gtk.ShadowType.IN)
|
||||
|
||||
# Text view for multi-line editing
|
||||
textBuffer = Gtk.TextBuffer()
|
||||
textBuffer.set_text(text)
|
||||
textView = Gtk.TextView.new_with_buffer(textBuffer)
|
||||
textView.set_wrap_mode(Gtk.WrapMode.WORD)
|
||||
textView.set_can_focus(True)
|
||||
|
||||
scrolled.add(textView)
|
||||
entryBox.pack_start(scrolled, True, True, 0)
|
||||
|
||||
# Add lock checkbox
|
||||
lockCheck = Gtk.CheckButton(label="Lock note (prevent expiration)")
|
||||
lockCheck.set_active(bool(locked))
|
||||
lockCheck.set_can_focus(True)
|
||||
entryBox.pack_start(lockCheck, False, False, 0)
|
||||
|
||||
# Show expiration date if it exists
|
||||
if expires and not locked:
|
||||
try:
|
||||
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
|
||||
expiryLabel = Gtk.Label(label=f"Expires on: {expiryDate.strftime('%Y-%m-%d')}")
|
||||
expiryLabel.set_halign(Gtk.Align.START)
|
||||
entryBox.pack_start(expiryLabel, False, False, 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
dialog.get_content_area().add(entryBox)
|
||||
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||
dialog.show_all()
|
||||
|
||||
# Set focus to text view
|
||||
textView.grab_focus()
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
# Get the text from the buffer
|
||||
start, end = textBuffer.get_bounds()
|
||||
newText = textBuffer.get_text(start, end, False)
|
||||
newLocked = lockCheck.get_active()
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Update the note
|
||||
cursor.execute(
|
||||
"UPDATE notes SET text = ?, locked = ? WHERE id = ?",
|
||||
(newText, 1 if newLocked else 0, noteId)
|
||||
)
|
||||
|
||||
# Update expiration if needed
|
||||
if not newLocked and int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE id = ?",
|
||||
(expiryStr, noteId)
|
||||
)
|
||||
|
||||
self.conn.commit()
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note updated")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error updating note: {e}")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def main():
|
||||
app = JotApp()
|
||||
return app.run(None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user