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 |