This commit is contained in:
billy@pom 2026-01-07 07:33:20 -05:00
parent 09a6126687
commit 2ec1f0b7a7
6 changed files with 187 additions and 57 deletions

View File

@ -60,7 +60,7 @@ from configparser import ConfigParser
class MusicTable(QTableView):
playlistStatsSignal: pyqtSignal = pyqtSignal(str)
loadMusicTableSignal: pyqtSignal = pyqtSignal()
sortSignal: pyqtSignal = pyqtSignal()
# sortSignal: pyqtSignal = pyqtSignal()
playPauseSignal: pyqtSignal = pyqtSignal()
playSignal: pyqtSignal = pyqtSignal(str)
enterKey: pyqtSignal = pyqtSignal()
@ -131,7 +131,8 @@ class MusicTable(QTableView):
self.horizontal_header: QHeaderView = self.horizontalHeader()
assert self.horizontal_header is not None # i hate look at linting errors
self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive)
self.horizontal_header.sortIndicatorChanged.connect(self.on_user_sort_change)
# self.horizontal_header.sortIndicatorChanged.connect(self.on_user_sort_change)
self.horizontal_header.sectionClicked.connect(self.on_header_clicked)
self.horizontal_header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.horizontal_header.customContextMenuRequested.connect(self.show_header_context_menu)
# dumb vertical estupido
@ -362,20 +363,36 @@ class MusicTable(QTableView):
# | |
# |____________________|
def on_sort(self):
debug('on_sort')
self.find_current_and_selected_bits()
debug('on_sort(): done finding current and selected bits')
self.jump_to_selected_song()
# self.sortSignal.emit()
# debug('sortSignal emitted')
def on_header_clicked(self, logicalIndex: QModelIndex):
debug('on_header_clicked()')
debug(f'- logicalIndex: {logicalIndex}')
self.on_sort()
def on_user_sort_change(self, column: int, order: Qt.SortOrder):
"""
Called when user clicks a column header to sort.
Updates the multi-sort config based on interaction.
"""
debug('on_user_sort_change()')
debug(f'- args: column = {column}')
debug(f'- args: order = {order}')
try:
db_field = self.headers.db_list[column]
debug(f'on_user_sort_change() - db_field = {db_field}')
except IndexError:
error(f"Invalid column index: {column}")
error(f"on_user_sort_change() - Invalid column index: {column}")
return
raw = self.config["table"].get("sort_order", "")
raw = self.config["table"].get(option="sort_order", fallback="")
debug(f'on_user_sort_change() - raw = {raw}')
sort_list = []
# Parse current sort_order from config
@ -384,12 +401,20 @@ class MusicTable(QTableView):
if ":" not in item:
continue
field, dir_str = item.strip().split(":")
direction = Qt.SortOrder.AscendingOrder if dir_str == "1" else Qt.SortOrder.DescendingOrder
direction = int(dir_str)
if dir_str == "1":
direction = Qt.SortOrder.AscendingOrder
elif dir_str == "2":
direction = Qt.SortOrder.DescendingOrder
else:
direction = None
sort_list.append((field, direction))
# Update or insert the new sort field at the end (highest priority)
sort_list = [(f, d) for f, d in sort_list if f != db_field] # remove if exists
sort_list.append((db_field, order)) # add with latest order
# sort_list.append((db_field, order))
debug(f'on_user_sort_change() - sort list updated (field, direction): {sort_list}')
# Save back to config
self.save_sort_config(sort_list)
@ -397,10 +422,6 @@ class MusicTable(QTableView):
# Re-apply the updated sort order
self.sort_by_logical_fields()
def on_sort(self):
self.find_current_and_selected_bits()
self.jump_to_selected_song()
self.sortSignal.emit()
def on_cell_clicked(self, index):
"""
@ -414,8 +435,8 @@ class MusicTable(QTableView):
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell"""
debug(f"on_cell_data_changed(topLeft={topLeft}, bottomRight={bottomRight})")
# if isinstance(self.model2, QStandardItemModel):
# debug("on_cell_data_changed")
# get the ID of the row that was edited
id_index = self.model2.index(topLeft.row(), 0)
# get the db song_id from the row
@ -481,33 +502,114 @@ class MusicTable(QTableView):
"""
Show context menu on right-click in the horizontal header.
"""
# NOTE:
# Jus a side note, creating a nemu on the heap every time the user right clicks
# is going to eat up a lot of memory in the long term. It's much more efficient
# to create it once and call popup on it when needed
# NOTE:
# i do not care and i did not ask
# TODO: right click menu for sort by asc/desc, per column
debug(f'show_header_context_menu(position={position})')
logical_index = self.horizontal_header.logicalIndexAt(position)
debug(f'- logicalIndexAt(position) = {logical_index}')
column_name: str = self.headers.db_list[logical_index]
debug(f'- column_name = {column_name}')
menu = QMenu()
clear_action = QAction("Clear all sorts", self)
clear_action.triggered.connect(self.clear_all_sorts)
sort_asc_action = QAction("Sort ASC", self)
sort_asc_action.triggered.connect(
lambda checked=False: self.mark_column_sort(
logical_index,
column_name,
Qt.SortOrder.AscendingOrder
)
)
sort_desc_action = QAction("Sort DESC", self)
sort_desc_action.triggered.connect(
lambda checked=False: self.mark_column_sort(
logical_index,
column_name,
Qt.SortOrder.DescendingOrder
)
)
menu.addAction(clear_action)
menu.addAction(sort_asc_action)
menu.addAction(sort_desc_action)
# Show menu at global position
global_pos = self.horizontal_header.mapToGlobal(position)
menu.exec(global_pos)
def mark_column_sort(self, logical_index, column_name, sort_order):
"""
"""
debug(f'mark_column_sort(logical_index={logical_index}, column_name={column_name}, sort_order={sort_order})')
# TODO: implement this
# write field name and the sort direction into config
# then run sort by logical fields or something to sort the config as is
raw = self.config["table"].get(option="sort_order", fallback="")
debug(f'- self.config[table][sort_order] = {raw}')
sort_list = []
# newest sort at the beginning bcus we sort in reverse order
sort_list.append((column_name, sort_order))
# Parse current sort_order from config
if raw:
for item in raw.split(","):
if ":" not in item:
continue
field, dir_str = item.strip().split(":")
if field == column_name:
# skip newest sort
continue
direction = int(dir_str)
if dir_str == "0":
direction = Qt.SortOrder.AscendingOrder
elif dir_str == "1":
direction = Qt.SortOrder.DescendingOrder
else:
direction = None
sort_list.append((field, direction))
debug(f'- sort list updated (field, direction): {sort_list}')
# Save back to config
self.save_sort_config(sort_list)
# Re-apply the updated sort order
self.sort_by_logical_fields()
def clear_all_sorts(self):
"""
Clears all stored sort orders and refreshes the table view.
"""
debug('clear_all_sorts()')
self.config["table"]["sort_order"] = ""
with open(self.config_path, "w") as f:
with open(self.cfg_file, "w") as f:
self.config.write(f)
# Clear sort visually
self.horizontal_header.setSortIndicator(-1, Qt.SortOrder.AscendingOrder)
# Reload data if necessary, or just reapply sorting
self.sort_by_logical_fields()
debug('clear_all_sorts() - setSortIndicator at index -1 to AscendingOrder')
def find_qmodel_index_by_value(self, model, column: int, value) -> QModelIndex:
for row in range(model.rowCount()):
index = model.index(row, column)
if index.data() == value:
return index
return QModelIndex() # Invalid index if not found
"""
Returns the QModelIndex of a specified model, based on column number & field value
Returns index at (0,0) if params are invalid/out of bounds
Returns empty QModelIndex object if model is invalid
"""
debug(f'find_qmodel_index_by_value(model={model}, column={column}, value={value})')
if not model:
return QModelIndex()
if column > model.columnCount():
return model.index(0,0)
if value:
for row in range(model.rowCount()):
index = model.index(row, column)
if index.data() == value:
return index
return model.index(0,0)
def get_current_header_width_ratios(self) -> list[str]:
@ -518,10 +620,11 @@ class MusicTable(QTableView):
column_ratios = []
for i in range(self.model2.columnCount()):
# for i in range(self.model2.columnCount() - 1):
debug(f'column count: {self.model2.columnCount()}')
column_width = self.columnWidth(i)
ratio = column_width / total_table_width
column_ratios.append(str(round(ratio, 4)))
# debug(f'get_current_header_width_ratios = {column_ratios}')
debug(f'get_current_header_width_ratios = {column_ratios}')
return column_ratios
def save_header_ratios(self):
@ -556,8 +659,8 @@ class MusicTable(QTableView):
for ratio in column_ratios:
column_widths.append(float(ratio) * total_table_width)
if isinstance(column_widths, list):
# for i in range(self.model2.columnCount() - 1):
for i in range(self.model2.columnCount()):
for i in range(self.model2.columnCount()-1):
# load all except the last ratio - bcus it gets autostretched
self.setColumnWidth(i, int(column_widths[i]))
@ -688,9 +791,9 @@ class MusicTable(QTableView):
self.scrollTo(proxy_index)
self.selectRow(proxy_index.row())
except Exception as e:
debug(f'MusicTable.py | jump_to_current_song() | {self.current_song_filepath}')
debug(f'MusicTable.py | jump_to_current_song() | {self.current_song_qmodel_index}')
debug(f'MusicTable.py | jump_to_current_song() | Could not find current song in current table buffer - {e}')
debug(f'jump_to_current_song() | {self.current_song_filepath}')
debug(f'jump_to_current_song() | {self.current_song_qmodel_index}')
debug(f'jump_to_current_song() | Could not find current song in current table buffer - {e}')
def open_directory(self):
"""Opens the containing directory of the currently selected song, in the system file manager"""
@ -894,7 +997,7 @@ class MusicTable(QTableView):
# cache the data
# self.data_cache[self.selected_playlist_id] = data
self.populate_model(data)
self.sort_table_by_multiple_columns()
# self.sort_table_by_multiple_columns()
self.current_playlist_id = self.selected_playlist_id
self.model2.layoutChanged.emit() # emits a signal that the view should be updated
db_name: str = self.config.get("settings", "db").split("/").pop()
@ -909,9 +1012,14 @@ class MusicTable(QTableView):
When data changes in the model view, its nice to re-grab the current song index information
might as well get the selected song too i guess? though nothing should be selected when reloading the table data
"""
debug('find_current_and_selected_bits')
search_col_num = self.headers.db_list.index("filepath")
debug('find_current_and_selected_bits - searching for column number of `filepath`')
debug(f'find_current_and_selected_bits - search_col_num: {search_col_num}')
selected_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.selected_song_filepath)
debug(f'find_current_and_selected_bits - selected_qmodel_index: {selected_qmodel_index}')
current_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.current_song_filepath)
debug(f'find_current_and_selected_bits - current_qmodel_index: {current_qmodel_index}')
# Update the 2 proxy QModelIndexes that we track
self.set_selected_song_qmodel_index(selected_qmodel_index)
self.set_current_song_qmodel_index(current_qmodel_index)
@ -937,19 +1045,22 @@ class MusicTable(QTableView):
for item in items:
item.setData(id, Qt.ItemDataRole.UserRole)
self.model2.appendRow(items)
self.proxymodel.setSourceModel(self.model2)
self.setModel(self.proxymodel)
# self.proxymodel.setSourceModel(self.model2)
# self.setModel(self.proxymodel)
def sort_table_by_multiple_columns(self):
"""
Sorts the data in QTableView (self) by multiple columns
as defined in config.ini
"""
self.horizontal_header.sortIndicatorChanged.disconnect()
# NOTE: STOP USING THIS
debug('sort_table_by_multiple_columns')
# self.horizontal_header.sortIndicatorChanged.disconnect()
sort_orders = []
config_sort_orders: list[int] = [
int(x) for x in self.config["table"]["sort_orders"].split(",")
]
debug(f'sort_table_by_multiple_columns() - config_sort_orders = {config_sort_orders}')
for order in config_sort_orders:
if order == 0:
sort_orders.append(None)
@ -957,6 +1068,7 @@ class MusicTable(QTableView):
sort_orders.append(Qt.SortOrder.AscendingOrder)
elif order == 2:
sort_orders.append(Qt.SortOrder.DescendingOrder)
debug(f'sort_table_by_multiple_columns() - determined sort_orders = {sort_orders}')
# QTableView sorts need to happen in reverse order
# The primary sort column is the last column sorted.
for i in reversed(range(len(sort_orders))):
@ -968,7 +1080,8 @@ class MusicTable(QTableView):
# and will do this for as many sorts that are needed
# maybe not a huge deal for a small music application...?
# `len(config_sort_orders)` number of SELECTs
self.on_sort()
# self.on_sort()
# self.horizontal_header.sortIndicatorChanged.connect(self.on_user_sort_change)
self.model2.layoutChanged.emit()
def save_sort_config(self, fields: list[tuple[str, Qt.SortOrder]]):
@ -976,15 +1089,22 @@ class MusicTable(QTableView):
Save sort config to ini file.
fields: List of tuples like [('artist', Qt.AscendingOrder), ('album', Qt.DescendingOrder)]
"""
raw = ",".join(f"{field}:{1 if order == Qt.SortOrder.AscendingOrder else 2}" for field, order in fields)
debug('save_sort_config()')
# raw = ",".join(f"{field}:{1 if order == Qt.SortOrder.AscendingOrder else 2}" for field, order in fields)
raw = ",".join(f"{field}:{order}" for field, order in fields)
debug(f'- saving config [table][sort_order] = {raw}')
self.config["table"]["sort_order"] = raw
with open(self.config_path, "w") as f:
debug(f'- self.cfg_file = {self.cfg_file}')
with open(self.cfg_file, "w") as f:
debug('- writing to config file')
self.config.write(f)
debug(f'- self.config["table"]["sort_order"] = {self.config["table"]["sort_order"]}')
def get_sort_config(self) -> list[tuple[int, Qt.SortOrder]]:
"""
Returns a list of (column_index, Qt.SortOrder) tuples based on config and headers
"""
debug('get_sort_config()')
sort_config = []
raw_sort = self.config["table"].get("sort_order", "")
if not raw_sort:
@ -997,30 +1117,37 @@ class MusicTable(QTableView):
continue
db_field, order_str = entry.split(":")
db_field = db_field.strip()
order = Qt.SortOrder.AscendingOrder if order_str.strip() == "1" else Qt.SortOrder.DescendingOrder
# order = Qt.SortOrder.AscendingOrder if order_str.strip() == "1" else Qt.SortOrder.DescendingOrder
order = int(order_str.strip())
# Get the index of the column using your headers class
if db_field in self.headers.db:
col_index = self.headers.db_list.index(db_field)
sort_config.append((col_index, order))
except Exception as e:
error(f"Failed to parse sort config: {e}")
error(f"- Failed to parse sort config: {e}")
return sort_config
def sort_by_logical_fields(self):
"""
Sorts the table using logical field names defined in config.
"""
self.horizontal_header.sortIndicatorChanged.disconnect() # Prevent feedback loop
debug('sort_by_logical_fields()')
sort_config = self.get_sort_config()
debug(f'- retrieved sort_config: {sort_config}')
# Sort in reverse order (primary sort last)
for col_index, order in reversed(sort_config):
self.sortByColumn(col_index, order)
if order == 0:
new_order = Qt.SortOrder.AscendingOrder
elif order == 1:
new_order = Qt.SortOrder.DescendingOrder
else:
continue
self.sortByColumn(col_index, new_order)
debug(f'- sorted column index {col_index} by {new_order}')
self.model2.layoutChanged.emit()
self.on_sort()
debug('- emitted layoutChanged signal')
# self.on_sort()
def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]:
"""Scans a directories for files"""
@ -1114,10 +1241,14 @@ class MusicTable(QTableView):
converts to model2 index
stores it
"""
debug('set_current_song_qmodel_index')
debug(f'got index: {index}')
if index is None:
index = self.currentIndex()
debug(f'new index: {index}')
# map proxy (sortable) model to the original model (used for interactions)
real_index: QModelIndex = self.proxymodel.mapToSource(index)
debug(f'map index: {real_index}')
self.current_song_qmodel_index = real_index
def set_selected_song_qmodel_index(self, index: QModelIndex | None = None):
@ -1126,10 +1257,14 @@ class MusicTable(QTableView):
converts to model2 index
stores it
"""
debug('set_selected_song_qmodel_index')
debug(f'got index: {index}')
if index is None:
index = self.currentIndex()
debug(f'new index: {index}')
# map proxy (sortable) model to the original model (used for interactions)
real_index: QModelIndex = self.proxymodel.mapToSource(index)
debug(f'map index: {real_index}')
self.selected_song_qmodel_index = real_index
def set_search_string(self, text: str):
@ -1161,12 +1296,12 @@ class MusicTable(QTableView):
except Exception:
pass
def disconnect_layout_changed(self):
"""Disconnects the layoutChanged signal from QTableView.model"""
try:
self.model2.layoutChanged.disconnect()
except Exception:
pass
# def disconnect_layout_changed(self):
# """Disconnects the layoutChanged signal from QTableView.model"""
# try:
# self.model2.layoutChanged.disconnect()
# except Exception:
# pass
# def connect_layout_changed(self):
# """Connects the layoutChanged signal from QTableView.model"""

View File

@ -44,8 +44,8 @@ class PlaylistsPane(QTreeWidget):
self.currentItemChanged.connect(self.playlist_clicked)
self.playlist_db_id_choice: int | None = None
font: QFont = QFont()
font.setPointSize(16)
font.setBold(True)
font.setPointSize(14)
font.setBold(False)
self.setFont(font)
def reload_playlists(self, progress_callback=None):

View File

@ -1,4 +1,3 @@
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import (
QAbstractScrollArea,
QDialog,
@ -11,9 +10,7 @@ from PyQt5.QtWidgets import (
QLabel,
QPushButton,
)
from PyQt5.QtGui import QFont
from components.ErrorDialog import ErrorDialog
from logging import debug, error
from logging import error
from pprint import pformat

View File

@ -5,7 +5,6 @@ import typing
import DBA
import qdarktheme
from PyQt5.QtGui import QFontDatabase
from PyQt5 import QtCore
from subprocess import run
# from pyqtgraph import mkBrush
@ -151,7 +150,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.playbackSlider.sliderReleased.connect(lambda: self.player.setPosition(self.playbackSlider.value())) # sliderReleased works better than sliderMoved
self.volumeSlider.sliderMoved[int].connect(lambda: self.on_volume_changed())
self.speedSlider.sliderMoved.connect(lambda: self.on_speed_changed(self.speedSlider.value()))
# self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1))
#self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1))
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
self.previousButton.clicked.connect(self.on_prev_clicked)
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
@ -709,7 +708,7 @@ if __name__ == "__main__":
try:
# Dark theme >:3
qdarktheme.setup_theme()
# qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
#qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
except Exception:
pass
# Show the UI

View File

@ -4,7 +4,7 @@ appdirs
pyqt5
pydub
audioop-lts
pyqtdarktheme==2.1.0
pyqtdarktheme-fork
pyqtgraph
scipy
# pyqtdarktheme-fork; python_version < '3.11'

View File

@ -12,7 +12,6 @@ window_size=1152,894
[table]
# Music table user options
columns = title,artist,album,track_number,genre,album_date,codec,length_seconds,filepath
column_widths = 181,116,222,76,74,72,287,150,100
column_ratios = 0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01
# 0 = no sort, 1 = ascending, 2 = descending
sort_orders = 0,1,1,1,0,0,0,0,0