|
|
@@ -0,0 +1,103 @@
|
|
|
+# BookShelf Module Overview
|
|
|
+
|
|
|
+The `BookShelf` module is the bridge between the data model (`BookList`) and the
|
|
|
+visual grid of book tiles. It packages a full GTK widget tree that can be
|
|
|
+embedded into any window: the widget takes care of subscribing to model
|
|
|
+updates, populating the `Gtk::GridView`, handling selection state, and exposing
|
|
|
+activation + filtering hooks back to the caller.
|
|
|
+
|
|
|
+## Responsibilities
|
|
|
+
|
|
|
+- Mirror the in-memory book collection into a `Gio::ListStore` suitable for a
|
|
|
+ `Gtk::GridView`.
|
|
|
+- Build and bind `BookTile` widgets on demand using a `Gtk::SignalListItemFactory`.
|
|
|
+- Provide user interaction glue:
|
|
|
+ - Emits `signalBookActivated()` when a tile is clicked.
|
|
|
+ - Tracks selection state via `Gtk::SingleSelection`.
|
|
|
+ - Clears selection when the user clicks outside the grid or moves the mouse
|
|
|
+ into empty space.
|
|
|
+- Implement a lightweight fuzzy filter so the grid only shows matches for the
|
|
|
+ active query.
|
|
|
+
|
|
|
+## Structure
|
|
|
+
|
|
|
+```
|
|
|
+BookShelf
|
|
|
+ ├── Gio::ListStore<BookObject> // backing data model
|
|
|
+ ├── Gtk::SingleSelection // selection controller passed to the grid
|
|
|
+ ├── Gtk::GridView // renders BookTile cells
|
|
|
+ ├── Gtk::SignalListItemFactory // setup / bind / unbind callbacks
|
|
|
+ ├── BookObject // small wrapper storing Book for the model
|
|
|
+ └── Filtering helpers // scoring & refresh logic
|
|
|
+```
|
|
|
+
|
|
|
+### BookObject wrapper
|
|
|
+
|
|
|
+`Gio::ListStore` expects items that derive from `Glib::Object`. `BookObject`
|
|
|
+wraps a `Book` so we can put books inside the GTK list store while still
|
|
|
+using value semantics when refreshing the grid.
|
|
|
+
|
|
|
+## Data flow
|
|
|
+
|
|
|
+1. Construction:
|
|
|
+ - `BookShelf` creates the list store, selection model, signal factory, and
|
|
|
+ sets up the GridView.
|
|
|
+ - Background gestures/controllers are added so clicking empty space clears
|
|
|
+ the selection.
|
|
|
+
|
|
|
+2. Synchronisation:
|
|
|
+ - `reload()`, `addTile()`, `updateTile()`, and `removeTile()` all forward to
|
|
|
+ `refreshVisible()`, which rebuilds the list store from `BookList` (using
|
|
|
+ the current filter, if any).
|
|
|
+ - `BookList` signals are connected to these entry points in the constructor
|
|
|
+ so UI stays in sync with database changes.
|
|
|
+
|
|
|
+3. Rendering:
|
|
|
+ - `onFactorySetup()` creates a `BookTile` and stores it in each `Gtk::ListItem`.
|
|
|
+ - `onFactoryBind()` pulls the corresponding `BookObject` from the store and
|
|
|
+ feeds it into the tile via `BookTile::setBook()`.
|
|
|
+ - `onFactoryUnbind()` resets the tile to a blank Book so stale metadata isn’t
|
|
|
+ shown when the item is recycled.
|
|
|
+
|
|
|
+4. Interaction:
|
|
|
+ - When the grid emits `signal_activate(position)`, the selection model is
|
|
|
+ updated and `signalBookActivated()` is forwarded to the outside world.
|
|
|
+ - Background gestures reset the selection so keyboard focus isn’t stuck.
|
|
|
+
|
|
|
+## Filtering algorithm
|
|
|
+
|
|
|
+`setFilter(query)` stores a lowercase copy of the query and calls
|
|
|
+`refreshVisible()`. The refresh routine:
|
|
|
+
|
|
|
+1. Clears the current selection and list store.
|
|
|
+2. If the query is empty, re-populates the grid with every book.
|
|
|
+3. Otherwise evaluates a fuzzy score for each book using:
|
|
|
+ - substring match (higher score for earlier matches), and
|
|
|
+ - subsequence match (reward tightly clustered characters).
|
|
|
+4. Keeps only books with a non-negative score, sorts descending by score, and
|
|
|
+ repopulates the store so the grid shows the ranked results.
|
|
|
+
|
|
|
+The helper functions `ascii_lower`, `substring_score`, `subsequence_score`, and
|
|
|
+`book_score` live inside the module to keep the fuzzy logic confined.
|
|
|
+
|
|
|
+## Extension points
|
|
|
+
|
|
|
+- **Custom tile content**: tweak `BookTile` (e.g., add badges or context menus);
|
|
|
+ the factory setup already exposes the widget instance.
|
|
|
+- **Filtering strategy**: adjust scoring or add new fields (tags, notes) inside
|
|
|
+ `book_score()`.
|
|
|
+- **Selection behaviour**: change the background event controllers if you want
|
|
|
+ multi-select or persistent selection.
|
|
|
+
|
|
|
+## Expected usage
|
|
|
+
|
|
|
+The module is designed to be embedded like so:
|
|
|
+
|
|
|
+```cpp
|
|
|
+m_shelf = std::make_unique<BookShelf>(m_bookList, 180);
|
|
|
+m_shelf->signalBookActivated().connect(...);
|
|
|
+container.append(*m_shelf);
|
|
|
+```
|
|
|
+
|
|
|
+Call `setFilter(query)` from your search UI, and `reload()` if you ever perform
|
|
|
+bulk operations that bypass the standard BookList signals.
|