Hopefully checkboxes will report there state now.
This commit is contained in:
1 parent
059e4e124b
commit
f39ec4d51a
5 files changed
+81
-15
No files matched your search
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user