Hopefully checkboxes will report there state now.

This commit is contained in:
Storm Dragon committed 2026-09-16 00:53:34 -04:00
1 parent 059e4e124b
commit f39ec4d51a
5 files changed
+81 -15

No files matched your search

+1 -1
View File
@@ -58,7 +58,7 @@ Logs are written to `~/.local/share/stormux/navipy/navipy.log`.
- Use `Backspace`, `Alt+Left`, or the Back button to return to the previous table and selection. - Use `Backspace`, `Alt+Left`, or the Back button to return to the previous table and selection.
- Use Play selected to play an entire selected album, artist, playlist, or genre without leaving the table. - Use Play selected to play an entire selected album, artist, playlist, or genre without leaving the table.
- Use `Shift+F10`, the Menu key, or Actions for play, queue, playlist, and favorite actions. - Use `Shift+F10`, the Menu key, or Actions for play, queue, playlist, and favorite actions.
- In library tables, press `Space` to check or uncheck an artist, album, or song. Checks remain selected while you browse other tabs and collections. `Alt+A` opens actions for all checked items. - Library tables have a first `Selected` checkbox column and initially focus the second, informational column. Press `Space` from either column to check or uncheck an artist, album, or song. Checks remain selected while you browse other tabs and collections. `Alt+A` opens actions for all checked items.
- Collection actions applies to the collection currently being viewed, including all artists or favorites. - Collection actions applies to the collection currently being viewed, including all artists or favorites.
- Albums and genre tracks have a Load more button when more results are available. - Albums and genre tracks have a Load more button when more results are available.
- Each tab keeps its own browsing position. Refreshing the library resets the tables. - Each tab keeps its own browsing position. Refreshing the library resets the tables.
@@ -1,6 +1,6 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org> # Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=navipy-git pkgname=navipy-git
pkgver=r13.gbd6cfb8 pkgver=r14.g059e4e1
pkgrel=1 pkgrel=1
pkgdesc='Accessible Subsonic desktop client tested with Navidrome' pkgdesc='Accessible Subsonic desktop client tested with Navidrome'
arch=('any') arch=('any')
+1
View File
@@ -368,6 +368,7 @@ class MainWindow(QMainWindow):
"""Connect signals after widgets exist.""" """Connect signals after widgets exist."""
self.libraryBrowser.playRequested.connect(self.playLibrarySelection) self.libraryBrowser.playRequested.connect(self.playLibrarySelection)
self.libraryBrowser.menuRequested.connect(self.onLibraryContextMenu) self.libraryBrowser.menuRequested.connect(self.onLibraryContextMenu)
self.libraryBrowser.checkedStateAnnouncement.connect(self.announceLive)
self.playButton.clicked.connect(self.playPauseAction.trigger) self.playButton.clicked.connect(self.playPauseAction.trigger)
self.prevButton.clicked.connect(self.previousAction.trigger) self.prevButton.clicked.connect(self.previousAction.trigger)
+46 -11
View File
@@ -59,16 +59,32 @@ class LibraryTableModel(QAbstractTableModel):
return 0 if parent.isValid() else len(self.rows) return 0 if parent.isValid() else len(self.rows)
def columnCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()):
return 0 if parent.isValid() else len(self.headers) return 0 if parent.isValid() else len(self.headers) + int(self.hasCheckboxColumn())
def hasCheckboxColumn(self):
return self.headers in (HEADERS['artist'], HEADERS['album'], HEADERS['song'])
def data(self, index, role=Qt.DisplayRole): def data(self, index, role=Qt.DisplayRole):
if (index.isValid() and index.column() == 0 if not index.isValid():
and role == Qt.CheckStateRole return None
and self.isCheckable(index)): if self.hasCheckboxColumn() and index.column() == 0:
return Qt.Checked if self.browser.isChecked( if not self.isCheckable(index):
self.rows[index.row()].data) else Qt.Unchecked return None
if index.isValid() and role in (Qt.DisplayRole, Qt.AccessibleTextRole, Qt.ToolTipRole): checked = self.browser.isChecked(self.rows[index.row()].data)
return str(self.rows[index.row()].values[index.column()]) if role == Qt.CheckStateRole:
return Qt.Checked if checked else Qt.Unchecked
label = self.rows[index.row()].data.get('label', 'item')
if role == Qt.AccessibleTextRole:
return f'Select {label}, {"checked" if checked else "unchecked"}'
if role == Qt.AccessibleDescriptionRole:
return ('Checked. Press Space to uncheck.' if checked
else 'Unchecked. Press Space to check.')
if role == Qt.ToolTipRole:
return f'{"Checked" if checked else "Unchecked"}: {label}'
return None
if role in (Qt.DisplayRole, Qt.AccessibleTextRole, Qt.ToolTipRole):
offset = 1 if self.hasCheckboxColumn() else 0
return str(self.rows[index.row()].values[index.column() - offset])
return None return None
def flags(self, index): def flags(self, index):
@@ -90,11 +106,16 @@ class LibraryTableModel(QAbstractTableModel):
def refreshChecks(self): def refreshChecks(self):
if self.rows and self.headers: if self.rows and self.headers:
self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, 0), self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, 0),
[Qt.CheckStateRole]) [Qt.CheckStateRole, Qt.AccessibleTextRole,
Qt.AccessibleDescriptionRole])
def headerData(self, section, orientation, role=Qt.DisplayRole): def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
return self.headers[section] if orientation == Qt.Horizontal else str(section + 1) if orientation == Qt.Horizontal:
if self.hasCheckboxColumn():
return 'Selected' if section == 0 else self.headers[section - 1]
return self.headers[section]
return str(section + 1)
return None return None
def replace(self, headers, rows): def replace(self, headers, rows):
@@ -266,8 +287,13 @@ class BrowserPage(QWidget):
self.heading.setText(state.title) self.heading.setText(state.title)
self.table.setAccessibleName(f'{state.title} table') self.table.setAccessibleName(f'{state.title} table')
self.model.replace(state.headers, state.rows) self.model.replace(state.headers, state.rows)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
if self.model.hasCheckboxColumn():
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
if state.rows: if state.rows:
self.table.setCurrentIndex(self.model.index(min(state.selected, len(state.rows) - 1), 0)) startColumn = 1 if self.model.hasCheckboxColumn() else 0
self.table.setCurrentIndex(
self.model.index(min(state.selected, len(state.rows) - 1), startColumn))
self.table.verticalScrollBar().setValue(state.scroll) self.table.verticalScrollBar().setValue(state.scroll)
self.backButton.setEnabled(len(self.history) > 1) self.backButton.setEnabled(len(self.history) > 1)
self.collectionButton.setEnabled(state.context.get('type') in ( self.collectionButton.setEnabled(state.context.get('type') in (
@@ -387,6 +413,7 @@ class BrowserPage(QWidget):
class LibraryBrowser(QTabWidget): class LibraryBrowser(QTabWidget):
playRequested = Signal(dict) playRequested = Signal(dict)
menuRequested = Signal(dict, object) menuRequested = Signal(dict, object)
checkedStateAnnouncement = Signal(str)
PAGE_SIZE = 200 PAGE_SIZE = 200
def __init__(self, runTask, currentSong, parent=None): def __init__(self, runTask, currentSong, parent=None):
@@ -457,6 +484,14 @@ class LibraryBrowser(QTabWidget):
else: else:
self._checked.pop(key, None) self._checked.pop(key, None)
self._refreshCheckedState() self._refreshCheckedState()
currentIndex = self.currentWidget().table.currentIndex()
if currentIndex.column() != 0:
count = self.checkedCount()
countText = (f'{count} item{"s" if count != 1 else ""} checked'
if count else 'no items checked')
self.checkedStateAnnouncement.emit(
f'{data.get("label", "Item")} {"checked" if checked else "unchecked"}; '
f'{countText}.')
def clearChecked(self): def clearChecked(self):
if not self._checked: if not self._checked:
+32 -2
View File
@@ -80,7 +80,7 @@ def test_browse_and_back_replace_rows_without_hidden_descendants(window):
QTest.keyClick(page.table, Qt.Key_Backspace) QTest.keyClick(page.table, Qt.Key_Backspace)
assert page.selectedData()['type'] == 'artist' assert page.selectedData()['type'] == 'artist'
assert page.model.rowCount() == 1 assert page.model.rowCount() == 1
assert page.model.columnCount() == 2 assert page.model.columnCount() == 3
assert not page.backButton.isEnabled() assert not page.backButton.isEnabled()
assert not pending assert not pending
assert page.table.hasFocus() assert page.table.hasFocus()
@@ -213,8 +213,38 @@ def test_native_accessibility_reports_table_headers_and_current_cells(window):
interface = QAccessible.queryAccessibleInterface(page.table) interface = QAccessible.queryAccessibleInterface(page.table)
assert interface.role() == QAccessible.Table assert interface.role() == QAccessible.Table
names = [interface.child(i).text(QAccessible.Name) for i in range(interface.childCount())] names = [interface.child(i).text(QAccessible.Name) for i in range(interface.childCount())]
assert 'Artist' in names and 'Albums' in names assert 'Selected' in names and 'Artist' in names and 'Albums' in names
assert 'Song' not in names and 'Album' not in names assert 'Song' not in names and 'Album' not in names
assert page.table.currentIndex().column() == 1
def test_checkbox_cell_exposes_checked_state_and_accessible_text(window):
w, _ = window
page = w.libraryBrowser.currentWidget()
checkboxIndex = page.model.index(0, 0)
announcements = []
w.libraryBrowser.checkedStateAnnouncement.connect(announcements.append)
assert page.model.headerData(0, Qt.Horizontal) == 'Selected'
assert page.model.data(checkboxIndex, Qt.AccessibleTextRole) == 'Select Artist, unchecked'
assert page.model.flags(checkboxIndex) & Qt.ItemIsUserCheckable
interface = QAccessible.queryAccessibleInterface(page.table)
checkbox = next(interface.child(i) for i in range(interface.childCount())
if interface.child(i).text(QAccessible.Name) == 'Select Artist, unchecked')
assert checkbox.state().checkable
assert not checkbox.state().checked
QTest.keyClick(page.table, Qt.Key_Space)
QApplication.processEvents()
assert page.model.data(checkboxIndex, Qt.CheckStateRole) == Qt.Checked
assert page.model.data(checkboxIndex, Qt.AccessibleTextRole) == 'Select Artist, checked'
assert announcements == ['Artist checked; 1 item checked.']
interface = QAccessible.queryAccessibleInterface(page.table)
checkbox = next(interface.child(i) for i in range(interface.childCount())
if interface.child(i).text(QAccessible.Name) == 'Select Artist, checked')
assert checkbox.state().checkable
assert checkbox.state().checked
def test_collection_actions_and_queue_use_current_payload(window, monkeypatch): def test_collection_actions_and_queue_use_current_payload(window, monkeypatch):