sorting yay

This commit is contained in:
tsi-billypom 2024-09-25 16:14:49 -04:00
parent 93a1ae61ee
commit 52bce599e3
2 changed files with 111 additions and 67 deletions

View File

@ -13,7 +13,6 @@ from PyQt5.QtWidgets import (
QAction, QAction,
QHeaderView, QHeaderView,
QMenu, QMenu,
QSizePolicy,
QTableView, QTableView,
QShortcut, QShortcut,
QMessageBox, QMessageBox,
@ -21,7 +20,6 @@ from PyQt5.QtWidgets import (
) )
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, Qt,
QAbstractItemModel,
QModelIndex, QModelIndex,
QThreadPool, QThreadPool,
pyqtSignal, pyqtSignal,
@ -63,10 +61,8 @@ class MusicTable(QTableView):
def __init__(self, parent=None, application_window=None): def __init__(self, parent=None, application_window=None):
super().__init__(parent) super().__init__(parent)
self.application_window = application_window self.application_window = application_window
self.model2: QStandardItemModel = QStandardItemModel()
# FIXME: why does this give me pyright errors self.setModel(self.model2)
self.model = QStandardItemModel()
self.setModel(self.model)
# Config # Config
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
@ -106,14 +102,13 @@ class MusicTable(QTableView):
self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setStretchLastSection(True)
self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
self.setSortingEnabled(False) self.setSortingEnabled(False)
# self.horizontalHeader().setCascadingSectionResizes(True)
# CONNECTIONS # CONNECTIONS
self.clicked.connect(self.set_selected_song_filepath) self.clicked.connect(self.set_selected_song_filepath)
self.doubleClicked.connect(self.set_current_song_filepath) self.doubleClicked.connect(self.set_current_song_filepath)
self.enterKey.connect(self.set_current_song_filepath) self.enterKey.connect(self.set_current_song_filepath)
self.deleteKey.connect(self.delete_songs) self.deleteKey.connect(self.delete_songs)
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells self.model2.dataChanged.connect(self.on_cell_data_changed) # editing cells
self.model.layoutChanged.connect(self.restore_scroll_position) self.model2.layoutChanged.connect(self.restore_scroll_position)
self.horizontalHeader().sectionResized.connect(self.header_was_resized) self.horizontalHeader().sectionResized.connect(self.header_was_resized)
# Final actions # Final actions
self.load_music_table() self.load_music_table()
@ -122,7 +117,7 @@ class MusicTable(QTableView):
# This doesn't work inside its own function for some reason # This doesn't work inside its own function for some reason
# self.load_header_widths() # self.load_header_widths()
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
for i in range(self.model.columnCount() - 1): for i in range(self.model2.columnCount() - 1):
self.setColumnWidth(i, int(table_view_column_widths[i])) self.setColumnWidth(i, int(table_view_column_widths[i]))
def load_header_widths(self): def load_header_widths(self):
@ -130,7 +125,7 @@ class MusicTable(QTableView):
Loads the header widths from the last application close. Loads the header widths from the last application close.
""" """
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
for i in range(self.model.columnCount() - 1): for i in range(self.model2.columnCount() - 1):
self.setColumnWidth(i, int(table_view_column_widths[i])) self.setColumnWidth(i, int(table_view_column_widths[i]))
def sort_table_by_multiple_columns(self): def sort_table_by_multiple_columns(self):
@ -138,13 +133,13 @@ class MusicTable(QTableView):
Sorts the data in QTableView (self) by multiple columns Sorts the data in QTableView (self) by multiple columns
as defined in config.ini as defined in config.ini
""" """
self.setSortingEnabled(False) # Disconnect these signals to prevent unnecessary loads
self.setSortingEnabled(True) self.disconnect_data_changed()
self.disconnect_layout_changed()
sort_orders = [] sort_orders = []
config_sort_orders: list[int] = [ config_sort_orders: list[int] = [
int(x) for x in self.config["table"]["sort_orders"].split(",") int(x) for x in self.config["table"]["sort_orders"].split(",")
] ]
print(f"config sort orders = {config_sort_orders}")
for order in config_sort_orders: for order in config_sort_orders:
if order == 0: if order == 0:
sort_orders.append(None) sort_orders.append(None)
@ -153,14 +148,18 @@ class MusicTable(QTableView):
elif order == 2: elif order == 2:
sort_orders.append(Qt.SortOrder.DescendingOrder) sort_orders.append(Qt.SortOrder.DescendingOrder)
print(f"sort_orders = {sort_orders}") # NOTE:
for i, order in enumerate(sort_orders): # this is bad because sortByColumn calls a SELECT statement,
print(f"i = {i}, order = {order}") # and will do this for as many sorts that are needed
if order is not None: # maybe not a huge deal for a small music application...
print(f"sorting column {i} by {order}") for i in reversed(range(len(sort_orders))):
self.sortByColumn(i, order) if sort_orders[i] is not None:
logging.info(f"sorting column {i} by {sort_orders[i]}")
self.sortByColumn(i, sort_orders[i])
self.model.layoutChanged.emit() self.connect_data_changed()
self.connect_layout_changed()
self.model2.layoutChanged.emit()
def resizeEvent(self, e: typing.Optional[QResizeEvent]) -> None: def resizeEvent(self, e: typing.Optional[QResizeEvent]) -> None:
"""Do something when the QTableView is resized""" """Do something when the QTableView is resized"""
@ -171,7 +170,7 @@ class MusicTable(QTableView):
def header_was_resized(self, logicalIndex, oldSize, newSize): def header_was_resized(self, logicalIndex, oldSize, newSize):
"""Handles keeping headers inside the viewport""" """Handles keeping headers inside the viewport"""
# https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections # https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections
col_count = self.model.columnCount() col_count = self.model2.columnCount()
qtableview_width = self.size().width() qtableview_width = self.size().width()
sum_of_cols = self.horizontalHeader().length() sum_of_cols = self.horizontalHeader().length()
@ -221,6 +220,34 @@ class MusicTable(QTableView):
if a0 is not None: if a0 is not None:
menu.exec_(a0.globalPos()) menu.exec_(a0.globalPos())
def disconnect_data_changed(self):
"""Disconnects the dataChanged signal from QTableView.model"""
try:
self.model2.dataChanged.disconnect(self.on_cell_data_changed)
except Exception:
pass
def connect_data_changed(self):
"""Connects the dataChanged signal from QTableView.model"""
try:
self.model2.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
def disconnect_layout_changed(self):
"""Disconnects the layoutChanged signal from QTableView.model"""
try:
self.model2.layoutChanged.disconnect(self.restore_scroll_position)
except Exception:
pass
def connect_layout_changed(self):
"""Connects the layoutChanged signal from QTableView.model"""
try:
self.model2.layoutChanged.connect(self.restore_scroll_position)
except Exception:
pass
def show_id3_tags_debug_menu(self): def show_id3_tags_debug_menu(self):
"""Shows ID3 tags for a specific .mp3 file""" """Shows ID3 tags for a specific .mp3 file"""
selected_song_filepath = self.get_selected_song_filepath() selected_song_filepath = self.get_selected_song_filepath()
@ -252,19 +279,13 @@ class MusicTable(QTableView):
def remove_selected_row_indices(self): def remove_selected_row_indices(self):
"""Removes rows from the QTableView based on a list of indices""" """Removes rows from the QTableView based on a list of indices"""
selected_indices = self.get_selected_rows() selected_indices = self.get_selected_rows()
try: self.disconnect_data_changed()
self.model.dataChanged.disconnect(self.on_cell_data_changed)
except Exception:
pass
for index in selected_indices: for index in selected_indices:
try: try:
self.model.removeRow(index) self.model2.removeRow(index)
except Exception as e: except Exception as e:
logging.info(f" delete_songs() failed | {e}") logging.info(f" delete_songs() failed | {e}")
try: self.connect_data_changed()
self.model.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
def open_directory(self): def open_directory(self):
"""Opens the currently selected song in the system file manager""" """Opens the currently selected song in the system file manager"""
@ -375,23 +396,23 @@ class MusicTable(QTableView):
if not e: if not e:
return return
key = e.key() key = e.key()
if key == Qt.Key_Space: # Spacebar to play/pause if key == Qt.Key.Key_Space: # Spacebar to play/pause
self.toggle_play_pause() self.toggle_play_pause()
elif key == Qt.Key_Up: # Arrow key navigation elif key == Qt.Key.Key_Up: # Arrow key navigation
current_index = self.currentIndex() current_index = self.currentIndex()
new_index = self.model.index( new_index = self.model2.index(
current_index.row() - 1, current_index.column() current_index.row() - 1, current_index.column()
) )
if new_index.isValid(): if new_index.isValid():
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
elif key == Qt.Key_Down: # Arrow key navigation elif key == Qt.Key.Key_Down: # Arrow key navigation
current_index = self.currentIndex() current_index = self.currentIndex()
new_index = self.model.index( new_index = self.model2.index(
current_index.row() + 1, current_index.column() current_index.row() + 1, current_index.column()
) )
if new_index.isValid(): if new_index.isValid():
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
elif key in (Qt.Key_Return, Qt.Key_Enter): elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self.state() != QAbstractItemView.EditingState: if self.state() != QAbstractItemView.EditingState:
self.enterKey.emit() # Enter key detected self.enterKey.emit() # Enter key detected
else: else:
@ -407,14 +428,14 @@ class MusicTable(QTableView):
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell""" """Handles updating ID3 tags when data changes in a cell"""
logging.info("on_cell_data_changed") logging.info("on_cell_data_changed")
if isinstance(self.model, QStandardItemModel): if isinstance(self.model2, QStandardItemModel):
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always id_index = self.model2.index(topLeft.row(), 0) # ID is column 0, always
song_id = self.model.data(id_index, Qt.UserRole) song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole)
# filepath is always the last column # filepath is always the last column
filepath_column_idx = self.model.columnCount() - 1 filepath_column_idx = self.model2.columnCount() - 1
filepath_index = self.model.index(topLeft.row(), filepath_column_idx)
# exact index of the edited cell in 2d space # exact index of the edited cell in 2d space
filepath = self.model.data(filepath_index) # filepath filepath_index = self.model2.index(topLeft.row(), filepath_column_idx)
filepath = self.model2.data(filepath_index) # filepath
# update the ID3 information # update the ID3 information
user_input_data = topLeft.data() user_input_data = topLeft.data()
edited_column_name = self.database_columns[topLeft.column()] edited_column_name = self.database_columns[topLeft.column()]
@ -520,21 +541,12 @@ class MusicTable(QTableView):
If playlist_id is given, load songs in a particular playlist If playlist_id is given, load songs in a particular playlist
playlist_id is emitted from PlaylistsPane as a tuple (1,) playlist_id is emitted from PlaylistsPane as a tuple (1,)
""" """
try: self.disconnect_data_changed()
# Loading the table also causes cell data to change, technically
# so we must disconnect the dataChanged trigger before loading
# then re-enable after we are done loading
self.model.dataChanged.disconnect(self.on_cell_data_changed)
except Exception as e:
logging.info(
f"load_music_table() | could not disconnect on_cell_data_changed trigger: {e}"
)
pass
self.vertical_scroll_position = ( self.vertical_scroll_position = (
self.verticalScrollBar().value() self.verticalScrollBar().value()
) # Get my scroll position before clearing ) # Get my scroll position before clearing
self.model.clear() self.model2.clear()
self.model.setHorizontalHeaderLabels(self.table_headers) self.model2.setHorizontalHeaderLabels(self.table_headers)
if playlist_id: if playlist_id:
selected_playlist_id = playlist_id[0] selected_playlist_id = playlist_id[0]
logging.info( logging.info(
@ -563,22 +575,29 @@ class MusicTable(QTableView):
return return
# Populate the model # Populate the model
for row_data in data: for row_data in data:
print(f"row_data: {row_data}")
id, *rest_of_data = row_data id, *rest_of_data = row_data
items = [QStandardItem(str(item) if item else "") for item in rest_of_data] # handle different datatypes
self.model.appendRow(items) items = []
for item in rest_of_data:
if isinstance(item, int):
std_item = QStandardItem()
std_item.setData(item, Qt.ItemDataRole.DisplayRole)
std_item.setData(item, Qt.ItemDataRole.EditRole)
else:
std_item = QStandardItem(str(item) if item else "")
items.append(std_item)
self.model2.appendRow(items)
# store id using setData - useful for later faster db fetching # store id using setData - useful for later faster db fetching
# row = self.model.rowCount() - 1
for item in items: for item in items:
item.setData(id, Qt.UserRole) item.setData(id, Qt.ItemDataRole.UserRole)
self.model.layoutChanged.emit() # emits a signal that the view should be updated self.model2.layoutChanged.emit() # emits a signal that the view should be updated
try: try:
self.restore_scroll_position() self.restore_scroll_position()
except Exception: except Exception:
pass pass
try: self.connect_data_changed()
self.model.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
def restore_scroll_position(self) -> None: def restore_scroll_position(self) -> None:
"""Restores the scroll position""" """Restores the scroll position"""
@ -611,7 +630,7 @@ class MusicTable(QTableView):
selected_rows = self.get_selected_rows() selected_rows = self.get_selected_rows()
filepaths = [] filepaths = []
for row in selected_rows: for row in selected_rows:
idx = self.model.index(row, self.table_headers.index("path")) idx = self.model2.index(row, self.table_headers.index("path"))
filepaths.append(idx.data()) filepaths.append(idx.data())
return filepaths return filepaths
@ -630,7 +649,7 @@ class MusicTable(QTableView):
return [] return []
selected_rows = set(index.row() for index in indexes) selected_rows = set(index.row() for index in indexes)
id_list = [ id_list = [
self.model.data(self.model.index(row, 0), Qt.UserRole) self.model2.data(self.model2.index(row, 0), Qt.ItemDataRole.UserRole)
for row in selected_rows for row in selected_rows
] ]
return id_list return id_list
@ -665,3 +684,28 @@ class MusicTable(QTableView):
def load_qapp(self, qapp) -> None: def load_qapp(self, qapp) -> None:
"""Necessary for using members and methods of main application window""" """Necessary for using members and methods of main application window"""
self.qapp = qapp self.qapp = qapp
# QT Roles
# In Qt, roles are used to specify different aspects or types of data associated with each item in a model. The roles are defined in the Qt.ItemDataRole enum. The three roles you asked about - DisplayRole, EditRole, and UserRole - are particularly important. Let's break them down:
#
# Qt.ItemDataRole.DisplayRole:
# This is the default role for displaying data in views. It determines how the data appears visually in the view.
#
# Purpose: To provide the text or image that will be shown in the view.
# Example: In a table view, this is typically the text you see in each cell.
#
#
# Qt.ItemDataRole.EditRole:
# This role is used when the item's data is being edited.
#
# Purpose: To provide the data in a format suitable for editing.
# Example: For a date field, DisplayRole might show "Jan 1, 2023", but EditRole could contain a QDate object or an ISO format string "2023-01-01".
#
#
# Qt.ItemDataRole.UserRole:
# This is a special role that serves as a starting point for custom roles defined by the user.
#
# Purpose: To store additional, custom data associated with an item that doesn't fit into the predefined roles.
# Example: Storing a unique identifier, additional metadata, or any other custom data you want to associate with the item.

View File

@ -266,7 +266,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
"""Save settings when closing the application""" """Save settings when closing the application"""
# MusicTable/tableView column widths # MusicTable/tableView column widths
list_of_column_widths = [] list_of_column_widths = []
for i in range(self.tableView.model.columnCount()): for i in range(self.tableView.model2.columnCount()):
list_of_column_widths.append(str(self.tableView.columnWidth(i))) list_of_column_widths.append(str(self.tableView.columnWidth(i)))
column_widths_as_string = ",".join(list_of_column_widths) column_widths_as_string = ",".join(list_of_column_widths)
self.config["table"]["column_widths"] = column_widths_as_string self.config["table"]["column_widths"] = column_widths_as_string