Playlist drag and drop; track colours
This commit is contained in:
parent
d3b739dc36
commit
cf52eb3c9c
@ -4,6 +4,13 @@ import os
|
|||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
AUDIO_SEGMENT_CHUNK_SIZE = 10
|
||||||
|
COLOUR_CURRENT_HEADER = "#d4edda"
|
||||||
|
COLOUR_CURRENT_PLAYLIST = "#28a745"
|
||||||
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
|
COLOUR_EVEN_PLAYLIST = "#d9d9d9"
|
||||||
|
COLOUR_NEXT_HEADER = "#fff3cd"
|
||||||
|
COLOUR_NEXT_PLAYLIST = "#ffc107"
|
||||||
|
COLOUR_PREVIOUS_HEADER = "#f8d7da"
|
||||||
DBFS_FADE = -12
|
DBFS_FADE = -12
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
DISPLAY_SQL = False
|
DISPLAY_SQL = False
|
||||||
|
|||||||
@ -8,8 +8,15 @@ from datetime import datetime, timedelta
|
|||||||
from log import DEBUG, ERROR
|
from log import DEBUG, ERROR
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow
|
from PyQt5.QtGui import QColor
|
||||||
from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog, QListWidgetItem
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QDialog,
|
||||||
|
QFileDialog,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMainWindow,
|
||||||
|
QTableWidgetItem,
|
||||||
|
)
|
||||||
|
|
||||||
from ui.main_window_ui import Ui_MainWindow
|
from ui.main_window_ui import Ui_MainWindow
|
||||||
from ui.dlg_search_database_ui import Ui_Dialog
|
from ui.dlg_search_database_ui import Ui_Dialog
|
||||||
@ -101,6 +108,12 @@ class Music:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def get_next_track_id(self):
|
||||||
|
try:
|
||||||
|
return self.next_track['meta'].id
|
||||||
|
except AttributeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
def fade_current(self):
|
def fade_current(self):
|
||||||
if not self.playing():
|
if not self.playing():
|
||||||
return
|
return
|
||||||
@ -193,6 +206,19 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
db_playlist = Playlists.get_only_playlist()
|
db_playlist = Playlists.get_only_playlist()
|
||||||
for track in db_playlist.get_tracks():
|
for track in db_playlist.get_tracks():
|
||||||
self.add_to_playlist(track)
|
self.add_to_playlist(track)
|
||||||
|
# Set the first playable track as next to play
|
||||||
|
for row in range(self.playlist.rowCount()):
|
||||||
|
if self.set_next_track_row(row):
|
||||||
|
break
|
||||||
|
|
||||||
|
pl = self.playlist
|
||||||
|
row = pl.rowCount()
|
||||||
|
pl.insertRow(row)
|
||||||
|
item = QTableWidgetItem("to be spanned")
|
||||||
|
pl.setItem(row, 1, item)
|
||||||
|
pl.setSpan(row, 1, 1, 3)
|
||||||
|
colour = QColor(125, 75, 25)
|
||||||
|
pl.set_row_colour(row, colour)
|
||||||
|
|
||||||
self.timer.start(Config.TIMER_MS)
|
self.timer.start(Config.TIMER_MS)
|
||||||
|
|
||||||
@ -220,14 +246,15 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
record.update({'f_int': self.y()})
|
record.update({'f_int': self.y()})
|
||||||
|
|
||||||
def connectSignalsSlots(self):
|
def connectSignalsSlots(self):
|
||||||
|
self.action_Clear_selection.triggered.connect(self.clear_selection)
|
||||||
self.actionFade.triggered.connect(self.fade)
|
self.actionFade.triggered.connect(self.fade)
|
||||||
self.actionPlay_next.triggered.connect(self.play_next)
|
self.actionPlay_next.triggered.connect(self.play_next)
|
||||||
self.actionPlay_selected.triggered.connect(self.play_next)
|
self.actionPlay_selected.triggered.connect(self.play_next)
|
||||||
self.actionSearch_database.triggered.connect(self.selectFromDatabase)
|
self.actionSearch_database.triggered.connect(self.selectFromDatabase)
|
||||||
self.btnSearchDatabase.clicked.connect(self.selectFromDatabase)
|
self.btnSearchDatabase.clicked.connect(self.selectFromDatabase)
|
||||||
|
self.btnSetNextTrack.clicked.connect(self.set_next_track)
|
||||||
self.btnSkipNext.clicked.connect(self.play_next)
|
self.btnSkipNext.clicked.connect(self.play_next)
|
||||||
self.btnStop.clicked.connect(self.fade)
|
self.btnStop.clicked.connect(self.fade)
|
||||||
self.playlist.itemSelectionChanged.connect(self.set_next_track)
|
|
||||||
|
|
||||||
self.timer.timeout.connect(self.tick)
|
self.timer.timeout.connect(self.tick)
|
||||||
|
|
||||||
@ -235,6 +262,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
dlg = DbDialog(self)
|
dlg = DbDialog(self)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
self.playlist.clearSelection()
|
||||||
|
|
||||||
def fade(self):
|
def fade(self):
|
||||||
self.music.fade_current()
|
self.music.fade_current()
|
||||||
|
|
||||||
@ -258,6 +288,29 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
|
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
|
||||||
self.label_fade_length.setText(ms_to_mmss(
|
self.label_fade_length.setText(ms_to_mmss(
|
||||||
silence_at - self.music.get_current_fade_at()))
|
silence_at - self.music.get_current_fade_at()))
|
||||||
|
self.set_playlist_colours()
|
||||||
|
|
||||||
|
def set_playlist_colours(self):
|
||||||
|
self.playlist.clearSelection()
|
||||||
|
current_track = self.music.get_current_track_id()
|
||||||
|
next_track = self.music.get_next_track_id()
|
||||||
|
for row in range(self.playlist.rowCount()):
|
||||||
|
try:
|
||||||
|
track_id = int(self.playlist.item(row, 0).text())
|
||||||
|
if track_id == next_track:
|
||||||
|
self.playlist.set_row_colour(
|
||||||
|
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
|
||||||
|
elif track_id == current_track:
|
||||||
|
self.playlist.set_row_colour(
|
||||||
|
row, QColor(Config.COLOUR_CURRENT_PLAYLIST))
|
||||||
|
else:
|
||||||
|
if row % 2:
|
||||||
|
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
|
||||||
|
else:
|
||||||
|
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
|
||||||
|
self.playlist.set_row_colour(row, colour)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def play_selected(self):
|
def play_selected(self):
|
||||||
if self.playlist.selectionModel().hasSelection():
|
if self.playlist.selectionModel().hasSelection():
|
||||||
@ -287,26 +340,18 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
def set_next_track(self):
|
def set_next_track(self):
|
||||||
"""
|
"""
|
||||||
Set the next track. In order of priority:
|
Sets the selected track as the next track.
|
||||||
|
|
||||||
- the highlighted track so long as it's not the current track
|
|
||||||
- if the current track is highlighted, the next track if there
|
|
||||||
is one
|
|
||||||
- none, in which case disable play next controls
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track_id = None
|
|
||||||
if self.playlist.selectionModel().hasSelection():
|
if self.playlist.selectionModel().hasSelection():
|
||||||
row = self.playlist.currentRow()
|
self.set_next_track_row(self.playlist.currentRow())
|
||||||
|
|
||||||
|
def set_next_track_row(self, row):
|
||||||
|
if self.playlist.item(row, 0):
|
||||||
track_id = int(self.playlist.item(row, 0).text())
|
track_id = int(self.playlist.item(row, 0).text())
|
||||||
if track_id == self.music.get_current_track_id():
|
if track_id == self.music.get_current_track_id():
|
||||||
# Current track highlighted: get next if it exists
|
# Don't set current track as next track
|
||||||
try:
|
return
|
||||||
track_id = int(self.playlist.item(row + 1, 0).text())
|
|
||||||
except AttributeError:
|
|
||||||
# There is no next track
|
|
||||||
track_id = None
|
|
||||||
if track_id:
|
|
||||||
DEBUG(f"set_next_track: track_id={track_id}")
|
DEBUG(f"set_next_track: track_id={track_id}")
|
||||||
if self.music.set_next_track(track_id) != track_id:
|
if self.music.set_next_track(track_id) != track_id:
|
||||||
ERROR("Can't set next track")
|
ERROR("Can't set next track")
|
||||||
@ -315,9 +360,11 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
f"{self.music.get_next_artist()}"
|
f"{self.music.get_next_artist()}"
|
||||||
)
|
)
|
||||||
self.enable_play_next_controls()
|
self.enable_play_next_controls()
|
||||||
else:
|
self.playlist.clearSelection()
|
||||||
self.next_track.setText("")
|
self.set_playlist_colours()
|
||||||
self.disable_play_next_controls()
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
# self.current_time.setText(now.strftime("%H:%M:%S"))
|
# self.current_time.setText(now.strftime("%H:%M:%S"))
|
||||||
|
|||||||
114
app/promoted_classes.py
Normal file
114
app/promoted_classes.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QDropEvent
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QTableWidget,
|
||||||
|
QAbstractItemView,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QWidget,
|
||||||
|
QHBoxLayout,
|
||||||
|
QApplication
|
||||||
|
)
|
||||||
|
|
||||||
|
from log import DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(QTableWidget):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.setDragEnabled(True)
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.viewport().setAcceptDrops(True)
|
||||||
|
self.setDragDropOverwriteMode(False)
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
|
||||||
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.setDragDropMode(QAbstractItemView.InternalMove)
|
||||||
|
|
||||||
|
def dropEvent(self, event: QDropEvent):
|
||||||
|
if not event.isAccepted() and event.source() == self:
|
||||||
|
drop_row = self.drop_on(event)
|
||||||
|
|
||||||
|
rows = sorted(set(item.row() for item in self.selectedItems()))
|
||||||
|
rows_to_move = [
|
||||||
|
[QTableWidgetItem(self.item(row_index, column_index)) for
|
||||||
|
column_index in range(self.columnCount())]
|
||||||
|
for row_index in rows
|
||||||
|
]
|
||||||
|
for row_index in reversed(rows):
|
||||||
|
self.removeRow(row_index)
|
||||||
|
if row_index < drop_row:
|
||||||
|
drop_row -= 1
|
||||||
|
|
||||||
|
for row_index, data in enumerate(rows_to_move):
|
||||||
|
row_index += drop_row
|
||||||
|
self.insertRow(row_index)
|
||||||
|
for column_index, column_data in enumerate(data):
|
||||||
|
self.setItem(row_index, column_index, column_data)
|
||||||
|
event.accept()
|
||||||
|
for row_index in range(len(rows_to_move)):
|
||||||
|
for column_index in range(self.columnCount()):
|
||||||
|
self.item(drop_row + row_index,
|
||||||
|
column_index).setSelected(True)
|
||||||
|
super().dropEvent(event)
|
||||||
|
DEBUG(f"Moved row(s) {rows} to become row {drop_row}")
|
||||||
|
|
||||||
|
def drop_on(self, event):
|
||||||
|
index = self.indexAt(event.pos())
|
||||||
|
if not index.isValid():
|
||||||
|
return self.rowCount()
|
||||||
|
|
||||||
|
return (index.row() + 1 if self.is_below(event.pos(), index)
|
||||||
|
else index.row())
|
||||||
|
|
||||||
|
def is_below(self, pos, index):
|
||||||
|
rect = self.visualRect(index)
|
||||||
|
margin = 2
|
||||||
|
if pos.y() - rect.top() < margin:
|
||||||
|
return False
|
||||||
|
elif rect.bottom() - pos.y() < margin:
|
||||||
|
return True
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return (
|
||||||
|
rect.contains(pos, True) and not
|
||||||
|
(int(self.model().flags(index)) & Qt.ItemIsDropEnabled)
|
||||||
|
and pos.y() >= rect.center().y() # noqa W503
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_row_colour(self, row, colour):
|
||||||
|
for j in range(self.columnCount()):
|
||||||
|
if self.item(row, j):
|
||||||
|
self.item(row, j).setBackground(colour)
|
||||||
|
|
||||||
|
|
||||||
|
class Window(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super(Window, self).__init__()
|
||||||
|
|
||||||
|
layout = QHBoxLayout()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
self.table_widget = Playlist()
|
||||||
|
layout.addWidget(self.table_widget)
|
||||||
|
|
||||||
|
# setup table widget
|
||||||
|
self.table_widget.setColumnCount(2)
|
||||||
|
self.table_widget.setHorizontalHeaderLabels(['Type', 'Name'])
|
||||||
|
|
||||||
|
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'),
|
||||||
|
('Silver', 'Chevy'), ('Black', 'BMW')]
|
||||||
|
self.table_widget.setRowCount(len(items))
|
||||||
|
for i, (color, model) in enumerate(items):
|
||||||
|
self.table_widget.setItem(i, 0, QTableWidgetItem(color))
|
||||||
|
self.table_widget.setItem(i, 1, QTableWidgetItem(model))
|
||||||
|
|
||||||
|
self.resize(400, 400)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = Window()
|
||||||
|
sys.exit(app.exec_())
|
||||||
@ -267,7 +267,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QTableWidget" name="playlist">
|
<widget class="Playlist" name="playlist">
|
||||||
<property name="editTriggers">
|
<property name="editTriggers">
|
||||||
<set>QAbstractItemView::NoEditTriggers</set>
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
</property>
|
</property>
|
||||||
@ -275,7 +275,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="selectionMode">
|
<property name="selectionMode">
|
||||||
<enum>QAbstractItemView::SingleSelection</enum>
|
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="selectionBehavior">
|
<property name="selectionBehavior">
|
||||||
<enum>QAbstractItemView::SelectRows</enum>
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
@ -732,6 +732,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionFade"/>
|
<addaction name="actionFade"/>
|
||||||
<addaction name="actionS_top"/>
|
<addaction name="actionS_top"/>
|
||||||
|
<addaction name="action_Clear_selection"/>
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuPlaylist"/>
|
<addaction name="menuPlaylist"/>
|
||||||
@ -801,7 +802,22 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<string>S&top</string>
|
<string>S&top</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_Clear_selection">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Clear selection</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Esc</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>Playlist</class>
|
||||||
|
<extends>QTableWidget</extends>
|
||||||
|
<header>promoted_classes</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user