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_load_error) # overrides def do_get_preferred_width(self): if self.data.get_n_screenshots() <= 1: pb = self.button.image.get_pixbuf() if pb: width = pb.get_width() + 20 return width, width return 320, 320 def do_get_preferred_height(self): pb = self.button.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 = Gtk.VBox() self.pack_start(self.screenshot, 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.add(self.spinner) # clickable screenshot button self.button = ScreenshotButton() self.screenshot.pack_start(self.button, 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.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.button.connect("clicked", self.on_clicked) self.button.connect('enter-notify-event', self._on_enter) self.button.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 seperate 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.button.get_style_context() tw, th = self.MAX_SIZE_CONSTRAINTS pb = self._downsize_pixbuf(self.screenshot_pixbuf, tw, th) self.button.image.set_from_pixbuf(pb) self.ready = True self.display_image()
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_load_error) # overrides def do_get_preferred_width(self): if self.data.get_n_screenshots() <= 1: pb = self.button.image.get_pixbuf() if pb: width = pb.get_width() + 20 return width, width return 320, 320 def do_get_preferred_height(self): pb = self.button.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 = Gtk.VBox() self.pack_start(self.screenshot, 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.add(self.spinner) # clickable screenshot button self.button = ScreenshotButton() self.screenshot.pack_start(self.button, 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.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.button.connect("clicked", self.on_clicked) self.button.connect('enter-notify-event', self._on_enter) self.button.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 seperate 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.button.get_style_context() tw, th = self.MAX_SIZE_CONSTRAINTS pb = self._downsize_pixbuf(self.screenshot_pixbuf, tw, th) self.button.image.set_from_pixbuf(pb) self.ready = True self.display_image()
class ScreenshotThumbnail(Gtk.Alignment): """ Widget that displays screenshot availability, download prrogress, and eventually the screenshot itself. """ MAX_SIZE = 300, 300 IDLE_SIZE = 300, 150 SPINNER_SIZE = 32, 32 ZOOM_ICON = "stock_zoom-page" def __init__(self, distro, icons): Gtk.Alignment.__init__(self) self.set(0.5, 0.0, 1.0, 1.0) # data self.distro = distro self.icons = icons self.pkgname = None self.appname = None self.thumb_url = None self.large_url = None # state tracking self.ready = False self.screenshot_pixbuf = None self.screenshot_available = False self.alpha = 0.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 # tip stuff self._hide_after = None self.tip_alpha = 0.0 self._tip_fader = 0 self._tip_layout = self.create_pango_layout("") #m = "<small><b>%s</b></small>" #~ self._tip_layout.set_markup(m % _("Click for fullsize screenshot")) #~ self._tip_layout.set_ellipsize(Pango.EllipsizeMode.END) self._tip_xpadding = 4 self._tip_ypadding = 1 # cache the tip dimensions w, h = self._tip_layout.get_pixel_size() self._tip_size = (w+2*self._tip_xpadding, h+2*self._tip_ypadding) # convienience 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() return def _build_ui(self): self.set_redraw_on_allocate(False) # the frame around the screenshot (placeholder) self.set_border_width(3) # eventbox so we can connect to event signals event = Gtk.EventBox() event.set_visible_window(False) self.spinner_alignment = Gtk.Alignment.new(0.5, 0.5, 1.0, 0.0) self.spinner = Gtk.Spinner() self.spinner.set_size_request(*self.SPINNER_SIZE) self.spinner_alignment.add(self.spinner) # the image self.image = Gtk.Image() self.image.set_redraw_on_allocate(False) event.add(self.image) self.eventbox = event # connect the image to our custom draw func for fading in self.image.connect('draw', self._on_image_draw) # unavailable layout l = Gtk.Label(label=_('No screenshot')) # force the label state to INSENSITIVE so we get the nice subtle etched in look l.set_state(Gtk.StateType.INSENSITIVE) # center children both horizontally and vertically self.unavailable = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0) self.unavailable.add(l) # set the widget to be reactive to events self.set_property("can-focus", True) event.set_events(Gdk.EventMask.BUTTON_PRESS_MASK| Gdk.EventMask.BUTTON_RELEASE_MASK| Gdk.EventMask.KEY_RELEASE_MASK| Gdk.EventMask.KEY_PRESS_MASK| Gdk.EventMask.ENTER_NOTIFY_MASK| Gdk.EventMask.LEAVE_NOTIFY_MASK) # connect events to signal handlers event.connect('enter-notify-event', self._on_enter) event.connect('leave-notify-event', self._on_leave) event.connect('button-press-event', self._on_press) event.connect('button-release-event', self._on_release) self.connect('focus-in-event', self._on_focus_in) # self.connect('focus-out-event', self._on_focus_out) self.connect("key-press-event", self._on_key_press) self.connect("key-release-event", self._on_key_release) # signal handlers def _on_enter(self, widget, event): if not self.get_is_actionable(): return self.get_window().set_cursor(self._zoom_cursor) self.show_tip(hide_after=3000) return def _on_leave(self, widget, event): self.get_window().set_cursor(None) self.hide_tip() return def _on_press(self, widget, event): if event.button != 1 or not self.get_is_actionable(): return self.set_state(Gtk.StateType.ACTIVE) return def _on_release(self, widget, event): if event.button != 1 or not self.get_is_actionable(): return self.set_state(Gtk.StateType.NORMAL) self._show_image_dialog() return def _on_focus_in(self, widget, event): self.show_tip(hide_after=3000) return # def _on_focus_out(self, widget, event): # return 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) return 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() return def _on_image_draw(self, widget, cr): """ If the alpha value is less than 1, we override the normal draw for the GtkImage so we can draw with transparencey. """ #~ #~ if widget.get_storage_type() != Gtk.ImageType.PIXBUF: #~ return #~ #~ pb = widget.get_pixbuf() #~ if not pb: return True #~ #~ a = widget.get_allocation() #~ cr.rectangle(a.x, a.y, a.width, a.height) #~ cr.clip() #~ #~ # draw the pixbuf with the current alpha value #~ cr.set_source_pixbuf(pb, a.x, a.y) #~ cr.paint_with_alpha(self.alpha) #~ #~ if not self.tip_alpha: return True #~ #~ tw, th = self._tip_size #~ if a.width > tw: #~ self._tip_layout.set_width(-1) #~ else: #~ # tip is image width #~ tw = a.width #~ self._tip_layout.set_width(1024*(tw-2*self._tip_xpadding)) #~ #~ tx, ty = a.x+a.width-tw, a.y+a.height-th #~ #~ rr = mkit.ShapeRoundedRectangleIrregular() #~ rr.layout(cr, tx, ty, tx+tw, ty+th, radii=(6, 0, 0, 0)) #~ #~ cr.set_source_rgba(0,0,0,0.85*self.tip_alpha) #~ cr.fill() #~ #~ cr.move_to(tx+self._tip_xpadding, ty+self._tip_ypadding) #~ cr.layout_path(self._tip_layout) #~ cr.set_source_rgba(1,1,1,self.tip_alpha) #~ cr.fill() #~ return True return def _fade_in(self): """ This callback increments the alpha value from zero to 1, stopping once 1 is reached or exceeded. """ self.alpha += 0.05 if self.alpha >= 1.0: self.alpha = 1.0 self.queue_draw() return False self.queue_draw() return True def _tip_fade_in(self): """ This callback increments the alpha value from zero to 1, stopping once 1 is reached or exceeded. """ self.tip_alpha += 0.1 #ia = self.image.get_allocation() tw, th = self._tip_size if self.tip_alpha >= 1.0: self.tip_alpha = 1.0 self.image.queue_draw() # self.image.queue_draw_area(ia.x+ia.width-tw, # ia.y+ia.height-th, # tw, th) return False self.image.queue_draw() # self.image.queue_draw_area(ia.x+ia.width-tw, # ia.y+ia.height-th, # tw, th) return True def _tip_fade_out(self): """ This callback increments the alpha value from zero to 1, stopping once 1 is reached or exceeded. """ self.tip_alpha -= 0.1 #ia = self.image.get_allocation() tw, th = self._tip_size if self.tip_alpha <= 0.0: self.tip_alpha = 0.0 # self.image.queue_draw_area(ia.x+ia.width-tw, # ia.y+ia.height-th, # tw, th) self.image.queue_draw() return False self.image.queue_draw() # self.image.queue_draw_area(ia.x+ia.width-tw, # ia.y+ia.height-th, # tw, th) return True def _show_image_dialog(self): """ Displays the large screenshot in a seperate dialog window """ if self.screenshot_pixbuf: title = _("%s - Screenshot") % self.appname toplevel = self.get_toplevel() d = SimpleShowImageDialog(title, self.screenshot_pixbuf, toplevel) d.run() d.destroy() return def _on_screenshot_load_error(self, loader, err_type, err_message): self.set_screenshot_available(False) self.ready = True return def _on_screenshot_query_complete(self, loader, reachable): self.set_screenshot_available(reachable) if not reachable: self.ready = True return def _downsize_pixbuf(self, pb, target_w, target_h): w = pb.get_width() h = pb.get_height() if w > h: sf = float(target_w) / w else: sf = float(target_h) / h sw = int(w*sf) sh = int(h*sf) return pb.scale_simple(sw, sh, GdkPixbuf.InterpType.BILINEAR) def _on_screenshot_download_complete(self, loader, screenshot_path): def setter_cb(path): try: self.screenshot_pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) except Exception, e: LOG.exception("Pixbuf.new_from_file() failed") self.loader.emit('error', GObject.GError, e) return False # remove the spinner if self.spinner_alignment.get_parent(): self.spinner.stop() self.spinner.hide() self.remove(self.spinner_alignment) pb = self._downsize_pixbuf(self.screenshot_pixbuf, *self.MAX_SIZE) if not self.eventbox.get_parent(): self.add(self.eventbox) if self.get_property("visible"): self.show_all() self.image.set_size_request(-1, -1) self.image.set_from_pixbuf(pb) # queue parent redraw if height of new pb is less than idle height if pb.get_height() < self.IDLE_SIZE[1]: if self.get_parent(): self.get_parent().queue_draw() # start the fade in GObject.timeout_add(50, self._fade_in) self.ready = True return False GObject.timeout_add(500, setter_cb, screenshot_path) return