2d8f1539c233aa20ebb7aa5788ba9dfb85b3d67e
[watersofshiloah] / src / presenter.py
1 import logging
2
3 import pango
4 import cairo
5 import gtk
6
7 import util.misc as misc_utils
8
9
10 _moduleLogger = logging.getLogger(__name__)
11
12
13 class StreamPresenter(object):
14
15         MINIMUM_MOVEMENT = 20
16
17         BUTTON_STATE_PLAY = "play"
18         BUTTON_STATE_PAUSE = "pause"
19         BUTTON_STATE_NEXT = "next"
20         BUTTON_STATE_BACK = "back"
21         BUTTON_STATE_UP = "up"
22         BUTTON_STATE_CANCEL = "cancel"
23
24         _NO_POSITION = -1, -1
25
26         _STATE_TO_IMAGE = {
27                 BUTTON_STATE_PLAY: "play.png",
28                 BUTTON_STATE_PAUSE: "pause.png",
29                 BUTTON_STATE_NEXT: "next.png",
30                 BUTTON_STATE_BACK: "prev.png",
31         }
32
33         def __init__(self, player, store):
34                 self._store = store
35
36                 self._player = player
37                 self._player.connect("state-change", self._on_player_state_change)
38                 self._player.connect("navigate-change", self._on_player_nav_change)
39                 self._player.connect("title-change", self._on_player_title_change)
40
41                 self._image = gtk.DrawingArea()
42                 self._image.connect("expose_event", self._on_expose)
43                 self._imageEvents = gtk.EventBox()
44                 self._imageEvents.connect("motion_notify_event", self._on_motion_notify)
45                 self._imageEvents.connect("button_press_event", self._on_button_press)
46                 self._imageEvents.connect("button_release_event", self._on_button_release)
47                 self._imageEvents.add(self._image)
48
49                 self._isPortrait = True
50
51                 self._canNavigate = True
52                 self._clickPosition = self._NO_POSITION
53                 self._potentialButtonState = self.BUTTON_STATE_PLAY
54                 self._currentButtonState = self.BUTTON_STATE_PLAY
55
56                 imagePath = self._store.STORE_LOOKUP[self._player.background]
57                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
58                 imagePath = self._STATE_TO_IMAGE[self._currentButtonState]
59                 self._buttonImage = self._store.get_surface_from_store(imagePath)
60
61                 if self._isPortrait:
62                         backWidth = self._backgroundImage.get_width()
63                         backHeight = self._backgroundImage.get_height()
64                 else:
65                         backHeight = self._backgroundImage.get_width()
66                         backWidth = self._backgroundImage.get_height()
67                 self._image.set_size_request(backWidth, backHeight)
68
69         @property
70         def toplevel(self):
71                 return self._imageEvents
72
73         def set_orientation(self, orientation):
74                 if orientation == gtk.ORIENTATION_VERTICAL:
75                         self._isPortrait = True
76                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
77                         self._isPortrait = False
78                 else:
79                         raise NotImplementedError(orientation)
80
81                 cairoContext = self._image.window.cairo_create()
82                 if not self._isPortrait:
83                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
84                 self._draw_presenter(cairoContext, self._currentButtonState)
85
86         @misc_utils.log_exception(_moduleLogger)
87         def _on_player_state_change(self, player, newState):
88                 if newState == "play":
89                         newState = self.BUTTON_STATE_PLAY
90                 elif newState == "pause":
91                         newState = self.BUTTON_STATE_PAUSE
92                 elif newState == "stop":
93                         newState = self.BUTTON_STATE_PAUSE
94                 else:
95                         newState = self._currentButtonState
96
97                 if newState != self._currentButtonState:
98                         self._currentButtonState = newState
99                         if self._clickPosition == self._NO_POSITION:
100                                 cairoContext = self._image.window.cairo_create()
101                                 if not self._isPortrait:
102                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
103                                 self._draw_state(cairoContext, self._currentButtonState)
104
105         @misc_utils.log_exception(_moduleLogger)
106         def _on_player_nav_change(self, player, newState):
107                 canNavigate = self._player.can_navigate
108                 newPotState = self._potentialButtonState
109                 if self._canNavigate != canNavigate:
110                         self._canNavigate = canNavigate
111                         if self._potentialButtonState in (self.BUTTON_STATE_NEXT, self.BUTTON_STATE_BACK):
112                                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
113                                         newPotState = self.BUTTON_STATE_PAUSE
114                                 else:
115                                         newPotState = self.BUTTON_STATE_PLAY
116
117                 if newPotState != self._potentialButtonState:
118                         self._potentialButtonState = newPotState
119                         if self._clickPosition == self._NO_POSITION:
120                                 cairoContext = self._image.window.cairo_create()
121                                 if not self._isPortrait:
122                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
123                                 self._draw_state(cairoContext, self._potentialButtonState)
124
125         @misc_utils.log_exception(_moduleLogger)
126         def _on_player_title_change(self, player, newState):
127                 if self._isPortrait:
128                         backWidth = self._backgroundImage.get_width()
129                         backHeight = self._backgroundImage.get_height()
130                 else:
131                         backHeight = self._backgroundImage.get_width()
132                         backWidth = self._backgroundImage.get_height()
133                 self._image.set_size_request(backWidth, backHeight)
134
135                 imagePath = self._store.STORE_LOOKUP[self._player.background]
136                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
137                 if self._clickPosition == self._NO_POSITION:
138                         cairoContext = self._image.window.cairo_create()
139                         if not self._isPortrait:
140                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
141                         self._draw_presenter(cairoContext, self._currentButtonState)
142                 else:
143                         cairoContext = self._image.window.cairo_create()
144                         if not self._isPortrait:
145                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
146                         self._draw_presenter(cairoContext, self._potentialButtonState)
147
148         @misc_utils.log_exception(_moduleLogger)
149         def _on_button_press(self, widget, event):
150                 self._clickPosition = event.get_coords()
151                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
152                         newState = self.BUTTON_STATE_PAUSE
153                 else:
154                         newState = self.BUTTON_STATE_PLAY
155                 self._potentialButtonState = newState
156                 cairoContext = self._image.window.cairo_create()
157                 if not self._isPortrait:
158                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
159                 self._draw_state(cairoContext, self._potentialButtonState)
160
161         @misc_utils.log_exception(_moduleLogger)
162         def _on_button_release(self, widget, event):
163                 try:
164                         mousePosition = event.get_coords()
165                         newState = self._calculate_state(mousePosition)
166                         if newState == self.BUTTON_STATE_PLAY:
167                                 self._player.play()
168                         elif newState == self.BUTTON_STATE_PAUSE:
169                                 self._player.pause()
170                         elif newState == self.BUTTON_STATE_NEXT:
171                                 self._player.next()
172                         elif newState == self.BUTTON_STATE_BACK:
173                                 self._player.back()
174                         elif newState == self.BUTTON_STATE_UP:
175                                 raise NotImplementedError("Drag-down not implemented yet")
176                         elif newState == self.BUTTON_STATE_CANCEL:
177                                 pass
178                 finally:
179                         if self._player.state == "play":
180                                 newState = self.BUTTON_STATE_PLAY
181                         else:
182                                 newState = self.BUTTON_STATE_PAUSE
183                         self._potentialButtonState = newState
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_state(cairoContext, self._potentialButtonState)
188                         self._clickPosition = self._NO_POSITION
189
190         @misc_utils.log_exception(_moduleLogger)
191         def _on_motion_notify(self, widget, event):
192                 if self._clickPosition == self._NO_POSITION:
193                         return
194
195                 mousePosition = event.get_coords()
196                 newState = self._calculate_state(mousePosition)
197                 if newState != self._potentialButtonState:
198                         self._potentialButtonState = newState
199                         cairoContext = self._image.window.cairo_create()
200                         if not self._isPortrait:
201                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
202                         self._draw_state(cairoContext, self._potentialButtonState)
203
204         def _calculate_state(self, newCoord):
205                 assert self._clickPosition != self._NO_POSITION
206
207                 if self._isPortrait:
208                         delta = (
209                                 newCoord[0] - self._clickPosition[0],
210                                 - (newCoord[1] - self._clickPosition[1])
211                         )
212                 else:
213                         delta = (
214                                 newCoord[1] - self._clickPosition[1],
215                                 - (newCoord[0] - self._clickPosition[0])
216                         )
217                 absDelta = (abs(delta[0]), abs(delta[1]))
218                 if max(*absDelta) < self.MINIMUM_MOVEMENT or not self._canNavigate:
219                         if self._currentButtonState == self.BUTTON_STATE_PLAY:
220                                 return self.BUTTON_STATE_PAUSE
221                         else:
222                                 return self.BUTTON_STATE_PLAY
223
224                 if absDelta[0] < absDelta[1]:
225                         if 0 < delta[1]:
226                                 return self.BUTTON_STATE_CANCEL
227                         else:
228                                 return self.BUTTON_STATE_UP
229                 else:
230                         if 0 < delta[0]:
231                                 return self.BUTTON_STATE_BACK
232                         else:
233                                 return self.BUTTON_STATE_NEXT
234
235         @misc_utils.log_exception(_moduleLogger)
236         def _on_expose(self, widget, event):
237                 self._potentialButtonState = self._player.state
238                 cairoContext = self._image.window.cairo_create()
239                 if not self._isPortrait:
240                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
241                 self._draw_presenter(cairoContext, self._player.state)
242
243         def _draw_presenter(self, cairoContext, state):
244                 assert state in (self._currentButtonState, self._potentialButtonState)
245
246                 # Blank things
247                 rect = self._image.get_allocation()
248                 cairoContext.rectangle(
249                         0,
250                         0,
251                         rect.width,
252                         rect.height,
253                 )
254                 cairoContext.set_source_rgb(0, 0, 0)
255                 cairoContext.fill()
256                 cairoContext.paint()
257
258                 # Draw Background
259                 cairoContext.set_source_surface(
260                         self._backgroundImage,
261                         0,
262                         0,
263                 )
264                 cairoContext.paint()
265
266                 # title
267                 if self._player.title:
268                         _moduleLogger.info("Displaying text")
269                         backWidth = self._backgroundImage.get_width()
270                         backHeight = self._backgroundImage.get_height()
271
272                         pangoContext = self._image.create_pango_context()
273                         textLayout = pango.Layout(pangoContext)
274                         textLayout.set_markup(self._player.title)
275
276                         textWidth, textHeight = textLayout.get_pixel_size()
277                         textX = backWidth / 2 - textWidth / 2
278                         textY = backHeight - textHeight - self._buttonImage.get_height()
279
280                         cairoContext.move_to(textX, textY)
281                         cairoContext.set_source_rgb(0, 0, 0)
282                         cairoContext.show_layout(textLayout)
283
284                 self._draw_state(cairoContext, state)
285
286         def _draw_state(self, cairoContext, state):
287                 assert state in (self._currentButtonState, self._potentialButtonState)
288                 if state == self.BUTTON_STATE_CANCEL:
289                         state = self._currentButtonState
290
291                 backWidth = self._backgroundImage.get_width()
292                 backHeight = self._backgroundImage.get_height()
293
294                 imagePath = self._STATE_TO_IMAGE[state]
295                 self._buttonImage = self._store.get_surface_from_store(imagePath)
296                 cairoContext.set_source_surface(
297                         self._buttonImage,
298                         backWidth / 2 - self._buttonImage.get_width() / 2,
299                         backHeight - self._buttonImage.get_height() + 5,
300                 )
301                 cairoContext.paint()
302
303
304 class StreamMiniPresenter(object):
305
306         def __init__(self, player, store):
307                 self._store = store
308                 self._player = player
309                 self._player.connect("state-change", self._on_player_state_change)
310
311                 self._button = gtk.Image()
312                 if self._player.state == "play":
313                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
314                 else:
315                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
316
317                 self._eventBox = gtk.EventBox()
318                 self._eventBox.add(self._button)
319                 self._eventBox.connect("button_release_event", self._on_button_release)
320
321         @property
322         def toplevel(self):
323                 return self._eventBox
324
325         @misc_utils.log_exception(_moduleLogger)
326         def _on_player_state_change(self, player, newState):
327                 if self._player.state == "play":
328                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["play"])
329                 else:
330                         self._store.set_image_from_store(self._button, self._store.STORE_LOOKUP["pause"])
331
332         @misc_utils.log_exception(_moduleLogger)
333         def _on_button_release(self, widget, event):
334                 if self._player.state == "play":
335                         self._player.pause()
336                 else:
337                         self._player.play()