Stefan Schuermans commited on 2013-11-22 20:44:32
Showing 4 changed files, with 223 additions and 71 deletions.
| ... | ... |
@@ -5,11 +5,31 @@ import re |
| 5 | 5 |
import time_fmt |
| 6 | 6 |
|
| 7 | 7 |
class Playlist: |
| 8 |
+ """ playlist object, reads a playlist form a file and handles the playlist |
|
| 9 |
+ entries |
|
| 10 |
+ |
|
| 11 |
+ playlist file format: |
|
| 12 |
+ - each line is an entry or a stop point: <line> = <entry> | <stop point> |
|
| 13 |
+ - entries are played one after another |
|
| 14 |
+ - playing halts at stop points |
|
| 15 |
+ - <entry> = <name> <whitespace> <duration> |
|
| 16 |
+ - <stop point> = "" |
|
| 17 |
+ - <name> = [A-Za-Z0-9_]+ |
|
| 18 |
+ - <duration> = ((<hours>:)?<minutes>:)?<seconds> |
|
| 19 |
+ - <hours> = [0-9]+ |
|
| 20 |
+ - <minutes> = [0-9]+ |
|
| 21 |
+ - <seconds> = [0-9]+(.[0-9]+)""" |
|
| 22 |
+ |
|
| 8 | 23 |
def __init__(self): |
| 9 |
- self.entries = [] |
|
| 24 |
+ """create a new, empty playlist object""" |
|
| 25 |
+ self.entries = [] # list of entries |
|
| 26 |
+ # entry = dictionary { "type": "normal" or "stop"
|
|
| 27 |
+ # "name": string, name of entry |
|
| 28 |
+ # "durtaion": float, in seconds } |
|
| 10 | 29 |
self.reEntry = re.compile("^\s*([A-Za-z0-9_]+)\s+([0-9:.]+)\s*$")
|
| 11 | 30 |
|
| 12 | 31 |
def read(self, filename): |
| 32 |
+ """read the playlist from filename, replacing the current playlist""" |
|
| 13 | 33 |
self.entries = [] |
| 14 | 34 |
f = open(filename, "r") |
| 15 | 35 |
for line in f: |
| ... | ... |
@@ -28,7 +48,10 @@ class Playlist: |
| 28 | 48 |
f.close() |
| 29 | 49 |
|
| 30 | 50 |
def update(self, store): |
| 51 |
+ """update the contents of a Gtk ListStore with the contents of this |
|
| 52 |
+ playlist""" |
|
| 31 | 53 |
store.clear() |
| 54 |
+ idx = 0 |
|
| 32 | 55 |
for entry in self.entries: |
| 33 | 56 |
if entry["type"] == "normal": |
| 34 | 57 |
name = entry["name"] |
| ... | ... |
@@ -36,5 +59,6 @@ class Playlist: |
| 36 | 59 |
else: |
| 37 | 60 |
name = "" |
| 38 | 61 |
duration = "STOP" |
| 39 |
- store.append([name, duration]) |
|
| 62 |
+ store.append([idx, name, duration]) |
|
| 63 |
+ idx = idx + 1 |
|
| 40 | 64 |
|
| ... | ... |
@@ -1,28 +1,6 @@ |
| 1 | 1 |
<?xml version="1.0" encoding="UTF-8"?> |
| 2 | 2 |
<interface> |
| 3 | 3 |
<!-- interface-requires gtk+ 3.0 --> |
| 4 |
- <object class="GtkListStore" id="PlaylistStore"> |
|
| 5 |
- <columns> |
|
| 6 |
- <!-- column-name Name --> |
|
| 7 |
- <column type="gchararray"/> |
|
| 8 |
- <!-- column-name Dauer --> |
|
| 9 |
- <column type="gchararray"/> |
|
| 10 |
- </columns> |
|
| 11 |
- <data> |
|
| 12 |
- <row> |
|
| 13 |
- <col id="0" translatable="yes">Erster Akt</col> |
|
| 14 |
- <col id="1" translatable="yes"/> |
|
| 15 |
- </row> |
|
| 16 |
- <row> |
|
| 17 |
- <col id="0" translatable="yes">Zweiter Akt</col> |
|
| 18 |
- <col id="1" translatable="yes"/> |
|
| 19 |
- </row> |
|
| 20 |
- <row> |
|
| 21 |
- <col id="0" translatable="yes">Weltuntergang</col> |
|
| 22 |
- <col id="1" translatable="yes"/> |
|
| 23 |
- </row> |
|
| 24 |
- </data> |
|
| 25 |
- </object> |
|
| 26 | 4 |
<object class="GtkWindow" id="MainWindow"> |
| 27 | 5 |
<property name="visible">True</property> |
| 28 | 6 |
<property name="can_focus">False</property> |
| ... | ... |
@@ -46,6 +24,8 @@ |
| 46 | 24 |
<property name="visible">True</property> |
| 47 | 25 |
<property name="can_focus">True</property> |
| 48 | 26 |
<property name="model">PlaylistStore</property> |
| 27 |
+ <signal name="cursor-changed" handler="onPlaylistClick" swapped="no"/> |
|
| 28 |
+ <signal name="row-activated" handler="onPlaylistDblClick" swapped="no"/> |
|
| 49 | 29 |
<child internal-child="selection"> |
| 50 | 30 |
<object class="GtkTreeSelection" id="treeview-selection2"/> |
| 51 | 31 |
</child> |
| ... | ... |
@@ -301,6 +281,33 @@ |
| 301 | 281 |
</object> |
| 302 | 282 |
</child> |
| 303 | 283 |
</object> |
| 284 |
+ <object class="GtkListStore" id="PlaylistStore"> |
|
| 285 |
+ <columns> |
|
| 286 |
+ <!-- column-name EntryIdx --> |
|
| 287 |
+ <column type="guint"/> |
|
| 288 |
+ <!-- column-name Name --> |
|
| 289 |
+ <column type="gchararray"/> |
|
| 290 |
+ <!-- column-name Dauer --> |
|
| 291 |
+ <column type="gchararray"/> |
|
| 292 |
+ </columns> |
|
| 293 |
+ <data> |
|
| 294 |
+ <row> |
|
| 295 |
+ <col id="0">0</col> |
|
| 296 |
+ <col id="1" translatable="yes">Erster Akt</col> |
|
| 297 |
+ <col id="2" translatable="yes">1:00:00</col> |
|
| 298 |
+ </row> |
|
| 299 |
+ <row> |
|
| 300 |
+ <col id="0">1</col> |
|
| 301 |
+ <col id="1" translatable="yes">Zweiter Akt</col> |
|
| 302 |
+ <col id="2" translatable="yes">23:42</col> |
|
| 303 |
+ </row> |
|
| 304 |
+ <row> |
|
| 305 |
+ <col id="0">2</col> |
|
| 306 |
+ <col id="1" translatable="yes">Weltuntergang</col> |
|
| 307 |
+ <col id="2" translatable="yes">0.5</col> |
|
| 308 |
+ </row> |
|
| 309 |
+ </data> |
|
| 310 |
+ </object> |
|
| 304 | 311 |
<object class="GtkAdjustment" id="Position"> |
| 305 | 312 |
<property name="upper">100</property> |
| 306 | 313 |
<property name="value">50</property> |
| ... | ... |
@@ -2,6 +2,8 @@ |
| 2 | 2 |
|
| 3 | 3 |
import os |
| 4 | 4 |
from gi.repository import Gtk |
| 5 |
+import gobject |
|
| 6 |
+import time |
|
| 5 | 7 |
|
| 6 | 8 |
import playlist |
| 7 | 9 |
import time_fmt |
| ... | ... |
@@ -9,21 +11,25 @@ import time_fmt |
| 9 | 11 |
scriptdir = os.path.dirname(os.path.abspath(__file__)) |
| 10 | 12 |
|
| 11 | 13 |
class SyncGui: |
| 14 |
+ |
|
| 12 | 15 |
def __init__(self): |
| 16 |
+ """construct a SyncGui object""" |
|
| 13 | 17 |
self.builder = Gtk.Builder() |
| 14 | 18 |
self.builder.add_from_file(scriptdir + "/sync_gui.glade") |
| 15 |
- self.playlistView = self.builder.get_object("PlaylistView")
|
|
| 16 |
- self.playlistStore = self.builder.get_object("PlaylistStore")
|
|
| 17 |
- self.position = self.builder.get_object("Position")
|
|
| 18 |
- self.positionScale = self.builder.get_object("PositionScale")
|
|
| 19 |
- self.positionAt = self.builder.get_object("PositionAt")
|
|
| 20 |
- self.positionRemaining = self.builder.get_object("PositionRemaining")
|
|
| 21 |
- self.btnPause = self.builder.get_object("Pause")
|
|
| 22 |
- self.btnPlay = self.builder.get_object("Play")
|
|
| 23 |
- self.status = self.builder.get_object("Status")
|
|
| 19 |
+ self.widPlaylistView = self.builder.get_object("PlaylistView")
|
|
| 20 |
+ self.widPlaylistStore = self.builder.get_object("PlaylistStore")
|
|
| 21 |
+ self.widPosition = self.builder.get_object("Position")
|
|
| 22 |
+ self.widPositionScale = self.builder.get_object("PositionScale")
|
|
| 23 |
+ self.widPositionAt = self.builder.get_object("PositionAt")
|
|
| 24 |
+ self.widPositionRemaining = self.builder.get_object("PositionRemaining")
|
|
| 25 |
+ self.widBtnPause = self.builder.get_object("Pause")
|
|
| 26 |
+ self.widBtnPlay = self.builder.get_object("Play")
|
|
| 27 |
+ self.widStatus = self.builder.get_object("Status")
|
|
| 24 | 28 |
self.configPlaylistColumns() |
| 25 | 29 |
handlers = {
|
| 26 | 30 |
"onDestroy": self.onDestroy, |
| 31 |
+ "onPlaylistClick": self.onPlaylistClick, |
|
| 32 |
+ "onPlaylistDblClick": self.onPlaylistDblClick, |
|
| 27 | 33 |
"onNewPosition": self.onNewPosition, |
| 28 | 34 |
"onPrevious": self.onPrevious, |
| 29 | 35 |
"onBackward": self.onBackward, |
| ... | ... |
@@ -36,68 +42,170 @@ class SyncGui: |
| 36 | 42 |
self.builder.connect_signals(handlers) |
| 37 | 43 |
self.playlist = playlist.Playlist() |
| 38 | 44 |
self.playlist.read("playlist.txt")
|
| 39 |
- self.playlist.update(self.playlistStore) |
|
| 40 |
- self.status.push(0, "TODO...") |
|
| 41 |
- self.showCurrentPosition() |
|
| 45 |
+ self.playlist.update(self.widPlaylistStore) |
|
| 46 |
+ self.widStatus.push(0, "TODO...") |
|
| 47 |
+ self.stEntryIdx = -1 # no entry selected |
|
| 48 |
+ self.stName = "" # no current entry name |
|
| 49 |
+ self.stDuration = 0 # current entry has zero size |
|
| 50 |
+ self.stPosition = 0 # at begin of current entry |
|
| 51 |
+ self.stPlaying = False # not playing |
|
| 52 |
+ gobject.timeout_add(10, self.onTimer10ms) |
|
| 53 |
+ self.updateDuration() |
|
| 54 |
+ self.updateButtonVisibility() |
|
| 42 | 55 |
|
| 43 | 56 |
def configPlaylistColumns(self): |
| 44 |
- i = 0 |
|
| 57 |
+ """configure the columns of the playlist widget at program start""" |
|
| 58 |
+ i = 1 # first column is index (not shown) |
|
| 45 | 59 |
for title in ["Name", "Dauer"]: |
| 46 | 60 |
column = Gtk.TreeViewColumn(title) |
| 47 |
- self.playlistView.append_column(column) |
|
| 61 |
+ self.widPlaylistView.append_column(column) |
|
| 48 | 62 |
cell = Gtk.CellRendererText() |
| 49 | 63 |
column.pack_start(cell, False) |
| 50 | 64 |
column.add_attribute(cell, "text", i) |
| 51 | 65 |
i = i + 1 |
| 52 | 66 |
|
| 53 |
- def showPosition(self, sec): |
|
| 54 |
- if sec < 0: |
|
| 55 |
- sec = 0 |
|
| 56 |
- if sec > self.position.get_upper(): |
|
| 57 |
- sec = self.position.get_upper() |
|
| 58 |
- posAt = time_fmt.sec2str(sec) |
|
| 59 |
- posRemaining = time_fmt.sec2str(self.position.get_upper() - sec) |
|
| 60 |
- self.positionAt.set_text(posAt) |
|
| 61 |
- self.positionRemaining.set_text(posRemaining) |
|
| 62 |
- |
|
| 63 |
- def showCurrentPosition(self): |
|
| 64 |
- sec = self.positionScale.get_value() |
|
| 65 |
- self.showPosition(sec) |
|
| 67 |
+ def showPosition(self): |
|
| 68 |
+ """update the position texts next to the position slider""" |
|
| 69 |
+ # format current time and remaining time |
|
| 70 |
+ posAt = time_fmt.sec2str(self.stPosition) |
|
| 71 |
+ posRemaining = time_fmt.sec2str(self.stDuration - self.stPosition) |
|
| 72 |
+ self.widPositionAt.set_text(posAt) |
|
| 73 |
+ self.widPositionRemaining.set_text(posRemaining) |
|
| 74 |
+ |
|
| 75 |
+ def updatePositionState(self): |
|
| 76 |
+ """update the position in the state, but not the slider""" |
|
| 77 |
+ # calculate (virtual) start time of playing |
|
| 78 |
+ # i.e. the time the playing would have had started to arrive at the |
|
| 79 |
+ # current position now if it had played continuosly |
|
| 80 |
+ self.stPlayStart = time.time() - self.stPosition |
|
| 81 |
+ # update position texts |
|
| 82 |
+ self.showPosition() |
|
| 83 |
+ |
|
| 84 |
+ def updatePosition(self): |
|
| 85 |
+ """update the position including the position slider""" |
|
| 86 |
+ # update GUI slider |
|
| 87 |
+ self.widPositionScale.set_value(self.stPosition) |
|
| 88 |
+ # update position state |
|
| 89 |
+ self.updatePositionState() |
|
| 90 |
+ |
|
| 91 |
+ def updateDuration(self): |
|
| 92 |
+ """update the duration (i.e. range for the slider) based on the current |
|
| 93 |
+ playlist entry""" |
|
| 94 |
+ # get duration of new playlist entry |
|
| 95 |
+ self.stDuration = 0 |
|
| 96 |
+ if self.stEntryIdx >= 0: |
|
| 97 |
+ entry = self.playlist.entries[self.stEntryIdx] |
|
| 98 |
+ if entry["type"] == "normal": |
|
| 99 |
+ self.stDuration = entry["duration"] |
|
| 100 |
+ # set position to begin |
|
| 101 |
+ self.stPosition = 0 |
|
| 102 |
+ # update value range |
|
| 103 |
+ self.widPosition.set_upper(self.stDuration) |
|
| 104 |
+ # update position of slider |
|
| 105 |
+ self.updatePosition() |
|
| 106 |
+ |
|
| 107 |
+ def updateButtonVisibility(self): |
|
| 108 |
+ """update the visibility of the buttons based on if playing or not""" |
|
| 109 |
+ self.widBtnPause.set_visible(self.stPlaying) |
|
| 110 |
+ self.widBtnPlay.set_visible(not self.stPlaying) |
|
| 66 | 111 |
|
| 67 | 112 |
def onDestroy(self, widget): |
| 113 |
+ """window will be destroyed""" |
|
| 68 | 114 |
Gtk.main_quit() |
| 69 | 115 |
|
| 116 |
+ def onPlaylistClick(self, widget): |
|
| 117 |
+ """playlist entry has been clicked or selected""" |
|
| 118 |
+ # get index of selected entry |
|
| 119 |
+ idx = -1 |
|
| 120 |
+ sel = self.widPlaylistView.get_selection() |
|
| 121 |
+ if sel is not None: |
|
| 122 |
+ (model, it) = sel.get_selected() |
|
| 123 |
+ if it is not None: |
|
| 124 |
+ (idx, ) = model.get(it, 0) |
|
| 125 |
+ print("DEBUG: playlist click idx=%d" % (idx))
|
|
| 126 |
+ # update playlist entry |
|
| 127 |
+ self.stEntryIdx = idx |
|
| 128 |
+ # update duration |
|
| 129 |
+ self.updateDuration() |
|
| 130 |
+ |
|
| 131 |
+ def onPlaylistDblClick(self, widget, row, col): |
|
| 132 |
+ """playlist entry has been double-clicked""" |
|
| 133 |
+ # get index of selected entry |
|
| 134 |
+ idx = -1 |
|
| 135 |
+ sel = self.widPlaylistView.get_selection() |
|
| 136 |
+ if sel is not None: |
|
| 137 |
+ (model, it) = sel.get_selected() |
|
| 138 |
+ if it is not None: |
|
| 139 |
+ (idx, ) = model.get(it, 0) |
|
| 140 |
+ print("DEBUG: playlist double-click idx=%d" % (idx))
|
|
| 141 |
+ # update playlist entry |
|
| 142 |
+ self.stEntryIdx = idx |
|
| 143 |
+ # update duration |
|
| 144 |
+ self.updateDuration() |
|
| 145 |
+ # start playing |
|
| 146 |
+ # TODO |
|
| 147 |
+ |
|
| 70 | 148 |
def onNewPosition(self, widget, scroll, value): |
| 71 |
- print("new position " + str(value));
|
|
| 72 |
- self.showPosition(value) |
|
| 149 |
+ """slider has been moved to a new position""" |
|
| 150 |
+ print("DEBUG: new position " + str(value));
|
|
| 151 |
+ # clamp position to valid range |
|
| 152 |
+ if value < 0: |
|
| 153 |
+ value = 0 |
|
| 154 |
+ if value > self.stDuration: |
|
| 155 |
+ value = self.stDuration |
|
| 156 |
+ # update current position - and play start time if playing |
|
| 157 |
+ self.stPosition = value |
|
| 158 |
+ # update position state (do not touch the slider) |
|
| 159 |
+ self.updatePositionState() |
|
| 73 | 160 |
|
| 74 | 161 |
def onPrevious(self, widget): |
| 75 |
- print("previous")
|
|
| 162 |
+ """previous button as been pressed""" |
|
| 163 |
+ print("DEBUG: previous")
|
|
| 76 | 164 |
|
| 77 | 165 |
def onBackward(self, widget): |
| 78 |
- print("backward")
|
|
| 166 |
+ """backward button has been pressed""" |
|
| 167 |
+ print("DEBUG: backward")
|
|
| 79 | 168 |
|
| 80 | 169 |
def onStop(self, widget): |
| 81 |
- print("stop")
|
|
| 82 |
- self.btnPause.set_visible(False) |
|
| 83 |
- self.btnPlay.set_visible(True) |
|
| 170 |
+ """stop button has been pressed""" |
|
| 171 |
+ print("DEBUG: stop")
|
|
| 172 |
+ self.stPlaying = False |
|
| 173 |
+ self.stPosition = 0 # stop goes back to begin |
|
| 174 |
+ self.updatePosition() |
|
| 175 |
+ self.updateButtonVisibility() |
|
| 84 | 176 |
|
| 85 | 177 |
def onPause(self, widget): |
| 86 |
- print("pause")
|
|
| 87 |
- self.btnPause.set_visible(False) |
|
| 88 |
- self.btnPlay.set_visible(True) |
|
| 178 |
+ """pause button has been pressed""" |
|
| 179 |
+ print("DEBUG: pause")
|
|
| 180 |
+ self.stPlaying = False |
|
| 181 |
+ self.updateButtonVisibility() |
|
| 89 | 182 |
|
| 90 | 183 |
def onPlay(self, widget): |
| 91 |
- print("play")
|
|
| 92 |
- self.btnPause.set_visible(True) |
|
| 93 |
- self.btnPlay.set_visible(False) |
|
| 184 |
+ """play button has been pressed""" |
|
| 185 |
+ print("DEBUG: play")
|
|
| 186 |
+ self.stPlaying = True |
|
| 187 |
+ self.updatePosition() |
|
| 188 |
+ self.updateButtonVisibility() |
|
| 94 | 189 |
|
| 95 | 190 |
def onForward(self, widget): |
| 96 |
- print("forward")
|
|
| 191 |
+ """forward button has been pressed""" |
|
| 192 |
+ print("DEBUG: forward")
|
|
| 97 | 193 |
|
| 98 | 194 |
def onNext(self, widget): |
| 99 |
- print("next")
|
|
| 100 |
- |
|
| 195 |
+ """next button has been pressed""" |
|
| 196 |
+ print("DEBUG: next")
|
|
| 197 |
+ |
|
| 198 |
+ def onTimer10ms(self): |
|
| 199 |
+ """timer callback, every 10ms""" |
|
| 200 |
+ # update position if playing |
|
| 201 |
+ if self.stPlaying: |
|
| 202 |
+ self.stPosition = time.time() - self.stPlayStart |
|
| 203 |
+ if self.stPosition > self.stDuration: |
|
| 204 |
+ self.stPosition = 0 # FIXME: go to next entry |
|
| 205 |
+ self.updatePosition() |
|
| 206 |
+ return True |
|
| 207 |
+ |
|
| 208 |
+# main application entry point |
|
| 101 | 209 |
if __name__ == "__main__": |
| 102 | 210 |
app = SyncGui() |
| 103 | 211 |
Gtk.main() |
| ... | ... |
@@ -1,12 +1,24 @@ |
| 1 | 1 |
#! /usr/bin/env python |
| 2 | 2 |
|
| 3 |
+"""time format converter |
|
| 4 |
+ |
|
| 5 |
+converts between time in seconds as floating point value and time as |
|
| 6 |
+human-readable string in hours, minutes and seconds |
|
| 7 |
+ |
|
| 8 |
+<time string> = ((<hours>:)?<minutes>:)?<seconds> |
|
| 9 |
+<hours> = [0-9]+ |
|
| 10 |
+<minutes> = [0-9]+ |
|
| 11 |
+<seconds> = [0-9]+(.[0-9]+)""" |
|
| 12 |
+ |
|
| 3 | 13 |
def sec2str(sec): |
| 14 |
+ """convert time in seconds to human-readable time string""" |
|
| 4 | 15 |
sign = "" |
| 5 |
- if sec < 0: |
|
| 16 |
+ sec100 = round(sec * 100) |
|
| 17 |
+ if sec100 < 0: |
|
| 6 | 18 |
sign = "-"; |
| 7 |
- sec = -sec; |
|
| 8 |
- sec1 = int(sec) |
|
| 9 |
- sec100 = round((sec - sec1) * 100) |
|
| 19 |
+ sec100 = -sec100; |
|
| 20 |
+ sec1 = sec100 // 100 |
|
| 21 |
+ sec100 = sec100 % 100 |
|
| 10 | 22 |
minu = sec1 // 60 |
| 11 | 23 |
sec1 = sec1 % 60 |
| 12 | 24 |
hour = minu // 60 |
| ... | ... |
@@ -14,6 +26,7 @@ def sec2str(sec): |
| 14 | 26 |
return "%s%u:%02u:%02u.%02u" % (sign, hour, minu, sec1, sec100) |
| 15 | 27 |
|
| 16 | 28 |
def str2sec(str): |
| 29 |
+ """convert a human readable time string into time in seconds""" |
|
| 17 | 30 |
total = 0 |
| 18 | 31 |
section = 0 |
| 19 | 32 |
sign = 1 |
| 20 | 33 |