Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/mediaview.py
diff options
context:
space:
mode:
Diffstat (limited to 'mediaview.py')
-rw-r--r--mediaview.py515
1 files changed, 515 insertions, 0 deletions
diff --git a/mediaview.py b/mediaview.py
new file mode 100644
index 0000000..01567ff
--- /dev/null
+++ b/mediaview.py
@@ -0,0 +1,515 @@
+import os
+from gettext import gettext as _
+
+import gobject
+import gtk
+from gtk import gdk
+
+import constants
+import utils
+
+COLOR_BLACK = gdk.color_parse('#000000')
+COLOR_WHITE = gdk.color_parse('#ffffff')
+COLOR_GREY = gdk.color_parse('#808080')
+
+class XoIcon(gtk.Image):
+ def __init__(self):
+ super(XoIcon, self).__init__()
+
+ def set_colors(self, stroke, fill):
+ pixbuf = utils.load_colored_svg('xo-guy.svg', stroke, fill)
+ self.set_from_pixbuf(pixbuf)
+
+
+class InfoView(gtk.EventBox):
+ """
+ A metadata view/edit widget, that presents a primary view area in the top
+ right and a secondary view area in the bottom left.
+ """
+ __gsignals__ = {
+ 'primary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ 'secondary-allocated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
+ 'tags-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+ }
+
+ def __init__(self):
+ super(InfoView, self).__init__()
+ self.modify_bg(gtk.STATE_NORMAL, COLOR_GREY)
+
+ self.connect('size-allocate', self._size_allocate)
+
+ self._outer_vbox = gtk.VBox(spacing=7)
+ self.add(self._outer_vbox)
+
+ hbox = gtk.HBox()
+ self._outer_vbox.pack_start(hbox, expand=True, fill=True)
+
+ inner_vbox = gtk.VBox(spacing=5)
+ hbox.pack_start(inner_vbox, expand=True, fill=True, padding=6)
+
+ author_hbox = gtk.HBox()
+ inner_vbox.pack_start(author_hbox, expand=False)
+
+ label = gtk.Label()
+ label.set_markup('<b>' + _('Author:') + '</b>')
+ author_hbox.pack_start(label, expand=False)
+
+ self._xo_icon = XoIcon()
+ author_hbox.pack_start(self._xo_icon, expand=False)
+
+ self._author_label = gtk.Label()
+ author_hbox.pack_start(self._author_label, expand=False)
+
+ self._date_label = gtk.Label()
+ self._date_label.set_line_wrap(True)
+ alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0)
+ alignment.add(self._date_label)
+ inner_vbox.pack_start(alignment, expand=False)
+
+ label = gtk.Label()
+ label.set_markup('<b>' + _('Tags:') + '</b>')
+ alignment = gtk.Alignment(0.0, 0.5, 0.0, 0.0)
+ alignment.add(label)
+ inner_vbox.pack_start(alignment, expand=False)
+
+ textview = gtk.TextView()
+ self._tags_buffer = textview.get_buffer()
+ self._tags_buffer.connect('changed', self._tags_changed)
+ inner_vbox.pack_start(textview, expand=True, fill=True)
+
+ # the main viewing widget will be painted exactly on top of this one
+ alignment = gtk.Alignment(1.0, 0.0, 0.0, 0.0)
+ self._view_bg = gtk.EventBox()
+ self._view_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ alignment.add(self._view_bg)
+ hbox.pack_start(alignment, expand=False)
+
+ # the secondary viewing widget will be painted exactly on top of this one
+ alignment = gtk.Alignment(0.0, 1.0, 0.0, 0.0)
+ self._live_bg = gtk.EventBox()
+ self._live_bg.modify_bg(gtk.STATE_NORMAL, COLOR_BLACK)
+ alignment.add(self._live_bg)
+ self._outer_vbox.pack_start(alignment, expand=False)
+
+ def fit_to_allocation(self, allocation):
+ # main viewing area: 50% of each dimension
+ scale = 0.5
+ w = int(allocation.width * scale)
+ h = int(allocation.height * scale)
+ self._view_bg.set_size_request(w, h)
+
+ # live area: 1/4 of each dimension
+ scale = 0.25
+ w = int(allocation.width * scale)
+ h = int(allocation.height * scale)
+ self._live_bg.set_size_request(w, h)
+
+ def show(self):
+ self.show_all()
+
+ def hide(self):
+ self.hide_all()
+
+ def set_author(self, name, stroke, fill):
+ self._xo_icon.set_colors(stroke, fill)
+ self._author_label.set_text(name)
+
+ def set_date(self, date):
+ self._date_label.set_markup('<b>' + _('Date:') + '</b> ' + date)
+
+ def set_tags(self, tags):
+ self._tags_buffer.set_text(tags)
+
+ def _size_allocate(self, widget, allocation):
+ self.emit('primary-allocated', self._view_bg.allocation)
+ self.emit('secondary-allocated', self._live_bg.allocation)
+
+ def _tags_changed(self, widget):
+ self.emit('tags-changed', widget)
+
+class VideoBox(gtk.EventBox):
+ """
+ A widget with its own window for rendering a gstreamer video sink onto.
+ """
+ def __init__(self):
+ super(VideoBox, self).__init__()
+ self.unset_flags(gtk.DOUBLE_BUFFERED)
+ self.set_flags(gtk.APP_PAINTABLE)
+ self._sink = None
+ self._xid = None
+ self.connect('realize', self._realize)
+
+ def _realize(self, widget):
+ self._xid = self.window.xid
+
+ def do_expose_event(self):
+ if self._sink:
+ self._sink.expose()
+ return False
+ else:
+ return True
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_sink(self, sink):
+ self._sink = sink
+ sink.set_xwindow_id(self._xid)
+
+class FullscreenButton(gtk.EventBox):
+ def __init__(self):
+ super(FullscreenButton, self).__init__()
+
+ path = os.path.join(constants.GFX_PATH, 'max-reduce.svg')
+ self._enlarge_pixbuf = gdk.pixbuf_new_from_file(path)
+ self.width = self._enlarge_pixbuf.get_width()
+ self.height = self._enlarge_pixbuf.get_height()
+
+ path = os.path.join(constants.GFX_PATH, 'max-enlarge.svg')
+ self._reduce_pixbuf = gdk.pixbuf_new_from_file_at_size(path, self.width, self.height)
+
+ self._image = gtk.Image()
+ self.set_enlarge()
+ self._image.show()
+ self.add(self._image)
+
+ def set_enlarge(self):
+ self._image.set_from_pixbuf(self._enlarge_pixbuf)
+
+ def set_reduce(self):
+ self._image.set_from_pixbuf(self._reduce_pixbuf)
+
+
+class InfoButton(gtk.EventBox):
+ def __init__(self):
+ super(InfoButton, self).__init__()
+
+ path = os.path.join(constants.GFX_PATH, 'corner-info.svg')
+ pixbuf = gdk.pixbuf_new_from_file(path)
+ self.width = pixbuf.get_width()
+ self.height = pixbuf.get_height()
+
+ self._image = gtk.image_new_from_pixbuf(pixbuf)
+ self._image.show()
+ self.add(self._image)
+
+
+class ImageBox(gtk.EventBox):
+ def __init__(self):
+ super(ImageBox, self).__init__()
+ self._pixbuf = None
+ self._image = gtk.Image()
+ self.add(self._image)
+
+ def show(self):
+ self._image.show()
+ super(ImageBox, self).show()
+
+ def hide(self):
+ self._image.hide()
+ super(ImageBox, self).hide()
+
+ def clear(self):
+ self._image.clear()
+ self._pixbuf = None
+
+ def set_pixbuf(self, pixbuf):
+ self._pixbuf = pixbuf
+
+ def set_size(self, width, height):
+ if self._pixbuf:
+ if width == self._pixbuf.get_width() and height == self._pixbuf.get_height():
+ pixbuf = self._pixbuf
+ else:
+ pixbuf = self._pixbuf.scale_simple(width, height, gdk.INTERP_BILINEAR)
+
+ self._image.set_from_pixbuf(pixbuf)
+
+ self._image.set_size_request(width, height)
+ self.set_size_request(width, height)
+
+class MediaView(gtk.EventBox):
+ """
+ A widget to show the main record UI with a video feed, but with some
+ extra features: possibility to show images, information UI about media,
+ etc.
+
+ It is made complicated because under the UI design, some widgets need
+ to be placed on top of others. In GTK+, this is not trivial. We achieve
+ this here by relying on the fact that GDK+ specifically allows us to
+ raise specific windows, and by packing all our Z-order-sensitive widgets
+ into EventBoxes (which have their own windows).
+ """
+ __gtype_name__ = "MediaView"
+ __gsignals__ = {
+ 'media-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'pip-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'full-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'info-clicked': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ 'tags-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,)),
+ }
+
+ MODE_LIVE = 0
+ MODE_PHOTO = 1
+ MODE_VIDEO = 2
+ MODE_STILL = 3
+ MODE_INFO_PHOTO = 4
+ MODE_INFO_VIDEO = 5
+
+ @staticmethod
+ def _raise_widget(widget):
+ widget.show()
+ widget.realize()
+ widget.window.raise_()
+
+ def __init__(self):
+ self._mode = None
+ self._allocation = None
+ self._hide_controls_timer = None
+
+ super(MediaView, self).__init__()
+ self.connect('size-allocate', self._size_allocate)
+ self.connect('motion-notify-event', self._motion_notify)
+ self.set_events(gdk.POINTER_MOTION_MASK | gdk.POINTER_MOTION_HINT_MASK)
+
+ self._fixed = gtk.Fixed()
+ self.add(self._fixed)
+
+ self._info_view = InfoView()
+ self._info_view.connect('primary-allocated', self._info_view_primary_allocated)
+ self._info_view.connect('secondary-allocated', self._info_view_secondary_allocated)
+ self._info_view.connect('tags-changed', self._info_view_tags_changed)
+ self._fixed.put(self._info_view, 0, 0)
+
+ self._image_box = ImageBox()
+ self._image_box.connect('button-release-event', self._image_clicked)
+ self._fixed.put(self._image_box, 0, 0)
+
+ self._video = VideoBox()
+ self._video.connect('button-release-event', self._video_clicked)
+ self._fixed.put(self._video, 0, 0)
+
+ self._video2 = VideoBox()
+ self._video2.connect('button-release-event', self._video2_clicked)
+ self._fixed.put(self._video2, 0, 0)
+
+ self._info_button = InfoButton()
+ self._info_button.connect('button-release-event', self._info_clicked)
+ self._fixed.put(self._info_button, 0, 0)
+
+ self._full_button = FullscreenButton()
+ self._full_button.connect('button-release-event', self._full_clicked)
+ self._fixed.put(self._full_button, 0, 0)
+
+ self._switch_mode(MediaView.MODE_LIVE)
+
+ def _size_allocate(self, widget, allocation):
+ # First check if we've already processed an allocation of this size.
+ # This is necessary because the operations we do in response to the
+ # size allocation cause another size allocation to happen.
+ if self._allocation == allocation:
+ return
+
+ self._allocation = allocation
+ self._place_widgets()
+
+ def _motion_notify(self, widget, event):
+ if self._hide_controls_timer:
+ # remove timer, it will be reprogrammed right after
+ gobject.source_remove(self._hide_controls_timer)
+ else:
+ self._show_controls()
+
+ self._hide_controls_timer = gobject.timeout_add(2000, self._hide_controls)
+
+ def _show_controls(self):
+ if self._mode in (MediaView.MODE_LIVE, MediaView.MODE_VIDEO, MediaView.MODE_PHOTO, MediaView.MODE_STILL):
+ self._raise_widget(self._full_button)
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._raise_widget(self._info_button)
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._raise_widget(self._video)
+
+ def _hide_controls(self):
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ self._full_button.hide()
+ if self._mode not in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._info_button.hide()
+
+ if self._mode in (MediaView.MODE_VIDEO, MediaView.MODE_PHOTO):
+ self._video.hide()
+
+ return False
+
+ def _place_widgets(self):
+ allocation = self._allocation
+
+ self._image_box.hide()
+ self._video.hide()
+ self._video2.hide()
+ self._info_view.hide()
+ self._info_button.hide()
+
+ border = 5
+ full_button_x = allocation.width - border - self._full_button.width
+ full_button_y = border
+ self._fixed.move(self._full_button, full_button_x, full_button_y)
+
+ info_x = allocation.width - self._info_button.width
+ info_y = allocation.height - self._info_button.height
+ self._fixed.move(self._info_button, info_x, info_y)
+
+ if self._mode == MediaView.MODE_LIVE:
+ self._fixed.move(self._video, 0, 0)
+ self._video.set_size_request(allocation.width, allocation.height)
+ self._video.show()
+ self._image_box.clear()
+ elif self._mode == MediaView.MODE_VIDEO:
+ self._fixed.move(self._video2, 0, 0)
+ self._video2.set_size_request(allocation.width, allocation.height)
+ self._video2.show()
+ self._image_box.clear()
+
+ vid_h = allocation.height / 6
+ vid_w = allocation.width / 6
+ self._video.set_size_request(vid_w, vid_h)
+
+ border = 20
+ vid_x = border
+ vid_y = self.allocation.height - border - vid_h
+ self._fixed.move(self._video, vid_x, vid_y)
+ elif self._mode == MediaView.MODE_PHOTO:
+ self._fixed.move(self._image_box, 0, 0)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._image_box.show()
+
+ vid_h = allocation.height / 6
+ vid_w = allocation.width / 6
+ self._video.set_size_request(vid_w, vid_h)
+
+ border = 20
+ vid_x = border
+ vid_y = self.allocation.height - border - vid_h
+ self._fixed.move(self._video, vid_x, vid_y)
+ elif self._mode == MediaView.MODE_STILL:
+ self._fixed.move(self._image_box, 0, 0)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._image_box.show()
+ elif self._mode in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._full_button.hide()
+ self._info_view.set_size_request(allocation.width, allocation.height)
+ self._info_view.fit_to_allocation(allocation)
+ self._info_view.show()
+ self._raise_widget(self._info_button)
+
+ def _info_view_primary_allocated(self, widget, allocation):
+ if self._mode == MediaView.MODE_INFO_PHOTO:
+ self._fixed.move(self._image_box, allocation.x, allocation.y)
+ self._image_box.set_size(allocation.width, allocation.height)
+ self._raise_widget(self._image_box)
+ elif self._mode == MediaView.MODE_INFO_VIDEO:
+ self._fixed.move(self._video2, allocation.x, allocation.y)
+ self._video2.set_size_request(allocation.width, allocation.height)
+ self._raise_widget(self._video2)
+
+ def _info_view_secondary_allocated(self, widget, allocation):
+ if self._mode in (MediaView.MODE_INFO_PHOTO, MediaView.MODE_INFO_VIDEO):
+ self._fixed.move(self._video, allocation.x, allocation.y)
+ self._video.set_size_request(allocation.width, allocation.height)
+ self._raise_widget(self._video)
+
+ def _info_view_tags_changed(self, widget, tbuffer):
+ self.emit('tags-changed', tbuffer)
+
+ def _switch_mode(self, new_mode):
+ if self._mode == MediaView.MODE_LIVE and new_mode == MediaView.MODE_LIVE:
+ return
+ self._mode = new_mode
+
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ if self._allocation:
+ self._place_widgets()
+
+ def _image_clicked(self, widget, event):
+ self.emit('media-clicked')
+
+ def _video_clicked(self, widget, event):
+ if self._mode != MediaView.MODE_LIVE:
+ self.emit('pip-clicked')
+
+ def _video2_clicked(self, widget, event):
+ self.emit('media-clicked')
+
+ def _full_clicked(self, widget, event):
+ self.emit('full-clicked')
+
+ def _info_clicked(self, widget, event):
+ self.emit('info-clicked')
+
+ def _show_info(self, mode, author, stroke, fill, date, tags):
+ self._info_view.set_author(author, stroke, fill)
+ self._info_view.set_date(date)
+ self._info_view.set_tags(tags)
+ self._switch_mode(mode)
+
+ def show_info_photo(self, author, stroke, fill, date, tags):
+ self._show_info(MediaView.MODE_INFO_PHOTO, author, stroke, fill, date, tags)
+
+ def show_info_video(self, author, stroke, fill, date, tags):
+ self._show_info(MediaView.MODE_INFO_VIDEO, author, stroke, fill, date, tags)
+
+ def set_fullscreen(self, fullscreen):
+ if self._hide_controls_timer:
+ gobject.source_remove(self._hide_controls_timer)
+ self._hide_controls_timer = None
+
+ if fullscreen:
+ self._full_button.set_reduce()
+ else:
+ self._full_button.set_enlarge()
+
+ def realize_video(self):
+ self._video.realize()
+ self._video2.realize()
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_video_sink(self, sink):
+ self._video.set_sink(sink)
+
+ # can be called from gstreamer thread, must not do any GTK+ stuff
+ def set_video2_sink(self, sink):
+ self._video2.set_sink(sink)
+
+ def show_still(self, pixbuf):
+ # don't modify the original...
+ pixbuf = pixbuf.copy()
+ pixbuf.saturate_and_pixelate(pixbuf, 0, 0)
+ self._image_box.set_pixbuf(pixbuf)
+ self._switch_mode(MediaView.MODE_STILL)
+
+ def show_photo(self, path):
+ if path:
+ pixbuf = gdk.pixbuf_new_from_file(path)
+ self._image_box.set_pixbuf(pixbuf)
+ self._switch_mode(MediaView.MODE_PHOTO)
+
+ def show_video(self):
+ self._switch_mode(MediaView.MODE_VIDEO)
+
+ def show_live(self):
+ self._switch_mode(MediaView.MODE_LIVE)
+
+ def show(self):
+ self._fixed.show()
+ super(MediaView, self).show()
+
+ def hide(self):
+ self._fixed.hide()
+ super(MediaView, self).hide()
+