Breaking free the navigation code so it can be reused in the radioview
[watersofshiloah] / src / presenter.py
1 import logging
2
3 import gobject
4 import pango
5 import cairo
6 import gtk
7
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 = 20
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                 if self._isPortrait:
63                         delta = (
64                                 newCoord[0] - self._clickPosition[0],
65                                 - (newCoord[1] - self._clickPosition[1])
66                         )
67                 else:
68                         delta = (
69                                 newCoord[1] - self._clickPosition[1],
70                                 - (newCoord[0] - self._clickPosition[0])
71                         )
72                 absDelta = (abs(delta[0]), abs(delta[1]))
73                 if max(*absDelta) < self.MINIMUM_MOVEMENT:
74                         return "clicking"
75
76                 if absDelta[0] < absDelta[1]:
77                         if 0 < delta[1]:
78                                 return "up"
79                         else:
80                                 return "down"
81                 else:
82                         if 0 < delta[0]:
83                                 return "right"
84                         else:
85                                 return "left"
86
87         @misc_utils.log_exception(_moduleLogger)
88         def _on_button_press(self, widget, event):
89                 if self._clickPosition != self._NO_POSITION:
90                         _moduleLogger.debug("Ignoring double click")
91                 self._clickPosition = event.get_coords()
92
93                 self.emit("navigating", "clicking")
94
95         @misc_utils.log_exception(_moduleLogger)
96         def _on_button_release(self, widget, event):
97                 assert self._clickPosition != self._NO_POSITION
98                 try:
99                         mousePosition = event.get_coords()
100                         state = self.get_state(mousePosition)
101                         assert state
102                         self.emit("action", state)
103                 finally:
104                         self._clickPosition = self._NO_POSITION
105
106         @misc_utils.log_exception(_moduleLogger)
107         def _on_motion_notify(self, widget, event):
108                 if self._clickPosition == self._NO_POSITION:
109                         return
110
111                 mousePosition = event.get_coords()
112                 newState = self.get_state(mousePosition)
113                 self.emit("navigating", newState)
114
115
116 gobject.type_register(NavigationBox)
117
118
119 class StreamPresenter(object):
120
121         BUTTON_STATE_PLAY = "play"
122         BUTTON_STATE_PAUSE = "pause"
123         BUTTON_STATE_NEXT = "next"
124         BUTTON_STATE_BACK = "back"
125         BUTTON_STATE_UP = "up"
126         BUTTON_STATE_CANCEL = "cancel"
127
128         _STATE_TO_IMAGE = {
129                 BUTTON_STATE_PLAY: "play.png",
130                 BUTTON_STATE_PAUSE: "pause.png",
131                 BUTTON_STATE_NEXT: "next.png",
132                 BUTTON_STATE_BACK: "prev.png",
133         }
134
135         def __init__(self, player, store):
136                 self._store = store
137
138                 self._player = player
139                 self._player.connect("state-change", self._on_player_state_change)
140                 self._player.connect("navigate-change", self._on_player_nav_change)
141                 self._player.connect("title-change", self._on_player_title_change)
142
143                 self._image = gtk.DrawingArea()
144                 self._image.connect("expose_event", self._on_expose)
145                 self._imageNav = NavigationBox()
146                 self._imageNav.toplevel.add(self._image)
147                 self._imageNav.connect("navigating", self._on_navigating)
148                 self._imageNav.connect("action", self._on_nav_action)
149
150                 self._isPortrait = True
151
152                 self._canNavigate = True
153                 self._potentialButtonState = self.BUTTON_STATE_PLAY
154                 self._currentButtonState = self.BUTTON_STATE_PLAY
155
156                 imagePath = self._store.STORE_LOOKUP[self._player.background]
157                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
158                 imagePath = self._STATE_TO_IMAGE[self._currentButtonState]
159                 self._buttonImage = self._store.get_surface_from_store(imagePath)
160
161                 if self._isPortrait:
162                         backWidth = self._backgroundImage.get_width()
163                         backHeight = self._backgroundImage.get_height()
164                 else:
165                         backHeight = self._backgroundImage.get_width()
166                         backWidth = self._backgroundImage.get_height()
167                 self._image.set_size_request(backWidth, backHeight)
168
169         @property
170         def toplevel(self):
171                 return self._imageNav.toplevel
172
173         def set_orientation(self, orientation):
174                 self._imageNav.set_orientation(orientation)
175
176                 if orientation == gtk.ORIENTATION_VERTICAL:
177                         self._isPortrait = True
178                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
179                         self._isPortrait = False
180                 else:
181                         raise NotImplementedError(orientation)
182
183                 cairoContext = self._image.window.cairo_create()
184                 if not self._isPortrait:
185                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
186                 self._draw_presenter(cairoContext, self._currentButtonState)
187
188         @misc_utils.log_exception(_moduleLogger)
189         def _on_player_state_change(self, player, newState):
190                 if newState == "play":
191                         newState = self.BUTTON_STATE_PLAY
192                 elif newState == "pause":
193                         newState = self.BUTTON_STATE_PAUSE
194                 elif newState == "stop":
195                         newState = self.BUTTON_STATE_PAUSE
196                 else:
197                         newState = self._currentButtonState
198
199                 if newState != self._currentButtonState:
200                         self._currentButtonState = newState
201                         if not self._imageNav.is_active():
202                                 cairoContext = self._image.window.cairo_create()
203                                 if not self._isPortrait:
204                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
205                                 self._draw_state(cairoContext, self._currentButtonState)
206
207         @misc_utils.log_exception(_moduleLogger)
208         def _on_player_nav_change(self, player, newState):
209                 canNavigate = self._player.can_navigate
210                 newPotState = self._potentialButtonState
211                 if self._canNavigate != canNavigate:
212                         self._canNavigate = canNavigate
213                         if self._potentialButtonState in (self.BUTTON_STATE_NEXT, self.BUTTON_STATE_BACK):
214                                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
215                                         newPotState = self.BUTTON_STATE_PAUSE
216                                 else:
217                                         newPotState = self.BUTTON_STATE_PLAY
218
219                 if newPotState != self._potentialButtonState:
220                         self._potentialButtonState = newPotState
221                         if not self._imageNav.is_active():
222                                 cairoContext = self._image.window.cairo_create()
223                                 if not self._isPortrait:
224                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
225                                 self._draw_state(cairoContext, self._potentialButtonState)
226
227         @misc_utils.log_exception(_moduleLogger)
228         def _on_player_title_change(self, player, newState):
229                 if self._isPortrait:
230                         backWidth = self._backgroundImage.get_width()
231                         backHeight = self._backgroundImage.get_height()
232                 else:
233                         backHeight = self._backgroundImage.get_width()
234                         backWidth = self._backgroundImage.get_height()
235                 self._image.set_size_request(backWidth, backHeight)
236
237                 imagePath = self._store.STORE_LOOKUP[self._player.background]
238                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
239                 if not self._imageNav.get_state():
240                         cairoContext = self._image.window.cairo_create()
241                         if not self._isPortrait:
242                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
243                         self._draw_presenter(cairoContext, self._currentButtonState)
244                 else:
245                         cairoContext = self._image.window.cairo_create()
246                         if not self._isPortrait:
247                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
248                         self._draw_presenter(cairoContext, self._potentialButtonState)
249
250         @misc_utils.log_exception(_moduleLogger)
251         def _on_navigating(self, widget, navState):
252                 buttonState = self._translate_state(navState)
253                 self._potentialButtonState = buttonState
254                 cairoContext = self._image.window.cairo_create()
255                 if not self._isPortrait:
256                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
257                 self._draw_state(cairoContext, self._potentialButtonState)
258
259         @misc_utils.log_exception(_moduleLogger)
260         def _on_nav_action(self, widget, navState):
261                 try:
262                         buttonState = self._translate_state(navState)
263                         if buttonState == self.BUTTON_STATE_PLAY:
264                                 self._player.play()
265                         elif buttonState == self.BUTTON_STATE_PAUSE:
266                                 self._player.pause()
267                         elif buttonState == self.BUTTON_STATE_NEXT:
268                                 self._player.next()
269                         elif buttonState == self.BUTTON_STATE_BACK:
270                                 self._player.back()
271                         elif buttonState == self.BUTTON_STATE_UP:
272                                 raise NotImplementedError("Drag-down not implemented yet")
273                         elif buttonState == self.BUTTON_STATE_CANCEL:
274                                 pass
275                 finally:
276                         if self._player.state == "play":
277                                 buttonState = self.BUTTON_STATE_PLAY
278                         else:
279                                 buttonState = self.BUTTON_STATE_PAUSE
280                         self._potentialButtonState = buttonState
281                         cairoContext = self._image.window.cairo_create()
282                         if not self._isPortrait:
283                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
284                         self._draw_state(cairoContext, self._potentialButtonState)
285
286         def _translate_state(self, navState):
287                 if navState == "clicking" or not self._canNavigate:
288                         if self._currentButtonState == self.BUTTON_STATE_PLAY:
289                                 return self.BUTTON_STATE_PAUSE
290                         else:
291                                 return self.BUTTON_STATE_PLAY
292                 elif navState == "down":
293                         return self.BUTTON_STATE_UP
294                 elif navState == "up":
295                         return self.BUTTON_STATE_CANCEL
296                 elif navState == "left":
297                         return self.BUTTON_STATE_NEXT
298                 elif navState == "right":
299                         return self.BUTTON_STATE_BACK
300
301         @misc_utils.log_exception(_moduleLogger)
302         def _on_expose(self, widget, event):
303                 self._potentialButtonState = self._player.state
304                 cairoContext = self._image.window.cairo_create()
305                 if not self._isPortrait:
306                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
307                 self._draw_presenter(cairoContext, self._player.state)
308
309         def _draw_presenter(self, cairoContext, state):
310                 assert state in (self._currentButtonState, self._potentialButtonState)
311
312                 # Blank things
313                 rect = self._image.get_allocation()
314                 cairoContext.rectangle(
315                         0,
316                         0,
317                         rect.width,
318                         rect.height,
319                 )
320                 cairoContext.set_source_rgb(0, 0, 0)
321                 cairoContext.fill()
322                 cairoContext.paint()
323
324                 # Draw Background
325                 cairoContext.set_source_surface(
326                         self._backgroundImage,
327                         0,
328                         0,
329                 )
330                 cairoContext.paint()
331
332                 # title
333                 if self._player.title:
334                         _moduleLogger.info("Displaying text")
335                         backWidth = self._backgroundImage.get_width()
336                         backHeight = self._backgroundImage.get_height()
337
338                         pangoContext = self._image.create_pango_context()
339                         textLayout = pango.Layout(pangoContext)
340                         textLayout.set_markup(self._player.title)
341
342                         textWidth, textHeight = textLayout.get_pixel_size()
343                         textX = backWidth / 2 - textWidth / 2
344                         textY = backHeight - textHeight - self._buttonImage.get_height()
345
346                         cairoContext.move_to(textX, textY)
347                         cairoContext.set_source_rgb(0, 0, 0)
348                         cairoContext.show_layout(textLayout)
349
350                 self._draw_state(cairoContext, state)
351
352         def _draw_state(self, cairoContext, state):
353                 assert state in (self._currentButtonState, self._potentialButtonState)
354                 if state == self.BUTTON_STATE_CANCEL:
355                         state = self._currentButtonState
356
357                 backWidth = self._backgroundImage.get_width()
358                 backHeight = self._backgroundImage.get_height()
359
360                 imagePath = self._STATE_TO_IMAGE[state]
361                 self._buttonImage = self._store.get_surface_from_store(imagePath)
362                 cairoContext.set_source_surface(
363                         self._buttonImage,
364                         backWidth / 2 - self._buttonImage.get_width() / 2,
365                         backHeight - self._buttonImage.get_height() + 5,
366                 )
367                 cairoContext.paint()
368
369
370 class StreamMiniPresenter(object):
371
372         def __init__(self, player, store):
373                 self._store = store
374                 self._player = player
375                 self._player.connect("state-change", self._on_player_state_change)
376
377                 self._button = gtk.Image()
378                 if self._player.state == "play":
379                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
380                 else:
381                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
382
383                 self._eventBox = gtk.EventBox()
384                 self._eventBox.add(self._button)
385                 self._eventBox.connect("button_release_event", self._on_button_release)
386
387         @property
388         def toplevel(self):
389                 return self._eventBox
390
391         @misc_utils.log_exception(_moduleLogger)
392         def _on_player_state_change(self, player, newState):
393                 if self._player.state == "play":
394                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
395                 else:
396                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
397
398         @misc_utils.log_exception(_moduleLogger)
399         def _on_button_release(self, widget, event):
400                 if self._player.state == "play":
401                         self._player.pause()
402                 else:
403                         self._player.play()