diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py
index aaeaed6..d8c2b4c 100644
--- a/tidal_dl_ng/gui.py
+++ b/tidal_dl_ng/gui.py
@@ -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)
diff --git a/tidal_dl_ng/helper/gui.py b/tidal_dl_ng/helper/gui.py
index 610be80..834d9b6 100644
--- a/tidal_dl_ng/helper/gui.py
+++ b/tidal_dl_ng/helper/gui.py
@@ -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
diff --git a/tidal_dl_ng/ui/main.py b/tidal_dl_ng/ui/main.py
index 0a999c8..9c1525d 100644
--- a/tidal_dl_ng/ui/main.py
+++ b/tidal_dl_ng/ui/main.py
@@ -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))
diff --git a/tidal_dl_ng/ui/main.ui b/tidal_dl_ng/ui/main.ui
index a62d77b..0ad3693 100644
--- a/tidal_dl_ng/ui/main.ui
+++ b/tidal_dl_ng/ui/main.ui
@@ -294,7 +294,7 @@
-
-
+
QAbstractItemView::NoEditTriggers
@@ -316,47 +316,6 @@
true
-
- true
-
-
- false
-
-
-
- #
-
-
-
-
- obj
-
-
-
-
- Artist
-
-
-
-
- Title
-
-
-
-
- Album
-
-
-
-
- Duration
-
-
-
-
- Quality
-
-
-