sorting yay
This commit is contained in:
parent
93a1ae61ee
commit
52bce599e3
@ -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.
|
||||||
|
|||||||
2
main.py
2
main.py
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user