Add context filter to Tasks TUI and fix calendar UI bugs
Tasks TUI: - Add context support to TaskBackend interface (get_context, set_context, get_contexts methods) - Implement context methods in DstaskClient - Add Context section to FilterSidebar (above projects/tags) - Context changes persist via backend CLI Calendar TUI: - Remove duplicate header from InvitesPanel (use border_title instead) - Fix border_title color to use $primary - Fix WeekGrid to always scroll to work day start (7am) on mount
This commit is contained in:
@@ -39,6 +39,7 @@ class InvitesPanel(Widget):
|
|||||||
min-height: 3;
|
min-height: 3;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
border: round $primary;
|
border: round $primary;
|
||||||
|
border-title-color: $primary;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -74,7 +75,12 @@ class InvitesPanel(Widget):
|
|||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Set border title on mount."""
|
"""Set border title on mount."""
|
||||||
self.border_title = "Invites"
|
self._update_border_title()
|
||||||
|
|
||||||
|
def _update_border_title(self) -> None:
|
||||||
|
"""Update border title with invite count."""
|
||||||
|
count = len(self.invites)
|
||||||
|
self.border_title = f"Invites ({count})" if count else "Invites"
|
||||||
|
|
||||||
def _get_theme_color(self, color_name: str) -> str:
|
def _get_theme_color(self, color_name: str) -> str:
|
||||||
"""Get a color from the current theme."""
|
"""Get a color from the current theme."""
|
||||||
@@ -97,25 +103,21 @@ class InvitesPanel(Widget):
|
|||||||
return fallbacks.get(color_name, "white")
|
return fallbacks.get(color_name, "white")
|
||||||
|
|
||||||
def get_content_height(self, container, viewport, width: int) -> int:
|
def get_content_height(self, container, viewport, width: int) -> int:
|
||||||
"""Calculate height: header + invite rows."""
|
"""Calculate height: invite rows only (no internal header)."""
|
||||||
# Header (1) + invites (2 lines each) + empty message if no invites
|
|
||||||
if not self.invites:
|
if not self.invites:
|
||||||
return 2 # Header + "No pending invites"
|
return 1 # "No pending invites"
|
||||||
return 1 + len(self.invites) * 2 # Header + 2 lines per invite
|
return len(self.invites) * 2 # 2 lines per invite
|
||||||
|
|
||||||
def render_line(self, y: int) -> Strip:
|
def render_line(self, y: int) -> Strip:
|
||||||
"""Render a line of the panel."""
|
"""Render a line of the panel."""
|
||||||
if y == 0:
|
|
||||||
return self._render_header()
|
|
||||||
|
|
||||||
if not self.invites:
|
if not self.invites:
|
||||||
if y == 1:
|
if y == 0:
|
||||||
return self._render_empty_message()
|
return self._render_empty_message()
|
||||||
return Strip.blank(self.size.width)
|
return Strip.blank(self.size.width)
|
||||||
|
|
||||||
# Each invite takes 2 lines
|
# Each invite takes 2 lines
|
||||||
invite_idx = (y - 1) // 2
|
invite_idx = y // 2
|
||||||
line_in_invite = (y - 1) % 2
|
line_in_invite = y % 2
|
||||||
|
|
||||||
if 0 <= invite_idx < len(self.invites):
|
if 0 <= invite_idx < len(self.invites):
|
||||||
return self._render_invite_line(
|
return self._render_invite_line(
|
||||||
@@ -126,15 +128,6 @@ class InvitesPanel(Widget):
|
|||||||
|
|
||||||
return Strip.blank(self.size.width)
|
return Strip.blank(self.size.width)
|
||||||
|
|
||||||
def _render_header(self) -> Strip:
|
|
||||||
"""Render the panel header."""
|
|
||||||
primary_color = self._get_theme_color("primary")
|
|
||||||
count = len(self.invites)
|
|
||||||
header = f"Invites ({count})" if count else "Invites"
|
|
||||||
header = header[: self.size.width].ljust(self.size.width)
|
|
||||||
style = Style(bold=True, color=primary_color)
|
|
||||||
return Strip([Segment(header, style)])
|
|
||||||
|
|
||||||
def _render_empty_message(self) -> Strip:
|
def _render_empty_message(self) -> Strip:
|
||||||
"""Render empty state message."""
|
"""Render empty state message."""
|
||||||
msg = "No pending invites"
|
msg = "No pending invites"
|
||||||
@@ -179,6 +172,7 @@ class InvitesPanel(Widget):
|
|||||||
self.invites = invites
|
self.invites = invites
|
||||||
if self.selected_index >= len(invites):
|
if self.selected_index >= len(invites):
|
||||||
self.selected_index = max(0, len(invites) - 1)
|
self.selected_index = max(0, len(invites) - 1)
|
||||||
|
self._update_border_title()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def select_next(self) -> None:
|
def select_next(self) -> None:
|
||||||
@@ -203,11 +197,11 @@ class InvitesPanel(Widget):
|
|||||||
"""Handle mouse clicks."""
|
"""Handle mouse clicks."""
|
||||||
y = event.y
|
y = event.y
|
||||||
|
|
||||||
if y == 0 or not self.invites:
|
if not self.invites:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Calculate which invite was clicked
|
# Calculate which invite was clicked (2 lines per invite)
|
||||||
invite_idx = (y - 1) // 2
|
invite_idx = y // 2
|
||||||
if 0 <= invite_idx < len(self.invites):
|
if 0 <= invite_idx < len(self.invites):
|
||||||
self.selected_index = invite_idx
|
self.selected_index = invite_idx
|
||||||
self.post_message(self.InviteSelected(self.invites[invite_idx]))
|
self.post_message(self.InviteSelected(self.invites[invite_idx]))
|
||||||
|
|||||||
@@ -648,13 +648,10 @@ class WeekGrid(Vertical):
|
|||||||
current_row = (now.hour * rows_per_hour) + (now.minute // minutes_per_row)
|
current_row = (now.hour * rows_per_hour) + (now.minute // minutes_per_row)
|
||||||
self.cursor_row = current_row
|
self.cursor_row = current_row
|
||||||
|
|
||||||
# Scroll to show work day start initially
|
# Always scroll to work day start initially (e.g., 7am)
|
||||||
if self._body:
|
if self._body:
|
||||||
work_start_row = config.work_day_start_hour() * rows_per_hour
|
work_start_row = config.work_day_start_hour() * rows_per_hour
|
||||||
# If current time is before work day start, scroll to work day start
|
self._body.scroll_to(y=work_start_row, animate=False)
|
||||||
# Otherwise scroll to show current time
|
|
||||||
scroll_target = min(work_start_row, current_row)
|
|
||||||
self._body.scroll_to(y=scroll_target, animate=False)
|
|
||||||
|
|
||||||
def watch_week_start(self, old: date, new: date) -> None:
|
def watch_week_start(self, old: date, new: date) -> None:
|
||||||
"""Handle week_start changes."""
|
"""Handle week_start changes."""
|
||||||
|
|||||||
@@ -349,3 +349,43 @@ class DstaskClient(TaskBackend):
|
|||||||
# This needs to run without capturing output
|
# This needs to run without capturing output
|
||||||
result = self._run_command(["note", task_id], capture_output=False)
|
result = self._run_command(["note", task_id], capture_output=False)
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def get_context(self) -> Optional[str]:
|
||||||
|
"""Get the current context filter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current context string, or None if no context is set
|
||||||
|
"""
|
||||||
|
result = self._run_command(["context"])
|
||||||
|
if result.returncode == 0:
|
||||||
|
context = result.stdout.strip()
|
||||||
|
return context if context else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_context(self, context: Optional[str]) -> bool:
|
||||||
|
"""Set the context filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Context string (e.g., "+work", "project:foo") or None to clear
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
if context is None or context.lower() == "none" or context == "":
|
||||||
|
result = self._run_command(["context", "none"])
|
||||||
|
else:
|
||||||
|
result = self._run_command(["context", context])
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def get_contexts(self) -> list[str]:
|
||||||
|
"""Get available contexts based on tags.
|
||||||
|
|
||||||
|
For dstask, contexts are typically tag-based filters like "+work".
|
||||||
|
We derive available contexts from the existing tags.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of context strings (tag-based)
|
||||||
|
"""
|
||||||
|
# Get all tags and convert to context format
|
||||||
|
tags = self.get_tags()
|
||||||
|
return [f"+{tag}" for tag in tags if tag]
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ class TasksApp(App):
|
|||||||
self.tags = []
|
self.tags = []
|
||||||
self.all_projects = [] # Stable list of all projects (not filtered)
|
self.all_projects = [] # Stable list of all projects (not filtered)
|
||||||
self.all_tags = [] # Stable list of all tags (not filtered)
|
self.all_tags = [] # Stable list of all tags (not filtered)
|
||||||
|
self.all_contexts = [] # Available contexts from backend
|
||||||
|
self.current_context = None # Current active context
|
||||||
self.current_project_filter = None
|
self.current_project_filter = None
|
||||||
self.current_tag_filters = []
|
self.current_tag_filters = []
|
||||||
self.current_sort_column = "priority"
|
self.current_sort_column = "priority"
|
||||||
@@ -393,12 +395,14 @@ class TasksApp(App):
|
|||||||
self._update_table()
|
self._update_table()
|
||||||
|
|
||||||
def _load_all_filters(self) -> None:
|
def _load_all_filters(self) -> None:
|
||||||
"""Load all projects and tags once for stable sidebar."""
|
"""Load all projects, tags, and contexts once for stable sidebar."""
|
||||||
if not self.backend:
|
if not self.backend:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.all_projects = self.backend.get_projects()
|
self.all_projects = self.backend.get_projects()
|
||||||
self.all_tags = self.backend.get_tags()
|
self.all_tags = self.backend.get_tags()
|
||||||
|
self.all_contexts = self.backend.get_contexts()
|
||||||
|
self.current_context = self.backend.get_context()
|
||||||
|
|
||||||
# Update sidebar with stable filter options
|
# Update sidebar with stable filter options
|
||||||
self._update_sidebar()
|
self._update_sidebar()
|
||||||
@@ -426,12 +430,19 @@ class TasksApp(App):
|
|||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
def _update_sidebar(self) -> None:
|
def _update_sidebar(self) -> None:
|
||||||
"""Update the filter sidebar with all available projects and tags."""
|
"""Update the filter sidebar with all available projects, tags, and contexts."""
|
||||||
try:
|
try:
|
||||||
sidebar = self.query_one("#sidebar", FilterSidebar)
|
sidebar = self.query_one("#sidebar", FilterSidebar)
|
||||||
# Use stable all_projects/all_tags, not filtered ones
|
# Use stable all_projects/all_tags/all_contexts, not filtered ones
|
||||||
project_data = [(p.name, p.task_count) for p in self.all_projects if p.name]
|
project_data = [(p.name, p.task_count) for p in self.all_projects if p.name]
|
||||||
sidebar.update_filters(projects=project_data, tags=self.all_tags)
|
sidebar.update_filters(
|
||||||
|
contexts=self.all_contexts,
|
||||||
|
projects=project_data,
|
||||||
|
tags=self.all_tags,
|
||||||
|
)
|
||||||
|
# Set current context in sidebar if loaded from backend
|
||||||
|
if self.current_context:
|
||||||
|
sidebar.set_current_context(self.current_context)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Sidebar may not be mounted yet
|
pass # Sidebar may not be mounted yet
|
||||||
|
|
||||||
@@ -694,6 +705,21 @@ class TasksApp(App):
|
|||||||
else:
|
else:
|
||||||
sidebar.add_class("hidden")
|
sidebar.add_class("hidden")
|
||||||
|
|
||||||
|
def on_filter_sidebar_context_changed(
|
||||||
|
self, event: FilterSidebar.ContextChanged
|
||||||
|
) -> None:
|
||||||
|
"""Handle context changes from sidebar."""
|
||||||
|
if event.context != self.current_context:
|
||||||
|
# Set context via backend (this persists it)
|
||||||
|
if self.backend:
|
||||||
|
self.backend.set_context(event.context)
|
||||||
|
self.current_context = event.context
|
||||||
|
self.load_tasks()
|
||||||
|
if event.context:
|
||||||
|
self.notify(f"Context set: {event.context}")
|
||||||
|
else:
|
||||||
|
self.notify("Context cleared")
|
||||||
|
|
||||||
def on_filter_sidebar_project_filter_changed(
|
def on_filter_sidebar_project_filter_changed(
|
||||||
self, event: FilterSidebar.ProjectFilterChanged
|
self, event: FilterSidebar.ProjectFilterChanged
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -269,3 +269,36 @@ class TaskBackend(ABC):
|
|||||||
True if successful
|
True if successful
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_context(self) -> Optional[str]:
|
||||||
|
"""Get the current context filter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current context string, or None if no context is set
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_context(self, context: Optional[str]) -> bool:
|
||||||
|
"""Set the context filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Context string (e.g., "+work", "project:foo") or None to clear
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_contexts(self) -> list[str]:
|
||||||
|
"""Get available predefined contexts.
|
||||||
|
|
||||||
|
For taskwarrior, returns named contexts from config.
|
||||||
|
For dstask, may return common tag-based contexts.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of context names/filters
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from textual.widgets.selection_list import Selection
|
|||||||
|
|
||||||
|
|
||||||
class FilterSidebar(Widget):
|
class FilterSidebar(Widget):
|
||||||
"""Collapsible sidebar with project filter, tag filter, and sort options."""
|
"""Collapsible sidebar with context, project filter, tag filter, and sort options."""
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
FilterSidebar {
|
FilterSidebar {
|
||||||
@@ -53,6 +53,13 @@ class FilterSidebar(Widget):
|
|||||||
border-title-style: bold;
|
border-title-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Context section - single selection list */
|
||||||
|
FilterSidebar #context-list {
|
||||||
|
height: auto;
|
||||||
|
max-height: 6;
|
||||||
|
min-height: 3;
|
||||||
|
}
|
||||||
|
|
||||||
FilterSidebar .sort-section {
|
FilterSidebar .sort-section {
|
||||||
height: auto;
|
height: auto;
|
||||||
border: round rgb(117, 106, 129);
|
border: round rgb(117, 106, 129);
|
||||||
@@ -110,6 +117,13 @@ class FilterSidebar(Widget):
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
class ContextChanged(Message):
|
||||||
|
"""Sent when context selection changes."""
|
||||||
|
|
||||||
|
def __init__(self, context: Optional[str]) -> None:
|
||||||
|
self.context = context
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
class SortChanged(Message):
|
class SortChanged(Message):
|
||||||
"""Sent when sort settings change."""
|
"""Sent when sort settings change."""
|
||||||
|
|
||||||
@@ -128,8 +142,10 @@ class FilterSidebar(Widget):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Reactive properties - use factory functions for mutable defaults
|
# Reactive properties - use factory functions for mutable defaults
|
||||||
|
contexts: reactive[list[str]] = reactive(list)
|
||||||
projects: reactive[list[tuple[str, int]]] = reactive(list)
|
projects: reactive[list[tuple[str, int]]] = reactive(list)
|
||||||
tags: reactive[list[str]] = reactive(list)
|
tags: reactive[list[str]] = reactive(list)
|
||||||
|
current_context: reactive[Optional[str]] = reactive(None)
|
||||||
current_project: reactive[Optional[str]] = reactive(None)
|
current_project: reactive[Optional[str]] = reactive(None)
|
||||||
current_tags: reactive[list[str]] = reactive(list)
|
current_tags: reactive[list[str]] = reactive(list)
|
||||||
current_sort_column: reactive[str] = reactive("priority")
|
current_sort_column: reactive[str] = reactive("priority")
|
||||||
@@ -137,8 +153,10 @@ class FilterSidebar(Widget):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
contexts: Optional[list[str]] = None,
|
||||||
projects: Optional[list[tuple[str, int]]] = None,
|
projects: Optional[list[tuple[str, int]]] = None,
|
||||||
tags: Optional[list[str]] = None,
|
tags: Optional[list[str]] = None,
|
||||||
|
current_context: Optional[str] = None,
|
||||||
current_project: Optional[str] = None,
|
current_project: Optional[str] = None,
|
||||||
current_tags: Optional[list[str]] = None,
|
current_tags: Optional[list[str]] = None,
|
||||||
current_sort_column: str = "priority",
|
current_sort_column: str = "priority",
|
||||||
@@ -146,8 +164,10 @@ class FilterSidebar(Widget):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.contexts = contexts or []
|
||||||
self.projects = projects or []
|
self.projects = projects or []
|
||||||
self.tags = tags or []
|
self.tags = tags or []
|
||||||
|
self.current_context = current_context
|
||||||
self.current_project = current_project
|
self.current_project = current_project
|
||||||
self.current_tags = current_tags or []
|
self.current_tags = current_tags or []
|
||||||
self.current_sort_column = current_sort_column
|
self.current_sort_column = current_sort_column
|
||||||
@@ -155,6 +175,9 @@ class FilterSidebar(Widget):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with ScrollableContainer(id="sidebar-scroll"):
|
with ScrollableContainer(id="sidebar-scroll"):
|
||||||
|
# Context filter section - bordered list (at top since it's global)
|
||||||
|
yield SelectionList[str](id="context-list", classes="filter-list")
|
||||||
|
|
||||||
# Project filter section - bordered list
|
# Project filter section - bordered list
|
||||||
yield SelectionList[str](id="project-list", classes="filter-list")
|
yield SelectionList[str](id="project-list", classes="filter-list")
|
||||||
|
|
||||||
@@ -187,6 +210,9 @@ class FilterSidebar(Widget):
|
|||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Initialize the sidebar with current filter state and set border titles."""
|
"""Initialize the sidebar with current filter state and set border titles."""
|
||||||
# Set border titles like mail app
|
# Set border titles like mail app
|
||||||
|
context_list = self.query_one("#context-list", SelectionList)
|
||||||
|
context_list.border_title = "Context"
|
||||||
|
|
||||||
project_list = self.query_one("#project-list", SelectionList)
|
project_list = self.query_one("#project-list", SelectionList)
|
||||||
project_list.border_title = "Projects"
|
project_list.border_title = "Projects"
|
||||||
|
|
||||||
@@ -197,12 +223,19 @@ class FilterSidebar(Widget):
|
|||||||
sort_section.border_title = "Sort"
|
sort_section.border_title = "Sort"
|
||||||
|
|
||||||
# Update the lists
|
# Update the lists
|
||||||
|
self._update_context_list()
|
||||||
self._update_project_list()
|
self._update_project_list()
|
||||||
self._update_tag_list()
|
self._update_tag_list()
|
||||||
self._update_subtitles()
|
self._update_subtitles()
|
||||||
|
|
||||||
def _update_subtitles(self) -> None:
|
def _update_subtitles(self) -> None:
|
||||||
"""Update border subtitles to show selection counts."""
|
"""Update border subtitles to show selection counts."""
|
||||||
|
context_list = self.query_one("#context-list", SelectionList)
|
||||||
|
if self.current_context:
|
||||||
|
context_list.border_subtitle = f"[b]{self.current_context}[/b]"
|
||||||
|
else:
|
||||||
|
context_list.border_subtitle = "none"
|
||||||
|
|
||||||
project_list = self.query_one("#project-list", SelectionList)
|
project_list = self.query_one("#project-list", SelectionList)
|
||||||
if self.current_project:
|
if self.current_project:
|
||||||
project_list.border_subtitle = f"[b]{self.current_project}[/b]"
|
project_list.border_subtitle = f"[b]{self.current_project}[/b]"
|
||||||
@@ -224,6 +257,20 @@ class FilterSidebar(Widget):
|
|||||||
)
|
)
|
||||||
sort_section.border_subtitle = f"{col_display} {direction}"
|
sort_section.border_subtitle = f"{col_display} {direction}"
|
||||||
|
|
||||||
|
def _update_context_list(self) -> None:
|
||||||
|
"""Update the context selection list."""
|
||||||
|
context_list = self.query_one("#context-list", SelectionList)
|
||||||
|
context_list.clear_options()
|
||||||
|
|
||||||
|
for ctx in self.contexts:
|
||||||
|
context_list.add_option(
|
||||||
|
Selection(
|
||||||
|
ctx,
|
||||||
|
ctx,
|
||||||
|
initial_state=ctx == self.current_context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _update_project_list(self) -> None:
|
def _update_project_list(self) -> None:
|
||||||
"""Update the project selection list."""
|
"""Update the project selection list."""
|
||||||
project_list = self.query_one("#project-list", SelectionList)
|
project_list = self.query_one("#project-list", SelectionList)
|
||||||
@@ -254,10 +301,14 @@ class FilterSidebar(Widget):
|
|||||||
|
|
||||||
def update_filters(
|
def update_filters(
|
||||||
self,
|
self,
|
||||||
|
contexts: Optional[list[str]] = None,
|
||||||
projects: Optional[list[tuple[str, int]]] = None,
|
projects: Optional[list[tuple[str, int]]] = None,
|
||||||
tags: Optional[list[str]] = None,
|
tags: Optional[list[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update available projects and tags."""
|
"""Update available contexts, projects and tags."""
|
||||||
|
if contexts is not None:
|
||||||
|
self.contexts = contexts
|
||||||
|
self._update_context_list()
|
||||||
if projects is not None:
|
if projects is not None:
|
||||||
self.projects = projects
|
self.projects = projects
|
||||||
self._update_project_list()
|
self._update_project_list()
|
||||||
@@ -272,6 +323,12 @@ class FilterSidebar(Widget):
|
|||||||
self._update_project_list()
|
self._update_project_list()
|
||||||
self._update_subtitles()
|
self._update_subtitles()
|
||||||
|
|
||||||
|
def set_current_context(self, context: Optional[str]) -> None:
|
||||||
|
"""Set the current context (updates UI to match)."""
|
||||||
|
self.current_context = context
|
||||||
|
self._update_context_list()
|
||||||
|
self._update_subtitles()
|
||||||
|
|
||||||
def set_current_tags(self, tags: list[str]) -> None:
|
def set_current_tags(self, tags: list[str]) -> None:
|
||||||
"""Set the current tag filters (updates UI to match)."""
|
"""Set the current tag filters (updates UI to match)."""
|
||||||
self.current_tags = tags
|
self.current_tags = tags
|
||||||
@@ -297,6 +354,30 @@ class FilterSidebar(Widget):
|
|||||||
|
|
||||||
self._update_subtitles()
|
self._update_subtitles()
|
||||||
|
|
||||||
|
@on(SelectionList.SelectedChanged, "#context-list")
|
||||||
|
def _on_context_selection_changed(
|
||||||
|
self, event: SelectionList.SelectedChanged
|
||||||
|
) -> None:
|
||||||
|
"""Handle context selection changes."""
|
||||||
|
selected = list(event.selection_list.selected)
|
||||||
|
# For context, we only allow single selection
|
||||||
|
if selected:
|
||||||
|
new_context = selected[0]
|
||||||
|
# If same context clicked again, deselect it (clear context)
|
||||||
|
if new_context == self.current_context:
|
||||||
|
self.current_context = None
|
||||||
|
event.selection_list.deselect(new_context)
|
||||||
|
else:
|
||||||
|
# Deselect previous if any
|
||||||
|
if self.current_context:
|
||||||
|
event.selection_list.deselect(self.current_context)
|
||||||
|
self.current_context = new_context
|
||||||
|
else:
|
||||||
|
self.current_context = None
|
||||||
|
|
||||||
|
self._update_subtitles()
|
||||||
|
self.post_message(self.ContextChanged(self.current_context))
|
||||||
|
|
||||||
@on(SelectionList.SelectedChanged, "#project-list")
|
@on(SelectionList.SelectedChanged, "#project-list")
|
||||||
def _on_project_selection_changed(
|
def _on_project_selection_changed(
|
||||||
self, event: SelectionList.SelectedChanged
|
self, event: SelectionList.SelectedChanged
|
||||||
|
|||||||
Reference in New Issue
Block a user