Merge lp:~mmcg069/software-center/multi-screenshot-gallery into lp:software-center

Proposed by Matthew McGowan
Status: Merged
Merged at revision: 2542
Proposed branch: lp:~mmcg069/software-center/multi-screenshot-gallery
Merge into: lp:software-center
Diff against target: 1221 lines (+476/-451)
10 files modified
data/ui/gtk3/css/softwarecenter.css (+6/-0)
softwarecenter/backend/reviews.py (+2/-1)
softwarecenter/db/application.py (+68/-1)
softwarecenter/distro/Debian.py (+2/-0)
softwarecenter/distro/SUSELINUX.py (+2/-1)
softwarecenter/distro/Ubuntu.py (+3/-0)
softwarecenter/ui/gtk3/models/appstore2.py (+6/-1)
softwarecenter/ui/gtk3/views/appdetailsview_gtk.py (+3/-6)
softwarecenter/ui/gtk3/widgets/thumbnail.py (+381/-440)
softwarecenter/utils.py (+3/-1)
To merge this branch: bzr merge lp:~mmcg069/software-center/multi-screenshot-gallery
Reviewer Review Type Date Requested Status
Michael Vogt Pending
Review via email: mp+81190@code.launchpad.net

Commit message

Enable the download and display of a selection of possible screenshots (if available) and provide appropriate UI additions to enable screenshot selection.

Description of the change

Retrieve multiple screenshots from the screenshot server and make available in the UI of the software-center details view. Additional UI provides a means for the user to select a screenshot preview from a list of smaller thumbnails.

To post a comment you must log in.
2528. By Matthew McGowan

cleanups. tweaks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/ui/gtk3/css/softwarecenter.css'
2--- data/ui/gtk3/css/softwarecenter.css 2011-09-21 11:44:56 +0000
3+++ data/ui/gtk3/css/softwarecenter.css 2011-11-03 22:05:37 +0000
4@@ -11,6 +11,12 @@
5 border-color: shade (@light-aubergine, 1.025);
6 }
7
8+#screenshot-preview {
9+ border-color: #000;
10+ color: #000;
11+ border-width: 2;
12+ border-radius: 3;
13+}
14
15 .backforward-left-button {
16 border-radius: 3 0 0 3;
17
18=== modified file 'softwarecenter/backend/reviews.py'
19--- softwarecenter/backend/reviews.py 2011-10-13 13:39:21 +0000
20+++ softwarecenter/backend/reviews.py 2011-11-03 22:05:37 +0000
21@@ -854,7 +854,8 @@
22 """ get the review statists and call callback when its there """
23 f=Gio.File(self.distro.REVIEW_STATS_URL)
24 f.set_data("callback", callback)
25- f.load_contents_async(self._gio_review_stats_download_finished_callback)
26+ f.load_contents_async(
27+ None, self._gio_review_stats_download_finished_callback, None)
28
29 class ReviewLoaderFake(ReviewLoader):
30
31
32=== modified file 'softwarecenter/db/application.py'
33--- softwarecenter/db/application.py 2011-10-25 18:38:08 +0000
34+++ softwarecenter/db/application.py 2011-11-03 22:05:37 +0000
35@@ -16,6 +16,8 @@
36 # this program; if not, write to the Free Software Foundation, Inc.,
37 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
38
39+from gi.repository import GObject
40+
41 import locale
42 import logging
43 import os
44@@ -30,6 +32,7 @@
45 SOFTWARE_CENTER_ICON_CACHE_DIR,
46 )
47 from softwarecenter.utils import utf8, split_icon_ext
48+from softwarecenter.distro import get_distro
49
50 LOG = logging.getLogger(__name__)
51
52@@ -134,15 +137,22 @@
53 return cmp(x.pkgname, y.pkgname)
54
55 # the details
56-class AppDetails(object):
57+class AppDetails(GObject.GObject):
58 """ The details for a Application. This contains all the information
59 we have available like website etc
60 """
61
62+ __gsignals__ = {"screenshots-available" : (GObject.SIGNAL_RUN_FIRST,
63+ GObject.TYPE_NONE,
64+ (GObject.TYPE_PYOBJECT,),
65+ ),
66+ }
67+
68 def __init__(self, db, doc=None, application=None):
69 """ Create a new AppDetails object. It can be created from
70 a xapian.Document or from a db.application.Application object
71 """
72+ GObject.GObject.__init__(self)
73 if not doc and not application:
74 raise ValueError("Need either document or application")
75 self._db = db
76@@ -158,6 +168,7 @@
77 # FIXME: why two error states ?
78 self._error = None
79 self._error_not_found = None
80+ self._screenshot_list = None
81
82 # load application
83 self._app = application
84@@ -472,6 +483,7 @@
85
86 @property
87 def screenshot(self):
88+ """ return screenshot url """
89 # if there is a custom screenshot url provided, use that
90 if self._doc:
91 if self._doc.get_value(XapianValues.SCREENSHOT_URL):
92@@ -481,6 +493,61 @@
93 'version' : self.version or 0 }
94
95 @property
96+ def screenshots(self):
97+ """ return list of screenshots, this requies that
98+ "query_multiple_screenshos" was run before and emited the signal
99+ """
100+ if not self._screenshot_list:
101+ return [ {'small_image_url': self.thumbnail,
102+ 'large_image_url': self.screenshot,
103+ 'version': self.version},
104+ ]
105+ return self._screenshot_list
106+
107+ def query_multiple_screenshots(self):
108+ """ query if multiple screenshots for the given app are available
109+ and if so, emit "screenshots-available" signal
110+ """
111+ # check if we have it cached
112+ if self._screenshot_list:
113+ self.emit("screenshots-available", self._screenshot_list)
114+ return
115+ # download it
116+ distro = get_distro()
117+ url = distro.SCREENSHOT_JSON_URL % self._app.pkgname
118+ try:
119+ from gi.repository import Gio
120+ # FIXME: this needs to be async
121+ f = Gio.File.new_for_uri(url)
122+ f.load_contents_async(
123+ None, self._gio_screenshots_json_download_complete_cb, None)
124+ except:
125+ LOG.exception("failed to load content")
126+
127+
128+ def _gio_screenshots_json_download_complete_cb(self, source, result, path):
129+ try:
130+ res, content, etag = source.load_contents_finish(result)
131+ except GObject.GError:
132+ # ignore read errors, most likely transient
133+ return
134+ if content is not None:
135+ import json
136+ content = json.loads(content)
137+
138+ if isinstance(content, dict):
139+ # a list of screenshots as listsed online
140+ screenshot_list = content['screenshots']
141+ else:
142+ # fallback to a list of screenshots as supplied by the axi
143+ screenshot_list = []
144+
145+ # save for later and emit
146+ self._screenshot_list = screenshot_list
147+ self.emit("screenshots-available", screenshot_list)
148+ return
149+
150+ @property
151 def summary(self):
152 if self._doc:
153 return self._db.get_summary(self._doc)
154
155=== modified file 'softwarecenter/distro/Debian.py'
156--- softwarecenter/distro/Debian.py 2011-09-21 15:15:28 +0000
157+++ softwarecenter/distro/Debian.py 2011-11-03 22:05:37 +0000
158@@ -30,6 +30,8 @@
159 # screenshot handling
160 SCREENSHOT_THUMB_URL = "http://screenshots.debian.net/thumbnail/%(pkgname)s"
161 SCREENSHOT_LARGE_URL = "http://screenshots.debian.net/screenshot/%(pkgname)s"
162+ # the json description of the available screenshots
163+ SCREENSHOT_JSON_URL = "http://screenshots.debian.net/json/package/%s"
164
165 def get_distro_channel_name(self):
166 """ The name in the Release file """
167
168=== modified file 'softwarecenter/distro/SUSELINUX.py'
169--- softwarecenter/distro/SUSELINUX.py 2011-08-10 11:39:55 +0000
170+++ softwarecenter/distro/SUSELINUX.py 2011-11-03 22:05:37 +0000
171@@ -28,7 +28,8 @@
172 # screenshot handling
173 SCREENSHOT_THUMB_URL = "http://screenshots.ubuntu.com/thumbnail-with-version/%(pkgname)s/%(version)s"
174 SCREENSHOT_LARGE_URL = "http://screenshots.ubuntu.com/screenshot-with-version/%(pkgname)s/%(version)s"
175-
176+ SCREENSHOT_JSON_URL = "http://screenshots.ubuntu.com/json/package/%s"
177+
178 # reviews
179 REVIEWS_SERVER = os.environ.get("SOFTWARE_CENTER_REVIEWS_HOST") or "http://reviews.ubuntu.com/reviews/api/1.0"
180 REVIEWS_URL = REVIEWS_SERVER + "/reviews/filter/%(language)s/%(origin)s/%(distroseries)s/%(version)s/%(pkgname)s%(appname)s/"
181
182=== modified file 'softwarecenter/distro/Ubuntu.py'
183--- softwarecenter/distro/Ubuntu.py 2011-10-05 00:58:57 +0000
184+++ softwarecenter/distro/Ubuntu.py 2011-11-03 22:05:37 +0000
185@@ -45,6 +45,9 @@
186 SCREENSHOT_THUMB_URL = "http://screenshots.ubuntu.com/thumbnail-with-version/%(pkgname)s/%(version)s"
187 SCREENSHOT_LARGE_URL = "http://screenshots.ubuntu.com/screenshot-with-version/%(pkgname)s/%(version)s"
188
189+ # the json description of the available screenshots
190+ SCREENSHOT_JSON_URL = "http://screenshots.ubuntu.com/json/package/%s"
191+
192 # purchase subscription
193 PURCHASE_APP_URL = BUY_SOMETHING_HOST+"/subscriptions/%s/ubuntu/%s/+new/?%s"
194
195
196=== modified file 'softwarecenter/ui/gtk3/models/appstore2.py'
197--- softwarecenter/ui/gtk3/models/appstore2.py 2011-10-20 08:08:45 +0000
198+++ softwarecenter/ui/gtk3/models/appstore2.py 2011-11-03 22:05:37 +0000
199@@ -438,7 +438,12 @@
200 end = n_matches
201
202 for i in range(start, end):
203- if self[(i,)][0]: continue
204+ try:
205+ row_content = self[(i,)][0]
206+ except IndexError:
207+ break
208+
209+ if row_content: continue
210 doc = db.get_document(matches[i].docid)
211 doc.available = doc.installed = None
212 self[(i,)][0] = doc
213
214=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview_gtk.py'
215--- softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-11-01 23:00:08 +0000
216+++ softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-11-03 22:05:37 +0000
217@@ -59,7 +59,7 @@
218 from softwarecenter.ui.gtk3.widgets.containers import SmallBorderRadiusFrame
219 from softwarecenter.ui.gtk3.widgets.stars import Star, StarRatingsWidget
220 from softwarecenter.ui.gtk3.widgets.description import AppDescription
221-from softwarecenter.ui.gtk3.widgets.thumbnail import ScreenshotThumbnail
222+from softwarecenter.ui.gtk3.widgets.thumbnail import ScreenshotGallery
223 from softwarecenter.ui.gtk3.widgets.weblivedialog import (
224 ShowWebLiveServerChooserDialog)
225 from softwarecenter.ui.gtk3.gmenusearch import GMenuSearcher
226@@ -969,7 +969,7 @@
227 body_hb.pack_start(self.desc, True, True, 0)
228
229 # the thumbnail/screenshot
230- self.screenshot = ScreenshotThumbnail(get_distro(), self.icons)
231+ self.screenshot = ScreenshotGallery(get_distro(), self.icons)
232 right_vb = Gtk.VBox()
233 right_vb.set_spacing(6)
234 body_hb.pack_start(right_vb, False, False, 0)
235@@ -1149,10 +1149,7 @@
236 def _update_app_screenshot(self, app_details):
237 # get screenshot urls and configure the ScreenshotView...
238 if app_details.thumbnail and app_details.screenshot:
239- self.screenshot.configure(app_details)
240-
241- # inititate the download and display series of callbacks
242- self.screenshot.download_and_display()
243+ self.screenshot.fetch_screenshots(app_details)
244 return
245
246 def _update_weblive(self, app_details):
247
248=== modified file 'softwarecenter/ui/gtk3/widgets/thumbnail.py'
249--- softwarecenter/ui/gtk3/widgets/thumbnail.py 2011-09-15 10:34:17 +0000
250+++ softwarecenter/ui/gtk3/widgets/thumbnail.py 2011-11-03 22:05:37 +0000
251@@ -18,13 +18,14 @@
252
253 import gi
254 gi.require_version("Gtk", "3.0")
255-from gi.repository import Gtk, Gdk, Atk, GObject, GdkPixbuf
256+from gi.repository import Gtk, Gdk, Atk, Gio, GObject, GdkPixbuf
257
258 import logging
259 import os
260
261 from softwarecenter.db.pkginfo import get_pkg_info
262 from softwarecenter.utils import SimpleFileDownloader
263+from softwarecenter.ui.gtk3.drawing import rounded_rect
264
265 from imagedialog import SimpleShowImageDialog
266
267@@ -33,37 +34,60 @@
268 LOG = logging.getLogger(__name__)
269
270
271-class ScreenshotThumbnail(Gtk.Alignment):
272-
273- """ Widget that displays screenshot availability, download prrogress,
274- and eventually the screenshot itself.
275- """
276-
277- MAX_SIZE = 300, 300
278- IDLE_SIZE = 300, 150
279+
280+class ScreenshotData(GObject.GObject):
281+
282+ __gsignals__ = {"screenshots-available" : (GObject.SIGNAL_RUN_FIRST,
283+ GObject.TYPE_NONE,
284+ (),),
285+ }
286+
287+ def __init__(self, app_details):
288+ GObject.GObject.__init__(self)
289+ self.app_details = app_details
290+ self.appname = app_details.display_name
291+ self.pkgname = app_details.pkgname
292+ self.app_details.connect(
293+ "screenshots-available", self._on_screenshots_available)
294+ self.app_details.query_multiple_screenshots()
295+ self.screenshots = []
296+ return
297+
298+ def _on_screenshots_available(self, screenshot_data, screenshots):
299+ self.screenshots = screenshots
300+ self.emit("screenshots-available")
301+
302+ def get_n_screenshots(self):
303+ return len(self.screenshots)
304+
305+ def get_nth_large_screenshot(self, index):
306+ return self.screenshots[index]['large_image_url']
307+
308+ def get_nth_small_screenshot(self, index):
309+ return self.screenshots[index]['small_image_url']
310+
311+
312+class ScreenshotWidget(Gtk.VBox):
313+
314+ MAX_SIZE_CONSTRAINTS = 300, 250
315 SPINNER_SIZE = 32, 32
316
317 ZOOM_ICON = "stock_zoom-page"
318+ NOT_AVAILABLE_STRING = _('No screenshot available')
319
320+ USE_CACHING = False
321
322 def __init__(self, distro, icons):
323- Gtk.Alignment.__init__(self)
324- self.set(0.5, 0.0, 1.0, 1.0)
325-
326- # data
327+ Gtk.VBox.__init__(self)
328+ # data
329 self.distro = distro
330 self.icons = icons
331-
332- self.pkgname = None
333- self.appname = None
334- self.thumb_url = None
335- self.large_url = None
336+ self.data = None
337
338 # state tracking
339 self.ready = False
340 self.screenshot_pixbuf = None
341 self.screenshot_available = False
342- self.alpha = 0.0
343
344 # zoom cursor
345 try:
346@@ -72,25 +96,9 @@
347 self._zoom_cursor = Gdk.Cursor.new_from_pixbuf(
348 Gdk.Display.get_default(),
349 zoom_pb,
350- 0, 0) # x, y
351+ 0, 0) # x, y
352 except:
353- self._zoom_cursor = None
354-
355- # tip stuff
356- self._hide_after = None
357- self.tip_alpha = 0.0
358- self._tip_fader = 0
359- self._tip_layout = self.create_pango_layout("")
360- #m = "<small><b>%s</b></small>"
361- #~ self._tip_layout.set_markup(m % _("Click for fullsize screenshot"))
362- #~ self._tip_layout.set_ellipsize(Pango.EllipsizeMode.END)
363-
364- self._tip_xpadding = 4
365- self._tip_ypadding = 1
366-
367- # cache the tip dimensions
368- w, h = self._tip_layout.get_pixel_size()
369- self._tip_size = (w+2*self._tip_xpadding, h+2*self._tip_ypadding)
370+ self._zoom_cursor = None
371
372 # convienience class for handling the downloading (or not) of any screenshot
373 self.loader = SimpleFileDownloader()
374@@ -102,218 +110,44 @@
375 return
376
377 def _build_ui(self):
378- self.set_redraw_on_allocate(False)
379 # the frame around the screenshot (placeholder)
380 self.set_border_width(3)
381-
382- # eventbox so we can connect to event signals
383- event = Gtk.EventBox()
384- event.set_visible_window(False)
385-
386- self.spinner_alignment = Gtk.Alignment.new(0.5, 0.5, 1.0, 0.0)
387+ self.screenshot = Gtk.VBox()
388+ self.pack_start(self.screenshot, True, True, 0)
389
390 self.spinner = Gtk.Spinner()
391 self.spinner.set_size_request(*self.SPINNER_SIZE)
392- self.spinner_alignment.add(self.spinner)
393-
394- # the image
395- self.image = Gtk.Image()
396- self.image.set_redraw_on_allocate(False)
397- event.add(self.image)
398- self.eventbox = event
399-
400- # connect the image to our custom draw func for fading in
401- self.image.connect('draw', self._on_image_draw)
402+ self.spinner.set_valign(Gtk.Align.CENTER)
403+ self.spinner.set_halign(Gtk.Align.CENTER)
404+ self.screenshot.add(self.spinner)
405+
406+ # clickable screenshot button
407+ self.button = ScreenshotButton()
408+ self.screenshot.pack_start(self.button, True, False, 0)
409
410 # unavailable layout
411- l = Gtk.Label(label=_('No screenshot'))
412+ self.unavailable = Gtk.Label(label=self.NOT_AVAILABLE_STRING)
413+ self.unavailable.set_alignment(0.5, 0.5)
414 # force the label state to INSENSITIVE so we get the nice subtle etched in look
415- l.set_state(Gtk.StateType.INSENSITIVE)
416- # center children both horizontally and vertically
417- self.unavailable = Gtk.Alignment.new(0.5, 0.5, 1.0, 1.0)
418- self.unavailable.add(l)
419-
420- # set the widget to be reactive to events
421- self.set_property("can-focus", True)
422- event.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|
423- Gdk.EventMask.BUTTON_RELEASE_MASK|
424- Gdk.EventMask.KEY_RELEASE_MASK|
425- Gdk.EventMask.KEY_PRESS_MASK|
426- Gdk.EventMask.ENTER_NOTIFY_MASK|
427- Gdk.EventMask.LEAVE_NOTIFY_MASK)
428-
429- # connect events to signal handlers
430- event.connect('enter-notify-event', self._on_enter)
431- event.connect('leave-notify-event', self._on_leave)
432- event.connect('button-press-event', self._on_press)
433- event.connect('button-release-event', self._on_release)
434-
435- self.connect('focus-in-event', self._on_focus_in)
436-# self.connect('focus-out-event', self._on_focus_out)
437- self.connect("key-press-event", self._on_key_press)
438- self.connect("key-release-event", self._on_key_release)
439-
440- # signal handlers
441- def _on_enter(self, widget, event):
442- if not self.get_is_actionable(): return
443-
444- self.get_window().set_cursor(self._zoom_cursor)
445- self.show_tip(hide_after=3000)
446- return
447-
448- def _on_leave(self, widget, event):
449- self.get_window().set_cursor(None)
450- self.hide_tip()
451- return
452-
453- def _on_press(self, widget, event):
454- if event.button != 1 or not self.get_is_actionable(): return
455- self.set_state(Gtk.StateType.ACTIVE)
456- return
457-
458- def _on_release(self, widget, event):
459- if event.button != 1 or not self.get_is_actionable(): return
460- self.set_state(Gtk.StateType.NORMAL)
461- self._show_image_dialog()
462- return
463-
464- def _on_focus_in(self, widget, event):
465- self.show_tip(hide_after=3000)
466- return
467-
468-# def _on_focus_out(self, widget, event):
469-# return
470-
471- def _on_key_press(self, widget, event):
472- # react to spacebar, enter, numpad-enter
473- if event.keyval in (Gdk.KEY_space,
474- Gdk.KEY_Return,
475- Gdk.KEY_KP_Enter) and self.get_is_actionable():
476- self.set_state(Gtk.StateType.ACTIVE)
477- return
478-
479- def _on_key_release(self, widget, event):
480- # react to spacebar, enter, numpad-enter
481- if event.keyval in (Gdk.KEY_space,
482- Gdk.KEY_Return,
483- Gdk.KEY_KP_Enter) and self.get_is_actionable():
484- self.set_state(Gtk.StateType.NORMAL)
485- self._show_image_dialog()
486- return
487-
488- def _on_image_draw(self, widget, cr):
489- """ If the alpha value is less than 1, we override the normal draw
490- for the GtkImage so we can draw with transparencey.
491- """
492-#~
493- #~ if widget.get_storage_type() != Gtk.ImageType.PIXBUF:
494- #~ return
495-#~
496- #~ pb = widget.get_pixbuf()
497- #~ if not pb: return True
498-#~
499- #~ a = widget.get_allocation()
500- #~ cr.rectangle(a.x, a.y, a.width, a.height)
501- #~ cr.clip()
502-#~
503- #~ # draw the pixbuf with the current alpha value
504- #~ cr.set_source_pixbuf(pb, a.x, a.y)
505- #~ cr.paint_with_alpha(self.alpha)
506- #~
507- #~ if not self.tip_alpha: return True
508-#~
509- #~ tw, th = self._tip_size
510- #~ if a.width > tw:
511- #~ self._tip_layout.set_width(-1)
512- #~ else:
513- #~ # tip is image width
514- #~ tw = a.width
515- #~ self._tip_layout.set_width(1024*(tw-2*self._tip_xpadding))
516-#~
517- #~ tx, ty = a.x+a.width-tw, a.y+a.height-th
518-#~
519- #~ rr = mkit.ShapeRoundedRectangleIrregular()
520- #~ rr.layout(cr, tx, ty, tx+tw, ty+th, radii=(6, 0, 0, 0))
521-#~
522- #~ cr.set_source_rgba(0,0,0,0.85*self.tip_alpha)
523- #~ cr.fill()
524-#~
525- #~ cr.move_to(tx+self._tip_xpadding, ty+self._tip_ypadding)
526- #~ cr.layout_path(self._tip_layout)
527- #~ cr.set_source_rgba(1,1,1,self.tip_alpha)
528- #~ cr.fill()
529-
530- #~ return True
531- return
532-
533- def _fade_in(self):
534- """ This callback increments the alpha value from zero to 1,
535- stopping once 1 is reached or exceeded.
536- """
537-
538- self.alpha += 0.05
539- if self.alpha >= 1.0:
540- self.alpha = 1.0
541- self.queue_draw()
542- return False
543- self.queue_draw()
544- return True
545-
546- def _tip_fade_in(self):
547- """ This callback increments the alpha value from zero to 1,
548- stopping once 1 is reached or exceeded.
549- """
550-
551- self.tip_alpha += 0.1
552- #ia = self.image.get_allocation()
553- tw, th = self._tip_size
554-
555- if self.tip_alpha >= 1.0:
556- self.tip_alpha = 1.0
557- self.image.queue_draw()
558-# self.image.queue_draw_area(ia.x+ia.width-tw,
559-# ia.y+ia.height-th,
560-# tw, th)
561- return False
562-
563- self.image.queue_draw()
564-# self.image.queue_draw_area(ia.x+ia.width-tw,
565-# ia.y+ia.height-th,
566-# tw, th)
567- return True
568-
569- def _tip_fade_out(self):
570- """ This callback increments the alpha value from zero to 1,
571- stopping once 1 is reached or exceeded.
572- """
573-
574- self.tip_alpha -= 0.1
575- #ia = self.image.get_allocation()
576- tw, th = self._tip_size
577-
578- if self.tip_alpha <= 0.0:
579- self.tip_alpha = 0.0
580-# self.image.queue_draw_area(ia.x+ia.width-tw,
581-# ia.y+ia.height-th,
582-# tw, th)
583- self.image.queue_draw()
584- return False
585-
586- self.image.queue_draw()
587-# self.image.queue_draw_area(ia.x+ia.width-tw,
588-# ia.y+ia.height-th,
589-# tw, th)
590- return True
591-
592- def _show_image_dialog(self):
593- """ Displays the large screenshot in a seperate dialog window """
594-
595- if self.screenshot_pixbuf:
596- title = _("%s - Screenshot") % self.appname
597- toplevel = self.get_toplevel()
598- d = SimpleShowImageDialog(title, self.screenshot_pixbuf, toplevel)
599- d.run()
600- d.destroy()
601+ self.unavailable.set_state(Gtk.StateType.INSENSITIVE)
602+ self.screenshot.add(self.unavailable)
603+ self.show_all()
604+
605+ def _on_screenshot_download_complete(self, loader, screenshot_path):
606+ try:
607+ self.screenshot_pixbuf = GdkPixbuf.Pixbuf.new_from_file(screenshot_path)
608+ except Exception, e:
609+ LOG.exception("Pixbuf.new_from_file() failed")
610+ self.loader.emit('error', GObject.GError, e)
611+ return False
612+
613+ context = self.button.get_style_context()
614+
615+ tw, th = self.MAX_SIZE_CONSTRAINTS
616+ pb = self._downsize_pixbuf(self.screenshot_pixbuf, tw, th)
617+ self.button.image.set_from_pixbuf(pb)
618+ self.ready = True
619+ self.display_image()
620 return
621
622 def _on_screenshot_load_error(self, loader, err_type, err_message):
623@@ -323,7 +157,8 @@
624
625 def _on_screenshot_query_complete(self, loader, reachable):
626 self.set_screenshot_available(reachable)
627- if not reachable: self.ready = True
628+ if not reachable:
629+ self.ready = True
630 return
631
632 def _downsize_pixbuf(self, pb, target_w, target_h):
633@@ -340,72 +175,73 @@
634
635 return pb.scale_simple(sw, sh, GdkPixbuf.InterpType.BILINEAR)
636
637- def _on_screenshot_download_complete(self, loader, screenshot_path):
638-
639- def setter_cb(path):
640- try:
641- self.screenshot_pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
642- except Exception, e:
643- LOG.exception("Pixbuf.new_from_file() failed")
644- self.loader.emit('error', GObject.GError, e)
645- return False
646-
647- # remove the spinner
648- if self.spinner_alignment.get_parent():
649- self.spinner.stop()
650- self.spinner.hide()
651- self.remove(self.spinner_alignment)
652-
653- pb = self._downsize_pixbuf(self.screenshot_pixbuf, *self.MAX_SIZE)
654-
655- if not self.eventbox.get_parent():
656- self.add(self.eventbox)
657- if self.get_property("visible"):
658- self.show_all()
659-
660- self.image.set_size_request(-1, -1)
661- self.image.set_from_pixbuf(pb)
662-
663- # queue parent redraw if height of new pb is less than idle height
664- if pb.get_height() < self.IDLE_SIZE[1]:
665- if self.get_parent():
666- self.get_parent().queue_draw()
667-
668- # start the fade in
669- GObject.timeout_add(50, self._fade_in)
670- self.ready = True
671- return False
672-
673- GObject.timeout_add(500, setter_cb, screenshot_path)
674- return
675-
676- def show_tip(self, hide_after=0):
677- if (not self.image.get_property('visible') or
678- self.tip_alpha >= 1.0):
679- return
680-
681- if self._tip_fader: GObject.source_remove(self._tip_fader)
682- self._tip_fader = GObject.timeout_add(25, self._tip_fade_in)
683-
684- if hide_after:
685- if self._hide_after:
686- GObject.source_remove(self._hide_after)
687- self._hide_after = GObject.timeout_add(hide_after, self.hide_tip)
688- return
689-
690- def hide_tip(self):
691- if (not self.image.get_property('visible') or
692- self.tip_alpha <= 0.0):
693- return
694-
695- if self._tip_fader:
696- GObject.source_remove(self._tip_fader)
697- self._tip_fader = GObject.timeout_add(25, self._tip_fade_out)
698+ def download_and_display_from_url(self, url):
699+ self.loader.download_file(url, use_cache=self.USE_CACHING)
700+ return
701+
702+ def clear(self):
703+ """ All state trackers are set to their intitial states, and
704+ the old screenshot is cleared from the view.
705+ """
706+
707+ self.screenshot_available = True
708+ self.ready = False
709+ self.display_spinner()
710+ return
711+
712+ def display_spinner(self):
713+ self.button.image.clear()
714+ self.button.hide()
715+ self.unavailable.hide()
716+ self.spinner.show()
717+ self.determine_size()
718+ self.spinner.start()
719+ return
720+
721+ def display_unavailable(self):
722+ self.spinner.hide()
723+ self.spinner.stop()
724+ self.unavailable.show()
725+ self.button.hide()
726+ self.determine_size()
727+ acc = self.get_accessible()
728+ acc.set_name(self.NOT_AVAILABLE_STRING)
729+ acc.set_role(Atk.Role.LABEL)
730+ return
731+
732+ def determine_size(self):
733+ has_thumbnails = self.thumbnails.get_children()
734+ #~ is_actionable = self.screenshot_available
735+ w_contraint, h_constraint = self.MAX_SIZE_CONSTRAINTS
736+ if not self.ready and not has_thumbnails:
737+ # no thumbs or main image, spinner
738+ self.screenshot.set_size_request(w_contraint, h_constraint)
739+ elif not self.ready and has_thumbnails:
740+ # still spinner vis but thumbs loaded
741+ self.screenshot.set_size_request(w_contraint, h_constraint)
742+ elif self.ready and not has_thumbnails:
743+ # main image loaded no thumbs
744+ self.screenshot.set_size_request(-1, -1)
745+ elif self.ready and has_thumbnails:
746+ # main image loaded with thumbs
747+ self.screenshot.set_size_request(-1, h_constraint)
748+ else:
749+ self.screenshot.set_size_request(w_contraint, h_constraint)
750+ print 'unhandled case', has_thumbnails, self.ready
751+ return
752+
753+ def display_image(self):
754+ self.unavailable.hide()
755+ self.spinner.stop()
756+ self.spinner.hide()
757+ self.button.show_all()
758+ self.thumbnails.show()
759+ self.determine_size()
760 return
761
762 def get_is_actionable(self):
763 """ Returns true if there is a screenshot available and
764- the download has completed
765+ the download has completed
766 """
767 return self.screenshot_available and self.ready
768
769@@ -414,137 +250,245 @@
770 is a screenshot available.
771 """
772 if not available:
773- if not self.eventbox.get_parent():
774- self.remove(self.spinner_alignment)
775- self.spinner.stop()
776- self.add(self.eventbox)
777-
778- if self.image.get_parent():
779- self.image.hide()
780- self.eventbox.remove(self.image)
781- self.eventbox.add(self.unavailable)
782- # set the size of the unavailable placeholder
783- # 160 pixels is the fixed width of the thumbnails
784- self.unavailable.set_size_request(*self.IDLE_SIZE)
785- acc = self.get_accessible()
786- acc.set_name(_('No screenshot available'))
787- acc.set_role(Atk.Role.LABEL)
788- else:
789- if self.unavailable.get_parent():
790- self.unavailable.hide()
791- self.eventbox.remove(self.unavailable)
792- self.eventbox.add(self.image)
793- acc = self.get_accessible()
794- acc.set_name(_('Screenshot'))
795- acc.set_role(Atk.Role.PUSH_BUTTON)
796-
797- if self.get_property("visible"):
798- self.show_all()
799+ self.display_unavailable()
800+ elif available and self.unavailable.get_property("visible"):
801+ self.display_spinner()
802 self.screenshot_available = available
803 return
804-
805- def configure(self, app_details):
806-
807+
808+
809+class ScreenshotButton(Gtk.Button):
810+
811+ def __init__(self):
812+ Gtk.Button.__init__(self)
813+ self.set_focus_on_click(False)
814+ self.set_valign(Gtk.Align.CENTER)
815+ self.image = Gtk.Image()
816+ self.add(self.image)
817+ return
818+
819+ def do_draw(self, cr):
820+ if self.has_focus():
821+ context = self.get_style_context()
822+ _a = self.get_allocation()
823+ a = self.image.get_allocation()
824+ pb = self.image.get_pixbuf()
825+ pbw, pbh = pb.get_width(), pb.get_height()
826+ Gtk.render_focus(
827+ context,
828+ cr,
829+ a.x - _a.x + (a.width-pbw)/2 - 4,
830+ a.y - _a.y + (a.height-pbh)/2 - 4,
831+ pbw+8, pbh+8)
832+
833+ for child in self:
834+ self.propagate_draw(child, cr)
835+ return
836+
837+
838+class ScreenshotGallery(ScreenshotWidget):
839+
840+ """ Widget that displays screenshot availability, download prrogress,
841+ and eventually the screenshot itself.
842+ """
843+
844+ def __init__(self, distro, icons):
845+ ScreenshotWidget.__init__(self, distro, icons)
846+ self._thumbnail_sigs = []
847+ return
848+
849+ def _build_ui(self):
850+ ScreenshotWidget._build_ui(self)
851+ self.thumbnails = ThumbnailGallery(self)
852+ self.thumbnails.set_margin_top(5)
853+ self.thumbnails.set_halign(Gtk.Align.CENTER)
854+ self.pack_end(self.thumbnails, False, False, 0)
855+ self.thumbnails.connect("thumb-selected", self.on_thumbnail_selected)
856+ self.button.connect("clicked", self.on_clicked)
857+ self.show_all()
858+ return
859+
860+ def on_clicked(self, button):
861+ if not self.get_is_actionable():
862+ return
863+ self._show_image_dialog()
864+ return
865+
866+ def _on_focus_in(self, widget, event):
867+ return
868+
869+ def _on_key_press(self, widget, event):
870+ # react to spacebar, enter, numpad-enter
871+ if event.keyval in (Gdk.KEY_space,
872+ Gdk.KEY_Return,
873+ Gdk.KEY_KP_Enter) and self.get_is_actionable():
874+ self.set_state(Gtk.StateType.ACTIVE)
875+ return
876+
877+ def _on_key_release(self, widget, event):
878+ # react to spacebar, enter, numpad-enter
879+ if event.keyval in (Gdk.KEY_space,
880+ Gdk.KEY_Return,
881+ Gdk.KEY_KP_Enter) and self.get_is_actionable():
882+ self.set_state(Gtk.StateType.NORMAL)
883+ self._show_image_dialog()
884+ return
885+
886+ def _show_image_dialog(self):
887+ """ Displays the large screenshot in a seperate dialog window """
888+
889+ if self.data and self.screenshot_pixbuf:
890+ title = _("%s - Screenshot") % self.data.appname
891+ toplevel = self.get_toplevel()
892+ d = SimpleShowImageDialog(title, self.screenshot_pixbuf, toplevel)
893+ d.run()
894+ d.destroy()
895+ return
896+
897+ def fetch_screenshots(self, app_details):
898 """ Called to configure the screenshotview for a new application.
899 The existing screenshot is cleared and the process of fetching a
900 new screenshot is instigated.
901 """
902-
903+ self.clear()
904 acc = self.get_accessible()
905 acc.set_name(_('Fetching screenshot ...'))
906-
907- self.clear()
908- self.appname = app_details.display_name
909- self.pkgname = app_details.pkgname
910-# self.thumbnail_url = app_details.thumbnail
911- self.thumbnail_url = app_details.screenshot
912- self.large_url = app_details.screenshot
913+ self.data = ScreenshotData(app_details)
914+ self.data.connect(
915+ "screenshots-available", self._on_screenshots_available)
916+ self.display_spinner()
917+ self.download_and_display_from_url(app_details.screenshot)
918 return
919
920+ def _on_screenshots_available(self, screenshots):
921+ self.thumbnails.set_thumbnails_from_data(screenshots)
922+ self.determine_size()
923+
924 def clear(self):
925-
926- """ All state trackers are set to their intitial states, and
927- the old screenshot is cleared from the view.
928- """
929-
930- self.screenshot_available = True
931- self.ready = False
932- self.alpha = 0.0
933-
934- if self.eventbox.get_parent():
935- self.eventbox.hide()
936- self.remove(self.eventbox)
937-
938- if not self.spinner_alignment.get_parent():
939- self.add(self.spinner_alignment)
940-
941- self.spinner_alignment.set_size_request(*self.IDLE_SIZE)
942-
943- self.spinner.start()
944-
945- return
946-
947- def download_and_display(self):
948- """ Download then displays the screenshot.
949- This actually does a query on the URL first to check if its
950- reachable, if so it downloads the thumbnail.
951- If not, it emits "file-url-reachable" False, then exits.
952- """
953-
954- self.loader.download_file(self.thumbnail_url)
955- # show it
956- if self.get_property('visible'):
957- self.show_all()
958-
959+ self.thumbnails.clear()
960+ ScreenshotWidget.clear(self)
961+
962+ def on_thumbnail_selected(self, gallery, id_):
963+ ScreenshotWidget.clear(self)
964+ large_url = self.data.get_nth_large_screenshot(id_)
965+ self.download_and_display_from_url(large_url)
966 return
967
968 def draw(self, widget, cr):
969 """ Draws the thumbnail frame """
970- #~ if not self.get_property("visible"): return
971-#~
972- #~ if self.eventbox.get_property('visible'):
973- #~ ia = self.eventbox.get_allocation()
974- #~ else:
975- #~ ia = self.spinner_alignment.get_allocation()
976-#~
977- #~ a = widget.get_allocation()
978-#~
979- #~ x = a.x
980- #~ y = a.y
981-#~
982- #~ if self.has_focus() or self.get_state() == Gtk.StateType.ACTIVE:
983- #~ cr.rectangle(x-2, y-2, ia.width+4, ia.height+4)
984- #~ cr.set_source_rgb(1,1,1)
985- #~ cr.fill_preserve()
986- #~ if self.get_state() == Gtk.StateType.ACTIVE:
987- #~ color = mkit.floats_from_gdkcolor(self.style.mid[self.get_state()])
988- #~ else:
989- #~ color = mkit.floats_from_gdkcolor(self.style.dark[Gtk.StateType.SELECTED])
990- #~ cr.set_source_rgb(*color)
991- #~ cr.stroke()
992- #~ else:
993- #~ cr.rectangle(x-3, y-3, ia.width+6, ia.height+6)
994- #~ cr.set_source_rgb(1,1,1)
995- #~ cr.fill()
996- #~ cr.save()
997- #~ cr.translate(0.5, 0.5)
998- #~ cr.set_line_width(1)
999- #~ cr.rectangle(x-3, y-3, ia.width+5, ia.height+5)
1000-#~
1001- #~ # FIXME: color
1002- #~ dark = color_floats("#000")
1003- #~ cr.set_source_rgb(*dark)
1004- #~ cr.stroke()
1005- #~ cr.restore()
1006-#~
1007- #~ if not self.screenshot_available:
1008- #~ cr.rectangle(x, y, ia.width, ia.height)
1009- #~ cr.set_source_rgb(1,0,0)
1010- #~ cr.fill()
1011- return
1012+ return
1013+
1014+
1015+class Thumbnail(Gtk.Button):
1016+
1017+ def __init__(self, id_, url, cancellable, gallery):
1018+ Gtk.Button.__init__(self)
1019+ self.id_ = id_
1020+
1021+ def download_complete_cb(loader, path):
1022+ width, height = ThumbnailGallery.THUMBNAIL_SIZE_CONTRAINTS
1023+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
1024+ path,
1025+ width, height, # width, height constraints
1026+ True) # respect image proportionality
1027+ im = Gtk.Image.new_from_pixbuf(pixbuf)
1028+ self.add(im)
1029+ self.show_all()
1030+ return
1031+
1032+ loader = SimpleFileDownloader()
1033+ loader.connect("file-download-complete", download_complete_cb)
1034+ loader.download_file(url, use_cache=ScreenshotWidget.USE_CACHING)
1035+
1036+ self.connect("draw", self.on_draw)
1037+ return
1038+
1039+ def on_draw(self, thumb, cr):
1040+ state = self.get_state_flags()
1041+ if self.has_focus() or (state & Gtk.StateFlags.ACTIVE) > 0:
1042+ return
1043+
1044+ for child in self:
1045+ self.propagate_draw(child, cr)
1046+ return True
1047+
1048+
1049+class ThumbnailGallery(Gtk.HBox):
1050+
1051+ __gsignals__ = {
1052+ "thumb-selected": (GObject.SIGNAL_RUN_LAST,
1053+ GObject.TYPE_NONE,
1054+ (int,),),}
1055+
1056+ THUMBNAIL_SIZE_CONTRAINTS = 90, 80
1057+ THUMBNAIL_MAX_COUNT = 3
1058+
1059+
1060+ def __init__(self, gallery):
1061+ Gtk.HBox.__init__(self)
1062+ self.gallery = gallery
1063+ self.distro = gallery.distro
1064+ self.icons = gallery.icons
1065+ self.cancel = Gio.Cancellable()
1066+ self._prev = None
1067+ self._handlers = []
1068+ return
1069+
1070+ def clear(self):
1071+ self.cancel.cancel()
1072+ self.cancel.reset()
1073+
1074+ for sig in self._handlers:
1075+ GObject.source_remove(sig)
1076+
1077+ for child in self:
1078+ child.destroy()
1079+ return
1080+
1081+ def set_thumbnails_from_data(self, data):
1082+ self.clear()
1083+
1084+ # if there are multiple screenshots
1085+ n = data.get_n_screenshots()
1086+
1087+ if n == 1:
1088+ return
1089+
1090+ # get a random selection of thumbnails from those avaialble
1091+ import random
1092+ seq = random.sample(
1093+ range(n),
1094+ min(n, ThumbnailGallery.THUMBNAIL_MAX_COUNT))
1095+
1096+ seq.sort()
1097+
1098+ for i in seq:
1099+ url = data.get_nth_small_screenshot(i)
1100+ self._create_thumbnail_for_url(i, url)
1101+
1102+ # set first child to selected
1103+ self._prev = self.get_children()[0]
1104+ self._prev.set_state_flags(Gtk.StateFlags.SELECTED, False)
1105+
1106+ self.show_all()
1107+ return
1108+
1109+ def _create_thumbnail_for_url(self, index, url):
1110+ thumbnail = Thumbnail(index, url, self.cancel, self.gallery)
1111+ self.pack_start(thumbnail, False, False, 0)
1112+ sig = thumbnail.connect("clicked", self.on_clicked)
1113+ self._handlers.append(sig)
1114+ return
1115+
1116+ def on_clicked(self, thumb):
1117+ if self._prev is not None:
1118+ self._prev.set_state_flags(Gtk.StateFlags.NORMAL, True)
1119+ thumb.set_state_flags(Gtk.StateFlags.SELECTED, False)
1120+ self._prev = thumb
1121+ self.emit("thumb-selected", thumb.id_)
1122+
1123
1124 def get_test_screenshot_thumbnail_window():
1125-
1126 icons = Gtk.IconTheme.get_default()
1127 icons.append_search_path("/usr/share/app-install/icons/")
1128
1129@@ -554,43 +498,37 @@
1130 win = Gtk.Window()
1131 win.set_border_width(10)
1132
1133- t = ScreenshotThumbnail(distro, icons)
1134+ t = ScreenshotGallery(distro, icons)
1135 t.connect('draw', t.draw)
1136 win.set_data("screenshot_thumbnail_widget", t)
1137
1138 vb = Gtk.VBox(spacing=6)
1139 win.add(vb)
1140
1141- vb.pack_start(Gtk.Button('A button for focus testing'), True, True, 0)
1142+ b = Gtk.Button('A button for focus testing')
1143+ vb.pack_start(b, True, True, 0)
1144+ win.set_data("screenshot_button_widget", b)
1145 vb.pack_start(t, True, True, 0)
1146
1147 win.show_all()
1148 win.connect('destroy', Gtk.main_quit)
1149
1150- from mock import Mock
1151- app_details = Mock()
1152- app_details.display_name = "display name"
1153- app_details.pkgname = "pkgname"
1154- url = "http://www.ubuntu.com/sites/default/themes/ubuntu10/images/footer_logo.png"
1155- app_details.thumbnail = url
1156- app_details.screenshot = url
1157-
1158- t.configure(app_details)
1159- t.download_and_display()
1160-
1161 return win
1162
1163 if __name__ == '__main__':
1164
1165- def testing_cycle_apps(thumb, apps, db):
1166-
1167- if not thumb.pkgname or thumb.pkgname == "uace":
1168- d = apps[0].get_details(db)
1169+ app_n = 0
1170+
1171+ def testing_cycle_apps(_, thumb, apps, db):
1172+ global app_n
1173+ d = apps[app_n].get_details(db)
1174+
1175+ if app_n + 1 < len(apps):
1176+ app_n += 1
1177 else:
1178- d = apps[1].get_details(db)
1179+ app_n = 0
1180
1181- thumb.configure(d)
1182- thumb.download_and_display()
1183+ thumb.fetch_screenshots(d)
1184 return True
1185
1186 logging.basicConfig(level=logging.DEBUG)
1187@@ -603,14 +541,17 @@
1188 pathname = os.path.join(xapian_base_path, "xapian")
1189 db = StoreDatabase(pathname, cache)
1190 db.open()
1191-
1192+
1193 w = get_test_screenshot_thumbnail_window()
1194 t = w.get_data("screenshot_thumbnail_widget")
1195+ b = w.get_data("screenshot_button_widget")
1196
1197 from softwarecenter.db.application import Application
1198 apps = [Application("Movie Player", "totem"),
1199+ Application("Comix", "comix"),
1200+ Application("Gimp", "gimp"),
1201 Application("ACE", "uace")]
1202
1203- GObject.timeout_add_seconds(6, testing_cycle_apps, t, apps, db)
1204+ b.connect("clicked", testing_cycle_apps, t, apps, db)
1205
1206 Gtk.main()
1207
1208=== modified file 'softwarecenter/utils.py'
1209--- softwarecenter/utils.py 2011-10-28 20:34:16 +0000
1210+++ softwarecenter/utils.py 2011-11-03 22:05:37 +0000
1211@@ -613,7 +613,9 @@
1212 # like bug #839462
1213 if self._cancellable:
1214 self._cancellable.cancel()
1215- self._cancellable = Gio.Cancellable()
1216+ self._cancellable.reset()
1217+ else:
1218+ self._cancellable = Gio.Cancellable()
1219
1220 # no need to cache file urls and no need to really download
1221 # them, its enough to adjust the dest_file_path