From 2fb52654633a713cad3880565cf73cb76ec8d3d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Hamal=20Dvo=C5=99=C3=A1k?= <mordae@anilinux.org>
Date: Sun, 9 Mar 2025 13:25:14 +0100
Subject: [PATCH] Use ListModel for faster directory traversal

---
 lazy_player/__init__.py   | 24 +++++++++-----------
 lazy_player/file_model.py | 47 +++++++++++++++++++++++++++++++++++++--
 2 files changed, 55 insertions(+), 16 deletions(-)

diff --git a/lazy_player/__init__.py b/lazy_player/__init__.py
index 433fe3b..3097188 100644
--- a/lazy_player/__init__.py
+++ b/lazy_player/__init__.py
@@ -7,20 +7,20 @@ from typing import Any, cast
 
 import gi
 
-from .file_model import FileItem, FileType
+from .file_model import FileItem, FileListModel, FileType
 
 gi.require_version("Gdk", "4.0")
 gi.require_version("Gtk", "4.0")
 gi.require_version("Gst", "1.0")
 gi.require_version("Pango", "1.0")
-from gi.repository import Gdk, Gio, Gst, Gtk, Pango  # NOQA: E402
+from gi.repository import Gdk, Gst, Gtk, Pango  # NOQA: E402
 
 
 class MainWindow(Gtk.ApplicationWindow):
     file_info_label: Gtk.Label
     stack: Gtk.Stack
     list_view: Gtk.ListView
-    list_store: Gio.ListStore
+    list_model: FileListModel
     selection_model: Gtk.SingleSelection
     video_widget: Gtk.Picture
     pipeline: Gst.Pipeline
@@ -137,10 +137,10 @@ class MainWindow(Gtk.ApplicationWindow):
 
         main_box.append(grid)
 
-        # Create list store and view
-        self.list_store = Gio.ListStore(item_type=FileItem)
+        # Create list model and view
+        self.list_model = FileListModel()
         self.list_view = Gtk.ListView()
-        self.selection_model = Gtk.SingleSelection.new(self.list_store)
+        self.selection_model = Gtk.SingleSelection.new(self.list_model)
         self.selection_model.connect("selection-changed", self._on_selection_changed)
         self.list_view.set_model(self.selection_model)
         self.list_view.set_vexpand(True)
@@ -285,8 +285,8 @@ class MainWindow(Gtk.ApplicationWindow):
         self._populate_file_list()
 
         # Find and select the directory we came from
-        for i in range(self.list_store.get_n_items()):
-            item = self.list_store.get_item(i)
+        for i in range(self.list_model.get_n_items()):
+            item = self.list_model.get_item(i)
             if not item:
                 continue
 
@@ -392,7 +392,7 @@ class MainWindow(Gtk.ApplicationWindow):
             file_item.save_attribute("position", duration)
 
         # Force the list to update the changed item
-        self.list_store.items_changed(self.selection_model.get_selected(), 1, 1)
+        self.list_model.items_changed(self.selection_model.get_selected(), 1, 1)
 
     def _on_menu_key_pressed(
         self,
@@ -551,11 +551,7 @@ class MainWindow(Gtk.ApplicationWindow):
         # Sort directories first, then files, both alphabetically
         items.sort(key=lambda x: (x.file_type != FileType.DIRECTORY, x.name.lower()))
 
-        while self.list_store.get_n_items():
-            self.list_store.remove(0)
-
-        for item in items:
-            self.list_store.append(item)
+        self.list_model.set_items(items)
 
         if items:
             self.file_info_label.set_text(items[0].name)
diff --git a/lazy_player/file_model.py b/lazy_player/file_model.py
index ed1ba41..8e53c56 100644
--- a/lazy_player/file_model.py
+++ b/lazy_player/file_model.py
@@ -3,12 +3,12 @@ from __future__ import annotations
 import os
 from enum import Enum, auto
 from pathlib import Path
-from typing import overload
+from typing import Optional, overload
 
 import gi
 
 gi.require_version("GObject", "2.0")
-from gi.repository import GObject  # NOQA: E402
+from gi.repository import Gio, GObject  # NOQA: E402
 
 
 class FileType(Enum):
@@ -54,3 +54,46 @@ class FileItem(GObject.Object):
             pass
 
         self.notify("attrs-changed")
+
+
+class FileListModel(GObject.Object, Gio.ListModel):
+    """A ListModel implementation for FileItems"""
+
+    __gtype_name__ = "FileListModel"
+
+    items: list[FileItem]
+
+    def __init__(self):
+        super().__init__()
+        self.items = []
+
+    def do_get_item_type(self) -> GObject.GType:
+        return GObject.type_from_name("FileItem")
+
+    def do_get_n_items(self) -> int:
+        return len(self.items)
+
+    def do_get_item(self, position: int) -> Optional[FileItem]:
+        if 0 <= position < len(self.items):
+            return self.items[position]
+        return None
+
+    def remove_all(self) -> None:
+        removed = len(self.items)
+        self.items = []
+        self.items_changed(0, removed, 0)
+
+    def set_items(self, items: list[FileItem]):
+        removed = len(self.items)
+        self.items = list(items)
+        self.items_changed(0, removed, len(self.items))
+
+    def append(self, item: FileItem) -> None:
+        pos = len(self.items)
+        self.items.append(item)
+        self.items_changed(pos, 0, 1)
+
+    def remove(self, position: int) -> None:
+        if 0 <= position < len(self.items):
+            self.items.pop(position)
+            self.items_changed(position, 1, 0)