#! /usr/bin/env python import os from gi.repository import Gtk import gobject import pango import sys import time import playlist import time_fmt scriptdir = os.path.dirname(os.path.abspath(__file__)) class SyncGui: def __init__(self): """construct a SyncGui object""" self.builder = Gtk.Builder() self.builder.add_from_file(scriptdir + "/sync_gui.glade") self.widMainWindow = self.builder.get_object("MainWindow") self.widPlaylistView = self.builder.get_object("PlaylistView") self.widPlaylistStore = self.builder.get_object("PlaylistStore") self.widPosition = self.builder.get_object("Position") self.widPositionScale = self.builder.get_object("PositionScale") self.widPositionAt = self.builder.get_object("PositionAt") self.widPositionRemaining = self.builder.get_object("PositionRemaining") self.widBtnPause = self.builder.get_object("Pause") self.widBtnPlay = self.builder.get_object("Play") self.widStatus = self.builder.get_object("Status") handlers = { "onDestroy": self.onDestroy, "onFileOpen": self.onFileOpen, "onFileExit": self.onFileExit, "onPlaylistDblClick": self.onPlaylistDblClick, "onNewPosition": self.onNewPosition, "onPrevious": self.onPrevious, "onStop": self.onStop, "onPause": self.onPause, "onPlay": self.onPlay, "onNext": self.onNext, } self.builder.connect_signals(handlers) self.playlist = playlist.Playlist() if len(sys.argv) >= 2: # load initial playlist from command line self.playlist.read(sys.argv[1]) self.playlist.update(self.widPlaylistStore) self.widStatus.push(0, "TODO...") self.stEntryIdx = -1 # no entry selected self.stName = "" # no current entry name self.stDuration = 0 # current entry has zero size self.stPosition = 0 # at begin of current entry self.stPlaying = False # not playing gobject.timeout_add(10, self.onTimer10ms) self.updateEntry() self.updateButtonVisibility() def showPosition(self): """update the position texts next to the position slider""" # format current time and remaining time posAt = time_fmt.sec2str(self.stPosition) posRemaining = time_fmt.sec2str(self.stDuration - self.stPosition) self.widPositionAt.set_text(posAt) self.widPositionRemaining.set_text(posRemaining) def updatePositionState(self): """update the position in the state, but not the slider""" # calculate (virtual) start time of playing # i.e. the time the playing would have had started to arrive at the # current position now if it had played continuosly self.stPlayStart = time.time() - self.stPosition # update position texts self.showPosition() def updatePosition(self): """update the position including the position slider""" # update GUI slider self.widPositionScale.set_value(self.stPosition) # update position state self.updatePositionState() def updateDuration(self): """update the duration (i.e. range for the slider) based on the current playlist entry""" # get duration of new playlist entry self.stDuration = 0 if self.stEntryIdx >= 0: entry = self.playlist.entries[self.stEntryIdx] if entry["type"] == "normal": self.stDuration = entry["duration"] # set position to begin self.stPosition = 0 # update value range self.widPosition.set_upper(self.stDuration) # update position of slider self.updatePosition() def updateEntry(self): """update current entry of playlist and duration, position, ...""" # clear selection of playlist sel = self.widPlaylistView.get_selection() if sel: sel.unselect_all() # sanity check for entry index if self.stEntryIdx < -1 or self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = -1 # get name of current entry self.stName = "" if self.stEntryIdx >= 0 and \ self.playlist.entries[self.stEntryIdx]["type"] == "normal": self.stName = self.playlist.entries[self.stEntryIdx]["name"] # make current entry bold, all others non-bold def update(model, path, it, user_data): (idx,) = model.get(it, 0) if idx == self.stEntryIdx: weight = pango.WEIGHT_BOLD else: weight = pango.WEIGHT_NORMAL model.set(it, 1, weight) self.widPlaylistStore.foreach(update, None) # playing and (no entry or stop entry) # -> stop playing and update button visibility if self.stPlaying and (self.stEntryIdx < 0 or \ self.playlist.entries[self.stEntryIdx]["type"] == "stop"): self.stPlaying = False self.updateButtonVisibility() # update duration, position, ... self.updateDuration() def updateButtonVisibility(self): """update the visibility of the buttons based on if playing or not""" self.widBtnPause.set_visible(self.stPlaying) self.widBtnPlay.set_visible(not self.stPlaying) def onDestroy(self, widget): """window will be destroyed""" Gtk.main_quit() def onFileOpen(self, widget): """File Open clicked in menu""" #print("DEBUG sync_gui File Open") # run file chooser dialog dialog = Gtk.FileChooserDialog("BlinkenArea Sync GUI - File Open..", self.widMainWindow, Gtk.FileChooserAction.OPEN, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) filt = Gtk.FileFilter() filt.set_name("All files") filt.add_pattern("*") dialog.add_filter(filt) response = dialog.run() if response == Gtk.ResponseType.OK: # dialog closed with OK -> load new playlist filename = dialog.get_filename() self.playlist.read(filename) self.playlist.update(self.widPlaylistStore) self.stEntryIdx = -1 # no entry selected self.stPlaying = False # not playing self.updateEntry() self.updateButtonVisibility() # cleanup dialog.destroy() def onFileExit(self, widget): """File Exit clicked in menu""" #print("DEBUG sync_gui File Exit") Gtk.main_quit() def onPlaylistDblClick(self, widget, row, col): """playlist entry has been double-clicked""" # get index of selected entry idx = -1 sel = self.widPlaylistView.get_selection() if sel is not None: (model, it) = sel.get_selected() if it is not None: (idx,) = model.get(it, 0) #print("DEBUG sync_gui playlist double-click idx=%d" % (idx)) # update playlist entry self.stEntryIdx = idx # set position to zero if playing if self.stPlaying: self.stPosition = 0 # update entry self.updateEntry() def onNewPosition(self, widget, scroll, value): """slider has been moved to a new position""" #print("DEBUG sync_gui new position " + str(value)); # clamp position to valid range if value < 0: value = 0 if value > self.stDuration: value = self.stDuration # update current position - and play start time if playing self.stPosition = value # update position state (do not touch the slider) self.updatePositionState() def onPrevious(self, widget): """previous button as been pressed""" #print("DEBUG sync_gui previous") # go to begin of previous entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx - 1 if self.stEntryIdx < 0: self.stEntryIdx = len(self.playlist.entries) - 1 self.updateEntry() def onStop(self, widget): """stop button has been pressed""" #print("DEBUG sync_gui stop") self.stPlaying = False self.stPosition = 0 # stop goes back to begin self.updatePosition() self.updateButtonVisibility() def onPause(self, widget): """pause button has been pressed""" #print("DEBUG sync_gui pause") self.stPlaying = False self.updateButtonVisibility() def onPlay(self, widget): """play button has been pressed""" #print("DEBUG sync_gui play") self.stPlaying = True self.updatePosition() self.updateButtonVisibility() def onNext(self, widget): """next button has been pressed""" #print("DEBUG sync_gui next") # go to begin of next entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx + 1 if self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = 0 self.updateEntry() def onTimer10ms(self): """timer callback, every 10ms""" # update position if playing if self.stPlaying: self.stPosition = time.time() - self.stPlayStart if self.stPosition >= self.stDuration: # end of entry reached --> go to begin of next entry (with wrap around) self.stPosition = 0 self.stEntryIdx = self.stEntryIdx + 1 if self.stEntryIdx >= len(self.playlist.entries): self.stEntryIdx = 0 self.updateEntry() else: self.updatePosition() return True # main application entry point if __name__ == "__main__": app = SyncGui() Gtk.main()