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 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.
- 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.
- 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.
@@ -1,6 +1,6 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=navipy-git
pkgver=r13.gbd6cfb8
pkgver=r14.g059e4e1
pkgrel=1
pkgdesc='Accessible Subsonic desktop client tested with Navidrome'
arch=('any')
+1
View File
@@ -368,6 +368,7 @@ class MainWindow(QMainWindow):
"""Connect signals after widgets exist."""
self.libraryBrowser.playRequested.connect(self.playLibrarySelection)
self.libraryBrowser.menuRequested.connect(self.onLibraryContextMenu)
self.libraryBrowser.checkedStateAnnouncement.connect(self.announceLive)
self.playButton.clicked.connect(self.playPauseAction.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)
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):
if (index.isValid() and index.column() == 0
and role == Qt.CheckStateRole
and self.isCheckable(index)):
return Qt.Checked if self.browser.isChecked(
self.rows[index.row()].data) else Qt.Unchecked
if index.isValid() and role in (Qt.DisplayRole, Qt.AccessibleTextRole, Qt.ToolTipRole):
return str(self.rows[index.row()].values[index.column()])
if not index.isValid():
return None
if self.hasCheckboxColumn() and index.column() == 0:
if not self.isCheckable(index):
return None
checked = self.browser.isChecked(self.rows[index.row()].data)
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
def flags(self, index):
@@ -90,11 +106,16 @@ class LibraryTableModel(QAbstractTableModel):
def refreshChecks(self):
if self.rows and self.headers:
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):
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
def replace(self, headers, rows):
@@ -266,8 +287,13 @@ class BrowserPage(QWidget):
self.heading.setText(state.title)
self.table.setAccessibleName(f'{state.title} table')
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:
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.backButton.setEnabled(len(self.history) > 1)
self.collectionButton.setEnabled(state.context.get('type') in (
@@ -387,6 +413,7 @@ class BrowserPage(QWidget):
class LibraryBrowser(QTabWidget):
playRequested = Signal(dict)
menuRequested = Signal(dict, object)
checkedStateAnnouncement = Signal(str)
PAGE_SIZE = 200
def __init__(self, runTask, currentSong, parent=None):
@@ -457,6 +484,14 @@ class LibraryBrowser(QTabWidget):
else:
self._checked.pop(key, None)
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):
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)
assert page.selectedData()['type'] == 'artist'
assert page.model.rowCount() == 1
assert page.model.columnCount() == 2
assert page.model.columnCount() == 3
assert not page.backButton.isEnabled()
assert not pending
assert page.table.hasFocus()
@@ -213,8 +213,38 @@ def test_native_accessibility_reports_table_headers_and_current_cells(window):
interface = QAccessible.queryAccessibleInterface(page.table)
assert interface.role() == QAccessible.Table
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 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):