[rigo] integrate package icons retrieval

This commit is contained in:
Fabio Erculiani
2012-02-12 23:19:09 +01:00
parent a8420b5ad5
commit a7e5f2f3cb
5 changed files with 459 additions and 134 deletions
+326 -93
View File
@@ -30,8 +30,11 @@ 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 rigo.utils import get_entropy_webservice
from rigo.enums import Icons
LOG = logging.getLogger(__name__)
@@ -42,14 +45,14 @@ class ReviewStats(object):
def __init__(self, app):
self.app = app
self.ratings_average = None
self.ratings_total = 0
self.downloads_total = 0
self.rating_spread = [0,0,0,0,0]
self.dampened_rating = 3.00
def __repr__(self):
return ("<ReviewStats '%s' ratings_average='%s' ratings_total='%s'"
return ("<ReviewStats '%s' ratings_average='%s' downloads_total='%s'"
" rating_spread='%s' dampened_rating='%s'>" %
(self.app, self.ratings_average, self.ratings_total,
(self.app, self.ratings_average, self.downloads_total,
self.rating_spread, self.dampened_rating))
class CategoryRowReference:
@@ -94,32 +97,63 @@ class ApplicationMetadata(object):
Start asynchronous Entropy Metadata retrieveal.
"""
ApplicationMetadata._RATING_THREAD.start()
ApplicationMetadata._ICON_THREAD.start()
@staticmethod
def _rating_thread_body():
"""
Thread executing package rating remote data retrieval.
"""
request_list = ["vote", "down"]
return ApplicationMetadata._generic_thread_body(
"RatingThread", ApplicationMetadata._RATING_SEM,
ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL,
ApplicationMetadata._RATING_THREAD_SLEEP_SECS,
ApplicationMetadata._RATING_QUEUE,
ApplicationMetadata._RATING_LOCK,
ApplicationMetadata._RATING_IN_FLIGHT,
request_list)
@staticmethod
def _icon_thread_body():
"""
Thread executing package icon remote data retrieval.
"""
request_list = ["icon"]
return ApplicationMetadata._generic_thread_body(
"IconThread", ApplicationMetadata._ICON_SEM,
ApplicationMetadata._ICON_THREAD_DISCARD_SIGNAL,
ApplicationMetadata._ICON_THREAD_SLEEP_SECS,
ApplicationMetadata._ICON_QUEUE,
ApplicationMetadata._ICON_LOCK,
ApplicationMetadata._ICON_IN_FLIGHT,
request_list)
@staticmethod
def _generic_thread_body(name, sem, discard_signal, sleep_secs,
queue, mutex, in_flight, request_list):
"""
Thread executing generic (both rating and doc) metadata retrieval.
"""
while True:
ApplicationMetadata._RATING_SEM.acquire()
ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL = False
sem.acquire()
discard_signal.set(False)
const_debug_write(__name__,
"_rating_thread_body, waking up")
"%s, waking up" % (name,))
# sleep a bit in order to catch more flies
time.sleep(ApplicationMetadata._RATING_THREAD_SLEEP_SECS)
time.sleep(sleep_secs)
# now catch the flies
local_queue = []
while True:
try:
local_queue.append(
ApplicationMetadata._RATING_QUEUE.popleft())
local_queue.append(queue.popleft())
except IndexError:
# no more items
break
if const_debug_enabled():
const_debug_write(__name__,
"_rating_thread_body, got: %s" % (local_queue,))
"%s, got: %s" % (name, local_queue,))
if not local_queue:
continue
@@ -136,8 +170,7 @@ class ApplicationMetadata(object):
obj = repo_map.setdefault(repo_id, set())
obj.add(key)
votes_map = {}
downloads_map = {}
request_outcome = {}
# issue requests
for repo_id, keys in repo_map.items():
@@ -146,65 +179,63 @@ class ApplicationMetadata(object):
if webserv is None:
continue
request_map = {
"vote": webserv.get_votes,
"down": webserv.get_downloads,
"icon": webserv.get_icons,
}
# FIXME: lxnay, who validates this cache?
uncached_keys = []
for key in keys:
try:
votes_map[(key, repo_id)] = webserv.get_votes(
[key], cache=True, cached=True)[key]
except WebService.CacheMiss:
uncached_keys.append(key)
for request in request_list:
outcome = {}
for key in uncached_keys:
if ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL:
break
try:
# FIXME, lxnay: work more instances in parallel?
votes_map[(key, repo_id)] = webserv.get_votes(
[key], cache = True)[key]
except WebService.WebServiceException as wse:
const_debug_write(
__name__,
"_rating_thread_body, WebServiceExc: %s" % (wse,))
votes_map[(key, repo_id)] = None
uncached_keys = []
for key in keys:
try:
outcome[(key, repo_id)] = request_map[request](
[key], cache=True, cached=True)[key]
except WebService.CacheMiss:
uncached_keys.append(key)
uncached_keys = []
for key in keys:
try:
downloads_map[(key, repo_id)] = webserv.get_downloads(
[key], cache=True, cached=True)[key]
except WebService.CacheMiss:
uncached_keys.append(key)
for key in uncached_keys:
if discard_signal.get():
break
try:
# FIXME, lxnay: work more instances in parallel?
outcome[(key, repo_id)] = request_map[request](
[key], cache = True)[key]
except WebService.WebServiceException as wse:
const_debug_write(
__name__,
"%s, WebServiceExc: %s" % (name, wse,)
)
outcome[(key, repo_id)] = None
for key in uncached_keys:
if ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL:
break
try:
# FIXME, lxnay: work more instances in parallel?
downloads_map[(key, repo_id)] = webserv.get_downloads(
[key], cache = True)[key]
except WebService.WebServiceException as wse:
const_debug_write(
__name__,
"_rating_thread_body, WebServiceExc: %s" % (wse,))
downloads_map[(key, repo_id)] = None
request_outcome[request] = outcome
if ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL:
ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL = False
# don't worry about races
if discard_signal.get():
const_debug_write(
__name__,
"%s, discard signal received." % (name,)
)
discard_signal.set(False)
request_outcome.clear()
continue
# dispatch results
for (key, repo_id), cb_ts_list in pkg_key_map.items():
for (cb, ts) in cb_ts_list:
with ApplicationMetadata._RATING_LOCK:
ApplicationMetadata._RATING_IN_FLIGHT.discard(
(key, repo_id))
with mutex:
in_flight.discard((key, repo_id))
if cb is not None:
vote = votes_map.get((key, repo_id))
down = downloads_map.get((key, repo_id))
task = ParallelTask(cb, (vote, down), ts)
task.name = "RatingThreadCb{%s, %s}" % (repo_id, key)
outcome_values = []
for request in request_list:
outcome_values.append(
request_outcome[request].get((key, repo_id)))
task = ParallelTask(cb, outcome_values, ts)
task.name = "%sCb{%s, %s}" % (name, repo_id, key)
task.start()
@staticmethod
@@ -213,19 +244,26 @@ class ApplicationMetadata(object):
Get Entropy WebService object for repository
"""
webserv = ApplicationMetadata._WEBSERV_CACHE.get(repository_id)
if webserv == -1:
return None # not available
if webserv is not None:
return webserv
try:
webserv = get_entropy_webservice(entropy_client, repository_id)
except WebService.UnsupportedService:
ApplicationMetadata._WEBSERV_CACHE[repository_id] = -1
return None
with ApplicationMetadata._WEBSERV_MUTEX:
webserv = ApplicationMetadata._WEBSERV_CACHE.get(repository_id)
if webserv == -1:
return None # not available
if webserv is not None:
return webserv
try:
webserv = get_entropy_webservice(entropy_client, repository_id)
except WebService.UnsupportedService:
ApplicationMetadata._WEBSERV_CACHE[repository_id] = -1
return None
ApplicationMetadata._WEBSERV_CACHE[repository_id] = webserv
ApplicationMetadata._WEBSERV_CACHE[repository_id] = webserv
return webserv
@staticmethod
@@ -234,34 +272,88 @@ class ApplicationMetadata(object):
Discard all the queued requests. No longer needed.
"""
const_debug_write(__name__,
"!!! ApplicationMetadata.discard() called !!!")
while True:
try:
ApplicationMetadata._RATING_QUEUE.popleft()
# we could use blocking mode, but no actual need
ApplicationMetadata._RATING_SEM.acquire(False)
except IndexError:
break
with ApplicationMetadata._RATING_LOCK:
ApplicationMetadata._RATING_IN_FLIGHT.clear()
ApplicationMetadata._RATING_THREAD_DISCARD_SIGNAL = True
"ApplicationMetadata.discard() called")
for th_info in ApplicationMetadata._REGISTERED_THREAD_INFO:
th, queue, sem, lock, \
discard_signal, in_flight = th_info
while True:
try:
queue.popleft()
# we could use blocking mode, but no actual need
sem.acquire(False)
except IndexError:
break
with lock:
in_flight.clear()
discard_signal.set(True)
@staticmethod
def enqueue_rating(entropy_client, package_key, repository_id, callback):
def _download_document(entropy_ws, document, cache=True):
"""
Enqueue the Rating (stars) for package key in given repository.
Dowload Document (Icon, File, Image, etc) through the Entropy
WebService interface.
Return path to just downloaded Document if success, None otherwise.
"""
local_path = None
try:
local_path = entropy_ws.get_document_url(document,
cache=cache)
except ClientWebService.DocumentError as err:
const_debug_write(__name__,
"_download_document: document error: %s" % (
err,))
return local_path
@staticmethod
def _enqueue_rating(entropy_client, package_key, repository_id, callback):
"""
Enqueue the retriveal of the Rating for package key in given repository.
Once the data is ready, callback() will be called passing the
payload.
This method is asynchronous and returns as soon as possible.
callback() signature is: callback(payload, request_timestamp_float).
If data is not available, payload will be None.
callback argument can be None.
_RATING_LOCK must be acquired by caller.
"""
if const_debug_enabled():
const_debug_write(
__name__,
"_enqueue_rating: %s, %s" % (package_key, repository_id))
request_time = time.time()
ApplicationMetadata._RATING_IN_FLIGHT.add((package_key, repository_id))
ApplicationMetadata._RATING_QUEUE.append((entropy_client, package_key,
in_flight = ApplicationMetadata._RATING_IN_FLIGHT
queue = ApplicationMetadata._RATING_QUEUE
sem = ApplicationMetadata._RATING_SEM
in_flight.add((package_key, repository_id))
queue.append((entropy_client, package_key,
repository_id, callback, request_time))
ApplicationMetadata._RATING_SEM.release()
sem.release()
@staticmethod
def _enqueue_icon(entropy_client, package_key, repository_id, callback):
"""
Enqueue the retrieval of the Icon for package key in given repository.
Once the data is ready, callback() will be called passing the
payload.
This method is asynchronous and returns as soon as possible.
callback() signature is: callback(payload, request_timestamp_float).
If data is not available, payload will be None.
callback argument can be None.
_ICON_LOCK must be acquired by caller.
"""
if const_debug_enabled():
const_debug_write(
__name__,
"_enqueue_icon: %s, %s" % (package_key, repository_id))
request_time = time.time()
in_flight = ApplicationMetadata._ICON_IN_FLIGHT
queue = ApplicationMetadata._ICON_QUEUE
sem = ApplicationMetadata._ICON_SEM
in_flight.add((package_key, repository_id))
queue.append((entropy_client, package_key,
repository_id, callback, request_time))
sem.release()
@staticmethod
def lazy_get_rating(entropy_client, package_key, repository_id,
@@ -284,12 +376,16 @@ class ApplicationMetadata(object):
down = webserv.get_downloads(
[package_key], cache=True, cached=True)[package_key]
except WebService.CacheMiss:
if const_debug_enabled():
const_debug_write(__name__,
"lazy_get_rating: cache miss for: %s, %s" % (
package_key, repository_id))
# not in cache
flight_key = (package_key, repository_id)
with ApplicationMetadata._RATING_LOCK:
if flight_key not in ApplicationMetadata._RATING_IN_FLIGHT:
# enqueue a new rating then
ApplicationMetadata.enqueue_rating(
ApplicationMetadata._enqueue_rating(
entropy_client, package_key,
repository_id, callback)
vote = None
@@ -297,20 +393,124 @@ class ApplicationMetadata(object):
return vote, down
@staticmethod
def lazy_get_icon(entropy_client, package_key, repository_id,
callback=None):
"""
Return a DocumentList of Icons for given package key, if it's available
in local cache. At the same time, if not available and not already
enqueued for download, do it, atomically.
Return None if not available, or DocumentList (see Entropy Services
API) otherwise. DocumentList contains a list of Document objects,
and calling Document.local_document() would give you the image path.
"""
webserv = ApplicationMetadata._get_webservice(
entropy_client, repository_id)
if webserv is None:
return None
def _pick_icon(icons):
return icons[0]
def _icon_callback(outcomes, ts):
icons = outcomes[0]
if not icons:
# sadly, no icons
return
local_path = ApplicationMetadata._download_document(
webserv, _pick_icon(icons))
if local_path:
# only if successful, otherwise we fall into
# infinite loop
callback(icons)
try:
icons = webserv.get_icons(
[package_key], cache=True, cached=True)[package_key]
except WebService.CacheMiss:
if const_debug_enabled():
const_debug_write(__name__,
"lazy_get_icon: cache miss for: %s, %s" % (
package_key, repository_id))
# not in cache
flight_key = (package_key, repository_id)
with ApplicationMetadata._ICON_LOCK:
if flight_key not in ApplicationMetadata._ICON_IN_FLIGHT:
# enqueue a new rating then
ApplicationMetadata._enqueue_icon(
entropy_client, package_key,
repository_id, _icon_callback)
icons = None
if not icons:
return None
# pick the first icon as document icon
icon = _pick_icon(icons)
# check if we have the file on-disk, otherwise
# spawn the fetch in parallel.
icon_path = icon.local_document()
if not os.path.isfile(icon_path):
task = ParallelTask(_icon_callback, [icons], time.time())
task.daemon = True
task.name = "FetchIconCb{(%s, %s)}" % ((package_key, repository_id))
task.start()
return icon
class SignalBoolean(object):
def __init__(self, val):
self.__val = val
def set(self, val):
self.__val = val
def get(self):
return self.__val
# Application Rating logic
_RATING_QUEUE = deque()
def _rating_thread_body_wrapper():
return ApplicationMetadata._rating_thread_body()
_RATING_THREAD = ParallelTask(_rating_thread_body_wrapper)
_RATING_THREAD.daemon = True
_RATING_THREAD.name = "RatingThread"
_RATING_THREAD_SLEEP_SECS = 1.5
_RATING_THREAD_DISCARD_SIGNAL = False
_RATING_THREAD_SLEEP_SECS = 1.0
_RATING_THREAD_DISCARD_SIGNAL = SignalBoolean(False)
_RATING_SEM = Semaphore(0)
_RATING_LOCK = Lock()
_RATING_IN_FLIGHT = set()
# Application Documents logic
_ICON_QUEUE = deque()
def _icon_thread_body_wrapper():
return ApplicationMetadata._icon_thread_body()
_ICON_THREAD = ParallelTask(_icon_thread_body_wrapper)
_ICON_THREAD.daemon = True
_ICON_THREAD.name = "IconThread"
_ICON_THREAD_SLEEP_SECS = 0.5
_ICON_THREAD_DISCARD_SIGNAL = SignalBoolean(False)
_ICON_SEM = Semaphore(0)
_ICON_LOCK = Lock()
_ICON_IN_FLIGHT = set()
_REGISTERED_THREAD_INFO = [
# rating
(_RATING_THREAD, _RATING_QUEUE,
_RATING_SEM, _RATING_LOCK,
_RATING_THREAD_DISCARD_SIGNAL,
_RATING_IN_FLIGHT),
# icon
(_ICON_THREAD, _ICON_QUEUE,
_ICON_SEM, _ICON_LOCK,
_ICON_THREAD_DISCARD_SIGNAL,
_ICON_IN_FLIGHT),
]
# WebService object cache
_WEBSERV_CACHE = {}
_WEBSERV_MUTEX = Lock()
# this is a very lean class as its used in the main listview
@@ -354,7 +554,7 @@ class Application(object):
def get_markup(self):
repo = self._entropy.open_repository(self._repo_id)
name = self.name
name = repo.retrieveName(self._pkg_id)
version = repo.retrieveVersion(self._pkg_id)
if version is None:
version = "N/A"
@@ -370,6 +570,13 @@ class Application(object):
return text
def get_review_stats(self):
"""
Return ReviewStats object containing user review
information about this Application, like
votes and number of downloads.
FIXME, lxnay: move to AppDetails()
"""
stat = ReviewStats(self)
stat.ratings_average = ReviewStats.NO_RATING
@@ -389,13 +596,16 @@ class Application(object):
stat.ratings_average = vote
if down is not None:
# otherwise 0 is shown
stat.ratings_total = down
stat.downloads_total = down
return stat
# get a AppDetails object for this Applications
def get_details(self):
""" return a new AppDetails object for this application """
return AppDetails(self._entropy, self._pkg_match, self)
"""
Return a new AppDetails object for this application
"""
return AppDetails(self._entropy, self._pkg_match, self,
redraw_callback=self._redraw_callback)
def __str__(self):
repo = self._entropy.open_repository(self._repo_id)
@@ -413,7 +623,8 @@ class AppDetails(object):
we have available like website etc
"""
def __init__(self, entropy_client, package_match, app):
def __init__(self, entropy_client, package_match, app,
redraw_callback=None):
"""
Create a new AppDetails object.
"""
@@ -421,6 +632,7 @@ class AppDetails(object):
self._pkg_match = package_match
self._pkg_id, self._repo_id = package_match
self._app = app
self._redraw_callback = redraw_callback
@property
def channelname(self):
@@ -447,9 +659,30 @@ class AppDetails(object):
@property
def icon(self):
"""Return icon name, as it should be searched through fd.o dbs"""
# FIXME, we have it
return Icons.MISSING_PKG
"""
Return Application Icon image path.
In case of missing icon, None is returned.
"""
repo = self._entropy.open_repository(self._repo_id)
key_slot = repo.retrieveKeySlot(self._pkg_id)
if key_slot is None:
return None
key, slot = key_slot
icon = ApplicationMetadata.lazy_get_icon(
self._entropy, key, self._repo_id,
callback=self._redraw_callback)
if const_debug_enabled():
const_debug_write(__name__,
"AppDetails{%s}.icon: icon: %s" % (
self._pkg_match,
icon))
if not icon:
return None
# pick the first one
icon_path = icon.local_document()
return icon_path
@property
def icon_file_name(self):
+54 -1
View File
@@ -20,8 +20,10 @@
#gi.require_version("Gtk", "3.0")
import os
import logging
import shutil
import tempfile
from gi.repository import Gtk
from gi.repository import Gtk, GdkPixbuf, GObject
from rigo.paths import ICON_PATH
@@ -85,3 +87,54 @@ def get_sc_icon_theme(datadir):
return icons
def resize_image(max_width, image_path, final_image_path):
dirname = os.path.dirname(final_image_path)
tmp_fd, new_image_path = tempfile.mkstemp(
dir=dirname, prefix="resize_image")
os.close(tmp_fd)
shutil.copy2(image_path, new_image_path)
img = Gtk.Image()
img.set_from_file(new_image_path)
img_buf = img.get_pixbuf()
w, h = img_buf.get_width(), img_buf.get_height()
if w > max_width:
# resize pix
new_w = max_width
new_h = new_w * h / w
img_buf = img_buf.scale_simple(int(new_w),
int(new_h), GdkPixbuf.InterpType.BILINEAR)
try:
img_buf.save(new_image_path, "png")
except GObject.GError:
# libpng issue? try jpeg
img_buf.save(new_image_path, "jpeg")
del img_buf
del img
os.rename(new_image_path, final_image_path)
def resize_image_height(max_height, image_path, final_image_path):
dirname = os.path.dirname(final_image_path)
tmp_fd, new_image_path = tempfile.mkstemp(
dir=dirname, prefix="resize_image")
os.close(tmp_fd)
shutil.copy2(image_path, new_image_path)
img = Gtk.Image()
img.set_from_file(new_image_path)
img_buf = img.get_pixbuf()
w, h = img_buf.get_width(), img_buf.get_height()
if h > max_height:
# resize pix
new_h = max_height
new_w = new_h*w/h
img_buf = img_buf.scale_simple(int(new_w),
int(new_h), GdkPixbuf.InterpType.BILINEAR)
try:
img_buf.save(new_image_path, "png")
except GObject.GError:
# libpng issue? try jpeg
img_buf.save(new_image_path, "jpeg")
del img_buf
del img
os.rename(new_image_path, final_image_path)
+3 -2
View File
@@ -220,7 +220,7 @@ class FeaturedTile(TileButton):
self.rating_box.pack_start(self.stars, False, False, 0)
self.n_ratings = Gtk.Label.new(
'<span font_desc="%i"> (%i)</span>' % (
em(0.45), stats.ratings_total))
em(0.45), stats.downloads_total))
self.n_ratings.set_use_markup(True)
self.n_ratings.set_name("subtle-label")
self.n_ratings.set_alignment(0.0, 0.5)
@@ -229,7 +229,8 @@ class FeaturedTile(TileButton):
# TRANSLATORS: this is an accessibility description for eg orca and
# is not visible in the ui
stats_a11y = _('%(stars)d stars - %(reviews)d reviews') % {
'stars': stats.ratings_average, 'reviews': stats.ratings_total}
'stars': stats.ratings_average,
'reviews': stats.downloads_total}
#work out width tile needs to be to ensure ratings text is all visible
req_width = (self.stars.size_request().width +
+1 -1
View File
@@ -212,7 +212,7 @@ class CellRendererAppView(Gtk.CellRendererText):
sr.render_star(context, cr, x, y)
# and nr-reviews in parenthesis to the right of the title
nreviews = stats.ratings_total
nreviews = stats.downloads_total
s = "(%i)" % nreviews
layout.set_markup("<small>%s</small>" % s, -1)
+75 -37
View File
@@ -12,23 +12,22 @@ sys.path.insert(5, "/usr/lib/entropy/rigo")
from threading import Lock
from gi.repository import Gtk, Gdk, Gio, GLib, GObject
from gi.repository import Gtk, Gdk, Gio, GLib, GObject, GdkPixbuf
from rigo.paths import DATA_DIR
from rigo.enums import Icons
from rigo.models.application import Application, ApplicationMetadata
from rigo.ui.gtk3.widgets.apptreeview import AppTreeView
from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme
from rigo.ui.gtk3.utils import init_sc_css_provider, get_sc_icon_theme, \
resize_image
from entropy.const import etpUi
from entropy.const import etpUi, const_debug_write, const_debug_enabled
from entropy.tools import kill_threads
from entropy.exceptions import RepositoryError
from entropy.client.interfaces import Client
from entropy.misc import TimeScheduled, ParallelTask
from entropy.i18n import _
etpUi['debug'] = True
class EntropyWebService(object):
def __init__(self, entropy_client):
@@ -105,56 +104,95 @@ class AppListStore(Gtk.ListStore):
# column id
COL_ROW_DATA = 0
# default icon size displayed in the treeview
ICON_SIZE = 32
# default icon size returned by Application.get_icon()
ICON_SIZE = 48
_MISSING_ICON = None
_MISSING_ICON_MUTEX = Lock()
_ICON_CACHE = {}
def __init__(self, entropy_client, view, icons, icon_size=48):
def __init__(self, entropy_client, view, icons):
Gtk.ListStore.__init__(self)
self._view = view
self._entropy = entropy_client
self._icons = icons
self._icon_size = icon_size
self._icon_cache = {}
self._missing_icon = self._icons.load_icon(
Icons.MISSING_APP, icon_size, 0)
self.set_column_types(self.COL_TYPES)
# Startup Entropy Package Metadata daemon
ApplicationMetadata.start()
def clear(self):
"""
Clear ListStore content (and Icon Cache).
"""
outcome = Gtk.ListStore.clear(self)
AppListStore._ICON_CACHE.clear()
return outcome
def _ui_redraw_callback(self, *args):
if const_debug_enabled():
const_debug_write(__name__,
"_ui_redraw_callback()")
GLib.idle_add(self._view.queue_draw)
@property
def _missing_icon(self):
"""
Return the missing icon Gtk.Image() if needed.
"""
if AppListStore._MISSING_ICON is not None:
return AppListStore._MISSING_ICON
with AppListStore._MISSING_ICON_MUTEX:
if AppListStore._MISSING_ICON is not None:
return AppListStore._MISSING_ICON
_missing_icon = self._icons.load_icon(
Icons.MISSING_APP, AppListStore.ICON_SIZE, 0)
AppListStore._MISSING_ICON = _missing_icon
return _missing_icon
def get_icon(self, pkg_match):
# FIXME, parallel load from UGC?
# or run UGC on separate task?
cached = AppListStore._ICON_CACHE.get(pkg_match)
if cached is not None:
return cached
app = Application(self._entropy, pkg_match,
redraw_callback=self._ui_redraw_callback)
name = app.name
icon = self._icon_cache.get(name)
if icon == -1:
# icon not available
icon_path = app.get_details().icon
if icon_path is None:
return self._missing_icon
if icon is not None:
return icon
if self._icons.has_icon(name):
if not os.path.isfile(icon_path):
return self._missing_icon
# FIXME, parallelize this?
# get current image size
# expensive?
img = Gtk.Image()
img.set_from_file(icon_path)
img_buf = img.get_pixbuf()
if img_buf is None:
# wth, invalid crap
return self._missing_icon
w, h = img_buf.get_width(), img_buf.get_height()
del img_buf
del img
if w < 1:
# not legit
return self._missing_icon
width = AppListStore.ICON_SIZE
height = width * h / w
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
icon_path, width, height)
except GObject.GError:
try:
icon = self._icons.load_icon(
name, self._icon_size, 0)
except (Gio.Error, GObject.GError):
# no such file or directory (gio.Error)
# unrecognized file format (gobject.GError)
icon = None
if icon:
self._icon_cache[name] = icon
else:
self._icon_cache[name] = -1
icon = self._missing_icon
else:
self._icon_cache[name] = -1
icon = self._missing_icon
return icon
os.remove(icon_path)
except OSError:
pass
return self._missing_icon
AppListStore._ICON_CACHE[pkg_match] = pixbuf
return pixbuf
def is_installed(self, pkg_match):
app = Application(self._entropy, pkg_match,
@@ -279,7 +317,7 @@ class WelcomeBox(Gtk.VBox):
image = Gtk.Image.new_from_file(self._image_path)
label = Gtk.Label()
label.set_markup(_("<i>Browse <b>Applications</b> with ease</i>"))
self.pack_start(image, False, False, False)
self.pack_start(image, False, False, 0)
self.pack_start(label, False, False, 0)
label.show()
image.show()