Navigating radio programs
[watersofshiloah] / src / windows.py
1 import ConfigParser
2 import datetime
3 import logging
4
5 import gobject
6 import gtk
7
8 import constants
9 import hildonize
10 import util.misc as misc_utils
11
12 import banners
13 import playcontrol
14 import presenter
15
16
17 _moduleLogger = logging.getLogger(__name__)
18
19
20 class BasicWindow(gobject.GObject):
21
22         __gsignals__ = {
23                 'quit' : (
24                         gobject.SIGNAL_RUN_LAST,
25                         gobject.TYPE_NONE,
26                         (),
27                 ),
28                 'fullscreen' : (
29                         gobject.SIGNAL_RUN_LAST,
30                         gobject.TYPE_NONE,
31                         (gobject.TYPE_PYOBJECT, ),
32                 ),
33         }
34
35         def __init__(self, player, store, index):
36                 gobject.GObject.__init__(self)
37                 self._isDestroyed = False
38
39                 self._player = player
40                 self._store = store
41                 self._index = index
42
43                 self._clipboard = gtk.clipboard_get()
44                 self._windowInFullscreen = False
45
46                 self._errorBanner = banners.StackingBanner()
47
48                 self._layout = gtk.VBox()
49                 self._layout.pack_start(self._errorBanner.toplevel, False, True)
50
51                 self._window = gtk.Window()
52                 self._window.add(self._layout)
53                 self._window = hildonize.hildonize_window(self, self._window)
54
55                 self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"]))
56                 self._window.connect("key-press-event", self._on_key_press)
57                 self._window.connect("window-state-event", self._on_window_state_change)
58                 self._window.connect("destroy", self._on_destroy)
59
60         @property
61         def window(self):
62                 return self._window
63
64         def save_settings(self, config, sectionName):
65                 config.add_section(sectionName)
66                 config.set(sectionName, "fullscreen", str(self._windowInFullscreen))
67
68         def load_settings(self, config, sectionName):
69                 try:
70                         self._windowInFullscreen = config.getboolean(sectionName, "fullscreen")
71                 except ConfigParser.NoSectionError, e:
72                         _moduleLogger.info(
73                                 "Settings file %s is missing section %s" % (
74                                         constants._user_settings_,
75                                         e.section,
76                                 )
77                         )
78
79                 if self._windowInFullscreen:
80                         self._window.fullscreen()
81                 else:
82                         self._window.unfullscreen()
83
84         @misc_utils.log_exception(_moduleLogger)
85         def _on_destroy(self, *args):
86                 self._isDestroyed = True
87
88         @misc_utils.log_exception(_moduleLogger)
89         def _on_window_state_change(self, widget, event, *args):
90                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
91                         self._windowInFullscreen = True
92                 else:
93                         self._windowInFullscreen = False
94                 self.emit("fullscreen", self._windowInFullscreen)
95
96         @misc_utils.log_exception(_moduleLogger)
97         def _on_key_press(self, widget, event, *args):
98                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
99                 isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
100                 if (
101                         event.keyval == gtk.keysyms.F6 or
102                         event.keyval in RETURN_TYPES and isCtrl
103                 ):
104                         # The "Full screen" hardware key has been pressed
105                         if self._windowInFullscreen:
106                                 self._window.unfullscreen ()
107                         else:
108                                 self._window.fullscreen ()
109                         return True
110                 elif (
111                         event.keyval in (gtk.keysyms.w, ) and
112                         event.get_state() & gtk.gdk.CONTROL_MASK
113                 ):
114                         self._window.destroy()
115                 elif (
116                         event.keyval in (gtk.keysyms.q, ) and
117                         event.get_state() & gtk.gdk.CONTROL_MASK
118                 ):
119                         self.emit("quit")
120                 elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
121                         with open(constants._user_logpath_, "r") as f:
122                                 logLines = f.xreadlines()
123                                 log = "".join(logLines)
124                                 self._clipboard.set_text(str(log))
125                         return True
126
127
128 class SourceSelector(BasicWindow):
129
130         def __init__(self, player, store, index):
131                 BasicWindow.__init__(self, player, store, index)
132
133                 self._radioButton = self._create_button("radio", "Radio")
134                 self._radioButton.connect("clicked", self._on_radio_selected)
135                 self._radioWrapper = gtk.VBox()
136                 self._radioWrapper.pack_start(self._radioButton, False, True)
137
138                 self._conferenceButton = self._create_button("conferences", "Conferences")
139                 #self._conferenceButton.connect("clicked", self._on_conference_selected)
140                 self._conferenceWrapper = gtk.VBox()
141                 self._conferenceWrapper.pack_start(self._conferenceButton, False, True)
142
143                 self._magazineButton = self._create_button("magazines", "Magazines")
144                 #self._magazineButton.connect("clicked", self._on_magazine_selected)
145                 self._magazineWrapper = gtk.VBox()
146                 self._magazineWrapper.pack_start(self._magazineButton, False, True)
147
148                 self._scriptureButton = self._create_button("scriptures", "Scriptures")
149                 #self._scriptureButton.connect("clicked", self._on_scripture_selected)
150                 self._scriptureWrapper = gtk.VBox()
151                 self._scriptureWrapper.pack_start(self._scriptureButton, False, True)
152
153                 self._buttonLayout = gtk.VBox(True, 5)
154                 self._buttonLayout.set_property("border-width", 5)
155                 self._buttonLayout.pack_start(self._radioWrapper, True, True)
156                 self._buttonLayout.pack_start(self._conferenceWrapper, True, True)
157                 self._buttonLayout.pack_start(self._magazineWrapper, True, True)
158                 self._buttonLayout.pack_start(self._scriptureWrapper, True, True)
159
160                 self._playcontrol = playcontrol.PlayControl(player, store)
161
162                 self._layout.pack_start(self._buttonLayout, True, True)
163                 self._layout.pack_start(self._playcontrol.toplevel, False, True)
164
165                 self._window.set_title(constants.__pretty_app_name__)
166                 self._window.show_all()
167                 self._errorBanner.toplevel.hide()
168                 self._playcontrol.toplevel.hide()
169
170         def _create_button(self, icon, message):
171                 image = self._store.get_image_from_store(self._store.STORE_LOOKUP[icon])
172
173                 label = gtk.Label()
174                 label.set_text(message)
175
176                 buttonLayout = gtk.HBox(False, 5)
177                 buttonLayout.pack_start(image, False, False)
178                 buttonLayout.pack_start(label, False, True)
179                 button = gtk.Button()
180                 button.add(buttonLayout)
181
182                 return button
183
184         @misc_utils.log_exception(_moduleLogger)
185         def _on_radio_selected(self, *args):
186                 radioView = RadioView(self._player, self._store, self._index)
187                 radioView.window.set_modal(True)
188                 radioView.window.set_transient_for(self._window)
189                 radioView.window.set_default_size(*self._window.get_size())
190
191
192 class RadioView(BasicWindow):
193
194         def __init__(self, player, store, index):
195                 BasicWindow.__init__(self, player, store, index)
196
197                 self._loadingBanner = banners.GenericBanner()
198
199                 headerPath = self._store.STORE_LOOKUP["radio_header"]
200                 self._header = self._store.get_image_from_store(headerPath)
201                 self._headerNavigation = presenter.NavigationBox()
202                 self._headerNavigation.toplevel.add(self._header)
203                 self._headerNavigation.connect("action", self._on_nav_action)
204
205
206                 self._programmingModel = gtk.ListStore(
207                         gobject.TYPE_STRING,
208                         gobject.TYPE_STRING,
209                 )
210
211                 textrenderer = gtk.CellRendererText()
212                 timeColumn = gtk.TreeViewColumn("Time")
213                 timeColumn.pack_start(textrenderer, expand=True)
214                 timeColumn.add_attribute(textrenderer, "text", 0)
215
216                 textrenderer = gtk.CellRendererText()
217                 titleColumn = gtk.TreeViewColumn("Program")
218                 titleColumn.pack_start(textrenderer, expand=True)
219                 titleColumn.add_attribute(textrenderer, "text", 1)
220
221                 self._treeView = gtk.TreeView()
222                 self._treeView.set_headers_visible(False)
223                 self._treeView.set_model(self._programmingModel)
224                 self._treeView.append_column(timeColumn)
225                 self._treeView.append_column(titleColumn)
226                 self._treeView.get_selection().connect("changed", self._on_row_changed)
227
228                 self._treeScroller = gtk.ScrolledWindow()
229                 self._treeScroller.add(self._treeView)
230                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
231
232                 self._presenter = presenter.StreamMiniPresenter(self._player, self._store)
233                 self._presenterNavigation = presenter.NavigationBox()
234                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
235                 self._presenterNavigation.connect("action", self._on_nav_action)
236
237                 self._radioLayout = gtk.VBox(False)
238                 self._radioLayout.pack_start(self._headerNavigation.toplevel, False, False)
239                 self._radioLayout.pack_start(self._treeScroller, True, True)
240                 self._radioLayout.pack_start(self._presenterNavigation.toplevel, False, True)
241
242                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
243                 self._layout.pack_start(self._radioLayout, True, True)
244
245                 self._window.set_title("Radio")
246                 self._window.show_all()
247                 self._errorBanner.toplevel.hide()
248                 self._loadingBanner.toplevel.hide()
249
250                 self._dateShown = datetime.datetime.now()
251                 self._refresh()
252
253         def _show_loading(self):
254                 animationPath = self._store.STORE_LOOKUP["loading"]
255                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
256                 self._loadingBanner.show(animation, "Loading...")
257
258         def _hide_loading(self):
259                 self._loadingBanner.hide()
260
261         def _refresh(self):
262                 self._show_loading()
263                 self._programmingModel.clear()
264                 self._index.download(
265                         "get_radio_channels",
266                         self._on_channels,
267                         self._on_load_error,
268                 )
269
270         def _get_current_row(self):
271                 nowTime = self._dateShown.strftime("%H:%M:%S")
272                 i = 0
273                 for i, row in enumerate(self._programmingModel):
274                         if nowTime < row[0]:
275                                 if i == 0:
276                                         return 0
277                                 else:
278                                         return i - 1
279                 else:
280                         return i
281
282         @misc_utils.log_exception(_moduleLogger)
283         def _on_nav_action(self, widget, navState):
284                 if navState == "clicking":
285                         pass
286                 elif navState == "down":
287                         self.window.destroy()
288                 elif navState == "up":
289                         pass
290                 elif navState == "left":
291                         self._dateShown += datetime.timedelta(days=1)
292                         self._refresh()
293                 elif navState == "right":
294                         self._dateShown -= datetime.timedelta(days=1)
295                         self._refresh()
296
297         @misc_utils.log_exception(_moduleLogger)
298         def _on_channels(self, channels):
299                 if self._isDestroyed:
300                         _moduleLogger.info("Download complete but window destroyed")
301                         return
302
303                 channels = list(channels)
304                 if 1 < len(channels):
305                         _moduleLogger.warning("More channels now available!")
306                 channel = channels[0]
307                 self._index.download(
308                         "get_radio_channel_programming",
309                         self._on_channel,
310                         self._on_load_error,
311                         channel["id"],
312                         self._dateShown
313                 )
314
315         @misc_utils.log_exception(_moduleLogger)
316         def _on_channel(self, programs):
317                 if self._isDestroyed:
318                         _moduleLogger.info("Download complete but window destroyed")
319                         return
320
321                 self._hide_loading()
322                 for program in programs:
323                         row = program["time"], program["title"]
324                         self._programmingModel.append(row)
325
326                 path = (self._get_current_row(), )
327                 self._treeView.scroll_to_cell(path)
328                 self._treeView.get_selection().select_path(path)
329
330         @misc_utils.log_exception(_moduleLogger)
331         def _on_load_error(self, exception):
332                 self._hide_loading()
333                 self._errorBanner.push_message(exception)
334
335         @misc_utils.log_exception(_moduleLogger)
336         def _on_row_changed(self, selection):
337                 if len(self._programmingModel) == 0:
338                         return
339
340                 rowIndex = self._get_current_row()
341                 path = (rowIndex, )
342                 if not selection.path_is_selected(path):
343                         # Undo the user's changing of the selection
344                         selection.select_path(path)