✨ Results tree uses View component and displays filter input fields.

This commit is contained in:
Robert Honz
2024-05-14 07:31:57 +02:00
parent 214018c4a2
commit f46e9cf3e0
4 changed files with 198 additions and 89 deletions
+68 -27
View File
@@ -1,19 +1,21 @@
import math
import sys
import time
from collections.abc import Callable
from collections.abc import Callable, Sequence
from PySide6.QtGui import QStandardItem
from requests.exceptions import HTTPError
from tidal_dl_ng import __version__, update_available
from tidal_dl_ng.dialog import DialogLogin, DialogPreferences, DialogVersion
from tidal_dl_ng.helper.gui import (
FilterHeader,
HumanProxyModel,
get_queue_download_media,
get_queue_download_quality,
get_results_media_item,
get_user_list_media_item,
set_queue_download_media,
set_results_media,
set_user_list_media,
)
from tidal_dl_ng.helper.path import get_format_template, resource_path
@@ -64,6 +66,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
tray: QtWidgets.QSystemTrayIcon
spinner: QtWaitingSpinner
cover_url_current: str = ""
model_tr_results: QtGui.QStandardItemModel = QtGui.QStandardItemModel()
proxy_tr_results: HumanProxyModel
s_spinner_start: QtCore.Signal = QtCore.Signal(QtWidgets.QWidget)
s_spinner_stop: QtCore.Signal = QtCore.Signal()
pb_item: QtWidgets.QProgressBar
@@ -98,7 +102,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.settings = Settings()
self._init_threads()
self._init_tree_results(self.tr_results)
self._init_tree_results_model(self.model_tr_results)
self._init_tree_results(self.tr_results, self.model_tr_results)
self._init_tree_lists(self.tr_lists_user)
self._init_tree_queue(self.tr_queue_download)
self._init_info()
@@ -213,7 +218,30 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.cb_search_type.setCurrentIndex(2)
def _init_tree_results(self, tree: QtWidgets.QTableWidget):
def handle_filter_activated(self, tree: QtWidgets.QTreeView):
header = tree.header()
filters = []
for i in range(header.count()):
text: str = header.filterText(i)
if text:
filters.append((i, text))
proxy = tree.model()
proxy.filters = filters
def _init_tree_results(self, tree: QtWidgets.QTreeView, model: QtGui.QStandardItemModel) -> None:
header = FilterHeader(tree)
self.proxy_tr_results = HumanProxyModel(self)
tree.setHeader(header)
tree.setModel(model)
self.proxy_tr_results.setSourceModel(model)
tree.setModel(self.proxy_tr_results)
header.set_filter_boxes(model.columnCount())
header.filter_activated.connect(lambda: self.handle_filter_activated(tree))
##
tree.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
tree.setColumnHidden(1, True)
tree.setColumnWidth(2, 150)
@@ -224,6 +252,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
def _init_tree_results_model(self, model: QtGui.QStandardItemModel) -> None:
model.setColumnCount(7)
model.setRowCount(0)
model.setHorizontalHeaderLabels(["#", "obj", "Artist", "Title", "Album", "Duration", "Quality"])
def _init_tree_queue(self, tree: QtWidgets.QTableWidget):
tree.setColumnHidden(1, True)
tree.setColumnWidth(2, 200)
@@ -357,7 +390,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.queue_download_media(queue_dl_item)
def search_populate_results(self, query: str, type_media: SearchTypes):
self.tr_results.clear()
self.model_tr_results.removeRows(0, self.model_tr_results.rowCount())
results: [ResultItem] = self.search(query, [type_media])
@@ -365,20 +398,22 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def populate_tree_results(self, results: [ResultItem], parent: QtWidgets.QTreeWidgetItem = None):
if not parent:
self.tr_results.clear()
self.model_tr_results.removeRows(0, self.model_tr_results.rowCount())
# Count how many digits the list length has,
count_digits: int = int(math.log10(len(results) if results else 1)) + 1
for item in results:
child = self.populate_tree_result_child(item=item, index_count_digits=count_digits)
child: tuple = self.populate_tree_result_child(item=item, index_count_digits=count_digits)
if parent:
parent.addChild(child)
else:
self.s_tr_results_add_top_level_item.emit(child)
def populate_tree_result_child(self, item: [Track | Video | Mix | Album | Playlist], index_count_digits: int):
def populate_tree_result_child(
self, item: [Track | Video | Mix | Album | Playlist], index_count_digits: int
) -> Sequence[QStandardItem]:
duration: str = ""
# TODO: Duration needs to be calculated later to properly fill with zeros.
@@ -391,26 +426,30 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
index: str = str(item.position + 1).zfill(index_count_digits)
# Populate child
child: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
child.setText(0, index)
set_results_media(child, item.obj)
child.setText(2, item.artist)
child.setText(3, item.title)
child.setText(4, item.album)
child.setText(5, duration)
child.setText(6, item.quality)
child_index: QtGui.QStandardItem = QtGui.QStandardItem(index)
# TODO: Move to own method
child_obj: QtGui.QStandardItem = QtGui.QStandardItem()
child_obj.setData(item.obj, QtCore.Qt.ItemDataRole.UserRole)
# set_results_media(child, item.obj)
child_artist: QtGui.QStandardItem = QtGui.QStandardItem(item.artist)
child_title: QtGui.QStandardItem = QtGui.QStandardItem(item.title)
child_album: QtGui.QStandardItem = QtGui.QStandardItem(item.album)
child_duration: QtGui.QStandardItem = QtGui.QStandardItem(duration)
child_quality: QtGui.QStandardItem = QtGui.QStandardItem(item.quality)
if isinstance(item.obj, Mix | Playlist | Album | Artist):
# Add a disabled dummy child, so expansion arrow will appear. This Child will be replaced on expansion.
child_dummy: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
child_dummy: QtGui.QStandardItem = QtGui.QStandardItem()
child_dummy.setDisabled(True)
child.addChild(child_dummy)
child_dummy.setEnabled(False)
child_index.appendRow(child_dummy)
return child
return child_index, child_obj, child_artist, child_title, child_album, child_duration, child_quality
def on_tr_results_add_top_level_item(self, widget_item: QtWidgets.QTreeWidgetItem):
self.tr_results.addTopLevelItem(widget_item)
def on_tr_results_add_top_level_item(self, item_child: Sequence[QtGui.QStandardItem]):
self.model_tr_results.appendRow(item_child)
def on_settings_save(self):
self.settings.save()
@@ -625,11 +664,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.a_version.triggered.connect(self.on_version)
self.a_preferences.triggered.connect(self.on_preferences)
self.a_logout.triggered.connect(self.on_logout)
self.a_updates_check.triggered.connect(lambda x: self.on_update_check(False))
self.a_updates_check.triggered.connect(lambda: self.on_update_check(False))
# Results
self.tr_results.itemExpanded.connect(self.on_tr_results_expanded)
self.tr_results.itemClicked.connect(self.on_result_item_clicked)
self.tr_results.expanded.connect(self.on_tr_results_expanded)
self.tr_results.clicked.connect(self.on_result_item_clicked)
# Download Queue
self.tr_queue_download.itemClicked.connect(self.on_queue_download_item_clicked)
@@ -678,8 +717,10 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.list_items_show_result(media_list)
self.cover_show(media_list)
def on_result_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None:
media: Track | Video | Album | Artist = get_results_media_item(item)
def on_result_item_clicked(self, index: QtCore.QModelIndex) -> None:
media: Track | Video | Album | Artist = get_results_media_item(
index, self.proxy_tr_results, self.model_tr_results
)
self.cover_show(media)
+121 -3
View File
@@ -1,8 +1,12 @@
from PySide6 import QtCore, QtWidgets
import re
from PySide6 import QtCore, QtGui, QtWidgets
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
from tidalapi.artist import Artist
from tidalapi.media import Quality
from tidal_dl_ng.helper.gui import HumanProxyModel
def get_table_data(
item: QtWidgets.QTreeWidgetItem, column: int
@@ -18,8 +22,12 @@ def get_table_text(item: QtWidgets.QTreeWidgetItem, column: int) -> str:
return result
def get_results_media_item(item: QtWidgets.QTreeWidgetItem) -> Track | Video | Album | Artist | Playlist | Mix:
result: Track | Video | Album | Artist | Playlist | Mix = get_table_data(item, 1)
def get_results_media_item(
index: QtCore.QModelIndex, proxy: HumanProxyModel, model: QtGui.QStandardItemModel
) -> Track | Video | Album | Artist | Playlist | Mix:
# Switch column to "obj" column and map proxy data to our model.
item: QtGui.QStandardItem = model.itemFromIndex(proxy.mapToSource(index.siblingAtColumn(1)))
result: Track | Video | Album | Artist = item.data(QtCore.Qt.ItemDataRole.UserRole)
return result
@@ -66,3 +74,113 @@ def set_queue_download_media(
item: QtWidgets.QTreeWidgetItem, media: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist
):
set_table_data(item, media, 1)
class FilterHeader(QtWidgets.QHeaderView):
filter_activated = QtCore.Signal()
def __init__(self, parent):
super().__init__(QtCore.Qt.Horizontal, parent)
self._editors = []
self._padding = 4
self.setStretchLastSection(True)
self.setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.setSortIndicatorShown(False)
self.setSectionsMovable(True)
self.sectionResized.connect(self.adjust_positions)
parent.horizontalScrollBar().valueChanged.connect(self.adjust_positions)
def set_filter_boxes(self, count):
while self._editors:
editor = self._editors.pop()
editor.deleteLater()
for _ in range(count):
editor = QtWidgets.QLineEdit(self.parent())
editor.setPlaceholderText("Filter")
editor.setClearButtonEnabled(True)
editor.returnPressed.connect(self.filter_activated.emit)
self._editors.append(editor)
self.adjust_positions()
def sizeHint(self):
size = super().sizeHint()
if self._editors:
height = self._editors[0].sizeHint().height()
size.setHeight(size.height() + height + self._padding)
return size
def updateGeometries(self):
if self._editors:
height = self._editors[0].sizeHint().height()
self.setViewportMargins(0, 0, 0, height + self._padding)
else:
self.setViewportMargins(0, 0, 0, 0)
super().updateGeometries()
self.adjust_positions()
def adjust_positions(self):
for index, editor in enumerate(self._editors):
height = editor.sizeHint().height()
editor.move(self.sectionPosition(index) - self.offset() + 2, height + (self._padding // 2))
editor.resize(self.sectionSize(index), height)
def filter_text(self, index) -> str:
if 0 <= index < len(self._editors):
return self._editors[index].text()
return ""
def set_filter_text(self, index, text):
if 0 <= index < len(self._editors):
self._editors[index].setText(text)
def clear_filters(self):
for editor in self._editors:
editor.clear()
class HumanProxyModel(QtCore.QSortFilterProxyModel):
def _human_key(self, key):
parts = re.split(r"(\d*\.\d+|\d+)", key)
return tuple((e.swapcase() if i % 2 == 0 else float(e)) for i, e in enumerate(parts))
def lessThan(self, source_left, source_right):
data_left = source_left.data()
data_right = source_right.data()
if isinstance(data_left, str) and isinstance(data_right, str):
return self._human_key(data_left) < self._human_key(data_right)
return super().lessThan(source_left, source_right)
@property
def filters(self):
if not hasattr(self, "_filters"):
self._filters = []
return self._filters
@filters.setter
def filters(self, filters):
print("filters() called.")
self._filters = filters
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent) -> bool:
for i, text in self.filters:
if 0 <= i < self.columnCount():
ix = self.sourceModel().index(sourceRow, i, sourceParent)
data = ix.data()
if str(data) in text:
return False
return True
+8 -17
View File
@@ -1,7 +1,7 @@
################################################################################
## Form generated from reading UI file 'main.ui'
##
## Created by: Qt User Interface Compiler version 6.6.2
## Created by: Qt User Interface Compiler version 6.7.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
QPushButton,
QSizePolicy,
QStatusBar,
QTreeView,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
@@ -223,7 +224,7 @@ class Ui_MainWindow:
self.lv_search_result.addLayout(self.lh_search)
self.tr_results = QTreeWidget(self.w_central)
self.tr_results = QTreeView(self.w_central)
self.tr_results.setObjectName("tr_results")
self.tr_results.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.tr_results.setProperty("showDropIndicator", False)
@@ -232,8 +233,6 @@ class Ui_MainWindow:
self.tr_results.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.tr_results.setIndentation(10)
self.tr_results.setSortingEnabled(True)
self.tr_results.header().setProperty("showSortIndicator", True)
self.tr_results.header().setStretchLastSection(False)
self.lv_search_result.addWidget(self.tr_results)
@@ -572,22 +571,14 @@ class Ui_MainWindow:
self.pb_reload_user_lists.setText(QCoreApplication.translate("MainWindow", "Reload", None))
self.pb_download_list.setText(QCoreApplication.translate("MainWindow", "Download List", None))
___qtreewidgetitem4 = self.tr_results.headerItem()
___qtreewidgetitem4.setText(6, QCoreApplication.translate("MainWindow", "Quality", None))
___qtreewidgetitem4.setText(5, QCoreApplication.translate("MainWindow", "Duration", None))
___qtreewidgetitem4.setText(4, QCoreApplication.translate("MainWindow", "Album", None))
___qtreewidgetitem4.setText(3, QCoreApplication.translate("MainWindow", "Title", None))
___qtreewidgetitem4.setText(2, QCoreApplication.translate("MainWindow", "Artist", None))
___qtreewidgetitem4.setText(1, QCoreApplication.translate("MainWindow", "obj", None))
___qtreewidgetitem4.setText(0, QCoreApplication.translate("MainWindow", "#", None))
self.te_debug.setPlaceholderText(QCoreApplication.translate("MainWindow", "Logs...", None))
self.l_pm_cover.setText("")
self.l_h_queue_download.setText(QCoreApplication.translate("MainWindow", "Download Queue", None))
___qtreewidgetitem5 = self.tr_queue_download.headerItem()
___qtreewidgetitem5.setText(4, QCoreApplication.translate("MainWindow", "Quality", None))
___qtreewidgetitem5.setText(3, QCoreApplication.translate("MainWindow", "Type", None))
___qtreewidgetitem5.setText(2, QCoreApplication.translate("MainWindow", "Name", None))
___qtreewidgetitem5.setText(1, QCoreApplication.translate("MainWindow", "obj", None))
___qtreewidgetitem4 = self.tr_queue_download.headerItem()
___qtreewidgetitem4.setText(4, QCoreApplication.translate("MainWindow", "Quality", None))
___qtreewidgetitem4.setText(3, QCoreApplication.translate("MainWindow", "Type", None))
___qtreewidgetitem4.setText(2, QCoreApplication.translate("MainWindow", "Name", None))
___qtreewidgetitem4.setText(1, QCoreApplication.translate("MainWindow", "obj", None))
self.pb_queue_download_remove.setText(QCoreApplication.translate("MainWindow", "Remove", None))
self.pb_queue_download_clear_finished.setText(QCoreApplication.translate("MainWindow", "Clear Finished", None))
self.pb_queue_download_clear_all.setText(QCoreApplication.translate("MainWindow", "Clear All", None))
+1 -42
View File
@@ -294,7 +294,7 @@
</layout>
</item>
<item>
<widget class="QTreeWidget" name="tr_results">
<widget class="QTreeView" name="tr_results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
@@ -316,47 +316,6 @@
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="headerShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>#</string>
</property>
</column>
<column>
<property name="text">
<string>obj</string>
</property>
</column>
<column>
<property name="text">
<string>Artist</string>
</property>
</column>
<column>
<property name="text">
<string>Title</string>
</property>
</column>
<column>
<property name="text">
<string>Album</string>
</property>
</column>
<column>
<property name="text">
<string>Duration</string>
</property>
</column>
<column>
<property name="text">
<string>Quality</string>
</property>
</column>
</widget>
</item>
<item>