[rigo] implement loading of Application Images

At the end of the App View, load images bound to App.
This commit is contained in:
Fabio Erculiani
2012-02-28 21:58:04 +01:00
parent 161ba2cfd9
commit 71308d09de
6 changed files with 463 additions and 45 deletions

View File

@@ -116,3 +116,14 @@ GtkInfoBar #message-area-error GtkLabel {
#comment-box-author {
color: brown;
}
/* Application View Image Box */
#image-box-title {
color: #613056; /* purple */
}
#image-box-comment {
color: #345f14;
}
#image-box-author {
color: brown;
}

View File

@@ -377,16 +377,7 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewCommentMoreLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">Want to see more?</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
<placeholder/>
</child>
</object>
<packing>
@@ -409,6 +400,42 @@
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="appViewImagesAlign">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">25</property>
<property name="right_padding">25</property>
<child>
<object class="GtkVBox" id="appViewImagesVbox">
<property name="width_request">300</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="appViewCommentMoreLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">Want to see more?</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>

View File

@@ -34,7 +34,8 @@ from entropy.const import const_debug_write, const_debug_enabled, \
from entropy.i18n import _
from entropy.misc import ParallelTask
from entropy.services.client import WebService
from entropy.client.services.interfaces import ClientWebService
from entropy.client.services.interfaces import ClientWebService, \
DocumentList
import entropy.tools
@@ -544,7 +545,7 @@ class ApplicationMetadata(object):
offset, callback):
"""
Asynchronously download updated information regarding the
comments of given package.
comments of the given application.
This request disables local cache usage and directly queries
the remote Web Service.
Once data is available, callback will be called passing the returned
@@ -579,6 +580,71 @@ class ApplicationMetadata(object):
task.daemon = True
task.start()
@staticmethod
def download_images_async(entropy_ws, package_key, repository_id,
offset, callback, ignore_icons=True):
"""
Asynchronously download updated information regarding the images
of the given application.
This request disables local cache usage and directly queries
the remote Web Service.
Once data is available, callback will be called passing the returned
payload as argument.
For this method, the signature of callback is:
callback(DocumentList)
If the Web Service is not available for repository, None is passed as
payload of callback.
Please note that the callback is called from another thread.
"""
webserv = entropy_ws.get(repository_id)
if webserv is None:
task = ParallelTask(callback, None)
task.name = "DownloadImagesAsync::None"
task.daemon = True
task.start()
return None
def _getter():
outcome = None
try:
images = webserv.get_images(
[package_key], cache=False,
latest=True, offset=offset)[package_key]
fetched_images = []
for image in images:
if image.is_icon() and ignore_icons:
continue
# check if we have the file on-disk, otherwise
# spawn the fetch in parallel.
image_path = image.local_document()
if not os.path.isfile(image_path):
local_path = ApplicationMetadata._download_document(
webserv, image)
if local_path:
fetched_images.append(image)
else:
fetched_images.append(image)
# final DocumentList may contain less elements
# than those advertised by total().
_outcome = DocumentList(
images.package_name(),
images.total(),
images.offset())
_outcome.extend(fetched_images)
outcome = _outcome
finally:
# ignore exceptions, if any, and always
# call callback.
callback(outcome)
task = ParallelTask(_getter)
task.name = "DownloadImagesAsync::Getter"
task.daemon = True
task.start()
class SignalBoolean(object):
@@ -927,6 +993,31 @@ class Application(object):
"Application{%s}.download_comments called" % (
self._pkg_match,))
def download_images(self, callback, offset=0):
"""
Return Application Images Entropy Document object.
In case of missing comments (locally), None is returned.
The actual outcome of this method is a DocumentList object.
"""
repo = self._entropy.open_repository(self._repo_id)
key_slot = repo.retrieveKeySlot(self._pkg_id)
if key_slot is None:
task = ParallelTask(callback, None)
task.name = "DownloadImagesNoneCallback"
task.daemon = True
task.start()
return
key, slot = key_slot
ApplicationMetadata.download_images_async(
self._entropy_ws, key, self._repo_id,
offset, callback)
if const_debug_enabled():
const_debug_write(__name__,
"Application{%s}.download_images called" % (
self._pkg_match,))
def is_webservice_available(self):
"""
Return whether the Entropy Web Service is available

View File

@@ -46,7 +46,7 @@ class CommentBox(Gtk.VBox):
self._comment[ts_id])
time_str = escape_markup(time_str)
label.set_markup(
"<small><b>%s</b>" % (self._comment[user_id],) \
"<small><b>%s</b>" % (escape_markup(self._comment[user_id]),) \
+ ", <i>" + time_str + "</i>" \
+ "</small>")
label.set_line_wrap(True)
@@ -63,32 +63,34 @@ class CommentBox(Gtk.VBox):
if title:
title_id = Document.DOCUMENT_TITLE_ID
label = Gtk.Label()
label.set_markup("<b>" + self._comment[title_id] + "</b>")
label_align = Gtk.Alignment()
label_align.set_padding(0, 3, 0, 0)
label_align.add(label)
label.set_markup(
"<b>" + escape_markup(self._comment[title_id]) + "</b>")
label.set_name("comment-box-title")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
label.show()
vbox.pack_start(label, False, False, 0)
vbox.pack_start(label_align, False, False, 0)
data_id = Document.DOCUMENT_DATA_ID
label = Gtk.Label()
label.set_markup("<small>" + self._comment[data_id] + "</small>")
label_align = Gtk.Alignment()
label_align.set_padding(0, 15, 0, 0)
label_align.add(label)
label.set_markup(
"<small>" + \
escape_markup(self._comment[data_id]) + "</small>")
label.set_name("comment-box-comment")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
label.show()
vbox.pack_start(label, False, False, 0)
vbox.pack_start(label_align, False, False, 0)
if self._is_last:
self.pack_start(vbox, False, False, 0)
vbox.show_all()
else:
align = Gtk.Alignment()
align.set_padding(0, 10, 0, 0)
align.add(vbox)
self.pack_start(align, False, False, 0)
align.show_all()
self.pack_start(vbox, False, False, 0)
vbox.show_all()

View File

@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2012 Fabio Erculiani
Authors:
Fabio Erculiani
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 os
from threading import Lock
from gi.repository import Gtk, Gdk, Pango, GObject, GdkPixbuf
from rigo.utils import escape_markup
from rigo.ui.gtk3.utils import get_sc_icon_theme
from rigo.paths import DATA_DIR
from rigo.enums import Icons
from rigo.utils import open_url
from entropy.client.services.interfaces import Document, DocumentFactory
import entropy.tools
class ImageBox(Gtk.VBox):
IMAGE_SIZE = 160
_MISSING_ICON = None
_MISSING_ICON_MUTEX = Lock()
_ICONS = None
_ICONS_MUTEX = Lock()
_hand = Gdk.Cursor.new(Gdk.CursorType.HAND2)
def __init__(self, image, is_last=False):
Gtk.VBox.__init__(self)
self._image = image
self.set_name("image-box")
self.set_spacing(2)
self._is_last = is_last
@property
def _icons(self):
"""
Get Icons Theme Object.
"""
if ImageBox._ICONS is not None:
return ImageBox._ICONS
with ImageBox._ICONS_MUTEX:
if ImageBox._ICONS is not None:
return ImageBox._ICONS
_icons = get_sc_icon_theme(DATA_DIR)
AppListStore._ICONS = _icons
return _icons
@property
def _missing_icon(self):
"""
Return the missing icon Gtk.Image() if needed.
"""
if ImageBox._MISSING_ICON is not None:
return ImageBox._MISSING_ICON
with ImageBox._MISSING_ICON_MUTEX:
if ImageBox._MISSING_ICON is not None:
return ImageBox._MISSING_ICON
_missing_icon = self._icons.load_icon(
Icons.MISSING_APP, ImageBox.IMAGE_SIZE, 0)
ImageBox._MISSING_ICON = _missing_icon
return _missing_icon
def _on_image_clicked(self, widget, event):
"""
Image clicked event, load image in browser.
"""
url = self._image.document_url()
if url is not None:
open_url(url)
def _on_image_enter(self, widget, event):
"""
Cursor over image, switch cursor.
"""
widget.get_window().set_cursor(ImageBox._hand)
def _on_image_leave(self, widget, event):
"""
Cursor leaving image, switch cursor.
"""
widget.get_window().set_cursor(None)
def render(self):
vbox = Gtk.VBox()
hbox = Gtk.HBox()
vbox.pack_start(hbox, False, False, 0)
use_missing = False
image_path = self._image.local_document()
if not os.path.isfile(image_path):
img_buf = self._missing_icon
use_missing = True
else:
img = Gtk.Image.new_from_file(image_path)
img_buf = img.get_pixbuf()
if img_buf is None:
use_missing = True
img_buf = self._missing_icon
del img
w, h = img_buf.get_width(), img_buf.get_height()
del img_buf
if w < 1:
# not legit
use_missing = True
img_buf = self._missing_icon
width = ImageBox.IMAGE_SIZE
height = width * h / w
if not use_missing:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
image_path, width, height)
except GObject.GError:
try:
os.remove(icon_path)
except OSError:
pass
pixbuf = self._missing_icon
use_missing = True
else:
pixbuf = self._missing_icon
event_image = Gtk.EventBox()
image = Gtk.Image.new_from_pixbuf(pixbuf)
event_image.add(image)
event_image.connect("button-press-event", self._on_image_clicked)
event_image.connect("leave-notify-event", self._on_image_leave)
event_image.connect("enter-notify-event", self._on_image_enter)
hbox.pack_start(event_image, False, False, 2)
right_vbox = Gtk.VBox()
right_align = Gtk.Alignment()
right_align.set_padding(0, 0, 8, 0)
right_align.add(right_vbox)
ts_id = Document.DOCUMENT_TIMESTAMP_ID
user_id = DocumentFactory.DOCUMENT_USERNAME_ID
label = Gtk.Label()
time_str = entropy.tools.convert_unix_time_to_human_time(
self._image[ts_id])
time_str = escape_markup(time_str)
label.set_markup(
"<small><b>%s</b>" % (escape_markup(self._image[user_id]),) \
+ ", <i>" + time_str + "</i>" \
+ "</small>")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 1.0)
label.set_selectable(True)
label.set_name("image-box-author")
right_vbox.pack_start(label, False, False, 0)
# title, keywords, ddata, document_id
title = self._image['title'].strip()
if title:
title_id = Document.DOCUMENT_TITLE_ID
label = Gtk.Label()
label.set_markup(
"<b>" + escape_markup(self._image[title_id]) + "</b>")
label.set_name("image-box-title")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
right_vbox.pack_start(label, False, False, 0)
desc_id = Document.DOCUMENT_DESCRIPTION_ID
label = Gtk.Label()
label_align = Gtk.Alignment()
label_align.set_padding(0, 5, 0, 0)
label_align.add(label)
label.set_markup(
"<small>" + escape_markup(self._image[desc_id]) + "</small>")
label.set_name("image-box-description")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
right_vbox.pack_start(label_align, False, False, 0)
keywords_id = Document.DOCUMENT_KEYWORDS_ID
label = Gtk.Label()
keywords_txt = "<b>%s:</b> " % (escape_markup(_("Keywords")),)
label.set_markup(
"<small>" + keywords_txt + \
escape_markup(self._image[keywords_id]) + "</small>")
label.set_name("image-box-keywords")
label.set_line_wrap(True)
label.set_line_wrap_mode(Pango.WrapMode.WORD)
label.set_alignment(0.0, 0.0)
label.set_selectable(True)
right_vbox.pack_start(label, False, False, 0)
hbox.pack_start(right_align, True, True, 0)
self.pack_start(vbox, False, False, 0)
vbox.show_all()

View File

@@ -47,6 +47,7 @@ from rigo.ui.gtk3.widgets.notifications import NotificationBox, \
from rigo.ui.gtk3.widgets.welcome import WelcomeBox
from rigo.ui.gtk3.widgets.stars import ReactiveStar
from rigo.ui.gtk3.widgets.comments import CommentBox
from rigo.ui.gtk3.widgets.images import ImageBox
from rigo.ui.gtk3.models.appliststore import AppListStore
from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme
from rigo.utils import build_application_store_url, build_register_url, \
@@ -425,6 +426,9 @@ class ApplicationViewController(GObject.Object):
self._stars_container.pack_start(self._stars_alignment, False, False, 0)
self._app_images_box = self._builder.get_object(
"appViewImagesVbox")
def set_notification_controller(self, nc):
"""
Bind NotificationController object to this class.
@@ -521,7 +525,7 @@ class ApplicationViewController(GObject.Object):
self._on_stars_clicked(self._stars, app=app)
box.add_button(_("_Vote now"), _send_vote)
box.add_destroy_button(_("_Abort"))
box.add_destroy_button(_("_Later"))
self._nc.append(box)
def _on_stars_login_failed(self, widget, app):
@@ -556,7 +560,7 @@ class ApplicationViewController(GObject.Object):
self._logout_webservice(app, _send_vote)
box.add_button(_("_No, logout!"), _logout_webservice)
box.add_destroy_button(_("_Abort"))
box.add_destroy_button(_("_Later"))
self._nc.append(box)
def _vote_submit(self, app, username, vote):
@@ -640,6 +644,8 @@ class ApplicationViewController(GObject.Object):
going to hide.
"""
self._last_app = None
for child in self._app_my_comments_box.get_children():
child.destroy()
self.emit("application-hide", self)
def _on_send_comment(self, widget, app=None):
@@ -703,16 +709,16 @@ class ApplicationViewController(GObject.Object):
context_id=self.COMMENT_NOTIFICATION_CONTEXT_ID)
def _comment_submit(widget):
#box.destroy()
self._comment_submit(app, username, text)
box.add_button(_("_Ok, cool!"), _comment_submit)
def _send_comment():
self._on_send_comment(None, app=app)
def _logout_webservice(widget):
#box.destroy()
self._logout_webservice(app)
self._logout_webservice(app, _send_comment)
box.add_button(_("_No, logout!"), _logout_webservice)
box.add_destroy_button(_("_Abort"))
box.add_destroy_button(_("_Later"))
self._nc.append(box)
def _comment_submit(self, app, username, text):
@@ -811,7 +817,7 @@ class ApplicationViewController(GObject.Object):
def _send_comment(widget):
self._on_send_comment(widget, app=app)
box.add_button(_("_Send now"), _send_comment)
box.add_destroy_button(_("_Abort"))
box.add_destroy_button(_("_Later"))
self._nc.append(box)
def _on_comment_login_failed(self, widget, app):
@@ -879,6 +885,60 @@ class ApplicationViewController(GObject.Object):
GLib.idle_add(self._append_comments, downloader, app,
comments, has_more)
def _append_images(self, downloader, app, images, has_more):
"""
Append given Entropy WebService Document objects to
the images area.
"""
# remove spinner if there, ugly O(n)
for child in self._app_images_box.get_children():
if not isinstance(child, ImageBox):
child.destroy()
if not images:
label = Gtk.Label()
label.set_markup(
_("<i>No <b>images</b> for this Application, yet!</i>"))
self._app_images_box.pack_start(label, False, False, 1)
label.show()
return
if has_more:
button_box = Gtk.HButtonBox()
button = Gtk.Button()
button.set_label(_("Older images"))
button.set_alignment(0.5, 0.5)
def _enqueue_download(widget):
widget.get_parent().destroy()
spinner = Gtk.Spinner()
spinner.set_size_request(24, 24)
spinner.set_tooltip_text(_("Loading older images..."))
spinner.set_name("image-box-spinner")
self._app_images_box.pack_end(spinner, False, False, 3)
spinner.show()
downloader.enqueue_download()
button.connect("clicked", _enqueue_download)
button_box.pack_start(button, False, False, 0)
self._app_images_box.pack_start(button_box, False, False, 1)
button_box.show_all()
idx = 0
length = len(images)
for doc in images:
idx += 1
box = ImageBox(doc, is_last=(not has_more and (idx == length)))
box.render()
self._app_images_box.pack_end(box, False, False, 2)
box.show()
def _append_images_safe(self, downloader, app, comments, has_more):
"""
Same as _append_images() but thread-safe.
"""
GLib.idle_add(self._append_images, downloader, app,
comments, has_more)
def _setup_application_stats(self, stats, icon):
"""
Setup widgets related to Application statistics (and icon).
@@ -933,23 +993,30 @@ class ApplicationViewController(GObject.Object):
spinner.show()
spinner.start()
downloader = ApplicationViewController.CommentsDownloader(
app, self, self._append_comments_safe)
downloader = ApplicationViewController.MetadataDownloader(
app, self, self._append_comments_safe,
app.download_comments)
downloader.start()
downloader = ApplicationViewController.MetadataDownloader(
app, self, self._append_images_safe,
app.download_images)
downloader.start()
self.emit("application-show", app)
class CommentsDownloader(GObject.Object):
class MetadataDownloader(GObject.Object):
"""
Automated Application comments downloader.
"""
def __init__(self, app, avc, callback):
def __init__(self, app, avc, callback, app_downloader_method):
self._app = app
self._avc = avc
self._offset = 0
self._callback = callback
self._task = ParallelTask(self._download)
self._app_downloader = app_downloader_method
def start(self):
"""
@@ -961,7 +1028,7 @@ class ApplicationViewController(GObject.Object):
def _download_callback(self, document_list):
"""
Callback called by download_comments() once data
Callback called by download_<something>() once data
is arrived from web service.
document_list can be None!
"""
@@ -976,12 +1043,12 @@ class ApplicationViewController(GObject.Object):
if const_debug_enabled():
const_debug_write(
__name__,
"CommentsDownloader._download_callback: %s, more: %s" % (
"MetadataDownloader._download_callback: %s, more: %s" % (
document_list, has_more))
if document_list is not None:
const_debug_write(
__name__,
"CommentsDownloader._download_callback: "
"MetadataDownloader._download_callback: "
"total: %s, offset: %s" % (
document_list.total(), document_list.offset()))
@@ -989,13 +1056,13 @@ class ApplicationViewController(GObject.Object):
def reset_offset(self):
"""
Reset Comments download offset to 0.
Reset Metadata download offset to 0.
"""
self._offset = 0
def get_offset(self):
"""
Get current Comments download offset.
Get current Metadata download offset.
"""
return self._offset
@@ -1008,10 +1075,10 @@ class ApplicationViewController(GObject.Object):
def _download(self):
"""
Thread body of the initial Comments downloader.
Thread body of the initial Metadata downloader.
"""
self._app.download_comments(self._download_callback,
offset=self._offset)
self._app_downloader(self._download_callback,
offset=self._offset)
class Rigo(Gtk.Application):