20359af839816b502ac3d29e181b4d4865712a37
[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("leave_notify_event", self._on_leave_notify)
46                 #self._imageEvents.connect("proximity_in_event", self._on_motion_notify)
47                 #self._imageEvents.connect("proximity_out_event", self._on_leave_notify)
48                 self._imageEvents.connect("button_press_event", self._on_button_press)
49                 self._imageEvents.connect("button_release_event", self._on_button_release)
50                 self._imageEvents.add(self._image)
51
52                 self._isPortrait = True
53
54                 self._canNavigate = True
55                 self._clickPosition = self._NO_POSITION
56                 self._potentialButtonState = self.BUTTON_STATE_PLAY
57                 self._currentButtonState = self.BUTTON_STATE_PLAY
58
59                 imagePath = self._store.STORE_LOOKUP[self._player.background]
60                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
61                 imagePath = self._STATE_TO_IMAGE[self._currentButtonState]
62                 self._buttonImage = self._store.get_surface_from_store(imagePath)
63
64                 if self._isPortrait:
65                         backWidth = self._backgroundImage.get_width()
66                         backHeight = self._backgroundImage.get_height()
67                 else:
68                         backHeight = self._backgroundImage.get_width()
69                         backWidth = self._backgroundImage.get_height()
70                 self._image.set_size_request(backWidth, backHeight)
71
72         @property
73         def toplevel(self):
74                 return self._imageEvents
75
76         def set_orientation(self, orientation):
77                 if orientation == gtk.ORIENTATION_VERTICAL:
78                         self._isPortrait = True
79                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
80                         self._isPortrait = False
81                 else:
82                         raise NotImplementedError(orientation)
83
84                 cairoContext = self._image.window.cairo_create()
85                 if not self._isPortrait:
86                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
87                 self._draw_presenter(cairoContext, self._currentButtonState)
88
89         @misc_utils.log_exception(_moduleLogger)
90         def _on_player_state_change(self, player, newState):
91                 if newState == "play":
92                         newState = self.BUTTON_STATE_PLAY
93                 elif newState == "pause":
94                         newState = self.BUTTON_STATE_PAUSE
95                 elif newState == "stop":
96                         newState = self.BUTTON_STATE_PAUSE
97                 else:
98                         newState = self._currentButtonState
99
100                 if newState != self._currentButtonState:
101                         self._currentButtonState = newState
102                         if self._clickPosition == self._NO_POSITION:
103                                 cairoContext = self._image.window.cairo_create()
104                                 if not self._isPortrait:
105                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
106                                 self._draw_state(cairoContext, self._currentButtonState)
107
108         @misc_utils.log_exception(_moduleLogger)
109         def _on_player_nav_change(self, player, newState):
110                 canNavigate = self._player.can_navigate
111                 newPotState = self._potentialButtonState
112                 if self._canNavigate != canNavigate:
113                         self._canNavigate = canNavigate
114                         if self._potentialButtonState in (self.BUTTON_STATE_NEXT, self.BUTTON_STATE_BACK):
115                                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
116                                         newPotState = self.BUTTON_STATE_PAUSE
117                                 else:
118                                         newPotState = self.BUTTON_STATE_PLAY
119
120                 if newPotState != self._potentialButtonState:
121                         self._potentialButtonState = newPotState
122                         if self._clickPosition == self._NO_POSITION:
123                                 cairoContext = self._image.window.cairo_create()
124                                 if not self._isPortrait:
125                                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
126                                 self._draw_state(cairoContext, self._potentialButtonState)
127
128         @misc_utils.log_exception(_moduleLogger)
129         def _on_player_title_change(self, player, newState):
130                 if self._isPortrait:
131                         backWidth = self._backgroundImage.get_width()
132                         backHeight = self._backgroundImage.get_height()
133                 else:
134                         backHeight = self._backgroundImage.get_width()
135                         backWidth = self._backgroundImage.get_height()
136                 self._image.set_size_request(backWidth, backHeight)
137
138                 imagePath = self._store.STORE_LOOKUP[self._player.background]
139                 self._backgroundImage = self._store.get_surface_from_store(imagePath)
140                 if self._clickPosition == self._NO_POSITION:
141                         cairoContext = self._image.window.cairo_create()
142                         if not self._isPortrait:
143                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
144                         self._draw_presenter(cairoContext, self._currentButtonState)
145                 else:
146                         cairoContext = self._image.window.cairo_create()
147                         if not self._isPortrait:
148                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
149                         self._draw_presenter(cairoContext, self._potentialButtonState)
150
151         @misc_utils.log_exception(_moduleLogger)
152         def _on_button_press(self, widget, event):
153                 self._clickPosition = event.get_coords()
154                 if self._currentButtonState == self.BUTTON_STATE_PLAY:
155                         newState = self.BUTTON_STATE_PAUSE
156                 else:
157                         newState = self.BUTTON_STATE_PLAY
158                 self._potentialButtonState = newState
159                 cairoContext = self._image.window.cairo_create()
160                 if not self._isPortrait:
161                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
162                 self._draw_state(cairoContext, self._potentialButtonState)
163
164         @misc_utils.log_exception(_moduleLogger)
165         def _on_button_release(self, widget, event):
166                 try:
167                         mousePosition = event.get_coords()
168                         newState = self._calculate_state(mousePosition)
169                         if newState == self.BUTTON_STATE_PLAY:
170                                 self._player.play()
171                         elif newState == self.BUTTON_STATE_PAUSE:
172                                 self._player.pause()
173                         elif newState == self.BUTTON_STATE_NEXT:
174                                 self._player.next()
175                         elif newState == self.BUTTON_STATE_BACK:
176                                 self._player.back()
177                         elif newState == self.BUTTON_STATE_UP:
178                                 raise NotImplementedError("Drag-down not implemented yet")
179                         elif newState == self.BUTTON_STATE_CANCEL:
180                                 pass
181                 finally:
182                         if self._player.state == "play":
183                                 newState = self.BUTTON_STATE_PLAY
184                         else:
185                                 newState = self.BUTTON_STATE_PAUSE
186                         self._potentialButtonState = newState
187                         cairoContext = self._image.window.cairo_create()
188                         if not self._isPortrait:
189                                 cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
190                         self._draw_state(cairoContext, self._potentialButtonState)
191                         self._clickPosition = self._NO_POSITION
192
193         @misc_utils.log_exception(_moduleLogger)
194         def _on_motion_notify(self, widget, event):
195                 if self._clickPosition == self._NO_POSITION:
196                         return
197
198                 mousePosition = event.get_coords()
199                 newState = self._calculate_state(mousePosition)
200                 if newState != self._potentialButtonState:
201                         self._potentialButtonState = newState
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._potentialButtonState)
206
207         def _calculate_state(self, newCoord):
208                 assert self._clickPosition != self._NO_POSITION
209
210                 if self._isPortrait:
211                         delta = (
212                                 newCoord[0] - self._clickPosition[0],
213                                 - (newCoord[1] - self._clickPosition[1])
214                         )
215                 else:
216                         delta = (
217                                 newCoord[1] - self._clickPosition[1],
218                                 - (newCoord[0] - self._clickPosition[0])
219                         )
220                 absDelta = (abs(delta[0]), abs(delta[1]))
221                 if max(*absDelta) < self.MINIMUM_MOVEMENT or not self._canNavigate:
222                         if self._currentButtonState == self.BUTTON_STATE_PLAY:
223                                 return self.BUTTON_STATE_PAUSE
224                         else:
225                                 return self.BUTTON_STATE_PLAY
226
227                 if absDelta[0] < absDelta[1]:
228                         if 0 < delta[1]:
229                                 return self.BUTTON_STATE_CANCEL
230                         else:
231                                 return self.BUTTON_STATE_UP
232                 else:
233                         if 0 < delta[0]:
234                                 return self.BUTTON_STATE_BACK
235                         else:
236                                 return self.BUTTON_STATE_NEXT
237
238         @misc_utils.log_exception(_moduleLogger)
239         def _on_expose(self, widget, event):
240                 self._potentialButtonState = self._player.state
241                 cairoContext = self._image.window.cairo_create()
242                 if not self._isPortrait:
243                         cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
244                 self._draw_presenter(cairoContext, self._player.state)
245
246         def _draw_presenter(self, cairoContext, state):
247                 assert state in (self._currentButtonState, self._potentialButtonState)
248
249                 # Blank things
250                 rect = self._image.get_allocation()
251                 cairoContext.rectangle(
252                         0,
253                         0,
254                         rect.width,
255                         rect.height,
256                 )
257                 cairoContext.set_source_rgb(0, 0, 0)
258                 cairoContext.fill()
259                 cairoContext.paint()
260
261                 # Draw Background
262                 cairoContext.set_source_surface(
263                         self._backgroundImage,
264                         0,
265                         0,
266                 )
267                 cairoContext.paint()
268
269                 # title
270                 if self._player.title:
271                         _moduleLogger.info("Displaying text")
272                         backWidth = self._backgroundImage.get_width()
273                         backHeight = self._backgroundImage.get_height()
274
275                         pangoContext = self._image.create_pango_context()
276                         textLayout = pango.Layout(pangoContext)
277                         textLayout.set_markup(self._player.title)
278
279                         textWidth, textHeight = textLayout.get_pixel_size()
280                         textX = backWidth / 2 - textWidth / 2
281                         textY = backHeight - textHeight - self._buttonImage.get_height()
282
283                         cairoContext.move_to(textX, textY)
284                         cairoContext.set_source_rgb(0, 0, 0)
285                         cairoContext.show_layout(textLayout)
286
287                 self._draw_state(cairoContext, state)
288
289         def _draw_state(self, cairoContext, state):
290                 assert state in (self._currentButtonState, self._potentialButtonState)
291                 if state == self.BUTTON_STATE_CANCEL:
292                         state = self._currentButtonState
293
294                 backWidth = self._backgroundImage.get_width()
295                 backHeight = self._backgroundImage.get_height()
296
297                 imagePath = self._STATE_TO_IMAGE[state]
298                 self._buttonImage = self._store.get_surface_from_store(imagePath)
299                 cairoContext.set_source_surface(
300                         self._buttonImage,
301                         backWidth / 2 - self._buttonImage.get_width() / 2,
302                         backHeight - self._buttonImage.get_height() + 5,
303                 )
304                 cairoContext.paint()