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