소스 검색

Add keyboard navigation support

- Enter/Return opens selected book from shelf view
- Escape: go back from details, close search bar, or clear selection
- Backspace goes back from details view to shelf
- Add getSelectedBook() and clearSelection() methods to BookShelf

Arrow key navigation in the grid is handled natively by GTK's GridView.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bernardo Magri 1 개월 전
부모
커밋
941ec681be
5개의 변경된 파일96개의 추가작업 그리고 6개의 파일을 삭제
  1. 10 6
      PLAN.md
  2. 61 0
      src/BibliothecaWindow.cpp
  3. 7 0
      src/BibliothecaWindow.hpp
  4. 13 0
      src/BookShelf.cpp
  5. 5 0
      src/BookShelf.hpp

+ 10 - 6
PLAN.md

@@ -185,13 +185,17 @@ Note: Skipped prompt dialog - auto-update is simpler and non-destructive
 ## Phase 4: Polish and Accessibility
 ## Phase 4: Polish and Accessibility
 
 
 ### 4.1 Keyboard Navigation
 ### 4.1 Keyboard Navigation
-**Files**: `src/BibliothecaWindow.cpp`, `src/BookShelf.cpp`
-**Status**: Not Started
+**Files**: `src/BibliothecaWindow.cpp`, `src/BibliothecaWindow.hpp`, `src/BookShelf.cpp`, `src/BookShelf.hpp`
+**Status**: COMPLETED
+
+Implemented:
+- [x] Enter/Return to open selected book from shelf view
+- [x] Escape to: go back from details, close search bar, or clear selection
+- [x] Backspace to go back from details view
+- [x] Added getSelectedBook() and clearSelection() to BookShelf
 
 
-- [ ] Arrow key navigation in grid
-- [ ] Enter to open selected book
-- [ ] Escape to clear selection/search
-- [ ] Tab navigation through UI elements
+Note: Arrow key navigation in grid is handled by GTK's GridView natively.
+Tab navigation works through GTK's focus system.
 
 
 ### 4.2 Accessibility Labels
 ### 4.2 Accessibility Labels
 **Files**: All UI files
 **Files**: All UI files

+ 61 - 0
src/BibliothecaWindow.cpp

@@ -8,6 +8,7 @@
 #include <giomm/asyncresult.h>
 #include <giomm/asyncresult.h>
 #include <glibmm/main.h>
 #include <glibmm/main.h>
 #include <glibmm/markup.h>
 #include <glibmm/markup.h>
+#include <gdk/gdkkeysyms.h>
 
 
 #include <thread>
 #include <thread>
 #include <iostream>
 #include <iostream>
@@ -148,6 +149,12 @@ BibliothecaWindow::BibliothecaWindow(DatabaseManager& db, BookList& bookList)
   m_bookList.signalBookUpdated().connect([this](const Book&) { updateVisibleView(); });
   m_bookList.signalBookUpdated().connect([this](const Book&) { updateVisibleView(); });
   m_bookList.signalBookRemoved().connect([this](const std::string&) { updateVisibleView(); });
   m_bookList.signalBookRemoved().connect([this](const std::string&) { updateVisibleView(); });
   m_bookList.signalBulkChanged().connect([this] { updateVisibleView(); });
   m_bookList.signalBulkChanged().connect([this] { updateVisibleView(); });
+
+  // Keyboard navigation
+  m_keyController = Gtk::EventControllerKey::create();
+  m_keyController->signal_key_pressed().connect(
+    sigc::mem_fun(*this, &BibliothecaWindow::onKeyPressed), false);
+  add_controller(m_keyController);
 }
 }
 
 
 void BibliothecaWindow::buildHeaderBar() {
 void BibliothecaWindow::buildHeaderBar() {
@@ -441,3 +448,57 @@ void BibliothecaWindow::onBookActivated(const Book& book) {
   m_bookDetails->set_book(&book);
   m_bookDetails->set_book(&book);
   m_stack.set_visible_child("details");
   m_stack.set_visible_child("details");
 }
 }
+
+bool BibliothecaWindow::onKeyPressed(guint keyval, guint /*keycode*/, Gdk::ModifierType /*state*/) {
+  const auto visible = m_stack.get_visible_child_name();
+
+  switch (keyval) {
+    case GDK_KEY_Escape:
+      if (visible == "details") {
+        // Go back to shelf from details
+        m_stack.set_visible_child("shelf");
+        if (!m_lastQuery.empty()) {
+          m_shelf->setFilter(m_lastQuery);
+          updateVisibleView();
+        }
+        return true;
+      } else if (m_searchBar.property_search_mode_enabled().get_value()) {
+        // Close search bar
+        m_searchBar.property_search_mode_enabled() = false;
+        return true;
+      } else if (m_shelf) {
+        // Clear selection
+        m_shelf->clearSelection();
+        return true;
+      }
+      break;
+
+    case GDK_KEY_Return:
+    case GDK_KEY_KP_Enter:
+      if (visible == "shelf" && m_shelf) {
+        // Activate selected book
+        if (auto book = m_shelf->getSelectedBook()) {
+          onBookActivated(*book);
+          return true;
+        }
+      }
+      break;
+
+    case GDK_KEY_BackSpace:
+      if (visible == "details") {
+        // Go back to shelf from details
+        m_stack.set_visible_child("shelf");
+        if (!m_lastQuery.empty()) {
+          m_shelf->setFilter(m_lastQuery);
+          updateVisibleView();
+        }
+        return true;
+      }
+      break;
+
+    default:
+      break;
+  }
+
+  return false; // Let other handlers process the key
+}

+ 7 - 0
src/BibliothecaWindow.hpp

@@ -12,6 +12,7 @@
 #include <gtkmm/searchentry.h>
 #include <gtkmm/searchentry.h>
 #include <gtkmm/overlay.h>
 #include <gtkmm/overlay.h>
 #include <gtkmm/revealer.h>
 #include <gtkmm/revealer.h>
+#include <gtkmm/eventcontrollerkey.h>
 
 
 #include "BookList.hpp"
 #include "BookList.hpp"
 #include "BookShelf.hpp"
 #include "BookShelf.hpp"
@@ -43,6 +44,9 @@ private:
   void showToast(const Glib::ustring& message, bool is_error = false);
   void showToast(const Glib::ustring& message, bool is_error = false);
   void hideToast();
   void hideToast();
 
 
+  // Keyboard handling
+  bool onKeyPressed(guint keyval, guint keycode, Gdk::ModifierType state);
+
   // Callbacks
   // Callbacks
   void onAddBookClicked();
   void onAddBookClicked();
   void onBookActivated(const Book& book);
   void onBookActivated(const Book& book);
@@ -84,4 +88,7 @@ private:
   std::unique_ptr<BookShelf> m_shelf;
   std::unique_ptr<BookShelf> m_shelf;
   std::unique_ptr<BookDetails> m_bookDetails;
   std::unique_ptr<BookDetails> m_bookDetails;
   std::string m_lastQuery;
   std::string m_lastQuery;
+
+  // Keyboard controller
+  Glib::RefPtr<Gtk::EventControllerKey> m_keyController;
 };
 };

+ 13 - 0
src/BookShelf.cpp

@@ -218,6 +218,19 @@ bool BookShelf::empty() const noexcept {
   return m_store->get_n_items() == 0;
   return m_store->get_n_items() == 0;
 }
 }
 
 
+std::optional<Book> BookShelf::getSelectedBook() const {
+  auto selected = m_selection->get_selected();
+  if (selected == GTK_INVALID_LIST_POSITION)
+    return std::nullopt;
+  if (auto obj = m_store->get_item(selected))
+    return obj->book();
+  return std::nullopt;
+}
+
+void BookShelf::clearSelection() {
+  m_selection->unselect_all();
+}
+
 void BookShelf::refreshVisible() {
 void BookShelf::refreshVisible() {
   m_selection->unselect_all();
   m_selection->unselect_all();
   m_store->remove_all();
   m_store->remove_all();

+ 5 - 0
src/BookShelf.hpp

@@ -33,6 +33,11 @@ public:
   const std::string& filter() const noexcept { return m_filter; }
   const std::string& filter() const noexcept { return m_filter; }
   bool empty() const noexcept;
   bool empty() const noexcept;
 
 
+  /// Get the currently selected book, if any
+  std::optional<Book> getSelectedBook() const;
+  /// Clear the current selection
+  void clearSelection();
+
 private:
 private:
   // signal handlers
   // signal handlers
   void onBookAdded(const Book& b);
   void onBookAdded(const Book& b);