Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/mediaview.py
diff options
context:
space:
mode:
authorDaniel Drake <dsd@laptop.org>2011-02-02 16:27:46 (GMT)
committer Daniel Drake <dsd@laptop.org>2011-02-02 16:37:48 (GMT)
commit3bc80c776f7ef2578a4b9fd26028574a12982ea3 (patch)
tree890aa0ee4a2b07d9c9704883d11f0385e03a2e70 /mediaview.py
parentca2bbd6d74342247de97cffa150b577246c63107 (diff)
UI rework
This is a rework of the UI that uses a more standard GTK+ principles than previously. There is still a small amount of black magic, kept away inside mediaview.py. The UI/model separation was also refined in places, and a lot of code was simplified. Overall the functionality remains identical, and I tried to keep the UI the same as before (but it is not pixel perfect). As this was quite big there may be some bugs to shake out.
Diffstat (limited to 'mediaview.py')
-rw-r--r--mediaview.py512
1 files changed, 512 insertions, 0 deletions
diff --git a/mediaview.py b/mediaview.py
new file mode 100644
index 0000000..670a0fa
--- /dev/null
+++ b/mediaview.py
@@ -0,0 +1,512 @@
+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 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):
+ 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()
+