# Copyright (C) 2011 Canonical # # Authors: # Matthew McGowan # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; version 3. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, Atk, Gio, GObject, GdkPixbuf, GLib import logging from softwarecenter.utils import SimpleFileDownloader from imagedialog import SimpleShowImageDialog from gettext import gettext as _ LOG = logging.getLogger(__name__) class ScreenshotData(GObject.GObject): __gsignals__ = {"screenshots-available": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (),), } def __init__(self): GObject.GObject.__init__(self) self._sig = 0 self._screenshots = [] def set_app_details(self, app_details): if self._sig > 0: self.app_details.disconnect(self._sig) self.app_details = app_details self.appname = app_details.display_name self.pkgname = app_details.pkgname self._sig = self.app_details.connect( "screenshots-available", self._on_screenshots_available) self._screenshots = [] self.app_details.query_multiple_screenshots() def _on_screenshots_available(self, screenshot_data, screenshots): self._screenshots = screenshots self.emit("screenshots-available") def get_n_screenshots(self): return len(self._screenshots) def get_nth_large_screenshot(self, index): return self._screenshots[index]['large_image_url'] def get_nth_small_screenshot(self, index): return self._screenshots[index]['small_image_url'] class ScreenshotButton(Gtk.Button): def __init__(self): Gtk.Button.__init__(self) self.set_focus_on_click(False) self.set_valign(Gtk.Align.CENTER) self.image = Gtk.Image() self.add(self.image) def do_draw(self, cr): if self.has_focus(): context = self.get_style_context() _a = self.get_allocation() a = self.image.get_allocation() pb = self.image.get_pixbuf() pbw, pbh = pb.get_width(), pb.get_height() Gtk.render_focus( context, cr, a.x - _a.x + (a.width - pbw) / 2 - 4, a.y - _a.y + (a.height - pbh) / 2 - 4, pbw + 8, pbh + 8) for child in self: self.propagate_draw(child, cr) class ScreenshotGallery(Gtk.VBox): """ Widget that displays screenshot availability, download progress, and eventually the screenshot itself. """ MAX_SIZE_CONSTRAINTS = 300, 250 SPINNER_SIZE = 32, 32 ZOOM_ICON = "stock_zoom-page" NOT_AVAILABLE_STRING = _('No screenshot available') USE_CACHING = True def __init__(self, distro, icons): Gtk.VBox.__init__(self) # data self.distro = distro self.icons = icons self.data = ScreenshotData() self.data.connect( "screenshots-available", self._on_screenshots_available) # state tracking self.ready = False self.screenshot_pixbuf = None self.screenshot_available = False self._thumbnail_sigs = [] self._height = 0 # zoom cursor try: zoom_pb = self.icons.load_icon(self.ZOOM_ICON, 22, 0) # FIXME self._zoom_cursor = Gdk.Cursor.new_from_pixbuf( Gdk.Display.get_default(), zoom_pb, 0, 0) # x, y except: self._zoom_cursor = None # convenience class for handling the downloading (or not) of # any screenshot self.loader = SimpleFileDownloader() self.loader.connect( 'error', self._on_screenshot_load_error) self.loader.connect( 'file-url-reachable', self._on_screenshot_query_complete) self.loader.connect( 'file-download-complete', self._on_screenshot_download_complete) self._build_ui() # add cleanup handler to avoid signals after we are destroyed self.connect("destroy", self._on_destroy) def _on_destroy(self, widget): # we need to disconnect here otherwise gtk segfaults when it # tries to set a already destroyed gtk image self.loader.disconnect_by_func( self._on_screenshot_download_complete) self.loader.disconnect_by_func( self._on_screenshot_query_complete) self.loader.disconnect_by_func( self._on_screenshot_load_error) # overrides def do_get_preferred_width(self): if self.data.get_n_screenshots() <= 1: pb = self.main_screenshot.image.get_pixbuf() if pb: width = pb.get_width() + 20 return width, width return 320, 320 def do_get_preferred_height(self): pb = self.main_screenshot.image.get_pixbuf() if pb: height = pb.get_height() if self.data.get_n_screenshots() <= 1: height += 20 height = max(self._height, height) self._height = height return height, height else: height += 110 height = max(self._height, height) self._height = height return height, height self._height = max(self._height, 250) return self._height, self._height # private def _build_ui(self): self.set_border_width(3) # the frame around the screenshot (placeholder) self.screenshot_frame = Gtk.VBox() self.pack_start(self.screenshot_frame, True, True, 0) self.spinner = Gtk.Spinner() self.spinner.set_size_request(*self.SPINNER_SIZE) self.spinner.set_valign(Gtk.Align.CENTER) self.spinner.set_halign(Gtk.Align.CENTER) self.screenshot_frame.add(self.spinner) # main big clickable screenshot button self.main_screenshot = ScreenshotButton() self.screenshot_frame.pack_start(self.main_screenshot, True, False, 0) # unavailable layout self.unavailable = Gtk.Label(label=_(self.NOT_AVAILABLE_STRING)) self.unavailable.set_alignment(0.5, 0.5) # force the label state to INSENSITIVE so we get the nice # subtle etched in look self.unavailable.set_state(Gtk.StateType.INSENSITIVE) self.screenshot_frame.add(self.unavailable) self.thumbnails = ThumbnailGallery(self) self.thumbnails.set_margin_top(5) self.thumbnails.set_halign(Gtk.Align.CENTER) self.pack_end(self.thumbnails, False, False, 0) self.thumbnails.connect( "thumb-selected", self.on_thumbnail_selected) self.main_screenshot.connect("clicked", self.on_clicked) self.main_screenshot.connect('enter-notify-event', self._on_enter) self.main_screenshot.connect('leave-notify-event', self._on_leave) self.show_all() def _on_enter(self, widget, event): if self.get_is_actionable(): self.get_window().set_cursor(self._zoom_cursor) def _on_leave(self, widget, event): self.get_window().set_cursor(None) def _on_key_press(self, widget, event): # react to spacebar, enter, numpad-enter if (event.keyval in ( Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_KP_Enter) and self.get_is_actionable()): self.set_state(Gtk.StateType.ACTIVE) def _on_key_release(self, widget, event): # react to spacebar, enter, numpad-enter if (event.keyval in (Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_KP_Enter) and self.get_is_actionable()): self.set_state(Gtk.StateType.NORMAL) self._show_image_dialog() def _show_image_dialog(self): """ Displays the large screenshot in a separate dialog window """ if self.data and self.screenshot_pixbuf: title = _("%s - Screenshot") % self.data.appname toplevel = self.get_toplevel() d = SimpleShowImageDialog( title, self.screenshot_pixbuf, toplevel) d.run() d.destroy() def _on_screenshots_available(self, screenshots): self.thumbnails.set_thumbnails_from_data(screenshots) def _on_screenshot_download_complete(self, loader, screenshot_path): try: self.screenshot_pixbuf = GdkPixbuf.Pixbuf.new_from_file( screenshot_path) except Exception, e: LOG.exception("Pixbuf.new_from_file() failed") self.loader.emit('error', GObject.GError, e) return False #context = self.main_screenshot.get_style_context() tw, th = self.MAX_SIZE_CONSTRAINTS pb = self._downsize_pixbuf(self.screenshot_pixbuf, tw, th) self.main_screenshot.image.set_from_pixbuf(pb) self.ready = True self.display_image() def _on_screenshot_load_error(self, loader, err_type, err_message): self.set_screenshot_available(False) self.ready = True def _on_screenshot_query_complete(self, loader, reachable): self.set_screenshot_available(reachable) if not reachable: self.ready = True def _downsize_pixbuf(self, pb, target_w, target_h): w = pb.get_width() h = pb.get_height() sf = min(float(target_w) / w, float(target_h) / h) sw = int(w * sf) sh = int(h * sf) return pb.scale_simple(sw, sh, GdkPixbuf.InterpType.BILINEAR) # public def download_and_display_from_url(self, url): self.loader.download_file(url, use_cache=self.USE_CACHING) def clear(self): """ All state trackers are set to their initial states, and the old screenshot is cleared from the view. """ self._height = 0 self.clear_main_screenshot() self.thumbnails.clear() def clear_main_screenshot(self): self.screenshot_available = True self.ready = False self.display_spinner() def display_spinner(self): self.main_screenshot.image.clear() self.main_screenshot.hide() self.unavailable.hide() self.spinner.show() self.spinner.start() def display_unavailable(self): self.spinner.hide() self.spinner.stop() self.unavailable.show() self.main_screenshot.hide() acc = self.get_accessible() acc.set_name(_(self.NOT_AVAILABLE_STRING)) acc.set_role(Atk.Role.LABEL) def display_image(self): self.unavailable.hide() self.spinner.stop() self.spinner.hide() self.main_screenshot.show_all() self.thumbnails.show() def get_is_actionable(self): """ Returns true if there is a screenshot available and the download has completed """ return self.screenshot_available and self.ready def set_screenshot_available(self, available): """ Configures the ScreenshotView depending on whether there is a screenshot available. """ if not available: self.display_unavailable() elif available and self.unavailable.get_property("visible"): self.display_spinner() self.screenshot_available = available def on_clicked(self, button): if self.get_is_actionable(): self._show_image_dialog() def fetch_screenshots(self, app_details): """ Called to configure the screenshotview for a new application. The existing screenshot is cleared and the process of fetching a new screenshot is instigated. """ self.clear() acc = self.get_accessible() acc.set_name(_('Fetching screenshot ...')) self.data.set_app_details(app_details) self.display_spinner() self.download_and_display_from_url(app_details.screenshot) def on_thumbnail_selected(self, gallery, id_): self.clear_main_screenshot() large_url = self.data.get_nth_large_screenshot(id_) self.download_and_display_from_url(large_url) def draw(self, widget, cr): """ Draws the thumbnail frame """ pass class Thumbnail(Gtk.Button): def __init__(self, id_, url, cancellable, gallery): Gtk.Button.__init__(self) self.id_ = id_ def download_complete_cb(loader, path): width, height = ThumbnailGallery.THUMBNAIL_SIZE_CONSTRAINTS pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( path, width, height, # width, height constraints True) # respect image proportionality im = Gtk.Image.new_from_pixbuf(pixbuf) self.add(im) self.show_all() loader = SimpleFileDownloader() loader.connect("file-download-complete", download_complete_cb) loader.download_file( url, use_cache=ScreenshotGallery.USE_CACHING) self.connect("draw", self.on_draw) def on_draw(self, thumb, cr): state = self.get_state_flags() if self.has_focus() or (state & Gtk.StateFlags.ACTIVE) > 0: return for child in self: self.propagate_draw(child, cr) return True class ThumbnailGallery(Gtk.HBox): __gsignals__ = { "thumb-selected": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (int,),), } THUMBNAIL_SIZE_CONSTRAINTS = 90, 80 THUMBNAIL_MAX_COUNT = 3 def __init__(self, gallery): Gtk.HBox.__init__(self) self.gallery = gallery self.distro = gallery.distro self.icons = gallery.icons self.cancel = Gio.Cancellable() self._prev = None self._handlers = [] def clear(self): self.cancel.cancel() self.cancel.reset() for sig in self._handlers: GLib.source_remove(sig) for child in self: child.destroy() def set_thumbnails_from_data(self, data): self.clear() # if there are multiple screenshots n = data.get_n_screenshots() if n <= 1: return # pick the first ones, the data is sorted by most appropriate # version first - if at some later point we have a lot of # screenshots we could consider randomizing again for i in range(min(n, ThumbnailGallery.THUMBNAIL_MAX_COUNT)): url = data.get_nth_small_screenshot(i) self._create_thumbnail_for_url(i, url) # set first child to selected self._prev = self.get_children()[0] self._prev.set_state_flags(Gtk.StateFlags.SELECTED, False) self.show_all() def _create_thumbnail_for_url(self, index, url): thumbnail = Thumbnail(index, url, self.cancel, self.gallery) self.pack_start(thumbnail, False, False, 0) sig = thumbnail.connect("clicked", self.on_clicked) self._handlers.append(sig) def on_clicked(self, thumb): if self._prev is not None: self._prev.set_state_flags(Gtk.StateFlags.NORMAL, True) thumb.set_state_flags(Gtk.StateFlags.SELECTED, False) self._prev = thumb self.emit("thumb-selected", thumb.id_)