예제 #1
0
파일: thumbnail.py 프로젝트: cs2c/AppStream
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()
예제 #2
0
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()
예제 #3
0
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