diff --git a/components/MusicTable.py b/components/MusicTable.py index b7941c7..64c31bb 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -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""" diff --git a/components/PlaylistsPane.py b/components/PlaylistsPane.py index f95f4cc..88af444 100644 --- a/components/PlaylistsPane.py +++ b/components/PlaylistsPane.py @@ -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): diff --git a/components/QuestionBoxDetails.py b/components/QuestionBoxDetails.py index bac6888..a426154 100644 --- a/components/QuestionBoxDetails.py +++ b/components/QuestionBoxDetails.py @@ -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 diff --git a/main.py b/main.py index 3b632ff..95baf22 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/requirements.txt b/requirements.txt index 94b0273..7eb738b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ appdirs pyqt5 pydub audioop-lts -pyqtdarktheme==2.1.0 +pyqtdarktheme-fork pyqtgraph scipy # pyqtdarktheme-fork; python_version < '3.11' diff --git a/sample_config.ini b/sample_config.ini index 286815b..d3a8bc7 100644 --- a/sample_config.ini +++ b/sample_config.ini @@ -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