Skip to content

Latest commit

 

History

History
937 lines (739 loc) · 28.3 KB

File metadata and controls

937 lines (739 loc) · 28.3 KB

Smart List Sorting Support Implementation Plan

Date: 2025-11-07 Goal: Add column-based sorting functionality to smart_list library

Overview

Add comprehensive sorting support to smart_list, allowing users to click column headers to sort the list by that column. Support both ascending and descending order with visual indicators.


Design Goals

  1. Clean API - Simple, intuitive interface for defining sortable columns
  2. Backwards Compatible - Existing code continues to work without changes
  3. Platform Consistent - Works on both ListView (Windows/Linux) and DataView (macOS)
  4. Visual Feedback - Show sort direction indicators (▲▼) in column headers
  5. Performance - Efficient sorting even with large lists
  6. Flexible - Support custom sort keys and multi-type columns

API Design

Column Class Enhancement

Current:

class Column:
    def __init__(self, title=None, width=-1, model_field=None):
        self.title = title
        self.model_field = model_field
        self.width = width

Proposed:

class Column:
    def __init__(self, title=None, width=-1, model_field=None,
                 sortable=True, sort_key=None, reverse_sort=False):
        self.title = title
        self.model_field = model_field
        self.width = width
        self.sortable = sortable  # Whether this column can be sorted
        self.sort_key = sort_key  # Custom sort key function (optional)
        self.reverse_sort = reverse_sort  # Default sort direction

Parameters:

  • sortable (bool): If False, clicking header does nothing. Default: True
  • sort_key (callable or None): Custom function for extracting sort value from model
    • If None, uses the same logic as get_model_value()
    • Signature: sort_key(model) -> comparable_value
    • Example: sort_key=lambda book: book.get("title", "").lower() for case-insensitive
  • reverse_sort (bool): Default sort direction. True = descending, False = ascending

Examples:

# Simple sortable column (uses model_field value)
Column(title="Name", model_field="name")

# Non-sortable column
Column(title="Actions", model_field=get_actions, sortable=False)

# Custom sort key for case-insensitive sorting
Column(title="Title", model_field="title",
       sort_key=lambda x: x.get("title", "").lower())

# Sort by length of author list, default descending
Column(title="Authors", model_field=format_authors,
       sort_key=lambda x: len(x.get("author", [])),
       reverse_sort=True)

SmartList Class Enhancement

New Methods:

def sort_by_column(self, column_index, reverse=False):
    """Sort the list by specified column.

    Args:
        column_index (int): Index of column to sort by (0-based)
        reverse (bool): Sort in descending order if True

    Raises:
        ValueError: If column_index is out of range
        RuntimeError: If column is not sortable
    """

def get_sort_state(self):
    """Get current sort state.

    Returns:
        tuple: (column_index, reverse) or (None, None) if unsorted
    """

def toggle_sort(self, column_index):
    """Toggle sort direction or sort by new column.

    If currently sorted by this column, reverses direction.
    If sorted by different column, sorts by this column ascending.

    Args:
        column_index (int): Index of column to sort by
    """

New Instance Variables:

self.sort_column = None  # Index of currently sorted column (None = unsorted)
self.sort_reverse = False  # Current sort direction
self.sort_enabled = True  # Master switch to enable/disable sorting

Modified Methods:

def set_columns(self, columns):
    """Configure columns and bind sort event handler."""
    # Existing logic...
    # NEW: Bind EVT_LIST_COL_CLICK if any columns are sortable

def add_items(self, items):
    """Add items and re-sort if currently sorted."""
    # Existing logic...
    # NEW: If self.sort_column is set, re-apply sort after adding

def delete_items(self, items):
    """Delete items (sorting state maintained)."""
    # Existing logic (no change needed)

def update_item(self, item, original=None):
    """Update item and re-sort if currently sorted."""
    # Existing logic...
    # NEW: If self.sort_column is set, may need to re-sort

Implementation Details

1. Column Class (smart_list/smart_list.py)

Location: Lines 393-446

Changes:

class Column(object):
    """Column definition for SmartList display.

    Defines how to extract and display a value from model objects.
    Supports three field resolution strategies:
    - Attribute access: model_field="name" -> obj.name
    - Dict key access: model_field="name" -> obj["name"]
    - Callable: model_field=lambda x: x.first + x.last

    Args:
        title: Column header text
        width: Column width in pixels (-1 for auto)
        model_field: Field name or callable for extracting values
        sortable: Whether column can be sorted (default True)
        sort_key: Custom callable for extracting sort value (optional)
        reverse_sort: Default to descending sort (default False)
    """

    def __init__(self, title=None, width=-1, model_field=None,
                 sortable=True, sort_key=None, reverse_sort=False):
        self.title = title
        self.model_field = model_field
        self.width = width
        self.sortable = sortable
        self.sort_key = sort_key
        self.reverse_sort = reverse_sort

    def get_sort_value(self, model):
        """Extract sort value from model object.

        Uses sort_key if provided, otherwise uses get_model_value logic.

        Args:
            model: Object to extract value from

        Returns:
            Comparable value for sorting

        Raises:
            RuntimeError: If field not found and sort_key not provided
        """
        if self.sort_key is not None:
            return self.sort_key(model)

        # Fall back to get_model_value logic
        if self.model_field is None:
            return ""
        if is_callable(self.model_field):
            return self.model_field(model)

        try:
            value = getattr(model, self.model_field)
        except (AttributeError, TypeError):
            try:
                value = model[self.model_field]
            except (KeyError, TypeError):
                raise RuntimeError(
                    "Unable to find a %r attribute or key on model %r"
                    % (self.model_field, model)
                )

        if is_callable(value):
            value = value()
        return value

    # Existing get_model_value method unchanged

2. SmartList Class (smart_list/smart_list.py)

Location: Lines 85-285

Changes:

a) init modification:

def __init__(self, parent=None, id=-1, *args, **kwargs):
    choices = kwargs.pop("choices", [])
    self.control = UnifiedList(
        parent_obj=self, parent=parent, id=id, *args, **kwargs
    )
    self.models = []
    self.list_items = []
    self.index_map = {}
    self.columns = []

    # NEW: Sorting state
    self.sort_column = None  # Index of column being sorted by
    self.sort_reverse = False  # Sort direction
    self.sort_enabled = True  # Master switch for sorting

    self.add_items(choices)

b) set_columns modification:

def set_columns(self, columns):
    """Configure column definitions for the list.

    Args:
        columns: List of Column objects defining display fields
    """
    self.columns = columns
    for column in columns:
        self.control.AppendColumn(column.title, column.width)

    # NEW: Bind column click event if any columns are sortable
    if any(col.sortable for col in columns):
        self.control.BindColumnClick(self._handle_column_click)

c) New sorting methods:

def _handle_column_click(self, event):
    """Handle column header click for sorting.

    Args:
        event: wx.ListEvent with column index
    """
    if not self.sort_enabled:
        return

    column_index = event.GetColumn()

    # Check if column is sortable
    if column_index < 0 or column_index >= len(self.columns):
        return
    if not self.columns[column_index].sortable:
        return

    self.toggle_sort(column_index)

def toggle_sort(self, column_index):
    """Toggle sort direction or sort by new column.

    If currently sorted by this column, reverses direction.
    If sorted by different column, sorts by this column (using column's default direction).

    Args:
        column_index (int): Index of column to sort by
    """
    if column_index == self.sort_column:
        # Toggle direction on same column
        self.sort_reverse = not self.sort_reverse
    else:
        # Sort by new column using its default direction
        self.sort_column = column_index
        self.sort_reverse = self.columns[column_index].reverse_sort

    self.sort_by_column(self.sort_column, self.sort_reverse)

def sort_by_column(self, column_index, reverse=False):
    """Sort the list by specified column.

    Args:
        column_index (int): Index of column to sort by (0-based)
        reverse (bool): Sort in descending order if True

    Raises:
        ValueError: If column_index is out of range
        RuntimeError: If column is not sortable
    """
    if column_index < 0 or column_index >= len(self.columns):
        raise ValueError(f"Column index {column_index} out of range")

    column = self.columns[column_index]
    if not column.sortable:
        raise RuntimeError(f"Column {column_index} ({column.title}) is not sortable")

    # Store sort state
    self.sort_column = column_index
    self.sort_reverse = reverse

    # Sort models using column's get_sort_value
    try:
        self.models.sort(
            key=lambda model: column.get_sort_value(model),
            reverse=reverse
        )
    except Exception as e:
        logger.exception(f"Error sorting by column {column_index}: {e}")
        raise

    # Rebuild display
    self._rebuild_display()

    # Update visual sort indicator
    self.control.SetSortIndicator(column_index, reverse)

def _rebuild_display(self):
    """Rebuild the list display from sorted models."""
    self.control.Freeze()
    try:
        # Clear display but keep models
        self.control.Clear()

        # Re-add all items in new order
        for model in self.models:
            columns = self.get_columns_for(model)
            self.control.Append(columns)

        # Rebuild index map
        self._rebuild_index_map()
    finally:
        self.control.Thaw()

def get_sort_state(self):
    """Get current sort state.

    Returns:
        tuple: (column_index, reverse) or (None, None) if unsorted
    """
    return (self.sort_column, self.sort_reverse)

def clear_sort(self):
    """Clear sorting and return to original order.

    Note: This doesn't restore insertion order, just clears the sort state.
    To restore original order, you'd need to track it separately.
    """
    self.sort_column = None
    self.sort_reverse = False
    self.control.ClearSortIndicator()

d) Modify add_items to maintain sort:

@freeze_and_thaw
def add_items(self, items):
    """Add multiple items to the list.

    If list is currently sorted, items are added and list is re-sorted.

    Args:
        items: Iterable of model objects to display
    """
    if self.index_map is None:
        self._rebuild_index_map()

    for item in items:
        self.models.append(item)
        if isinstance(item, MutableMapping):
            item = freeze_dict(item)
        # Don't add to display yet if we need to re-sort

    # If sorted, re-sort and rebuild. Otherwise add to display.
    if self.sort_column is not None:
        self.sort_by_column(self.sort_column, self.sort_reverse)
    else:
        # Add items to display in order
        for item in items:
            columns = self.get_columns_for(item)
            self.control.Append(columns)
            if isinstance(item, MutableMapping):
                item = freeze_dict(item)
            self.index_map[item] = len(self.models) - 1

3. UnifiedList Class (smart_list/unified_list.py)

Location: Lines 22-189

New Methods:

def BindColumnClick(self, handler):
    """Bind column header click event.

    Args:
        handler: Callback function(event)
    """
    if self.use_dataview:
        # DataView uses EVT_DATAVIEW_COLUMN_HEADER_CLICK
        self.control.Bind(wx.dataview.EVT_DATAVIEW_COLUMN_HEADER_CLICK, handler)
    else:
        # ListView uses EVT_LIST_COL_CLICK
        self.control.Bind(wx.EVT_LIST_COL_CLICK, handler)

def SetSortIndicator(self, column_index, reverse=False):
    """Show sort indicator on column header.

    Args:
        column_index (int): Column to show indicator on
        reverse (bool): True for descending (▼), False for ascending (▲)
    """
    if self.use_dataview:
        # DataView supports sort indicators
        col = self.control.GetColumn(column_index)
        if col:
            if reverse:
                col.SetSortOrder(False)  # Descending
            else:
                col.SetSortOrder(True)   # Ascending
            # Note: May need to set col as sort column explicitly
            # self.control.SetSortingColumn(column_index)
    else:
        # ListView: Use SortItems or manually draw indicator
        # wx.ListCtrl doesn't have built-in sort indicators on all platforms
        # May need to prepend ▲/▼ to column title as workaround
        self._update_column_title_with_indicator(column_index, reverse)

def ClearSortIndicator(self):
    """Clear all sort indicators from column headers."""
    if self.use_dataview:
        # Clear DataView sort indicator
        # self.control.ClearSortingColumn()  # If this method exists
        pass
    else:
        # Clear ListView title indicators
        self._clear_all_column_indicators()

def _update_column_title_with_indicator(self, column_index, reverse):
    """Add ▲/▼ to column title (ListView workaround).

    Args:
        column_index (int): Column to update
        reverse (bool): Direction indicator
    """
    # First clear all existing indicators
    self._clear_all_column_indicators()

    # Get column info
    col_info = self.control.GetColumn(column_index)
    title = col_info.GetText()

    # Remove any existing indicators
    title = title.rstrip(" ▲▼↑↓")

    # Add new indicator
    indicator = " ▼" if reverse else " ▲"
    col_info.SetText(title + indicator)
    self.control.SetColumn(column_index, col_info)

def _clear_all_column_indicators(self):
    """Remove ▲/▼ from all column titles (ListView workaround)."""
    for i in range(self.control.GetColumnCount()):
        col_info = self.control.GetColumn(i)
        title = col_info.GetText()
        title = title.rstrip(" ▲▼↑↓")
        col_info.SetText(title)
        self.control.SetColumn(i, col_info)

4. VirtualSmartList Considerations

Challenge: Virtual lists don't store models in memory, so sorting requires special handling.

Options:

Option A: Disable sorting for virtual lists (simplest)

class VirtualSmartList(SmartList):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.sort_enabled = False  # Disable sorting for virtual lists

Option B: Support sorting with callback (advanced)

class VirtualSmartList(SmartList):
    def __init__(self, get_virtual_item=None, update_cache=None,
                 sort_callback=None, *args, **kwargs):
        # ...existing init...
        self.sort_callback = sort_callback  # Callable(column_index, reverse)

    def sort_by_column(self, column_index, reverse=False):
        """Delegate sorting to callback if provided."""
        if self.sort_callback:
            self.sort_callback(column_index, reverse)
            self.sort_column = column_index
            self.sort_reverse = reverse
            self.control.SetSortIndicator(column_index, reverse)
            self.refresh()
        else:
            raise RuntimeError("Virtual lists require sort_callback for sorting")

Recommendation: Start with Option A (disable for virtual lists), add Option B later if needed.


Testing Strategy

Unit Tests (New file: smart_list/tests/test_sorting.py)

import unittest
import wx
from smart_list import SmartList, Column


class TestSorting(unittest.TestCase):
    def setUp(self):
        self.app = wx.App()
        self.frame = wx.Frame(None)

    def tearDown(self):
        self.frame.Destroy()
        self.app.Destroy()

    def test_sort_by_string_column(self):
        """Test sorting by string attribute."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([
            Column(title="Name", model_field="name"),
            Column(title="Age", model_field="age")
        ])

        items = [
            {"name": "Charlie", "age": 30},
            {"name": "Alice", "age": 25},
            {"name": "Bob", "age": 35},
        ]
        lst.add_items(items)

        # Sort by name ascending
        lst.sort_by_column(0, reverse=False)
        self.assertEqual(lst.models[0]["name"], "Alice")
        self.assertEqual(lst.models[1]["name"], "Bob")
        self.assertEqual(lst.models[2]["name"], "Charlie")

        # Sort by name descending
        lst.sort_by_column(0, reverse=True)
        self.assertEqual(lst.models[0]["name"], "Charlie")
        self.assertEqual(lst.models[2]["name"], "Alice")

    def test_sort_by_numeric_column(self):
        """Test sorting by numeric attribute."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([
            Column(title="Name", model_field="name"),
            Column(title="Age", model_field="age")
        ])

        items = [
            {"name": "Charlie", "age": 30},
            {"name": "Alice", "age": 25},
            {"name": "Bob", "age": 35},
        ]
        lst.add_items(items)

        lst.sort_by_column(1, reverse=False)
        self.assertEqual(lst.models[0]["age"], 25)
        self.assertEqual(lst.models[1]["age"], 30)
        self.assertEqual(lst.models[2]["age"], 35)

    def test_toggle_sort(self):
        """Test toggle_sort switches direction."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([Column(title="Name", model_field="name")])

        items = [{"name": "B"}, {"name": "A"}, {"name": "C"}]
        lst.add_items(items)

        # First toggle: sort ascending
        lst.toggle_sort(0)
        self.assertEqual(lst.sort_column, 0)
        self.assertEqual(lst.sort_reverse, False)
        self.assertEqual(lst.models[0]["name"], "A")

        # Second toggle: reverse to descending
        lst.toggle_sort(0)
        self.assertEqual(lst.sort_reverse, True)
        self.assertEqual(lst.models[0]["name"], "C")

    def test_custom_sort_key(self):
        """Test sorting with custom sort_key function."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([
            Column(
                title="Name",
                model_field="name",
                sort_key=lambda x: x["name"].lower()  # Case-insensitive
            )
        ])

        items = [
            {"name": "charlie"},
            {"name": "Alice"},
            {"name": "BOB"},
        ]
        lst.add_items(items)

        lst.sort_by_column(0, reverse=False)
        self.assertEqual(lst.models[0]["name"], "Alice")
        self.assertEqual(lst.models[1]["name"], "BOB")
        self.assertEqual(lst.models[2]["name"], "charlie")

    def test_non_sortable_column(self):
        """Test that non-sortable columns raise error."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([
            Column(title="Name", model_field="name", sortable=False)
        ])

        items = [{"name": "A"}, {"name": "B"}]
        lst.add_items(items)

        with self.assertRaises(RuntimeError):
            lst.sort_by_column(0)

    def test_sort_state_persists_on_add(self):
        """Test that adding items maintains sort order."""
        lst = SmartList(parent=self.frame)
        lst.set_columns([Column(title="Name", model_field="name")])

        lst.add_items([{"name": "C"}, {"name": "A"}])
        lst.sort_by_column(0, reverse=False)

        # Add new item
        lst.add_items([{"name": "B"}])

        # Should still be sorted
        self.assertEqual(lst.models[0]["name"], "A")
        self.assertEqual(lst.models[1]["name"], "B")
        self.assertEqual(lst.models[2]["name"], "C")

Manual Testing Checklist

  • Click column header sorts ascending
  • Click same header again sorts descending
  • Click different header sorts by new column ascending
  • Sort indicator (▲▼) appears on correct column
  • Only one column shows indicator at a time
  • Non-sortable columns don't respond to clicks
  • Adding items maintains sort order
  • Deleting items maintains sort order
  • Works with dict models
  • Works with object models
  • Works with callable model_field
  • Custom sort_key functions work correctly
  • Sort persists across Freeze/Thaw operations
  • Visual indicator clears when sort is cleared

Implementation Phases

Phase 1: Core Sorting (Minimal Viable Feature)

Files:

  • smart_list/smart_list.py (Column, SmartList)
  • smart_list/unified_list.py (UnifiedList)

Tasks:

  1. Add sortable, sort_key, reverse_sort to Column.init
  2. Add get_sort_value() to Column class
  3. Add sort state variables to SmartList.init
  4. Add sort_by_column() method to SmartList
  5. Add toggle_sort() method to SmartList
  6. Add _rebuild_display() method to SmartList
  7. Modify set_columns() to bind column click event
  8. Add _handle_column_click() to SmartList
  9. Add BindColumnClick() to UnifiedList

Deliverable: Can programmatically sort list via lst.sort_by_column(0)

Phase 2: Visual Indicators

Files:

  • smart_list/unified_list.py

Tasks:

  1. Add SetSortIndicator() to UnifiedList
  2. Add ClearSortIndicator() to UnifiedList
  3. Add _update_column_title_with_indicator() for ListView
  4. Add _clear_all_column_indicators() for ListView
  5. Call SetSortIndicator() from SmartList.sort_by_column()

Deliverable: Sort direction shown in column header (▲▼)

Phase 3: Sort Persistence

Files:

  • smart_list/smart_list.py

Tasks:

  1. Modify add_items() to re-sort after adding
  2. Add get_sort_state() method
  3. Add clear_sort() method
  4. Test that delete_items maintains sort order (should work automatically)

Deliverable: Sort order maintained when items added/removed

Phase 4: Virtual List Support (Optional)

Files:

  • smart_list/smart_list.py (VirtualSmartList)

Tasks:

  1. Decide on approach (disable or callback)
  2. Implement chosen approach
  3. Document virtual list sorting limitations/requirements

Deliverable: Virtual lists either disable sorting gracefully or support via callback

Phase 5: Testing & Documentation

Files:

  • smart_list/tests/test_sorting.py (new)
  • smart_list/README.md
  • Example scripts

Tasks:

  1. Write comprehensive unit tests
  2. Update README with sorting examples
  3. Create example script demonstrating sorting features
  4. Manual testing on Windows, Linux, macOS

Deliverable: Fully tested and documented feature


Backwards Compatibility

Breaking Changes: NONE

All changes are backwards compatible:

  • Column class accepts new optional parameters (all default to backwards-compatible values)
  • SmartList behavior unchanged unless user clicks column headers
  • Existing code continues to work without modification

Migration Guide

Before (still works):

lst.set_columns([
    Column(title="Name", model_field="name", width=100),
    Column(title="Age", model_field="age", width=50)
])

After (with sorting features):

lst.set_columns([
    Column(title="Name", model_field="name", width=100,
           sort_key=lambda x: x.name.lower()),  # Case-insensitive
    Column(title="Age", model_field="age", width=50),
    Column(title="Actions", model_field=format_actions,
           sortable=False)  # Disable sorting on this column
])

Edge Cases & Error Handling

1. Empty List

  • Clicking header on empty list should be no-op
  • get_sort_state() returns (None, None) for unsorted list

2. Missing/None Values

  • sort_key should handle None gracefully
  • Consider: sort_key=lambda x: (x.get("name") or "", x.get("id", 0))
  • Or: sort_key=lambda x: x.get("name") or ""

3. Mixed Types in Column

  • If column has mixed types (e.g., some ints, some strings), sort will fail
  • Provide helpful error message
  • Document that columns should have consistent types for sorting

4. Large Lists

  • Freeze/Thaw prevents flickering during rebuild
  • For very large lists (>10k items), consider profiling
  • May need to batch updates or use virtual list

5. Custom Objects

  • Ensure sort_key works with custom objects
  • Document that objects must be comparable if using default sort

6. Platform Differences

  • Test sort indicators on all platforms
  • ListView (Windows/Linux) may need title-based indicators
  • DataView (macOS) has native sort indicator support

Performance Considerations

Sorting Algorithm

  • Python's list.sort() is Timsort (O(n log n))
  • Very efficient for partially sorted data
  • Stable sort preserves relative order of equal elements

Display Rebuild

  • Freeze/Thaw prevents multiple redraws
  • Clear + re-add all items may be slow for very large lists
  • Alternative: Update each row in place (more complex)

Index Map Rebuild

  • _rebuild_index_map() is O(n)
  • Called after every sort
  • Using frozendict for dict keys adds overhead

Optimization Opportunities

  1. Lazy Sorting: Only sort when visible (defer until Thaw)
  2. Incremental Updates: Update changed rows instead of full rebuild
  3. Sort Cache: Cache sort keys to avoid recomputing
  4. Virtual Sorting: For virtual lists, sort indices not items

Future Enhancements (Post-MVP)

Multi-Column Sorting

lst.sort_by_columns([
    (0, False),  # Primary: column 0 ascending
    (1, True)    # Secondary: column 1 descending
])

Sort Indicators with Custom Icons

  • Allow custom sort indicator characters/icons
  • Support themes (dark mode, high contrast)

Sortable Column Groups

  • Define column groups that sort together
  • E.g., "First Name" + "Last Name" as single sortable unit

Server-Side Sorting

  • Callback to request sorted data from server
  • Useful for paginated/virtual lists with backend

Sort History

  • Remember last N sort operations
  • Allow undo/redo of sorts

Accessibility

  • Announce sort state to screen readers
  • Keyboard shortcuts for sorting (e.g., Alt+Click for multi-column)

Open Questions

  1. Should we support multi-column sorting in Phase 1?

    • Recommendation: No, add later if needed
  2. What should happen when clicking a non-sortable column header?

    • Recommendation: No-op (no visual feedback)
    • Alternative: Show tooltip "This column cannot be sorted"
  3. Should sort state be exposed in API for persistence?

    • Use case: Save/restore sort preference across sessions
    • Recommendation: Add get_sort_state() in Phase 3
  4. How should we handle sort on columns with None values?

    • Recommendation: None values sort to end (ascending) or beginning (descending)
    • Implementation: Use custom key like key=lambda x: (x is None, x)
  5. Should we add sort animation/transition?

    • Recommendation: No, keep it simple for MVP
    • May cause issues with accessibility
  6. Do we need column click modifiers (Shift for secondary sort)?

    • Recommendation: Not for MVP, consider for future

Success Criteria

Feature is complete when:

  • ✅ User can click any sortable column header to sort
  • ✅ Clicking again reverses sort direction
  • ✅ Visual indicator shows which column is sorted and direction
  • ✅ Sort persists when adding new items
  • ✅ Non-sortable columns cannot be sorted
  • ✅ Custom sort_key functions work correctly
  • ✅ Works on Windows/Linux (ListView) and macOS (DataView)
  • ✅ All unit tests pass
  • ✅ No regressions in existing functionality
  • ✅ Documentation complete with examples
  • ✅ Backwards compatible with existing code

Timeline Estimate

  • Phase 1 (Core): 4-6 hours
  • Phase 2 (Visual): 2-3 hours
  • Phase 3 (Persistence): 2-3 hours
  • Phase 4 (Virtual - optional): 2-4 hours
  • Phase 5 (Testing/Docs): 3-4 hours

Total: 13-20 hours (excluding virtual list support)


Notes

  • Keep changes minimal and focused
  • Prioritize code clarity over cleverness
  • Test incrementally after each phase
  • Consider impact on other projects using smart_list
  • Document any platform-specific behavior