Bump to 1.0.6
[watersofshiloah] / src / windows / radio.py
1 import datetime
2 import logging
3
4 import gobject
5 import gtk
6
7 import hildonize
8 import util.misc as misc_utils
9 import util.time_utils as time_utils
10 import util.go_utils as go_utils
11 import banners
12 import presenter
13
14 import windows
15
16
17 _moduleLogger = logging.getLogger(__name__)
18
19
20 class RadioWindow(windows._base.BasicWindow):
21
22         def __init__(self, app, player, store, node):
23                 windows._base.BasicWindow.__init__(self, app, player, store)
24                 self._node = node
25                 self._childNode = None
26
27                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
28                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
29
30                 self._loadingBanner = banners.GenericBanner()
31
32                 self._programmingModel = gtk.ListStore(
33                         gobject.TYPE_STRING,
34                         gobject.TYPE_STRING,
35                 )
36
37                 textrenderer = gtk.CellRendererText()
38                 timeColumn = gtk.TreeViewColumn("Time")
39                 textrenderer.set_property("scale", 0.75)
40                 timeColumn.set_property("sizing", gtk.TREE_VIEW_COLUMN_FIXED)
41                 timeColumn.set_property("fixed-width", 80)
42                 timeColumn.pack_start(textrenderer, expand=True)
43                 timeColumn.add_attribute(textrenderer, "text", 0)
44
45                 textrenderer = gtk.CellRendererText()
46                 titleColumn = gtk.TreeViewColumn("Program")
47                 titleColumn.set_property("sizing", gtk.TREE_VIEW_COLUMN_FIXED)
48                 titleColumn.pack_start(textrenderer, expand=True)
49                 titleColumn.add_attribute(textrenderer, "text", 1)
50
51                 self._treeView = gtk.TreeView()
52                 self._treeView.set_property("fixed-height-mode", True)
53                 self._treeView.set_headers_visible(False)
54                 self._treeView.set_model(self._programmingModel)
55                 self._treeView.append_column(timeColumn)
56                 self._treeView.append_column(titleColumn)
57                 self._treeView.get_selection().connect("changed", self._on_row_changed)
58
59                 self._viewport = gtk.Viewport()
60                 self._viewport.add(self._treeView)
61
62                 self._treeScroller = gtk.ScrolledWindow()
63                 self._treeScroller.add(self._viewport)
64                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
65                 self._treeScroller = hildonize.hildonize_scrollwindow(self._treeScroller)
66
67                 self._presenter = presenter.StreamMiniPresenter(self._store)
68                 self._presenterNavigation = presenter.NavigationBox()
69                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
70                 self.connect_auto(self._presenterNavigation, "action", self._on_nav_action)
71                 self.connect_auto(self._presenterNavigation, "navigating", self._on_navigating)
72                 self.connect_auto(self._player, "error", self._on_player_error)
73
74                 self._radioLayout = gtk.VBox(False)
75                 self._radioLayout.pack_start(self._treeScroller, True, True)
76                 self._radioLayout.pack_start(self._presenterNavigation.toplevel, False, True)
77
78                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
79                 self._layout.pack_start(self._radioLayout, True, True)
80
81                 self._dateShown = datetime.datetime.now(tz=time_utils.Mountain)
82                 self._currentTime = self._dateShown
83                 self._update_title()
84
85                 self._continualUpdate = go_utils.Timeout(self._on_continual_update, once = False)
86                 self._continualUpdate.start(seconds=60)
87
88         def show(self):
89                 windows._base.BasicWindow.show(self)
90
91                 self._errorBanner.toplevel.hide()
92                 self._loadingBanner.toplevel.hide()
93
94                 self._refresh()
95
96         def jump_to(self, node):
97                 _moduleLogger.info("Only 1 channel, nothing to jump to")
98
99         def _update_time(self, newTime):
100                 oldTime = self._dateShown
101                 self._dateShown = newTime
102                 if newTime.date() == oldTime.date():
103                         self._select_row()
104                 else:
105                         self._update_title()
106                         self._refresh()
107
108         def _update_title(self):
109                 self._window.set_title("%s - %s" % (self._node.title, self._dateShown.strftime("%m/%d")))
110
111         @property
112         def _active(self):
113                 return self._player.node is self._childNode
114
115         def _set_context(self, state):
116                 if state == self._player.STATE_PLAY:
117                         if self._active:
118                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
119                         else:
120                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
121                 elif state == self._player.STATE_PAUSE:
122                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
123                 elif state == self._player.STATE_STOP:
124                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
125                 else:
126                         _moduleLogger.info("Unhandled player state %s" % state)
127                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
128
129         def _show_loading(self):
130                 animationPath = self._store.STORE_LOOKUP["loading"]
131                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
132                 self._loadingBanner.show(animation, "Loading...")
133
134         def _hide_loading(self):
135                 self._loadingBanner.hide()
136
137         def _refresh(self):
138                 self._show_loading()
139                 self._programmingModel.clear()
140                 self._node.get_children(
141                         self._on_channels,
142                         self._on_load_error,
143                 )
144                 self._set_context(self._player.state)
145
146         def _get_current_row(self):
147                 nowTime = self._dateShown.strftime("%H:%M:%S")
148                 i = 0
149                 for i, row in enumerate(self._programmingModel):
150                         if nowTime < row[0]:
151                                 if i == 0:
152                                         return 0
153                                 else:
154                                         return i - 1
155                 else:
156                         return i
157
158         @misc_utils.log_exception(_moduleLogger)
159         def _on_player_error(self, player, err, debug):
160                 assert not self._isDestroyed
161                 _moduleLogger.error("%r - %r" % (err, debug))
162                 self._errorBanner.push_message(err)
163
164         @misc_utils.log_exception(_moduleLogger)
165         def _on_continual_update(self, *args):
166                 if self._isDestroyed:
167                         return False
168                 newTime = datetime.datetime.now(tz=time_utils.Mountain)
169                 oldTime = self._currentTime
170                 shownTime = self._dateShown
171
172                 self._currentTime = newTime
173                 if shownTime.date() == oldTime.date():
174                         _moduleLogger.info("Today selected, updating selection")
175                         self._update_time(newTime)
176                 return True
177
178         @misc_utils.log_exception(_moduleLogger)
179         def _on_player_state_change(self, player, newState):
180                 if self._presenterNavigation.is_active():
181                         return
182
183                 self._set_context(newState)
184
185         @misc_utils.log_exception(_moduleLogger)
186         def _on_player_title_change(self, player, node):
187                 if node is not self._childNode or node is None:
188                         _moduleLogger.info("Player title magically changed to %s" % player.title)
189                         return
190
191         @misc_utils.log_exception(_moduleLogger)
192         def _on_navigating(self, widget, navState):
193                 if navState == "clicking":
194                         if self._player.state == self._player.STATE_PLAY:
195                                 if self._active:
196                                         imageName = "pause_pressed"
197                                 else:
198                                         imageName = "play_pressed"
199                         elif self._player.state == self._player.STATE_PAUSE:
200                                 imageName = "play_pressed"
201                         elif self._player.state == self._player.STATE_STOP:
202                                 imageName = "play_pressed"
203                         else:
204                                 imageName = "play_pressed"
205                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
206                 elif navState == "down":
207                         imageName = "home"
208                 else:
209                         if self._player.state == self._player.STATE_PLAY:
210                                 imageName = "pause"
211                         else:
212                                 imageName = "play"
213
214                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
215
216         @misc_utils.log_exception(_moduleLogger)
217         def _on_nav_action(self, widget, navState):
218                 self._set_context(self._player.state)
219
220                 if navState == "clicking":
221                         if self._player.state == self._player.STATE_PLAY:
222                                 if self._active:
223                                         self._player.pause()
224                                 else:
225                                         self._player.set_piece_by_node(self._childNode)
226                                         self._player.play()
227                         elif self._player.state == self._player.STATE_PAUSE:
228                                 self._player.play()
229                         elif self._player.state == self._player.STATE_STOP:
230                                 self._player.set_piece_by_node(self._childNode)
231                                 self._player.play()
232                         else:
233                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
234                 elif navState == "down":
235                         self.window.destroy()
236                 elif navState == "up":
237                         pass
238                 elif navState == "left":
239                         self._update_time(self._dateShown + datetime.timedelta(days=1))
240                 elif navState == "right":
241                         self._update_time(self._dateShown - datetime.timedelta(days=1))
242
243         @misc_utils.log_exception(_moduleLogger)
244         def _on_channels(self, channels):
245                 if self._isDestroyed:
246                         _moduleLogger.info("Download complete but window destroyed")
247                         return
248
249                 channels = channels
250                 if 1 < len(channels):
251                         _moduleLogger.warning("More channels now available!")
252                 self._childNode = channels[0]
253                 self._childNode.get_programming(
254                         self._dateShown,
255                         self._on_channel,
256                         self._on_load_error,
257                 )
258
259         @misc_utils.log_exception(_moduleLogger)
260         def _on_channel(self, programs):
261                 if self._isDestroyed:
262                         _moduleLogger.info("Download complete but window destroyed")
263                         return
264
265                 self._hide_loading()
266                 for program in programs:
267                         row = program["time"], program["title"]
268                         self._programmingModel.append(row)
269
270                 currentDate = self._currentTime
271                 if currentDate.date() != self._dateShown.date():
272                         self._treeView.get_selection().set_mode(gtk.SELECTION_NONE)
273                 else:
274                         self._treeView.get_selection().set_mode(gtk.SELECTION_SINGLE)
275                         self._select_row()
276                         go_utils.Async(self._on_delay_scroll).start()
277
278         @misc_utils.log_exception(_moduleLogger)
279         def _on_delay_scroll(self, *args):
280                 self._scroll_to_row()
281
282         @misc_utils.log_exception(_moduleLogger)
283         def _on_load_error(self, exception):
284                 self._hide_loading()
285                 self._errorBanner.push_message(str(exception))
286
287         @misc_utils.log_exception(_moduleLogger)
288         def _on_row_changed(self, selection):
289                 if len(self._programmingModel) == 0:
290                         return
291
292                 # Undo the user's changing of the selection
293                 self._select_row()
294
295         def _select_row(self):
296                 rowIndex = self._get_current_row()
297                 if rowIndex < 0:
298                         return
299                 path = (rowIndex, )
300                 if not self._treeView.get_selection().path_is_selected(path):
301                         self._treeView.get_selection().select_path(path)
302
303         def _scroll_to_row(self):
304                 if self._isDestroyed:
305                         return
306                 rowIndex = self._get_current_row()
307                 if rowIndex < 0:
308                         return
309
310                 path = (rowIndex, )
311                 self._treeView.scroll_to_cell(path)
312
313                 treeViewHeight = self._treeView.get_allocation().height
314                 viewportHeight = self._viewport.get_allocation().height
315
316                 viewsPerPort = treeViewHeight / float(viewportHeight)
317                 maxRows = len(self._programmingModel)
318                 percentThrough = rowIndex / float(maxRows)
319                 dxByIndex = int(viewsPerPort * percentThrough * viewportHeight)
320
321                 dxMax = max(treeViewHeight - viewportHeight, 0)
322
323                 dx = min(dxByIndex, dxMax)
324                 adjustment = self._treeScroller.get_vadjustment()
325                 adjustment.value = dx
326
327
328 gobject.type_register(RadioWindow)