~osomon/software-center/qml

« back to all changes in this revision

Viewing changes to softwarecenter/ui/gtk/widgets/thumbnail.py

  • Committer: Olivier Tilloy
  • Date: 2011-11-03 16:42:37 UTC
  • mfrom: (1773.49.652 software-center)
  • Revision ID: olivier@tilloy.net-20111103164237-xhkcn96dmh1gu8a3
Merged the latest changes from trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
 
2
 
import atk
3
 
import gtk
4
 
import gobject
5
 
import mkit
6
 
import pangocairo
7
 
import logging
8
 
import os
9
 
 
10
 
from softwarecenter.utils import SimpleFileDownloader, uri_to_filename
11
 
 
12
 
from imagedialog import ShowImageDialog
13
 
 
14
 
from gettext import gettext as _
15
 
 
16
 
LOG = logging.getLogger(__name__)
17
 
 
18
 
class ScreenshotThumbnail(gtk.Alignment):
19
 
 
20
 
    """ Widget that displays screenshot availability, download prrogress,
21
 
        and eventually the screenshot itself.
22
 
    """
23
 
 
24
 
    MAX_SIZE = 225, 225
25
 
    IDLE_SIZE = 225, 150
26
 
    SPINNER_SIZE = 32, 32
27
 
 
28
 
    def __init__(self, distro, icons):
29
 
        gtk.Alignment.__init__(self, 0.5, 0.0)
30
 
 
31
 
        # data 
32
 
        self.distro = distro
33
 
        self.icons = icons
34
 
 
35
 
        self.pkgname = None
36
 
        self.appname = None
37
 
        self.thumb_url = None
38
 
        self.large_url = None
39
 
 
40
 
        # state tracking
41
 
        self.ready = False
42
 
        self.screenshot_pixbuf = None
43
 
        self.screenshot_available = False
44
 
        self.alpha = 0.0
45
 
 
46
 
        # zoom cursor
47
 
        theme = gtk.icon_theme_get_default()
48
 
        zoom_pb = theme.load_icon(gtk.STOCK_ZOOM_IN, 22, 0)
49
 
        self._zoom_cursor = gtk.gdk.Cursor(gtk.gdk.display_get_default(),
50
 
                                           zoom_pb, 0, 0)
51
 
                                           
52
 
 
53
 
        # tip stuff
54
 
        self._hide_after = None
55
 
        self.tip_alpha = 0.0
56
 
        self._tip_fader = 0
57
 
        self._tip_layout = self.create_pango_layout("")
58
 
        m = "<small><b>%s</b></small>"
59
 
        self._tip_layout.set_markup(m % _("Click for fullsize screenshot"))
60
 
        import pango
61
 
        self._tip_layout.set_ellipsize(pango.ELLIPSIZE_END)
62
 
 
63
 
        self._tip_xpadding = 4
64
 
        self._tip_ypadding = 1
65
 
 
66
 
        # cache the tip dimensions
67
 
        w, h = self._tip_layout.get_pixel_extents()[1][2:]
68
 
        self._tip_size = (w+2*self._tip_xpadding, h+2*self._tip_ypadding)
69
 
 
70
 
        # convienience class for handling the downloading (or not) of any screenshot
71
 
        self.loader = SimpleFileDownloader()
72
 
        self.loader.connect('error', self._on_screenshot_load_error)
73
 
        self.loader.connect('file-url-reachable', self._on_screenshot_query_complete)
74
 
        self.loader.connect('file-download-complete', self._on_screenshot_download_complete)
75
 
 
76
 
        self._build_ui()
77
 
        return
78
 
 
79
 
    def _build_ui(self):
80
 
        self.set_redraw_on_allocate(False)
81
 
        # the frame around the screenshot (placeholder)
82
 
        self.set_border_width(3)
83
 
 
84
 
        # eventbox so we can connect to event signals
85
 
        event = gtk.EventBox()
86
 
        event.set_visible_window(False)
87
 
 
88
 
        self.spinner_alignment = gtk.Alignment(0.5, 0.5, yscale=0)
89
 
 
90
 
        self.spinner = gtk.Spinner()
91
 
        self.spinner.set_size_request(*self.SPINNER_SIZE)
92
 
        self.spinner_alignment.add(self.spinner)
93
 
 
94
 
        # the image
95
 
        self.image = gtk.Image()
96
 
        self.image.set_redraw_on_allocate(False)
97
 
        event.add(self.image)
98
 
        self.eventbox = event
99
 
 
100
 
        # connect the image to our custom draw func for fading in
101
 
        self.image.connect('expose-event', self._on_image_expose)
102
 
 
103
 
        # unavailable layout
104
 
        l = gtk.Label(_('No screenshot'))
105
 
        # force the label state to INSENSITIVE so we get the nice subtle etched in look
106
 
        l.set_state(gtk.STATE_INSENSITIVE)
107
 
        # center children both horizontally and vertically
108
 
        self.unavailable = gtk.Alignment(0.5, 0.5)
109
 
        self.unavailable.add(l)
110
 
 
111
 
        # set the widget to be reactive to events
112
 
        self.set_flags(gtk.CAN_FOCUS)
113
 
        event.set_events(gtk.gdk.BUTTON_PRESS_MASK|
114
 
                         gtk.gdk.BUTTON_RELEASE_MASK|
115
 
                         gtk.gdk.KEY_RELEASE_MASK|
116
 
                         gtk.gdk.KEY_PRESS_MASK|
117
 
                         gtk.gdk.ENTER_NOTIFY_MASK|
118
 
                         gtk.gdk.LEAVE_NOTIFY_MASK)
119
 
 
120
 
        # connect events to signal handlers
121
 
        event.connect('enter-notify-event', self._on_enter)
122
 
        event.connect('leave-notify-event', self._on_leave)
123
 
        event.connect('button-press-event', self._on_press)
124
 
        event.connect('button-release-event', self._on_release)
125
 
 
126
 
        self.connect('focus-in-event', self._on_focus_in)
127
 
#        self.connect('focus-out-event', self._on_focus_out)
128
 
        self.connect("key-press-event", self._on_key_press)
129
 
        self.connect("key-release-event", self._on_key_release)
130
 
 
131
 
    # signal handlers
132
 
    def _on_enter(self, widget, event):
133
 
        if not self.get_is_actionable(): return
134
 
 
135
 
        self.window.set_cursor(self._zoom_cursor)
136
 
        self.show_tip(hide_after=3000)
137
 
        return
138
 
 
139
 
    def _on_leave(self, widget, event):
140
 
        self.window.set_cursor(None)
141
 
        self.hide_tip()
142
 
        return
143
 
 
144
 
    def _on_press(self, widget, event):
145
 
        if event.button != 1 or not self.get_is_actionable(): return
146
 
        self.set_state(gtk.STATE_ACTIVE)
147
 
        return
148
 
 
149
 
    def _on_release(self, widget, event):
150
 
        if event.button != 1 or not self.get_is_actionable(): return
151
 
        self.set_state(gtk.STATE_NORMAL)
152
 
        self._show_image_dialog()
153
 
        return
154
 
 
155
 
    def _on_focus_in(self, widget, event):
156
 
        self.show_tip(hide_after=3000)
157
 
        return
158
 
 
159
 
#    def _on_focus_out(self, widget, event):
160
 
#        return
161
 
 
162
 
    def _on_key_press(self, widget, event):
163
 
        # react to spacebar, enter, numpad-enter
164
 
        if event.keyval in (gtk.keysyms.space, 
165
 
                            gtk.keysyms.Return, 
166
 
                            gtk.keysyms.KP_Enter) and self.get_is_actionable():
167
 
            self.set_state(gtk.STATE_ACTIVE)
168
 
        return
169
 
 
170
 
    def _on_key_release(self, widget, event):
171
 
        # react to spacebar, enter, numpad-enter
172
 
        if event.keyval in (gtk.keysyms.space,
173
 
                            gtk.keysyms.Return, 
174
 
                            gtk.keysyms.KP_Enter) and self.get_is_actionable():
175
 
            self.set_state(gtk.STATE_NORMAL)
176
 
            self._show_image_dialog()
177
 
        return
178
 
 
179
 
    def _on_image_expose(self, widget, event):
180
 
        """ If the alpha value is less than 1, we override the normal draw
181
 
            for the GtkImage so we can draw with transparencey.
182
 
        """
183
 
 
184
 
        if widget.get_storage_type() != gtk.IMAGE_PIXBUF:
185
 
            return
186
 
 
187
 
        pb = widget.get_pixbuf()
188
 
        if not pb: return True
189
 
 
190
 
        a = widget.allocation
191
 
        cr = widget.window.cairo_create()
192
 
 
193
 
        cr.rectangle(a)
194
 
        cr.clip()
195
 
 
196
 
        # draw the pixbuf with the current alpha value
197
 
        cr.set_source_pixbuf(pb, a.x, a.y)
198
 
        cr.paint_with_alpha(self.alpha)
199
 
        
200
 
        if not self.tip_alpha: return True
201
 
 
202
 
        tw, th = self._tip_size
203
 
        if a.width > tw:
204
 
            self._tip_layout.set_width(-1)
205
 
        else:
206
 
            # tip is image width
207
 
            tw = a.width
208
 
            self._tip_layout.set_width(1024*(tw-2*self._tip_xpadding))
209
 
 
210
 
        tx, ty = a.x+a.width-tw, a.y+a.height-th
211
 
 
212
 
        rr = mkit.ShapeRoundedRectangleIrregular()
213
 
        rr.layout(cr, tx, ty, tx+tw, ty+th, radii=(6, 0, 0, 0))
214
 
 
215
 
        cr.set_source_rgba(0,0,0,0.85*self.tip_alpha)
216
 
        cr.fill()
217
 
 
218
 
        pcr = pangocairo.CairoContext(cr)
219
 
        pcr.move_to(tx+self._tip_xpadding, ty+self._tip_ypadding)
220
 
        pcr.layout_path(self._tip_layout)
221
 
        pcr.set_source_rgba(1,1,1,self.tip_alpha)
222
 
        pcr.fill()
223
 
 
224
 
        return True
225
 
 
226
 
    def _fade_in(self):
227
 
        """ This callback increments the alpha value from zero to 1,
228
 
            stopping once 1 is reached or exceeded.
229
 
        """
230
 
 
231
 
        self.alpha += 0.05
232
 
        if self.alpha >= 1.0:
233
 
            self.alpha = 1.0
234
 
            self.queue_draw()
235
 
            return False
236
 
        self.queue_draw()
237
 
        return True
238
 
 
239
 
    def _tip_fade_in(self):
240
 
        """ This callback increments the alpha value from zero to 1,
241
 
            stopping once 1 is reached or exceeded.
242
 
        """
243
 
 
244
 
        self.tip_alpha += 0.1
245
 
#        ia = self.image.allocation
246
 
        tw, th = self._tip_size
247
 
 
248
 
        if self.tip_alpha >= 1.0:
249
 
            self.tip_alpha = 1.0
250
 
            self.image.queue_draw()
251
 
#            self.image.queue_draw_area(ia.x+ia.width-tw,
252
 
#                                       ia.y+ia.height-th,
253
 
#                                       tw, th)
254
 
            return False
255
 
 
256
 
        self.image.queue_draw()
257
 
#        self.image.queue_draw_area(ia.x+ia.width-tw,
258
 
#                                   ia.y+ia.height-th,
259
 
#                                   tw, th)
260
 
        return True
261
 
 
262
 
    def _tip_fade_out(self):
263
 
        """ This callback increments the alpha value from zero to 1,
264
 
            stopping once 1 is reached or exceeded.
265
 
        """
266
 
 
267
 
        self.tip_alpha -= 0.1
268
 
#        ia = self.image.allocation
269
 
        tw, th = self._tip_size
270
 
 
271
 
        if self.tip_alpha <= 0.0:
272
 
            self.tip_alpha = 0.0
273
 
#            self.image.queue_draw_area(ia.x+ia.width-tw,
274
 
#                                       ia.y+ia.height-th,
275
 
#                                       tw, th)
276
 
            self.image.queue_draw()
277
 
            return False
278
 
 
279
 
        self.image.queue_draw()
280
 
#        self.image.queue_draw_area(ia.x+ia.width-tw,
281
 
#                                   ia.y+ia.height-th,
282
 
#                                   tw, th)
283
 
        return True
284
 
 
285
 
    def _show_image_dialog(self):
286
 
        """ Displays the large screenshot in a seperate dialog window """
287
 
 
288
 
        url = self.large_url
289
 
        title = _("%s - Screenshot") % self.appname
290
 
        d = ShowImageDialog(
291
 
            title, url,
292
 
            self.distro.IMAGE_FULL_MISSING,
293
 
            os.path.join(self.loader.tmpdir, uri_to_filename(url)))
294
 
        d.run()
295
 
        d.destroy()
296
 
        return
297
 
 
298
 
    def _on_screenshot_load_error(self, loader, err_type, err_message):
299
 
        self.set_screenshot_available(False)
300
 
        self.ready = True
301
 
        return
302
 
 
303
 
    def _on_screenshot_query_complete(self, loader, reachable):
304
 
        self.set_screenshot_available(reachable)
305
 
        if not reachable: self.ready = True
306
 
        return
307
 
 
308
 
    def _downsize_pixbuf(self, pb, target_w, target_h):
309
 
        w = pb.get_width()
310
 
        h = pb.get_height()
311
 
 
312
 
        if w > h:
313
 
            sf = float(target_w) / w
314
 
        else:
315
 
            sf = float(target_h) / h
316
 
 
317
 
        sw = int(w*sf)
318
 
        sh = int(h*sf)
319
 
 
320
 
        return pb.scale_simple(sw, sh, gtk.gdk.INTERP_BILINEAR)
321
 
 
322
 
    def _on_screenshot_download_complete(self, loader, screenshot_path):
323
 
 
324
 
        def setter_cb(path):
325
 
            try:
326
 
                self.screenshot_pixbuf = gtk.gdk.pixbuf_new_from_file(path)
327
 
                #pb = gtk.gdk.pixbuf_new_from_file(path)
328
 
            except Exception, e:
329
 
                LOG.warn('Screenshot downloaded but the file could not be opened.', e)
330
 
                return False
331
 
 
332
 
            # remove the spinner
333
 
            if self.spinner_alignment.parent:
334
 
                self.spinner.stop()
335
 
                self.spinner.hide()
336
 
                self.remove(self.spinner_alignment)
337
 
 
338
 
            pb = self._downsize_pixbuf(self.screenshot_pixbuf, *self.MAX_SIZE)
339
 
 
340
 
            if not self.eventbox.parent:
341
 
                self.add(self.eventbox)
342
 
                if self.get_property("visible"):
343
 
                    self.show_all()
344
 
 
345
 
            self.image.set_size_request(-1, -1)
346
 
            self.image.set_from_pixbuf(pb)
347
 
 
348
 
            # queue parent redraw if height of new pb is less than idle height
349
 
            if pb.get_height() < self.IDLE_SIZE[1]:
350
 
                if self.parent:
351
 
                    self.parent.queue_draw()
352
 
 
353
 
            # start the fade in
354
 
            gobject.timeout_add(50, self._fade_in)
355
 
            self.ready = True
356
 
            return False
357
 
 
358
 
        gobject.idle_add(setter_cb, screenshot_path)
359
 
        return
360
 
 
361
 
    def show_tip(self, hide_after=0):
362
 
        if (not self.image.get_property('visible') or
363
 
            self.tip_alpha >= 1.0): 
364
 
            return
365
 
 
366
 
        if self._tip_fader: gobject.source_remove(self._tip_fader)
367
 
        self._tip_fader = gobject.timeout_add(25, self._tip_fade_in)
368
 
 
369
 
        if hide_after:
370
 
            if self._hide_after: 
371
 
                gobject.source_remove(self._hide_after)
372
 
            self._hide_after = gobject.timeout_add(hide_after, self.hide_tip)
373
 
        return
374
 
 
375
 
    def hide_tip(self):
376
 
        if (not self.image.get_property('visible') or
377
 
            self.tip_alpha <= 0.0):
378
 
            return
379
 
 
380
 
        if self._tip_fader: 
381
 
            gobject.source_remove(self._tip_fader)
382
 
        self._tip_fader = gobject.timeout_add(25, self._tip_fade_out)
383
 
        return
384
 
 
385
 
    def get_is_actionable(self):
386
 
        """ Returns true if there is a screenshot available and
387
 
            the download has completed 
388
 
        """
389
 
        return self.screenshot_available and self.ready
390
 
 
391
 
    def set_screenshot_available(self, available):
392
 
        """ Configures the ScreenshotView depending on whether there
393
 
            is a screenshot available.
394
 
        """
395
 
        if not available:
396
 
            if not self.eventbox.parent:
397
 
                self.remove(self.spinner_alignment)
398
 
                self.spinner.stop()
399
 
                self.add(self.eventbox)
400
 
 
401
 
            if self.image.parent:
402
 
                self.image.hide()
403
 
                self.eventbox.remove(self.image)
404
 
                self.eventbox.add(self.unavailable)
405
 
                # set the size of the unavailable placeholder
406
 
                # 160 pixels is the fixed width of the thumbnails
407
 
                self.unavailable.set_size_request(*self.IDLE_SIZE)
408
 
            acc = self.get_accessible()
409
 
            acc.set_name(_('No screenshot available'))
410
 
            acc.set_role(atk.ROLE_LABEL)
411
 
        else:
412
 
            if self.unavailable.parent:
413
 
                self.unavailable.hide()
414
 
                self.eventbox.remove(self.unavailable)
415
 
                self.eventbox.add(self.image)
416
 
            acc = self.get_accessible()
417
 
            acc.set_name(_('Screenshot'))
418
 
            acc.set_role(atk.ROLE_PUSH_BUTTON)
419
 
 
420
 
        if self.get_property("visible"):
421
 
            self.show_all()
422
 
        self.screenshot_available = available
423
 
        return
424
 
 
425
 
    def configure(self, app_details):
426
 
 
427
 
        """ Called to configure the screenshotview for a new application.
428
 
            The existing screenshot is cleared and the process of fetching a
429
 
            new screenshot is instigated.
430
 
        """
431
 
 
432
 
        acc = self.get_accessible()
433
 
        acc.set_name(_('Fetching screenshot ...'))
434
 
 
435
 
        self.clear()
436
 
        self.appname = app_details.display_name
437
 
        self.pkgname = app_details.pkgname
438
 
#        self.thumbnail_url = app_details.thumbnail
439
 
        self.thumbnail_url = app_details.screenshot
440
 
        self.large_url = app_details.screenshot
441
 
        return
442
 
 
443
 
    def clear(self):
444
 
 
445
 
        """ All state trackers are set to their intitial states, and
446
 
            the old screenshot is cleared from the view.
447
 
        """
448
 
 
449
 
        self.screenshot_available = True
450
 
        self.ready = False
451
 
        self.alpha = 0.0
452
 
 
453
 
        if self.eventbox.parent:
454
 
            self.eventbox.hide()
455
 
            self.remove(self.eventbox)
456
 
 
457
 
        if not self.spinner_alignment.parent:
458
 
            self.add(self.spinner_alignment)
459
 
 
460
 
        self.spinner_alignment.set_size_request(*self.IDLE_SIZE)
461
 
 
462
 
        self.spinner.start()
463
 
 
464
 
        return
465
 
 
466
 
    def download_and_display(self):
467
 
        """ Download then displays the screenshot.
468
 
            This actually does a query on the URL first to check if its 
469
 
            reachable, if so it downloads the thumbnail.
470
 
            If not, it emits "file-url-reachable" False, then exits.
471
 
        """
472
 
 
473
 
        self.loader.download_file(self.thumbnail_url)
474
 
        # show it
475
 
        if self.get_property('visible'):
476
 
            self.show_all()
477
 
 
478
 
        return
479
 
 
480
 
    def draw(self, cr, a, expose_area):
481
 
        """ Draws the thumbnail frame """
482
 
 
483
 
        if mkit.not_overlapping(a, expose_area): return
484
 
 
485
 
        if self.eventbox.get_property('visible'):
486
 
            ia = self.eventbox.allocation
487
 
        else:
488
 
            ia = self.spinner_alignment.allocation
489
 
 
490
 
        x = a.x + (a.width - ia.width)/2
491
 
        y = ia.y
492
 
 
493
 
        if self.has_focus() or self.state == gtk.STATE_ACTIVE:
494
 
            cr.rectangle(x-2, y-2, ia.width+4, ia.height+4)
495
 
            cr.set_source_rgb(1,1,1)
496
 
            cr.fill_preserve()
497
 
            if self.state == gtk.STATE_ACTIVE:
498
 
                color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
499
 
            else:
500
 
                color = mkit.floats_from_gdkcolor(self.style.dark[gtk.STATE_SELECTED])
501
 
            cr.set_source_rgb(*color)
502
 
            cr.stroke()
503
 
        else:
504
 
            cr.rectangle(x-3, y-3, ia.width+6, ia.height+6)
505
 
            cr.set_source_rgb(1,1,1)
506
 
            cr.fill()
507
 
            cr.save()
508
 
            cr.translate(0.5, 0.5)
509
 
            cr.set_line_width(1)
510
 
            cr.rectangle(x-3, y-3, ia.width+5, ia.height+5)
511
 
 
512
 
            dark = mkit.floats_from_gdkcolor(self.style.dark[self.state])
513
 
            cr.set_source_rgb(*dark)
514
 
            cr.stroke()
515
 
            cr.restore()
516
 
 
517
 
        if not self.screenshot_available:
518
 
            cr.rectangle(x, y, ia.width, ia.height)
519
 
            cr.set_source_rgb(*mkit.floats_from_gdkcolor(self.style.bg[self.state]))
520
 
            cr.fill()
521
 
        return
522
 
 
523
 
 
524
 
if __name__ == '__main__':
525
 
 
526
 
    def testing_expose_handler(thumb, event):
527
 
        cr = thumb.window.cairo_create()
528
 
        thumb.draw(cr, thumb.allocation, event.area)
529
 
        del cr
530
 
        return
531
 
 
532
 
    def testing_cycle_apps(thumb, apps, db):
533
 
 
534
 
        if not thumb.pkgname or thumb.pkgname == "uace":
535
 
            d = apps[0].get_details(db)
536
 
        else:
537
 
            d = apps[1].get_details(db)
538
 
 
539
 
        thumb.configure(d)
540
 
        thumb.download_and_display()
541
 
        return True
542
 
 
543
 
 
544
 
    import sys, logging
545
 
    logging.basicConfig(level=logging.DEBUG)
546
 
 
547
 
    if len(sys.argv) > 1:
548
 
        datadir = sys.argv[1]
549
 
    elif os.path.exists("./data"):
550
 
        datadir = "./data"
551
 
    else:
552
 
        datadir = "/usr/share/software-center"
553
 
 
554
 
    xapian_base_path = "/var/cache/software-center"
555
 
    pathname = os.path.join(xapian_base_path, "xapian")
556
 
    from softwarecenter.db.pkginfo import get_pkg_info
557
 
    cache = get_pkg_info()
558
 
    cache.open()
559
 
 
560
 
    from softwarecenter.db.database import StoreDatabase
561
 
    db = StoreDatabase(pathname, cache)
562
 
    db.open()
563
 
 
564
 
    icons = gtk.icon_theme_get_default()
565
 
    icons.append_search_path("/usr/share/app-install/icons/")
566
 
 
567
 
    import softwarecenter.distro
568
 
    distro = softwarecenter.distro.get_distro()
569
 
 
570
 
    t = ScreenshotThumbnail(distro, icons)
571
 
    t.connect('expose-event', testing_expose_handler)
572
 
 
573
 
    w = gtk.Window()
574
 
    w.set_border_width(10)
575
 
 
576
 
    vb = gtk.VBox(spacing=6)
577
 
    w.add(vb)
578
 
 
579
 
    vb.pack_start(gtk.Button('A button for focus testing'))
580
 
    vb.pack_start(t)
581
 
 
582
 
    w.show_all()
583
 
    w.connect('destroy', gtk.main_quit)
584
 
 
585
 
    from softwarecenter.db.application import Application
586
 
    apps = [Application("Movie Player", "totem"),
587
 
            Application("ACE", "uace")]
588
 
 
589
 
    testing_cycle_apps(t, apps, db)
590
 
 
591
 
    gobject.timeout_add(6000, testing_cycle_apps, t, apps, db)
592
 
 
593
 
    gtk.main()