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 `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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in new issue
Block a user