Files
Modus/modules/todo/todo_widget.py
T
sankalp c22ea5dfda refactor(modules): simplify SVG asset handling across modules
Enhancements:
- Add optimized night‑variant detection for weather icons using a predefined list
- Replace `Svg` usage with direct `svg_file` calls throughout the todo widget, removing redundant relative‑path logic
- Update dock and battery modules to load assets via direct paths, eliminating unnecessary `get_relative_path` calls- Introduce `WEATHER_CACHE_TIMEOUT` constant and tidy up unused imports (`WEATHER_EMOJI_MAP` removed)
- Minor formatting and structural clean‑ups that improve code readability

Bug Fixes:
- Remove dead import of `WEATHER_EMOJI_MAP` and unused constant `WEATHER_EMOJI_MAP`
- Correct syntax of `condition_em` initialization to avoid potential rendering issues

New Features:
- None.
2026-04-02 23:13:47 +05:45

447 lines
13 KiB
Python

# Standard library imports
from datetime import datetime
# Fabric imports
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GLib
# Local imports
from utils.utils import svg_file
from services.todo import get_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_file(
"todo/" + checkbox_icon,
name="todo-checkbox-icon",
size=24,
)
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_file(
"todo/edit.svg",
name="todo-edit-icon",
size=12,
)
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_file(
"todo/delete-symbolic.svg",
name="todo-delete-icon",
size=12,
)
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"""
get_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)]
get_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:
get_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"""
get_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_file(
"todo/" + checkbox_icon,
name="todo-checkbox-icon",
size=20,
)
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
get_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_file(
"todo/plus-symbolic.svg",
name="add-todo-icon",
size=12,
)
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:
get_todo_service().add_todo(text)
self.new_todo_entry.set_text("")
def _clear_completed(self, *_):
"""Clear all completed todos"""
get_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 = get_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 = get_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
get_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,
)