Merge lp:~mmcg069/software-center/multi-screenshot-gallery into lp:software-center
- multi-screenshot-gallery
- Merge into trunk
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 |
Related bugs: |
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 |