diff --git a/components/MusicTable.py b/components/MusicTable.py index 3525dc0..0536d58 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -131,6 +131,9 @@ 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.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.horizontal_header.customContextMenuRequested.connect(self.show_header_context_menu) # dumb vertical estupido self.vertical_header: QHeaderView = self.verticalHeader() assert self.vertical_header is not None @@ -360,12 +363,40 @@ class MusicTable(QTableView): # | | # |____________________| - 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 + + + 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. + """ + try: + db_field = self.headers.db_list[column] + except IndexError: + error(f"Invalid column index: {column}") + return + + raw = self.config["table"].get("sort_order", "") + sort_list = [] + + # Parse current sort_order from config + if raw: + for item in raw.split(","): + if ":" not in item: + continue + field, dir_str = item.strip().split(":") + direction = Qt.SortOrder.AscendingOrder if dir_str == "1" else Qt.SortOrder.DescendingOrder + 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 + + # Save back to config + self.save_sort_config(sort_list) + + # Re-apply the updated sort order + self.sort_by_logical_fields() def on_sort(self): self.find_current_and_selected_bits() @@ -446,6 +477,40 @@ class MusicTable(QTableView): # | | # |____________________| + + def show_header_context_menu(self, position): + """ + Show context menu on right-click in the horizontal header. + """ + menu = QMenu() + clear_action = QAction("Clear all sorts", self) + clear_action.triggered.connect(self.clear_all_sorts) + menu.addAction(clear_action) + # Show menu at global position + global_pos = self.horizontal_header.mapToGlobal(position) + menu.exec(global_pos) + + + def clear_all_sorts(self): + """ + Clears all stored sort orders and refreshes the table view. + """ + self.config["table"]["sort_order"] = "" + with open(self.config_path, "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() + + 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 + + def get_current_header_width_ratios(self) -> list[str]: """ Get the current header widths, as ratios @@ -759,11 +824,7 @@ class MusicTable(QTableView): if self.selected_playlist_id == playlist_id[0]: # Don't reload if we clicked the same item return - # self.disconnect_data_changed() - # self.disconnect_layout_changed() - # self.save_scroll_position(self.current_playlist_id) self.model2.clear() - # self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers()) self.model2.setHorizontalHeaderLabels(self.headers.db_list) fields = ", ".join(self.headers.db_list) search_clause = ( @@ -834,30 +895,15 @@ 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.current_playlist_id = self.selected_playlist_id self.model2.layoutChanged.emit() # emits a signal that the view should be updated - - # reloading the model destroys and makes new indexes - # so we look for the new index of the current song on load - # current_song_filepath = self.get_current_song_filepath() - # debug(f"load_music_table() | current filepath: {current_song_filepath}") - # for row in range(self.model2.rowCount()): - # real_index = self.model2.index( - # row, self.headers.db_list.index("filepath") - # ) - # if real_index.data() == current_song_filepath: - # self.current_song_qmodel_index = real_index - db_name: str = self.config.get("settings", "db").split("/").pop() db_filename = self.config.get("settings", "db") self.playlistStatsSignal.emit(f"Songs: {self.model2.rowCount()} | {db_name} | {db_filename}") self.loadMusicTableSignal.emit() - # self.connect_data_changed() - # self.connect_layout_changed() - # set the current song and such self.find_current_and_selected_bits() self.jump_to_current_song() - # self.restore_scroll_position() def find_current_and_selected_bits(self): """ @@ -900,8 +946,6 @@ class MusicTable(QTableView): Sorts the data in QTableView (self) by multiple columns as defined in config.ini """ - # self.disconnect_data_changed() # not needed? - # self.disconnect_layout_changed() # not needed? self.horizontal_header.sortIndicatorChanged.disconnect() sort_orders = [] config_sort_orders: list[int] = [ @@ -926,27 +970,58 @@ class MusicTable(QTableView): # maybe not a huge deal for a small music application...? # `len(config_sort_orders)` number of SELECTs self.on_sort() - # self.connect_data_changed() # not needed? - # self.connect_layout_changed() # not needed? self.model2.layoutChanged.emit() - def save_scroll_position(self, playlist_id: int | None): - """Save the current scroll position of the table""" - # FIXME: does not work - except i'm using jump_to_current_song as a - # stand in for scroll position features - scroll_position = self.verticalScrollBar().value() - self.playlist_scroll_positions[playlist_id] = scroll_position - debug(f'save scroll position: {playlist_id}:{scroll_position}') + def save_sort_config(self, fields: list[tuple[str, Qt.SortOrder]]): + """ + 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) + self.config["table"]["sort_order"] = raw + with open(self.config_path, "w") as f: + self.config.write(f) - def restore_scroll_position(self): - """Set the scroll position to the given value""" - # FIXME: does not work - except i'm using jump_to_current_song as a - # stand in for scroll position features - if self.current_playlist_id in self.playlist_scroll_positions: - scroll_position = self.playlist_scroll_positions[self.current_playlist_id] - # self.restore_scroll_position(scroll_position) - self.verticalScrollBar().setValue(scroll_position) - debug(f'restore scroll position: {scroll_position}') + def get_sort_config(self) -> list[tuple[int, Qt.SortOrder]]: + """ + Returns a list of (column_index, Qt.SortOrder) tuples based on config and headers + """ + sort_config = [] + raw_sort = self.config["table"].get("sort_order", "") + if not raw_sort: + return [] + + try: + sort_fields = [x.strip() for x in raw_sort.split(",")] + for entry in sort_fields: + if ":" not in entry: + 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 + + # 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}") + 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 + + sort_config = self.get_sort_config() + + # Sort in reverse order (primary sort last) + for col_index, order in reversed(sort_config): + self.sortByColumn(col_index, order) + + self.model2.layoutChanged.emit() + self.on_sort() def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]: """Scans a directories for files""" diff --git a/main.py b/main.py index f7cd507..3b632ff 100644 --- a/main.py +++ b/main.py @@ -706,9 +706,12 @@ if __name__ == "__main__": # Start the app app = QApplication(sys.argv) clipboard = app.clipboard() - # Dark theme >:3 - qdarktheme.setup_theme() - # qdarktheme.setup_theme("auto") # this is supposed to work but doesnt + try: + # Dark theme >:3 + qdarktheme.setup_theme() + # qdarktheme.setup_theme("auto") # this is supposed to work but doesnt + except Exception: + pass # Show the UI ui = ApplicationWindow(clipboard) # window size diff --git a/sample_config.ini b/sample_config.ini index f8106b7..286815b 100644 --- a/sample_config.ini +++ b/sample_config.ini @@ -13,5 +13,6 @@ window_size=1152,894 # 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