Initial checkin of the presentor
[watersofshiloah] / src / presenter.py
diff --git a/src/presenter.py b/src/presenter.py
new file mode 100644 (file)
index 0000000..20359af
--- /dev/null
@@ -0,0 +1,304 @@
+import logging
+
+import pango
+import cairo
+import gtk
+
+import util.misc as misc_utils
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+class StreamPresenter(object):
+
+       MINIMUM_MOVEMENT = 20
+
+       BUTTON_STATE_PLAY = "play"
+       BUTTON_STATE_PAUSE = "pause"
+       BUTTON_STATE_NEXT = "next"
+       BUTTON_STATE_BACK = "back"
+       BUTTON_STATE_UP = "up"
+       BUTTON_STATE_CANCEL = "cancel"
+
+       _NO_POSITION = -1, -1
+
+       _STATE_TO_IMAGE = {
+               BUTTON_STATE_PLAY: "play.png",
+               BUTTON_STATE_PAUSE: "pause.png",
+               BUTTON_STATE_NEXT: "next.png",
+               BUTTON_STATE_BACK: "prev.png",
+       }
+
+       def __init__(self, player, store):
+               self._store = store
+
+               self._player = player
+               self._player.connect("state-change", self._on_player_state_change)
+               self._player.connect("navigate-change", self._on_player_nav_change)
+               self._player.connect("title-change", self._on_player_title_change)
+
+               self._image = gtk.DrawingArea()
+               self._image.connect("expose_event", self._on_expose)
+               self._imageEvents = gtk.EventBox()
+               self._imageEvents.connect("motion_notify_event", self._on_motion_notify)
+               #self._imageEvents.connect("leave_notify_event", self._on_leave_notify)
+               #self._imageEvents.connect("proximity_in_event", self._on_motion_notify)
+               #self._imageEvents.connect("proximity_out_event", self._on_leave_notify)
+               self._imageEvents.connect("button_press_event", self._on_button_press)
+               self._imageEvents.connect("button_release_event", self._on_button_release)
+               self._imageEvents.add(self._image)
+
+               self._isPortrait = True
+
+               self._canNavigate = True
+               self._clickPosition = self._NO_POSITION
+               self._potentialButtonState = self.BUTTON_STATE_PLAY
+               self._currentButtonState = self.BUTTON_STATE_PLAY
+
+               imagePath = self._store.STORE_LOOKUP[self._player.background]
+               self._backgroundImage = self._store.get_surface_from_store(imagePath)
+               imagePath = self._STATE_TO_IMAGE[self._currentButtonState]
+               self._buttonImage = self._store.get_surface_from_store(imagePath)
+
+               if self._isPortrait:
+                       backWidth = self._backgroundImage.get_width()
+                       backHeight = self._backgroundImage.get_height()
+               else:
+                       backHeight = self._backgroundImage.get_width()
+                       backWidth = self._backgroundImage.get_height()
+               self._image.set_size_request(backWidth, backHeight)
+
+       @property
+       def toplevel(self):
+               return self._imageEvents
+
+       def set_orientation(self, orientation):
+               if orientation == gtk.ORIENTATION_VERTICAL:
+                       self._isPortrait = True
+               elif orientation == gtk.ORIENTATION_HORIZONTAL:
+                       self._isPortrait = False
+               else:
+                       raise NotImplementedError(orientation)
+
+               cairoContext = self._image.window.cairo_create()
+               if not self._isPortrait:
+                       cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+               self._draw_presenter(cairoContext, self._currentButtonState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_state_change(self, player, newState):
+               if newState == "play":
+                       newState = self.BUTTON_STATE_PLAY
+               elif newState == "pause":
+                       newState = self.BUTTON_STATE_PAUSE
+               elif newState == "stop":
+                       newState = self.BUTTON_STATE_PAUSE
+               else:
+                       newState = self._currentButtonState
+
+               if newState != self._currentButtonState:
+                       self._currentButtonState = newState
+                       if self._clickPosition == self._NO_POSITION:
+                               cairoContext = self._image.window.cairo_create()
+                               if not self._isPortrait:
+                                       cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                               self._draw_state(cairoContext, self._currentButtonState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_nav_change(self, player, newState):
+               canNavigate = self._player.can_navigate
+               newPotState = self._potentialButtonState
+               if self._canNavigate != canNavigate:
+                       self._canNavigate = canNavigate
+                       if self._potentialButtonState in (self.BUTTON_STATE_NEXT, self.BUTTON_STATE_BACK):
+                               if self._currentButtonState == self.BUTTON_STATE_PLAY:
+                                       newPotState = self.BUTTON_STATE_PAUSE
+                               else:
+                                       newPotState = self.BUTTON_STATE_PLAY
+
+               if newPotState != self._potentialButtonState:
+                       self._potentialButtonState = newPotState
+                       if self._clickPosition == self._NO_POSITION:
+                               cairoContext = self._image.window.cairo_create()
+                               if not self._isPortrait:
+                                       cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                               self._draw_state(cairoContext, self._potentialButtonState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_title_change(self, player, newState):
+               if self._isPortrait:
+                       backWidth = self._backgroundImage.get_width()
+                       backHeight = self._backgroundImage.get_height()
+               else:
+                       backHeight = self._backgroundImage.get_width()
+                       backWidth = self._backgroundImage.get_height()
+               self._image.set_size_request(backWidth, backHeight)
+
+               imagePath = self._store.STORE_LOOKUP[self._player.background]
+               self._backgroundImage = self._store.get_surface_from_store(imagePath)
+               if self._clickPosition == self._NO_POSITION:
+                       cairoContext = self._image.window.cairo_create()
+                       if not self._isPortrait:
+                               cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                       self._draw_presenter(cairoContext, self._currentButtonState)
+               else:
+                       cairoContext = self._image.window.cairo_create()
+                       if not self._isPortrait:
+                               cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                       self._draw_presenter(cairoContext, self._potentialButtonState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_button_press(self, widget, event):
+               self._clickPosition = event.get_coords()
+               if self._currentButtonState == self.BUTTON_STATE_PLAY:
+                       newState = self.BUTTON_STATE_PAUSE
+               else:
+                       newState = self.BUTTON_STATE_PLAY
+               self._potentialButtonState = newState
+               cairoContext = self._image.window.cairo_create()
+               if not self._isPortrait:
+                       cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+               self._draw_state(cairoContext, self._potentialButtonState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_button_release(self, widget, event):
+               try:
+                       mousePosition = event.get_coords()
+                       newState = self._calculate_state(mousePosition)
+                       if newState == self.BUTTON_STATE_PLAY:
+                               self._player.play()
+                       elif newState == self.BUTTON_STATE_PAUSE:
+                               self._player.pause()
+                       elif newState == self.BUTTON_STATE_NEXT:
+                               self._player.next()
+                       elif newState == self.BUTTON_STATE_BACK:
+                               self._player.back()
+                       elif newState == self.BUTTON_STATE_UP:
+                               raise NotImplementedError("Drag-down not implemented yet")
+                       elif newState == self.BUTTON_STATE_CANCEL:
+                               pass
+               finally:
+                       if self._player.state == "play":
+                               newState = self.BUTTON_STATE_PLAY
+                       else:
+                               newState = self.BUTTON_STATE_PAUSE
+                       self._potentialButtonState = newState
+                       cairoContext = self._image.window.cairo_create()
+                       if not self._isPortrait:
+                               cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                       self._draw_state(cairoContext, self._potentialButtonState)
+                       self._clickPosition = self._NO_POSITION
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_motion_notify(self, widget, event):
+               if self._clickPosition == self._NO_POSITION:
+                       return
+
+               mousePosition = event.get_coords()
+               newState = self._calculate_state(mousePosition)
+               if newState != self._potentialButtonState:
+                       self._potentialButtonState = newState
+                       cairoContext = self._image.window.cairo_create()
+                       if not self._isPortrait:
+                               cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+                       self._draw_state(cairoContext, self._potentialButtonState)
+
+       def _calculate_state(self, newCoord):
+               assert self._clickPosition != self._NO_POSITION
+
+               if self._isPortrait:
+                       delta = (
+                               newCoord[0] - self._clickPosition[0],
+                               - (newCoord[1] - self._clickPosition[1])
+                       )
+               else:
+                       delta = (
+                               newCoord[1] - self._clickPosition[1],
+                               - (newCoord[0] - self._clickPosition[0])
+                       )
+               absDelta = (abs(delta[0]), abs(delta[1]))
+               if max(*absDelta) < self.MINIMUM_MOVEMENT or not self._canNavigate:
+                       if self._currentButtonState == self.BUTTON_STATE_PLAY:
+                               return self.BUTTON_STATE_PAUSE
+                       else:
+                               return self.BUTTON_STATE_PLAY
+
+               if absDelta[0] < absDelta[1]:
+                       if 0 < delta[1]:
+                               return self.BUTTON_STATE_CANCEL
+                       else:
+                               return self.BUTTON_STATE_UP
+               else:
+                       if 0 < delta[0]:
+                               return self.BUTTON_STATE_BACK
+                       else:
+                               return self.BUTTON_STATE_NEXT
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_expose(self, widget, event):
+               self._potentialButtonState = self._player.state
+               cairoContext = self._image.window.cairo_create()
+               if not self._isPortrait:
+                       cairoContext.transform(cairo.Matrix(0, 1, 1, 0, 0, 0))
+               self._draw_presenter(cairoContext, self._player.state)
+
+       def _draw_presenter(self, cairoContext, state):
+               assert state in (self._currentButtonState, self._potentialButtonState)
+
+               # Blank things
+               rect = self._image.get_allocation()
+               cairoContext.rectangle(
+                       0,
+                       0,
+                       rect.width,
+                       rect.height,
+               )
+               cairoContext.set_source_rgb(0, 0, 0)
+               cairoContext.fill()
+               cairoContext.paint()
+
+               # Draw Background
+               cairoContext.set_source_surface(
+                       self._backgroundImage,
+                       0,
+                       0,
+               )
+               cairoContext.paint()
+
+               # title
+               if self._player.title:
+                       _moduleLogger.info("Displaying text")
+                       backWidth = self._backgroundImage.get_width()
+                       backHeight = self._backgroundImage.get_height()
+
+                       pangoContext = self._image.create_pango_context()
+                       textLayout = pango.Layout(pangoContext)
+                       textLayout.set_markup(self._player.title)
+
+                       textWidth, textHeight = textLayout.get_pixel_size()
+                       textX = backWidth / 2 - textWidth / 2
+                       textY = backHeight - textHeight - self._buttonImage.get_height()
+
+                       cairoContext.move_to(textX, textY)
+                       cairoContext.set_source_rgb(0, 0, 0)
+                       cairoContext.show_layout(textLayout)
+
+               self._draw_state(cairoContext, state)
+
+       def _draw_state(self, cairoContext, state):
+               assert state in (self._currentButtonState, self._potentialButtonState)
+               if state == self.BUTTON_STATE_CANCEL:
+                       state = self._currentButtonState
+
+               backWidth = self._backgroundImage.get_width()
+               backHeight = self._backgroundImage.get_height()
+
+               imagePath = self._STATE_TO_IMAGE[state]
+               self._buttonImage = self._store.get_surface_from_store(imagePath)
+               cairoContext.set_source_surface(
+                       self._buttonImage,
+                       backWidth / 2 - self._buttonImage.get_width() / 2,
+                       backHeight - self._buttonImage.get_height() + 5,
+               )
+               cairoContext.paint()