Refactor into modules
This commit is contained in:
parent
cf52eb3c9c
commit
bcfd076a93
19
app/helpers.py
Normal file
19
app/helpers.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from log import DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
def ms_to_mmss(ms, decimals=0, negative=False):
|
||||||
|
if not ms:
|
||||||
|
return "-"
|
||||||
|
sign = ""
|
||||||
|
if ms < 0:
|
||||||
|
if negative:
|
||||||
|
sign = "-"
|
||||||
|
else:
|
||||||
|
ms = 0
|
||||||
|
|
||||||
|
minutes, remainder = divmod(ms, 60 * 1000)
|
||||||
|
seconds = remainder / 1000
|
||||||
|
if int(seconds) == 60:
|
||||||
|
DEBUG(f"ms_to_mmss({ms}) gave 60 seconds")
|
||||||
|
|
||||||
|
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
|
||||||
10
app/model.py
10
app/model.py
@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import os
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -102,7 +101,8 @@ class Playlists(Base):
|
|||||||
tr = session.query(Tracks).filter(Tracks.id == 676).one()
|
tr = session.query(Tracks).filter(Tracks.id == 676).one()
|
||||||
|
|
||||||
tr
|
tr
|
||||||
<Track(id=676, title=Seven Nation Army, artist=White Stripes, path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
|
<Track(id=676, title=Seven Nation Army, artist=White Stripes,
|
||||||
|
path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
|
||||||
|
|
||||||
glue.track_id = tr.id
|
glue.track_id = tr.id
|
||||||
|
|
||||||
@ -132,8 +132,10 @@ class Playlists(Base):
|
|||||||
# Currently we only support one playlist, so make that obvious from
|
# Currently we only support one playlist, so make that obvious from
|
||||||
# function name
|
# function name
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_only_playlist(cls):
|
def get_playlist_by_name(cls, name):
|
||||||
return session.query(Playlists).filter(Playlists.id == 1).one()
|
"Returns a playlist object for named playlist"
|
||||||
|
|
||||||
|
return session.query(Playlists).filter(Playlists.name == name).one()
|
||||||
|
|
||||||
def add_track(self, track, position):
|
def add_track(self, track, position):
|
||||||
glue = PlaylistTracks(sort=position)
|
glue = PlaylistTracks(sort=position)
|
||||||
|
|||||||
94
app/music.py
Normal file
94
app/music.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import vlc
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from log import DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
class Music:
|
||||||
|
"""
|
||||||
|
Manage the playing of music tracks
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fading = False
|
||||||
|
self.player = None
|
||||||
|
self.track_path = None
|
||||||
|
|
||||||
|
def fade(self):
|
||||||
|
"""
|
||||||
|
Fade the currently playing track.
|
||||||
|
|
||||||
|
Return the current track path and position.
|
||||||
|
|
||||||
|
The actual management of fading runs in its own thread so as not
|
||||||
|
to hold up the UI during the fade.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG("fade()")
|
||||||
|
|
||||||
|
if not self.playing():
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
path = self.track_path
|
||||||
|
position = self.player.get_position()
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self._fade)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return (path, position)
|
||||||
|
|
||||||
|
def _fade(self):
|
||||||
|
"""
|
||||||
|
Implementation of fading the current track in a separate thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG("_fade()")
|
||||||
|
|
||||||
|
fade_time = Config.FADE_TIME / 1000
|
||||||
|
sleep_time = fade_time / Config.FADE_STEPS
|
||||||
|
step_percent = int((100 / Config.FADE_STEPS) * -1)
|
||||||
|
|
||||||
|
# Take a copy of current player to allow another track to be
|
||||||
|
# started without interfering here
|
||||||
|
p = self.player
|
||||||
|
for i in range(100, 0, step_percent):
|
||||||
|
p.audio_set_volume(i)
|
||||||
|
sleep(sleep_time)
|
||||||
|
|
||||||
|
p.pause()
|
||||||
|
p.release()
|
||||||
|
|
||||||
|
def get_playtime(self):
|
||||||
|
"Return elapsed play time"
|
||||||
|
|
||||||
|
return self.player.get_time()
|
||||||
|
|
||||||
|
def play(self, path):
|
||||||
|
"""
|
||||||
|
Start playing the track at path.
|
||||||
|
|
||||||
|
Raise AttributeError if path not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"play({path})")
|
||||||
|
|
||||||
|
if not os.access(path, os.R_OK):
|
||||||
|
raise AttributeError(f"Cannot access {path}")
|
||||||
|
|
||||||
|
self.track_path = path
|
||||||
|
|
||||||
|
self.player = vlc.MediaPlayer(path)
|
||||||
|
self.player.audio_set_volume(100)
|
||||||
|
self.player.play()
|
||||||
|
|
||||||
|
def playing(self):
|
||||||
|
"Return True if currently playing a track, else False"
|
||||||
|
|
||||||
|
if self.player:
|
||||||
|
return self.player.is_playing()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
@ -1,180 +1,25 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
import vlc
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
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.QtGui import QColor
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QTableWidgetItem,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ui.main_window_ui import Ui_MainWindow
|
import helpers
|
||||||
from ui.dlg_search_database_ui import Ui_Dialog
|
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from model import Playdates, Playlists, Settings, Tracks
|
from model import Playlists, Settings, Tracks
|
||||||
from time import sleep
|
from ui.dlg_search_database_ui import Ui_Dialog
|
||||||
|
from ui.main_window_ui import Ui_MainWindow
|
||||||
|
|
||||||
class Music:
|
|
||||||
def __init__(self):
|
|
||||||
self.current_track = {
|
|
||||||
"player": None,
|
|
||||||
"meta": None
|
|
||||||
}
|
|
||||||
self.next_track = {
|
|
||||||
"player": None,
|
|
||||||
"meta": None
|
|
||||||
}
|
|
||||||
self.previous_track = {
|
|
||||||
"player": None,
|
|
||||||
"meta": None
|
|
||||||
}
|
|
||||||
self.fading = False
|
|
||||||
|
|
||||||
def get_current_artist(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].artist
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_current_duration(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].duration
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_current_fade_at(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].fade_at
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_current_playtime(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['player'].get_time()
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_current_silence_at(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].silence_at
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_current_title(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].title
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_current_track_id(self):
|
|
||||||
try:
|
|
||||||
return self.current_track['meta'].id
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_previous_artist(self):
|
|
||||||
try:
|
|
||||||
return self.previous_track['meta'].artist
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_previous_title(self):
|
|
||||||
try:
|
|
||||||
return self.previous_track['meta'].title
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_next_artist(self):
|
|
||||||
try:
|
|
||||||
return self.next_track['meta'].artist
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_next_title(self):
|
|
||||||
try:
|
|
||||||
return self.next_track['meta'].title
|
|
||||||
except AttributeError:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_next_track_id(self):
|
|
||||||
try:
|
|
||||||
return self.next_track['meta'].id
|
|
||||||
except AttributeError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def fade_current(self):
|
|
||||||
if not self.playing():
|
|
||||||
return
|
|
||||||
thread = threading.Thread(target=self._fade_current)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def _fade_current(self):
|
|
||||||
fade_time = Config.FADE_TIME / 1000
|
|
||||||
sleep_time = fade_time / Config.FADE_STEPS
|
|
||||||
step_percent = int((100 / Config.FADE_STEPS) * -1)
|
|
||||||
player = self.current_track['player']
|
|
||||||
position = player.get_position()
|
|
||||||
for i in range(100, 0, step_percent):
|
|
||||||
player.audio_set_volume(i)
|
|
||||||
sleep(sleep_time)
|
|
||||||
# If the user clicks "fade" and then, before the track has
|
|
||||||
# finished fading, click "play next", the "play next" will also
|
|
||||||
# call fade_current. When that second call to fade_current gets
|
|
||||||
# to the player.pause() line below, it will unpause it. So, we
|
|
||||||
# only pause if we're acutally playing.
|
|
||||||
if player.is_playing():
|
|
||||||
player.pause()
|
|
||||||
player.audio_set_volume(100)
|
|
||||||
player.set_position(position)
|
|
||||||
|
|
||||||
def play_next(self):
|
|
||||||
if self.previous_track['player']:
|
|
||||||
self.previous_track['player'].release()
|
|
||||||
if self.current_track['player']:
|
|
||||||
self.fade_current()
|
|
||||||
self.previous_track = self.current_track
|
|
||||||
self.current_track = self.next_track
|
|
||||||
# Next in case player was left in odd (ie, silenced) state
|
|
||||||
self.current_track['player'].audio_set_volume(100)
|
|
||||||
self.current_track['player'].play()
|
|
||||||
Playdates.add_playdate(self.current_track['meta'])
|
|
||||||
|
|
||||||
# Tidy up
|
|
||||||
self.next_track = {
|
|
||||||
"player": None,
|
|
||||||
"meta": None
|
|
||||||
}
|
|
||||||
|
|
||||||
def playing(self):
|
|
||||||
if self.current_track['player']:
|
|
||||||
return self.current_track['player'].is_playing()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def resume_previous(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_next_track(self, id):
|
|
||||||
track = Tracks.get_track(id)
|
|
||||||
if not track:
|
|
||||||
ERROR(f"set_next_track({id}): can't find track")
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.next_track['player'] = vlc.MediaPlayer(track.path)
|
|
||||||
self.next_track['meta'] = track
|
|
||||||
return track.id
|
|
||||||
|
|
||||||
|
|
||||||
class Window(QMainWindow, Ui_MainWindow):
|
class Window(QMainWindow, Ui_MainWindow):
|
||||||
@ -182,8 +27,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.timer = QTimer()
|
self.timer = QTimer()
|
||||||
self.connectSignalsSlots()
|
self.connect_signals_slots()
|
||||||
self.music = Music()
|
|
||||||
self.disable_play_next_controls()
|
self.disable_play_next_controls()
|
||||||
|
|
||||||
record = Settings.get_int("mainwindow_x")
|
record = Settings.get_int("mainwindow_x")
|
||||||
@ -196,39 +40,12 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
height = record.f_int or 981
|
height = record.f_int or 981
|
||||||
self.setGeometry(x, y, width, height)
|
self.setGeometry(x, y, width, height)
|
||||||
|
|
||||||
for column in range(self.playlist.columnCount()):
|
# Hard code to the only playlist we have for now
|
||||||
name = f"playlist_col_{str(column)}_width"
|
self.playlist.load_playlist("Default")
|
||||||
record = Settings.get_int(name)
|
|
||||||
if record.f_int is not None:
|
|
||||||
self.playlist.setColumnWidth(column, record.f_int)
|
|
||||||
|
|
||||||
# Load playlist
|
|
||||||
db_playlist = Playlists.get_only_playlist()
|
|
||||||
for track in db_playlist.get_tracks():
|
|
||||||
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)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
for column in range(self.playlist.columnCount()):
|
|
||||||
name = f"playlist_col_{str(column)}_width"
|
|
||||||
record = Settings.get_int(name)
|
|
||||||
if record.f_int != self.playlist.columnWidth(column):
|
|
||||||
record.update({'f_int': self.playlist.columnWidth(column)})
|
|
||||||
|
|
||||||
record = Settings.get_int("mainwindow_height")
|
record = Settings.get_int("mainwindow_height")
|
||||||
if record.f_int != self.height():
|
if record.f_int != self.height():
|
||||||
record.update({'f_int': self.height()})
|
record.update({'f_int': self.height()})
|
||||||
@ -245,28 +62,47 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
if record.f_int != self.y():
|
if record.f_int != self.y():
|
||||||
record.update({'f_int': self.y()})
|
record.update({'f_int': self.y()})
|
||||||
|
|
||||||
def connectSignalsSlots(self):
|
def add_file(self):
|
||||||
|
dlg = QFileDialog()
|
||||||
|
dlg.setFileMode(QFileDialog.ExistingFile)
|
||||||
|
dlg.setViewMode(QFileDialog.Detail)
|
||||||
|
dlg.setDirectory(Config.ROOT)
|
||||||
|
dlg.setNameFilter("Music files (*.flac *.mp3)")
|
||||||
|
|
||||||
|
if dlg.exec_():
|
||||||
|
pass
|
||||||
|
# TODO Add to database
|
||||||
|
# for fname in dlg.selectedFiles():
|
||||||
|
# trackwest = Track(fname)
|
||||||
|
# self.add_to_playlist(track)
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
self.playlist.clearSelection()
|
||||||
|
|
||||||
|
def connect_signals_slots(self):
|
||||||
self.action_Clear_selection.triggered.connect(self.clear_selection)
|
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.search_database)
|
||||||
self.btnSearchDatabase.clicked.connect(self.selectFromDatabase)
|
self.btnPrevious.clicked.connect(self.play_previous)
|
||||||
self.btnSetNextTrack.clicked.connect(self.set_next_track)
|
self.btnSearchDatabase.clicked.connect(self.search_database)
|
||||||
|
self.btnSetNextTrack.clicked.connect(self.playlist.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.timer.timeout.connect(self.tick)
|
self.timer.timeout.connect(self.tick)
|
||||||
|
|
||||||
def selectFromDatabase(self):
|
def disable_play_next_controls(self):
|
||||||
dlg = DbDialog(self)
|
self.actionPlay_selected.setEnabled(False)
|
||||||
dlg.exec()
|
self.actionPlay_next.setEnabled(False)
|
||||||
|
|
||||||
def clear_selection(self):
|
def enable_play_next_controls(self):
|
||||||
self.playlist.clearSelection()
|
self.actionPlay_selected.setEnabled(True)
|
||||||
|
self.actionPlay_next.setEnabled(True)
|
||||||
|
|
||||||
def fade(self):
|
def fade(self):
|
||||||
self.music.fade_current()
|
self.playlist.fade()
|
||||||
|
|
||||||
def play_next(self):
|
def play_next(self):
|
||||||
self.music.play_next()
|
self.music.play_next()
|
||||||
@ -286,141 +122,72 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
silence_at = self.music.get_current_silence_at()
|
silence_at = self.music.get_current_silence_at()
|
||||||
silence_time = now + timedelta(milliseconds=silence_at)
|
silence_time = now + timedelta(milliseconds=silence_at)
|
||||||
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(helpers.ms_to_mmss(
|
||||||
silence_at - self.music.get_current_fade_at()))
|
silence_at - self.music.get_current_fade_at()))
|
||||||
self.set_playlist_colours()
|
self.set_playlist_colours()
|
||||||
|
|
||||||
def set_playlist_colours(self):
|
def play_previous(self):
|
||||||
self.playlist.clearSelection()
|
"Resume playing last track"
|
||||||
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):
|
# TODO
|
||||||
if self.playlist.selectionModel().hasSelection():
|
pass
|
||||||
row = self.playlist.currentRow()
|
|
||||||
track_id = int(self.playlist.item(row, 0).text())
|
|
||||||
DEBUG(f"play_selected: track_id={track_id}")
|
|
||||||
# TODO: get_path may raise exception
|
|
||||||
track_path = Tracks.get_path(track_id)
|
|
||||||
if track_path:
|
|
||||||
DEBUG(f"play_selected: track_path={track_path}")
|
|
||||||
player = vlc.MediaPlayer(track_path)
|
|
||||||
player.play()
|
|
||||||
|
|
||||||
def selectFile(self):
|
def search_database(self):
|
||||||
dlg = QFileDialog()
|
dlg = DbDialog(self)
|
||||||
dlg.setFileMode(QFileDialog.ExistingFile)
|
dlg.exec()
|
||||||
dlg.setViewMode(QFileDialog.Detail)
|
|
||||||
dlg.setDirectory(Config.ROOT)
|
|
||||||
dlg.setNameFilter("Music files (*.flac *.mp3)")
|
|
||||||
|
|
||||||
if dlg.exec_():
|
|
||||||
pass
|
|
||||||
# TODO Add to database
|
|
||||||
# for fname in dlg.selectedFiles():
|
|
||||||
# track = Track(fname)
|
|
||||||
# self.add_to_playlist(track)
|
|
||||||
|
|
||||||
def set_next_track(self):
|
|
||||||
"""
|
|
||||||
Sets the selected track as the next track.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.playlist.selectionModel().hasSelection():
|
|
||||||
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())
|
|
||||||
if track_id == self.music.get_current_track_id():
|
|
||||||
# Don't set current track as next track
|
|
||||||
return
|
|
||||||
DEBUG(f"set_next_track: track_id={track_id}")
|
|
||||||
if self.music.set_next_track(track_id) != track_id:
|
|
||||||
ERROR("Can't set next track")
|
|
||||||
self.next_track.setText(
|
|
||||||
f"{self.music.get_next_title()} - "
|
|
||||||
f"{self.music.get_next_artist()}"
|
|
||||||
)
|
|
||||||
self.enable_play_next_controls()
|
|
||||||
self.playlist.clearSelection()
|
|
||||||
self.set_playlist_colours()
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
|
pass
|
||||||
# self.current_time.setText(now.strftime("%H:%M:%S"))
|
# self.current_time.setText(now.strftime("%H:%M:%S"))
|
||||||
if self.music.playing():
|
# if self.music.playing():
|
||||||
playtime = self.music.get_current_playtime()
|
# playtime = self.music.get_current_playtime()
|
||||||
self.label_elapsed_timer.setText(ms_to_mmss(playtime))
|
# self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
|
||||||
self.label_fade_timer.setText(
|
# self.label_fade_timer.setText(
|
||||||
ms_to_mmss(self.music.get_current_fade_at() - playtime))
|
# helpers.ms_to_mmss(self.music.get_current_fade_at() - playtime)
|
||||||
self.label_silent_timer.setText(
|
# )
|
||||||
ms_to_mmss(self.music.get_current_silence_at() - playtime))
|
# self.label_silent_timer.setText(
|
||||||
self.label_end_timer.setText(
|
# helpers.ms_to_mmss(
|
||||||
ms_to_mmss(self.music.get_current_duration() - playtime))
|
# self.music.get_current_silence_at() - playtime))
|
||||||
else:
|
# self.label_end_timer.setText(
|
||||||
# When music ends, ensure next track is selected
|
# helpers.ms_to_mmss(
|
||||||
if self.playlist.selectionModel().hasSelection():
|
# self.music.get_current_duration() - playtime))
|
||||||
row = self.playlist.currentRow()
|
# else:
|
||||||
track_id = int(self.playlist.item(row, 0).text())
|
# # When music ends, ensure next track is selected
|
||||||
if track_id == self.music.get_current_track_id():
|
# if self.playlist.selectionModel().hasSelection():
|
||||||
# Current track highlighted: select next
|
# row = self.playlist.currentRow()
|
||||||
try:
|
# track_id = int(self.playlist.item(row, 0).text())
|
||||||
self.playlist.selectRow(row + 1)
|
# if track_id == self.music.get_current_track_id():
|
||||||
except AttributeError:
|
# # Current track highlighted: select next
|
||||||
pass
|
# try:
|
||||||
|
# self.playlist.selectRow(row + 1)
|
||||||
|
# except AttributeError:
|
||||||
|
# # TODO
|
||||||
|
# pass
|
||||||
|
|
||||||
def add_to_playlist(self, track):
|
|
||||||
"""
|
|
||||||
Add track to playlist
|
|
||||||
|
|
||||||
track is an instance of Track
|
def update_tod_clock(self):
|
||||||
"""
|
"Update time of day clock"
|
||||||
|
|
||||||
DEBUG(f"add_to_playlist: track.id={track.id}")
|
# TODO
|
||||||
pl = self.playlist
|
pass
|
||||||
row = pl.rowCount()
|
|
||||||
pl.insertRow(row)
|
|
||||||
item = QTableWidgetItem(str(track.id))
|
|
||||||
pl.setItem(row, 0, item)
|
|
||||||
item = QTableWidgetItem(str(track.start_gap))
|
|
||||||
pl.setItem(row, 1, item)
|
|
||||||
item = QTableWidgetItem(track.title)
|
|
||||||
pl.setItem(row, 2, item)
|
|
||||||
item = QTableWidgetItem(track.artist)
|
|
||||||
pl.setItem(row, 3, item)
|
|
||||||
item = QTableWidgetItem(ms_to_mmss(track.duration))
|
|
||||||
pl.setItem(row, 4, item)
|
|
||||||
item = QTableWidgetItem(track.path)
|
|
||||||
pl.setItem(row, 7, item)
|
|
||||||
|
|
||||||
def disable_play_next_controls(self):
|
def update_track_headers(self):
|
||||||
self.actionPlay_selected.setEnabled(False)
|
"Update last/current/next track header"
|
||||||
self.actionPlay_next.setEnabled(False)
|
|
||||||
|
|
||||||
def enable_play_next_controls(self):
|
# TODO
|
||||||
self.actionPlay_selected.setEnabled(True)
|
pass
|
||||||
self.actionPlay_next.setEnabled(True)
|
|
||||||
|
|
||||||
|
def update_track_clocks(self):
|
||||||
|
"Update started at, silent at, clock times"
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_track_timers(self):
|
||||||
|
"Update elapsed time, etc, timers"
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
class DbDialog(QDialog):
|
class DbDialog(QDialog):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -454,7 +221,7 @@ class DbDialog(QDialog):
|
|||||||
t = QListWidgetItem()
|
t = QListWidgetItem()
|
||||||
t.setText(
|
t.setText(
|
||||||
f"{track.title} - {track.artist} "
|
f"{track.title} - {track.artist} "
|
||||||
f"[{ms_to_mmss(track.duration)}]"
|
f"[{helpers.ms_to_mmss(track.duration)}]"
|
||||||
)
|
)
|
||||||
t.setData(Qt.UserRole, track.id)
|
t.setData(Qt.UserRole, track.id)
|
||||||
self.ui.matchList.addItem(t)
|
self.ui.matchList.addItem(t)
|
||||||
@ -472,24 +239,6 @@ class DbDialog(QDialog):
|
|||||||
self.parent().add_to_playlist(track)
|
self.parent().add_to_playlist(track)
|
||||||
|
|
||||||
|
|
||||||
def ms_to_mmss(ms, decimals=0, negative=False):
|
|
||||||
if not ms:
|
|
||||||
return "-"
|
|
||||||
sign = ""
|
|
||||||
if ms < 0:
|
|
||||||
if negative:
|
|
||||||
sign = "-"
|
|
||||||
else:
|
|
||||||
ms = 0
|
|
||||||
|
|
||||||
minutes, remainder = divmod(ms, 60 * 1000)
|
|
||||||
seconds = remainder / 1000
|
|
||||||
if int(seconds) == 60:
|
|
||||||
DEBUG(f"ms_to_mmss({ms}) gave 60 seconds")
|
|
||||||
|
|
||||||
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
win = Window()
|
win = Window()
|
||||||
|
|||||||
365
app/playlists.py
Normal file
365
app/playlists.py
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QColor, QDropEvent
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QTableWidget,
|
||||||
|
QAbstractItemView,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QWidget,
|
||||||
|
QHBoxLayout,
|
||||||
|
QApplication
|
||||||
|
)
|
||||||
|
|
||||||
|
import helpers
|
||||||
|
import music
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from log import DEBUG
|
||||||
|
from model import Playlists, Settings, Tracks
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(QTableWidget):
|
||||||
|
# Column names
|
||||||
|
COL_INDEX = 0
|
||||||
|
COL_MSS = 1
|
||||||
|
COL_TITLE = 2
|
||||||
|
COL_ARTIST = 3
|
||||||
|
COL_DURATION = 4
|
||||||
|
COL_ENDTIME = 5
|
||||||
|
COL_PATH = 6
|
||||||
|
|
||||||
|
# UserRoles
|
||||||
|
NEXT_TRACK = 1
|
||||||
|
CURRENT_TRACK = 2
|
||||||
|
COMMENT = 3
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.music = music.Music()
|
||||||
|
|
||||||
|
self.current_track = None
|
||||||
|
self.next_track = None
|
||||||
|
self.previous_track_path = None
|
||||||
|
self.previous_track_position = None
|
||||||
|
self.played_tracks = []
|
||||||
|
|
||||||
|
# Column widths from settings
|
||||||
|
for column in range(self.columnCount()):
|
||||||
|
name = f"playlist_col_{str(column)}_width"
|
||||||
|
record = Settings.get_int(name)
|
||||||
|
if record.f_int is not None:
|
||||||
|
self.setColumnWidth(column, record.f_int)
|
||||||
|
|
||||||
|
# How to put in non-track entries for later
|
||||||
|
# row = self.rowCount()
|
||||||
|
# self.insertRow(row)
|
||||||
|
# item = QTableWidgetItem("to be spanned")
|
||||||
|
# self.setItem(row, 1, item)
|
||||||
|
# self.setSpan(row, 1, 1, 3)
|
||||||
|
# colour = QColor(125, 75, 25)
|
||||||
|
# self.set_row_colour(row, colour)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"Save column widths"
|
||||||
|
|
||||||
|
for column in range(self.columnCount()):
|
||||||
|
name = f"playlist_col_{str(column)}_width"
|
||||||
|
record = Settings.get_int(name)
|
||||||
|
if record.f_int != self.columnWidth(column):
|
||||||
|
record.update({'f_int': self.columnWidth(column)})
|
||||||
|
|
||||||
|
def add_to_playlist(self, track):
|
||||||
|
"""
|
||||||
|
Add track to playlist
|
||||||
|
|
||||||
|
track is an instance of Track
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEBUG(f"add_to_playlist: track.id={track.id}")
|
||||||
|
row = self.rowCount()
|
||||||
|
self.insertRow(row)
|
||||||
|
DEBUG(f"Adding to row {row}")
|
||||||
|
item = QTableWidgetItem(str(track.id))
|
||||||
|
self.setItem(row, self.COL_INDEX, item)
|
||||||
|
item = QTableWidgetItem(str(track.start_gap))
|
||||||
|
self.setItem(row, self.COL_MSS, item)
|
||||||
|
item = QTableWidgetItem(track.title)
|
||||||
|
self.setItem(row, self.COL_TITLE, item)
|
||||||
|
item = QTableWidgetItem(track.artist)
|
||||||
|
self.setItem(row, self.COL_ARTIST, item)
|
||||||
|
item = QTableWidgetItem(helpers.ms_to_mmss(track.duration))
|
||||||
|
self.setItem(row, self.COL_DURATION, item)
|
||||||
|
item = QTableWidgetItem(track.path)
|
||||||
|
self.setItem(row, self.COL_PATH, item)
|
||||||
|
|
||||||
|
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 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 face(self):
|
||||||
|
self.music.fade()
|
||||||
|
|
||||||
|
def get_current_artist(self):
|
||||||
|
try:
|
||||||
|
return self.current_track.artist
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_current_duration(self):
|
||||||
|
try:
|
||||||
|
return self.current_track.duration
|
||||||
|
except AttributeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_current_fade_at(self):
|
||||||
|
try:
|
||||||
|
return self.current_track.fade_at
|
||||||
|
except AttributeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_current_playtime(self):
|
||||||
|
|
||||||
|
return self.music.get_playtime()
|
||||||
|
|
||||||
|
def get_current_silence_at(self):
|
||||||
|
try:
|
||||||
|
return self.current_track.silence_at
|
||||||
|
except AttributeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_current_title(self):
|
||||||
|
try:
|
||||||
|
return self.current_track.title
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_next_artist(self):
|
||||||
|
try:
|
||||||
|
return self.next_track.artist
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_next_title(self):
|
||||||
|
try:
|
||||||
|
return self.next_track.title
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_next_track_id(self):
|
||||||
|
try:
|
||||||
|
return self.next_track.id
|
||||||
|
except AttributeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_previous_artist(self):
|
||||||
|
try:
|
||||||
|
return self.previous_track.artist
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_previous_title(self):
|
||||||
|
try:
|
||||||
|
return self.previous_track.title
|
||||||
|
except AttributeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
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 load_playlist(self, name):
|
||||||
|
"Load tracks from named playlist"
|
||||||
|
|
||||||
|
for track in Playlists.get_playlist_by_name(name).get_tracks():
|
||||||
|
self.add_to_playlist(track)
|
||||||
|
# Set the first playable track as next to play
|
||||||
|
for row in range(self.rowCount()):
|
||||||
|
if self.set_row_as_next_track(row):
|
||||||
|
break
|
||||||
|
|
||||||
|
def play_next(self):
|
||||||
|
"""
|
||||||
|
Play next track.
|
||||||
|
|
||||||
|
If there is no next track set, return.
|
||||||
|
If there's currently a track playing, fade it.
|
||||||
|
Move next track to current track.
|
||||||
|
Cue up next track in playlist if there is one.
|
||||||
|
Play (new) current.
|
||||||
|
Tell database to record it as played
|
||||||
|
Remember it was played this session
|
||||||
|
Update playlist appearance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.next_track:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.music.playing():
|
||||||
|
path, position = self.music.fade()
|
||||||
|
self.previous_track_path = path
|
||||||
|
self.previous_track_position = position
|
||||||
|
|
||||||
|
self.current_track = self.next_track
|
||||||
|
self.music.play(self.current_track.path)
|
||||||
|
|
||||||
|
# Find the playlist row for current track
|
||||||
|
current_track_id = self.current_track.track_id
|
||||||
|
self.current_track_row = None
|
||||||
|
for row in range(self.rowCount):
|
||||||
|
if self.item(row, 0):
|
||||||
|
if current_track_id == int(self.item(row, 0).text()):
|
||||||
|
self.current_track_row = row
|
||||||
|
break
|
||||||
|
|
||||||
|
self.next_track = None
|
||||||
|
|
||||||
|
def set_next_track(self):
|
||||||
|
"""
|
||||||
|
Sets the selected track as the next track.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
# if self.selectionModel().hasSelection():
|
||||||
|
# self.set_next_track_row(self.playlist.currentRow())
|
||||||
|
# if self.selectionModel().hasSelection():
|
||||||
|
# row = self.currentRow()
|
||||||
|
# track_id = int(self.item(row, 0).text())
|
||||||
|
# DEBUG(f"play_selected: track_id={track_id}")
|
||||||
|
# # TODO: get_path may raise exception
|
||||||
|
# music.play_track(track_id)
|
||||||
|
# track = Tracks.get_track(id)
|
||||||
|
# if not track:
|
||||||
|
# ERROR(f"set_next_track({id}): can't find track")
|
||||||
|
# return None
|
||||||
|
|
||||||
|
# # self.next_track['player'] = vlc.MediaPlayer(track.path)
|
||||||
|
# self.next_track = track
|
||||||
|
# return track.id
|
||||||
|
|
||||||
|
def set_row_as_next_track(self, row):
|
||||||
|
"""
|
||||||
|
If passed row is track row, indicated by having a track_id in the
|
||||||
|
COL_INDEX column, set that track as the next track to be played
|
||||||
|
and return True. Otherwise return False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.item(row, self.COL_INDEX):
|
||||||
|
track_id = int(self.item(row, self.COL_INDEX).text())
|
||||||
|
DEBUG(f"set_row_as_next_track: track_id={track_id}")
|
||||||
|
self.next_track = Tracks.get_track(track_id)
|
||||||
|
# Mark row as next track
|
||||||
|
self.item(row, self.COL_INDEX).setData(
|
||||||
|
Qt.UserRole, self.NEXT_TRACK)
|
||||||
|
self.set_playlist_colours()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_playlist_colours(self):
|
||||||
|
self.clearSelection()
|
||||||
|
for row in range(self.rowCount()):
|
||||||
|
if self.item(row, self.COL_INDEX):
|
||||||
|
data = self.item(row, self.COL_INDEX).data(Qt.UserRole)
|
||||||
|
if data == self.NEXT_TRACK:
|
||||||
|
self.set_row_colour(
|
||||||
|
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
|
||||||
|
elif data == self.CURRENT_TRACK:
|
||||||
|
self.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.set_row_colour(row, colour)
|
||||||
|
|
||||||
|
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_())
|
||||||
@ -1,114 +0,0 @@
|
|||||||
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_())
|
|
||||||
@ -287,7 +287,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="columnCount">
|
<property name="columnCount">
|
||||||
<number>8</number>
|
<number>7</number>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
@ -322,11 +322,6 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<string>End time</string>
|
<string>End time</string>
|
||||||
</property>
|
</property>
|
||||||
</column>
|
</column>
|
||||||
<column>
|
|
||||||
<property name="text">
|
|
||||||
<string>Autoplay next</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
<column>
|
<column>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Path</string>
|
<string>Path</string>
|
||||||
@ -694,6 +689,9 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="btnTrackInfo">
|
<widget class="QPushButton" name="btnTrackInfo">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Track &info</string>
|
<string>Track &info</string>
|
||||||
</property>
|
</property>
|
||||||
@ -732,6 +730,8 @@ 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="separator"/>
|
||||||
|
<addaction name="action_Resume_previous"/>
|
||||||
<addaction name="action_Clear_selection"/>
|
<addaction name="action_Clear_selection"/>
|
||||||
</widget>
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
@ -751,7 +751,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<normaloff>icon-play.png</normaloff>icon-play.png</iconset>
|
<normaloff>icon-play.png</normaloff>icon-play.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Play selected</string>
|
<string>&Play next</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut">
|
||||||
<string>Return</string>
|
<string>Return</string>
|
||||||
@ -810,12 +810,21 @@ border: 1px solid rgb(85, 87, 83);</string>
|
|||||||
<string>Esc</string>
|
<string>Esc</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_Resume_previous">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>previous.png</normaloff>previous.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>&Resume previous</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>Playlist</class>
|
<class>Playlist</class>
|
||||||
<extends>QTableWidget</extends>
|
<extends>QTableWidget</extends>
|
||||||
<header>promoted_classes</header>
|
<header>playlists</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user