More connect_auto usage
[watersofshiloah] / src / presenter.py
1 import logging
2
3 import gobject
4 import pango
5 import gtk
6
7 import util.go_utils as go_utils
8 import util.misc as misc_utils
9
10
11 _moduleLogger = logging.getLogger(__name__)
12
13
14 class NavigationBox(gobject.GObject):
15
16         __gsignals__ = {
17                 'action' : (
18                         gobject.SIGNAL_RUN_LAST,
19                         gobject.TYPE_NONE,
20                         (gobject.TYPE_STRING, ),
21                 ),
22                 'navigating' : (
23                         gobject.SIGNAL_RUN_LAST,
24                         gobject.TYPE_NONE,
25                         (gobject.TYPE_STRING, ),
26                 ),
27         }
28
29         MINIMUM_MOVEMENT = 32
30
31         _NO_POSITION = -1, -1
32
33         def __init__(self):
34                 gobject.GObject.__init__(self)
35                 self._eventBox = gtk.EventBox()
36                 self._eventBox.connect("button_press_event", self._on_button_press)
37                 self._eventBox.connect("button_release_event", self._on_button_release)
38                 self._eventBox.connect("motion_notify_event", self._on_motion_notify)
39
40                 self._isPortrait = True
41                 self._clickPosition = self._NO_POSITION
42
43         @property
44         def toplevel(self):
45                 return self._eventBox
46
47         def set_orientation(self, orientation):
48                 if orientation == gtk.ORIENTATION_VERTICAL:
49                         self._isPortrait = True
50                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
51                         self._isPortrait = False
52                 else:
53                         raise NotImplementedError(orientation)
54
55         def is_active(self):
56                 return self._clickPosition != self._NO_POSITION
57
58         def get_state(self, newCoord):
59                 if self._clickPosition == self._NO_POSITION:
60                         return ""
61
62                 delta = (
63                         newCoord[0] - self._clickPosition[0],
64                         - (newCoord[1] - self._clickPosition[1])
65                 )
66                 absDelta = (abs(delta[0]), abs(delta[1]))
67                 if max(*absDelta) < self.MINIMUM_MOVEMENT:
68                         return "clicking"
69
70                 if absDelta[0] < absDelta[1]:
71                         if 0 < delta[1]:
72                                 return "up"
73                         else:
74                                 return "down"
75                 else:
76                         if 0 < delta[0]:
77                                 return "right"
78                         else:
79                                 return "left"
80
81         @misc_utils.log_exception(_moduleLogger)
82         def _on_button_press(self, widget, event):
83                 if self._clickPosition != self._NO_POSITION:
84                         _moduleLogger.debug("Ignoring double click")
85                 self._clickPosition = event.get_coords()
86
87                 self.emit("navigating", "clicking")
88
89         @misc_utils.log_exception(_moduleLogger)
90         def _on_button_release(self, widget, event):
91                 assert self._clickPosition != self._NO_POSITION
92                 try:
93                         mousePosition = event.get_coords()
94                         state = self.get_state(mousePosition)
95                         assert state
96                 finally:
97                         self._clickPosition = self._NO_POSITION
98                 self.emit("action", state)
99
100         @misc_utils.log_exception(_moduleLogger)
101         def _on_motion_notify(self, widget, event):
102                 if self._clickPosition == self._NO_POSITION:
103                         return
104
105                 mousePosition = event.get_coords()
106                 newState = self.get_state(mousePosition)
107                 self.emit("navigating", newState)
108
109
110 gobject.type_register(NavigationBox)
111
112
113 class StreamPresenter(object):
114
115         def __init__(self, store):
116                 self._store = store
117
118                 self._image = gtk.DrawingArea()
119                 self._image.connect("expose_event", self._on_expose)
120
121                 self._isPortrait = True
122
123                 self._backgroundImage = None
124                 self._title = ""
125                 self._subtitle = ""
126                 self._buttonImage = None
127                 self._imageName = ""
128                 self._dims = 0, 0
129
130         @property
131         def toplevel(self):
132                 return self._image
133
134         def set_orientation(self, orientation):
135                 if orientation == gtk.ORIENTATION_VERTICAL:
136                         self._isPortrait = True
137                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
138                         self._isPortrait = False
139                 else:
140                         raise NotImplementedError(orientation)
141
142                 self._image.queue_draw()
143
144         def set_state(self, stateImage):
145                 if stateImage == self._imageName:
146                         return
147                 self._imageName = stateImage
148                 self._buttonImage = self._store.get_surface_from_store(stateImage)
149
150                 self._image.queue_draw()
151
152         def set_context(self, backgroundImage, title, subtitle):
153                 self._backgroundImage = self._store.get_surface_from_store(backgroundImage)
154                 self._title = title
155                 self._subtitle = subtitle
156
157                 backWidth = self._backgroundImage.get_width()
158                 backHeight = self._backgroundImage.get_height()
159                 self._image.set_size_request(backWidth, backHeight)
160
161                 self._image.queue_draw()
162
163         @misc_utils.log_exception(_moduleLogger)
164         def _on_expose(self, widget, event):
165                 cairoContext = self._image.window.cairo_create()
166                 self._draw_presenter(cairoContext)
167
168         def _draw_presenter(self, cairoContext):
169                 rect = self._image.get_allocation()
170                 self._dims = rect.width, rect.height
171
172                 # Blank things
173                 cairoContext.rectangle(
174                         0,
175                         0,
176                         rect.width,
177                         rect.height,
178                 )
179                 cairoContext.set_source_rgb(0, 0, 0)
180                 cairoContext.fill()
181
182                 # Draw Background
183                 if self._backgroundImage is not None:
184                         cairoContext.set_source_surface(
185                                 self._backgroundImage,
186                                 0,
187                                 0,
188                         )
189                         cairoContext.paint()
190
191                 pangoContext = self._image.create_pango_context()
192
193                 titleLayout = pango.Layout(pangoContext)
194                 titleLayout.set_markup("<i>%s</i>" % self._subtitle)
195                 textWidth, textHeight = titleLayout.get_pixel_size()
196                 subtitleTextX = self._dims[0] / 2 - textWidth / 2
197                 subtitleTextY = self._dims[1] - textHeight - self._buttonImage.get_height() + 10
198
199                 subtitleLayout = pango.Layout(pangoContext)
200                 subtitleLayout.set_markup("<b>%s</b>" % self._title)
201                 textWidth, textHeight = subtitleLayout.get_pixel_size()
202                 textX = self._dims[0] / 2 - textWidth / 2
203                 textY = subtitleTextY - textHeight
204
205                 xPadding = min((self._dims[0] - textWidth) / 2 - 5, 5)
206                 yPadding = 5
207                 startContent = xPadding, textY - yPadding
208                 endContent = self._dims[0] - xPadding,  self._dims[1] - yPadding
209
210                 # Control background
211                 cairoContext.rectangle(
212                         startContent[0],
213                         startContent[1],
214                         endContent[0] - startContent[0],
215                         endContent[1] - startContent[1],
216                 )
217                 cairoContext.set_source_rgba(0.9, 0.9, 0.9, 0.75)
218                 cairoContext.fill()
219
220                 # title
221                 if self._title or self._subtitle:
222                         cairoContext.move_to(subtitleTextX, subtitleTextY)
223                         cairoContext.set_source_rgb(0, 0, 0)
224                         cairoContext.show_layout(titleLayout)
225
226                         cairoContext.move_to(textX, textY)
227                         cairoContext.set_source_rgb(0, 0, 0)
228                         cairoContext.show_layout(subtitleLayout)
229
230                 self._draw_state(cairoContext)
231
232         def _draw_state(self, cairoContext):
233                 if self._backgroundImage is None or self._buttonImage is None:
234                         return
235                 cairoContext.set_source_surface(
236                         self._buttonImage,
237                         self._dims[0] / 2 - self._buttonImage.get_width() / 2,
238                         self._dims[1] - self._buttonImage.get_height() + 5,
239                 )
240                 cairoContext.paint()
241
242
243 class StreamMiniPresenter(object):
244
245         def __init__(self, store):
246                 self._store = store
247
248                 self._button = gtk.Image()
249
250         @property
251         def toplevel(self):
252                 return self._button
253
254         def set_orientation(self, orientation):
255                 pass
256
257         def set_state(self, stateImage):
258                 self._store.set_image_from_store(self._button, stateImage)
259
260         def set_context(self, backgroundImage, title, subtitle):
261                 pass
262
263
264 class NavControl(gobject.GObject, go_utils.AutoSignal):
265
266         __gsignals__ = {
267                 'home' : (
268                         gobject.SIGNAL_RUN_LAST,
269                         gobject.TYPE_NONE,
270                         (),
271                 ),
272                 'jump-to' : (
273                         gobject.SIGNAL_RUN_LAST,
274                         gobject.TYPE_NONE,
275                         (gobject.TYPE_PYOBJECT, ),
276                 ),
277         }
278
279         def __init__(self, player, store):
280                 gobject.GObject.__init__(self)
281                 self._layout = gtk.HBox()
282                 go_utils.AutoSignal.__init__(self, self.toplevel)
283
284                 self._store = store
285
286                 self._controlButton = store.get_image_from_store(store.STORE_LOOKUP["play"])
287
288                 self._controlBox = NavigationBox()
289                 self._controlBox.toplevel.add(self._controlButton)
290                 self.connect_auto(self._controlBox, "action", self._on_nav_action)
291                 self.connect_auto(self._controlBox, "navigating", self._on_navigating)
292
293                 self._titleButton = gtk.Label()
294
295                 self._displayBox = NavigationBox()
296                 self._displayBox.toplevel.add(self._titleButton)
297                 self.connect_auto(self._displayBox, "action", self._on_nav_action)
298                 self.connect_auto(self._displayBox, "navigating", self._on_navigating)
299
300                 self._layout.pack_start(self._controlBox.toplevel, False, False)
301                 self._layout.pack_start(self._displayBox.toplevel, True, True)
302                 self._player = player
303                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
304                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
305                 self._titleButton.set_label(self._player.title)
306
307         def refresh(self):
308                 self._titleButton.set_label(self._player.title)
309                 self._set_context(self._player.state)
310
311         def _set_context(self, state):
312                 if state == self._player.STATE_PLAY:
313                         stateImage = self._store.STORE_LOOKUP["pause"]
314                         self._store.set_image_from_store(self._controlButton, stateImage)
315                         self.toplevel.show()
316                 elif state == self._player.STATE_PAUSE:
317                         stateImage = self._store.STORE_LOOKUP["play"]
318                         self._store.set_image_from_store(self._controlButton, stateImage)
319                         self.toplevel.show()
320                 elif state == self._player.STATE_STOP:
321                         self._titleButton.set_label("")
322                         self.toplevel.hide()
323                 else:
324                         _moduleLogger.info("Unhandled player state %s" % state)
325                         stateImage = self._store.STORE_LOOKUP["pause"]
326                         self._store.set_image_from_store(self._controlButton, stateImage)
327
328         @property
329         def toplevel(self):
330                 return self._layout
331
332         def set_orientation(self, orientation):
333                 pass
334
335         @misc_utils.log_exception(_moduleLogger)
336         def _on_player_state_change(self, player, newState):
337                 if self._controlBox.is_active() or self._displayBox.is_active():
338                         return
339
340                 self._set_context(newState)
341
342         @misc_utils.log_exception(_moduleLogger)
343         def _on_player_title_change(self, player, node):
344                 _moduleLogger.info("Title change: %s" % self._player.title)
345                 self._titleButton.set_label(self._player.title)
346
347         @misc_utils.log_exception(_moduleLogger)
348         def _on_navigating(self, widget, navState):
349                 if navState == "down":
350                         imageName = "home"
351                 elif navState == "clicking":
352                         if widget is self._controlBox:
353                                 if self._player.state == self._player.STATE_PLAY:
354                                         imageName = "pause_pressed"
355                                 else:
356                                         imageName = "play_pressed"
357                         else:
358                                 if self._player.state == self._player.STATE_PLAY:
359                                         imageName = "pause"
360                                 else:
361                                         imageName = "play"
362                 elif self._player.can_navigate:
363                         if navState == "up":
364                                 imageName = "play"
365                         elif navState == "left":
366                                 imageName = "next"
367                         elif navState == "right":
368                                 imageName = "prev"
369                 else:
370                         if self._player.state == self._player.STATE_PLAY:
371                                 imageName = "pause"
372                         else:
373                                 imageName = "play"
374
375                 imagePath = self._store.STORE_LOOKUP[imageName]
376                 self._store.set_image_from_store(self._controlButton, imagePath)
377
378         @misc_utils.log_exception(_moduleLogger)
379         def _on_nav_action(self, widget, navState):
380                 self._set_context(self._player.state)
381
382                 if navState == "clicking":
383                         if widget is self._controlBox:
384                                 if self._player.state == self._player.STATE_PLAY:
385                                         self._player.pause()
386                                 else:
387                                         self._player.play()
388                         elif widget is self._displayBox:
389                                 self.emit("jump-to", self._player.node)
390                         else:
391                                 raise NotImplementedError()
392                 elif navState == "down":
393                         self.emit("home")
394                 elif navState == "up":
395                         pass
396                 elif navState == "left":
397                         self._player.next()
398                 elif navState == "right":
399                         self._player.back()
400
401
402 gobject.type_register(NavControl)