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
|
# System information bound to s
|
||||||
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
|
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
|
# Exit panel mode without any action
|
||||||
bindsym Escape mode "default"
|
bindsym Escape mode "default"
|
||||||
bindsym Control+g 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