2541b400a161429f0ecfe8aa2df653ecad40fa7c
[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                 BUTTON_STATE_UP: "home.png",
134         }
135
136         def __init__(self, player, store):
137                 self._store = store
138
139                 self._player = player
140                 self._player.connect("state-change", self._on_player_state_change)
141                 self._player.connect("navigate-change", self._on_player_nav_change)
142                 self._player.connect("title-change", self._on_player_title_change)
143
144                 self._image = gtk.DrawingArea()
145                 self._image.connect("expose_event", self._on_expose)
146                 self._imageNav = NavigationBox()
147                 self._imageNav.toplevel.add(self._image)
148                 self._imageNav.connect("navigating", self._on_navigating)
149                 self._imageNav.connect("action", self._on_nav_action)
150
151                 self._isPortrait = True
152
153                 self._canNavigate = True
154                 self._potentialButtonState = self.BUTTON_STATE_PLAY
155                 self._currentButtonState = self.BUTTON_STATE_PLAY
156
157                 imagePath = self._store.STORE_LOOKUP[self._player.background]
158                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
159                 imagePath = self._STATE_TO_IMAGE[self._currentButtonState]
160                 self._buttonImage = self._store.get_surface_from_store(imagePath)
161
162                 if self._isPortrait:
163                         backWidth = self._backgroundImage.get_width()
164                         backHeight = self._backgroundImage.get_height()
165                 else:
166                         backHeight = self._backgroundImage.get_width()
167                         backWidth = self._backgroundImage.get_height()
168                 self._image.set_size_request(backWidth, backHeight)
169
170         @property
171         def toplevel(self):
172                 return self._imageNav.toplevel
173
174         def set_orientation(self, orientation):
175                 self._imageNav.set_orientation(orientation)
176
177                 if orientation == gtk.ORIENTATION_VERTICAL:
178                         self._isPortrait = True
179                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
180                         self._isPortrait = False
181                 else:
182                         raise NotImplementedError(orientation)
183
184                 cairoContext = self._image.window.cairo_create()
185                 if not self._isPortrait:
186                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
187                 self._draw_presenter(cairoContext, self._currentButtonState)
188
189         @misc_utils.log_exception(_moduleLogger)
190         def _on_player_state_change(self, player, newState):
191                 # @bug We only want to folow changes in player when its active
192                 if newState == "play":
193                         newState = self.BUTTON_STATE_PLAY
194                 elif newState == "pause":
195                         newState = self.BUTTON_STATE_PAUSE
196                 elif newState == "stop":
197                         newState = self.BUTTON_STATE_PAUSE
198                 else:
199                         newState = self._currentButtonState
200
201                 if newState != self._currentButtonState:
202                         self._currentButtonState = newState
203                         if not self._imageNav.is_active():
204                                 cairoContext = self._image.window.cairo_create()
205                                 if not self._isPortrait:
206                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
207                                 self._draw_state(cairoContext, self._currentButtonState)
208
209         @misc_utils.log_exception(_moduleLogger)
210         def _on_player_nav_change(self, player, newState):
211                 # @bug We only want to folow changes in player when its active
212                 canNavigate = self._player.can_navigate
213                 newPotState = self._potentialButtonState
214                 if self._canNavigate != canNavigate:
215                         self._canNavigate = canNavigate
216                         if self._potentialButtonState in (self.BUTTON_STATE_NEXT, self.BUTTON_STATE_BACK):
217                                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
218                                         newPotState = self.BUTTON_STATE_PAUSE
219                                 else:
220                                         newPotState = self.BUTTON_STATE_PLAY
221
222                 if newPotState != self._potentialButtonState:
223                         self._potentialButtonState = newPotState
224                         if not self._imageNav.is_active():
225                                 cairoContext = self._image.window.cairo_create()
226                                 if not self._isPortrait:
227                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
228                                 self._draw_state(cairoContext, self._potentialButtonState)
229
230         @misc_utils.log_exception(_moduleLogger)
231         def _on_player_title_change(self, player, newState):
232                 # @bug We only want to folow changes in player when its active
233                 if self._isPortrait:
234                         backWidth = self._backgroundImage.get_width()
235                         backHeight = self._backgroundImage.get_height()
236                 else:
237                         backHeight = self._backgroundImage.get_width()
238                         backWidth = self._backgroundImage.get_height()
239                 self._image.set_size_request(backWidth, backHeight)
240
241                 imagePath = self._store.STORE_LOOKUP[self._player.background]
242                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
243                 if not self._imageNav.get_state():
244                         cairoContext = self._image.window.cairo_create()
245                         if not self._isPortrait:
246                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
247                         self._draw_presenter(cairoContext, self._currentButtonState)
248                 else:
249                         cairoContext = self._image.window.cairo_create()
250                         if not self._isPortrait:
251                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
252                         self._draw_presenter(cairoContext, self._potentialButtonState)
253
254         @misc_utils.log_exception(_moduleLogger)
255         def _on_navigating(self, widget, navState):
256                 buttonState = self._translate_state(navState)
257                 self._potentialButtonState = buttonState
258                 cairoContext = self._image.window.cairo_create()
259                 if not self._isPortrait:
260                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
261                 self._draw_state(cairoContext, self._potentialButtonState)
262
263         @misc_utils.log_exception(_moduleLogger)
264         def _on_nav_action(self, widget, navState):
265                 # @bug We only want to folow changes in player when its active
266                 try:
267                         buttonState = self._translate_state(navState)
268                         if buttonState == self.BUTTON_STATE_PLAY:
269                                 self._player.play()
270                         elif buttonState == self.BUTTON_STATE_PAUSE:
271                                 self._player.pause()
272                         elif buttonState == self.BUTTON_STATE_NEXT:
273                                 self._player.next()
274                         elif buttonState == self.BUTTON_STATE_BACK:
275                                 self._player.back()
276                         elif buttonState == self.BUTTON_STATE_UP:
277                                 raise NotImplementedError("Drag-down not implemented yet")
278                         elif buttonState == self.BUTTON_STATE_CANCEL:
279                                 pass
280                 finally:
281                         if self._player.state == "play":
282                                 buttonState = self.BUTTON_STATE_PLAY
283                         else:
284                                 buttonState = self.BUTTON_STATE_PAUSE
285                         self._potentialButtonState = buttonState
286                         cairoContext = self._image.window.cairo_create()
287                         if not self._isPortrait:
288                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
289                         self._draw_state(cairoContext, self._potentialButtonState)
290
291         def _translate_state(self, navState):
292                 if navState == "clicking" or not self._canNavigate:
293                         if self._currentButtonState == self.BUTTON_STATE_PLAY:
294                                 return self.BUTTON_STATE_PAUSE
295                         else:
296                                 return self.BUTTON_STATE_PLAY
297                 elif navState == "down":
298                         return self.BUTTON_STATE_UP
299                 elif navState == "up":
300                         return self.BUTTON_STATE_CANCEL
301                 elif navState == "left":
302                         return self.BUTTON_STATE_NEXT
303                 elif navState == "right":
304                         return self.BUTTON_STATE_BACK
305
306         @misc_utils.log_exception(_moduleLogger)
307         def _on_expose(self, widget, event):
308                 self._potentialButtonState = self._player.state
309                 cairoContext = self._image.window.cairo_create()
310                 if not self._isPortrait:
311                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
312                 self._draw_presenter(cairoContext, self._player.state)
313
314         def _draw_presenter(self, cairoContext, state):
315                 assert state in (self._currentButtonState, self._potentialButtonState)
316
317                 # Blank things
318                 rect = self._image.get_allocation()
319                 cairoContext.rectangle(
320                         0,
321                         0,
322                         rect.width,
323                         rect.height,
324                 )
325                 cairoContext.set_source_rgb(0, 0, 0)
326                 cairoContext.fill()
327                 cairoContext.paint()
328
329                 # Draw Background
330                 cairoContext.set_source_surface(
331                         self._backgroundImage,
332                         0,
333                         0,
334                 )
335                 cairoContext.paint()
336
337                 # title
338                 if self._player.title:
339                         _moduleLogger.info("Displaying text")
340                         backWidth = self._backgroundImage.get_width()
341                         backHeight = self._backgroundImage.get_height()
342
343                         pangoContext = self._image.create_pango_context()
344                         textLayout = pango.Layout(pangoContext)
345                         textLayout.set_markup(self._player.title)
346
347                         textWidth, textHeight = textLayout.get_pixel_size()
348                         textX = backWidth / 2 - textWidth / 2
349                         textY = backHeight - textHeight - self._buttonImage.get_height()
350
351                         cairoContext.move_to(textX, textY)
352                         cairoContext.set_source_rgb(0, 0, 0)
353                         cairoContext.show_layout(textLayout)
354
355                 self._draw_state(cairoContext, state)
356
357         def _draw_state(self, cairoContext, state):
358                 assert state in (self._currentButtonState, self._potentialButtonState)
359                 if state == self.BUTTON_STATE_CANCEL:
360                         state = self._currentButtonState
361
362                 backWidth = self._backgroundImage.get_width()
363                 backHeight = self._backgroundImage.get_height()
364
365                 imagePath = self._STATE_TO_IMAGE[state]
366                 self._buttonImage = self._store.get_surface_from_store(imagePath)
367                 cairoContext.set_source_surface(
368                         self._buttonImage,
369                         backWidth / 2 - self._buttonImage.get_width() / 2,
370                         backHeight - self._buttonImage.get_height() + 5,
371                 )
372                 cairoContext.paint()
373
374
375 class StreamMiniPresenter(object):
376
377         def __init__(self, player, store):
378                 self._store = store
379                 self._player = player
380                 self._player.connect("state-change", self._on_player_state_change)
381
382                 self._button = gtk.Image()
383                 if self._player.state == "play":
384                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
385                 else:
386                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
387
388                 self._eventBox = gtk.EventBox()
389                 self._eventBox.add(self._button)
390                 self._eventBox.connect("button_release_event", self._on_button_release)
391
392         @property
393         def toplevel(self):
394                 return self._eventBox
395
396         @misc_utils.log_exception(_moduleLogger)
397         def _on_player_state_change(self, player, newState):
398                 if self._player.state == "play":
399                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
400                 else:
401                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
402
403         @misc_utils.log_exception(_moduleLogger)
404         def _on_button_release(self, widget, event):
405                 if self._player.state == "play":
406                         self._player.pause()
407                 else:
408                         self._player.play()