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('' + _('Author:') + '')
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('' + _('Tags:') + '')
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('' + _('Date:') + ' ' + 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()