Files
Modus/modules/todo/todo_widget.py
T
2025-09-01 12:39:28 +05:45

457 lines
14 KiB
Python

# Standard library imports
from datetime import datetime
# Fabric imports
from fabric.utils import get_relative_path
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.svg import Svg
from gi.repository import GLib
# Local imports
from services.todo import todo_service
from widgets.mousecapture import MouseCapture
from widgets.wayland import WaylandWindow as Window
class TodoItem(Box):
"""Individual todo item widget"""
def __init__(self, todo_data, todo_list_widget, **kwargs):
self.todo_data = todo_data
self.todo_list_widget = todo_list_widget
self.editing = False
super().__init__(
name="todo-item",
orientation="h",
spacing=8,
style_classes=["menu"],
**kwargs,
)
self._build_ui()
def _build_ui(self):
"""Build the todo item UI"""
# Checkbox for completion - using SVG icons
checkbox_icon = (
"checkbox-check.svg"
if self.todo_data["completed"]
else "checkbox-uncheck.svg"
)
self.checkbox_icon = Svg(
name="todo-checkbox-icon",
size=24,
svg_file=get_relative_path(
"../../config/assets/icons/todo/" + checkbox_icon
),
)
self.checkbox = Button(
name="todo-checkbox",
child=self.checkbox_icon,
on_clicked=self._toggle_completion,
)
# Todo text (can be converted to entry for editing)
text_content = self.todo_data["text"]
if self.todo_data["completed"]:
text_content = f"<s>{text_content}</s>"
self.text_label = Label(
markup=text_content,
name="todo-text",
h_align="start",
h_expand=True,
line_wrap="word-char",
style_classes=(
["title-widget"]
if not self.todo_data["completed"]
else ["status-label"]
),
)
# Date/time label
created_at = datetime.fromisoformat(self.todo_data["created_at"])
date_text = created_at.strftime("%b %d, %Y at %I:%M %p")
self.date_label = Label(
label=date_text,
name="todo-date",
h_align="start",
style_classes=["todo-date-text"],
)
self.text_entry = Entry(
name="todo-text-entry",
text=self.todo_data["text"],
h_expand=True,
visible=False,
)
self.text_entry.connect("activate", self._save_edit)
# Priority indicator
# priority_symbols = {"high": "🔴", "medium": "🟡", "low": "🟢"}
#
# self.priority_label = Label(
# label=priority_symbols.get(self.todo_data["priority"], "🟡"),
# name="todo-priority-label",
# )
# self.priority_button = Button(
# name="todo-priority",
# size=(20, 20),
# child=self.priority_label,
# on_clicked=self._cycle_priority,
# )
#
# Edit button - using SVG icon
self.edit_icon = Svg(
name="todo-edit-icon",
size=12,
svg_file=get_relative_path("../../config/assets/icons/todo/edit.svg"),
)
self.edit_button = Button(
name="todo-edit",
child=self.edit_icon,
on_clicked=self._start_edit,
)
# Delete button - using SVG icon
self.delete_icon = Svg(
name="todo-delete-icon",
size=12,
svg_file=get_relative_path(
"../../config/assets/icons/todo/delete-symbolic.svg"
),
)
self.delete_button = Button(
name="todo-delete",
child=self.delete_icon,
on_clicked=self._delete_todo,
)
# Text container that switches between label and entry
self.text_container = Box(
orientation="v",
h_expand=True,
children=[
Box(orientation="h", children=[self.text_label]),
self.date_label,
],
)
self.children = [
self.checkbox,
self.text_container,
# self.priority_button,
self.edit_button,
self.delete_button,
]
def _toggle_completion(self, *_):
"""Toggle todo completion status"""
todo_service.toggle_todo(self.todo_data["id"])
def _cycle_priority(self, *_):
"""Cycle through priority levels"""
priorities = ["low", "medium", "high"]
current_index = priorities.index(self.todo_data["priority"])
new_priority = priorities[(current_index + 1) % len(priorities)]
todo_service.set_priority(self.todo_data["id"], new_priority)
def _start_edit(self, *_):
"""Start editing the todo text"""
if self.editing:
return
self.editing = True
self.text_container.children = [
Box(orientation="h", children=[self.text_entry]),
self.date_label,
]
self.text_entry.set_visible(True)
self.text_entry.grab_focus()
self.text_entry.set_position(-1) # Move cursor to end
def _save_edit(self, *_):
"""Save the edited todo text"""
if not self.editing:
return
new_text = self.text_entry.get_text().strip()
if new_text:
todo_service.edit_todo(self.todo_data["id"], new_text)
self._cancel_edit()
def _cancel_edit(self):
"""Cancel editing and revert to label"""
self.editing = False
self.text_container.children = [
Box(orientation="h", children=[self.text_label]),
self.date_label,
]
self.text_entry.set_visible(False)
def _delete_todo(self, *_):
"""Delete this todo"""
todo_service.delete_todo(self.todo_data["id"])
def update_from_data(self, todo_data):
"""Update the widget from new todo data"""
self.todo_data = todo_data
# Update checkbox icon by recreating it
checkbox_icon = (
"checkbox-check.svg" if todo_data["completed"] else "checkbox-uncheck.svg"
)
new_checkbox_icon = Svg(
name="todo-checkbox-icon",
size=20,
svg_file=get_relative_path(
"../../config/assets/icons/todo/" + checkbox_icon
),
)
self.checkbox.set_child(new_checkbox_icon)
self.checkbox_icon = new_checkbox_icon
# Update text and styling with markup
text_content = todo_data["text"]
if todo_data["completed"]:
text_content = f"<s>{text_content}</s>"
self.text_label.set_markup(text_content)
self.text_label.style_classes = (
["title-widget"] if not todo_data["completed"] else ["status-label"]
)
# Update date/time
created_at = datetime.fromisoformat(todo_data["created_at"])
date_text = created_at.strftime("%b %d, %Y at %I:%M %p")
self.date_label.set_label(date_text)
class TodoListWidget(Window):
"""Main todo list widget window"""
def __init__(self, **kwargs):
super().__init__(
title="modus-todo",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
name="todo-list-window",
visible=False, # Back to hidden by default
**kwargs,
)
self.todo_items = {} # Maps todo IDs to TodoItem widgets
# Register callback with todo service
todo_service.add_callback(self._on_todo_event)
self._build_ui()
self._refresh_todos()
# Add keybinding for escape
self.add_keybinding("Escape", self.hide_todo_list)
def _build_ui(self):
"""Build the main UI"""
# Header with title and stats
self.stats_label = Label(
label="",
name="todo-stats",
style_classes=["status-label"],
h_align="start",
)
self.header = Box(
name="todo-header",
orientation="v",
children=[
Label(
label="Todo List",
name="todo-title",
style_classes=["title"],
h_align="start",
),
self.stats_label,
],
)
# Add new todo section
self.new_todo_entry = Entry(
name="new-todo-entry",
placeholder_text="Add a new task...",
h_expand=True,
)
self.new_todo_entry.connect("activate", self._add_todo)
# Add button - using SVG icon
self.add_icon = Svg(
name="add-todo-icon",
size=12,
svg_file=get_relative_path(
"../../config/assets/icons/todo/plus-symbolic.svg"
),
)
self.add_button = Button(
name="add-todo-button",
child=self.add_icon,
on_clicked=self._add_todo,
)
self.add_section = Box(
name="todo-add-section",
orientation="h",
spacing=8,
style_classes=["menu"],
children=[
self.new_todo_entry,
self.add_button,
],
)
# Todo items container
self.todos_container = Box(
name="todos-container",
orientation="v",
spacing=4,
)
# Scrolled window for todos
self.scrolled = ScrolledWindow(
name="todos-scrolled",
min_content_height=300,
max_content_height=500,
min_content_width=400,
child=self.todos_container,
policy="automatic",
v_expand=True, # Allow vertical expansion
)
# Clear completed button
self.clear_button = Button(
name="clear-completed-button",
label="Clear Completed",
style_classes=["status-label"],
on_clicked=self._clear_completed,
)
# Main container
self.main_container = Box(
name="todo-main-container",
orientation="v",
spacing=8,
style_classes=["menu"],
children=[
self.header,
self.add_section,
self.scrolled,
self.clear_button,
],
)
self.children = [self.main_container]
def _add_todo(self, *_):
"""Add a new todo"""
text = self.new_todo_entry.get_text().strip()
if text:
todo_service.add_todo(text)
self.new_todo_entry.set_text("")
def _clear_completed(self, *_):
"""Clear all completed todos"""
todo_service.clear_completed()
def _on_todo_event(self, event_type, data=None):
"""Handle todo service events via callback"""
if event_type == "todos-changed":
GLib.idle_add(self._refresh_todos)
elif event_type == "todo-added":
GLib.idle_add(self._refresh_todos)
elif event_type == "todo-deleted":
GLib.idle_add(self._refresh_todos)
elif event_type in ["todo-toggled", "todo-edited", "todo-priority-changed"]:
if data and data["id"] in self.todo_items:
GLib.idle_add(
lambda: self.todo_items[data["id"]].update_from_data(data)
)
GLib.idle_add(self._update_stats)
def _refresh_todos(self, *_):
"""Refresh the entire todo list"""
# Clear existing items
self.todo_items.clear()
self.todos_container.children = []
# Get all todos
todos = todo_service.todos
# Sort todos: incomplete first, then by priority, then by creation date
def sort_key(todo):
priority_order = {"high": 0, "medium": 1, "low": 2}
return (
todo["completed"], # False (incomplete) comes before True (completed)
priority_order.get(todo["priority"], 1),
todo["created_at"],
)
sorted_todos = sorted(todos, key=sort_key)
# Create todo item widgets
for todo in sorted_todos:
todo_item = TodoItem(todo, self)
self.todo_items[todo["id"]] = todo_item
# Update container children
self.todos_container.children = list(self.todo_items.values())
# Update stats
self._update_stats()
def _update_stats(self):
"""Update the statistics display"""
stats = todo_service.get_stats()
stats_text = f"{stats['pending']} pending, {stats['completed']} completed"
self.stats_label.set_label(stats_text)
def set_visible(self, visible):
"""Override set_visible for debugging"""
super().set_visible(visible)
def hide_todo_list(self, *_):
"""Hide the todo list"""
if hasattr(self, "_mousecapture_parent"):
self._mousecapture_parent.toggle_mousecapture()
self.set_visible(False)
def _init_mousecapture(self, mousecapture):
"""Initialize mousecapture parent reference"""
self._mousecapture_parent = mousecapture
def destroy(self):
"""Clean up when destroyed"""
# Remove callback from todo service
todo_service.remove_callback(self._on_todo_event)
super().destroy()
class TodoListCapture(MouseCapture):
"""MouseCapture wrapper for the todo list"""
def __init__(self, **kwargs):
super().__init__(
layer="top",
child_window=TodoListWidget(),
**kwargs,
)