10
from softwarecenter.utils import SimpleFileDownloader, uri_to_filename
12
from imagedialog import ShowImageDialog
14
from gettext import gettext as _
16
LOG = logging.getLogger(__name__)
18
class ScreenshotThumbnail(gtk.Alignment):
20
""" Widget that displays screenshot availability, download prrogress,
21
and eventually the screenshot itself.
28
def __init__(self, distro, icons):
29
gtk.Alignment.__init__(self, 0.5, 0.0)
42
self.screenshot_pixbuf = None
43
self.screenshot_available = False
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(),
54
self._hide_after = None
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"))
61
self._tip_layout.set_ellipsize(pango.ELLIPSIZE_END)
63
self._tip_xpadding = 4
64
self._tip_ypadding = 1
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)
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)
80
self.set_redraw_on_allocate(False)
81
# the frame around the screenshot (placeholder)
82
self.set_border_width(3)
84
# eventbox so we can connect to event signals
85
event = gtk.EventBox()
86
event.set_visible_window(False)
88
self.spinner_alignment = gtk.Alignment(0.5, 0.5, yscale=0)
90
self.spinner = gtk.Spinner()
91
self.spinner.set_size_request(*self.SPINNER_SIZE)
92
self.spinner_alignment.add(self.spinner)
95
self.image = gtk.Image()
96
self.image.set_redraw_on_allocate(False)
100
# connect the image to our custom draw func for fading in
101
self.image.connect('expose-event', self._on_image_expose)
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)
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)
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)
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)
132
def _on_enter(self, widget, event):
133
if not self.get_is_actionable(): return
135
self.window.set_cursor(self._zoom_cursor)
136
self.show_tip(hide_after=3000)
139
def _on_leave(self, widget, event):
140
self.window.set_cursor(None)
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)
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()
155
def _on_focus_in(self, widget, event):
156
self.show_tip(hide_after=3000)
159
# def _on_focus_out(self, widget, event):
162
def _on_key_press(self, widget, event):
163
# react to spacebar, enter, numpad-enter
164
if event.keyval in (gtk.keysyms.space,
166
gtk.keysyms.KP_Enter) and self.get_is_actionable():
167
self.set_state(gtk.STATE_ACTIVE)
170
def _on_key_release(self, widget, event):
171
# react to spacebar, enter, numpad-enter
172
if event.keyval in (gtk.keysyms.space,
174
gtk.keysyms.KP_Enter) and self.get_is_actionable():
175
self.set_state(gtk.STATE_NORMAL)
176
self._show_image_dialog()
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.
184
if widget.get_storage_type() != gtk.IMAGE_PIXBUF:
187
pb = widget.get_pixbuf()
188
if not pb: return True
190
a = widget.allocation
191
cr = widget.window.cairo_create()
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)
200
if not self.tip_alpha: return True
202
tw, th = self._tip_size
204
self._tip_layout.set_width(-1)
208
self._tip_layout.set_width(1024*(tw-2*self._tip_xpadding))
210
tx, ty = a.x+a.width-tw, a.y+a.height-th
212
rr = mkit.ShapeRoundedRectangleIrregular()
213
rr.layout(cr, tx, ty, tx+tw, ty+th, radii=(6, 0, 0, 0))
215
cr.set_source_rgba(0,0,0,0.85*self.tip_alpha)
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)
227
""" This callback increments the alpha value from zero to 1,
228
stopping once 1 is reached or exceeded.
232
if self.alpha >= 1.0:
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.
244
self.tip_alpha += 0.1
245
# ia = self.image.allocation
246
tw, th = self._tip_size
248
if self.tip_alpha >= 1.0:
250
self.image.queue_draw()
251
# self.image.queue_draw_area(ia.x+ia.width-tw,
256
self.image.queue_draw()
257
# self.image.queue_draw_area(ia.x+ia.width-tw,
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.
267
self.tip_alpha -= 0.1
268
# ia = self.image.allocation
269
tw, th = self._tip_size
271
if self.tip_alpha <= 0.0:
273
# self.image.queue_draw_area(ia.x+ia.width-tw,
276
self.image.queue_draw()
279
self.image.queue_draw()
280
# self.image.queue_draw_area(ia.x+ia.width-tw,
285
def _show_image_dialog(self):
286
""" Displays the large screenshot in a seperate dialog window """
289
title = _("%s - Screenshot") % self.appname
292
self.distro.IMAGE_FULL_MISSING,
293
os.path.join(self.loader.tmpdir, uri_to_filename(url)))
298
def _on_screenshot_load_error(self, loader, err_type, err_message):
299
self.set_screenshot_available(False)
303
def _on_screenshot_query_complete(self, loader, reachable):
304
self.set_screenshot_available(reachable)
305
if not reachable: self.ready = True
308
def _downsize_pixbuf(self, pb, target_w, target_h):
313
sf = float(target_w) / w
315
sf = float(target_h) / h
320
return pb.scale_simple(sw, sh, gtk.gdk.INTERP_BILINEAR)
322
def _on_screenshot_download_complete(self, loader, screenshot_path):
326
self.screenshot_pixbuf = gtk.gdk.pixbuf_new_from_file(path)
327
#pb = gtk.gdk.pixbuf_new_from_file(path)
329
LOG.warn('Screenshot downloaded but the file could not be opened.', e)
333
if self.spinner_alignment.parent:
336
self.remove(self.spinner_alignment)
338
pb = self._downsize_pixbuf(self.screenshot_pixbuf, *self.MAX_SIZE)
340
if not self.eventbox.parent:
341
self.add(self.eventbox)
342
if self.get_property("visible"):
345
self.image.set_size_request(-1, -1)
346
self.image.set_from_pixbuf(pb)
348
# queue parent redraw if height of new pb is less than idle height
349
if pb.get_height() < self.IDLE_SIZE[1]:
351
self.parent.queue_draw()
354
gobject.timeout_add(50, self._fade_in)
358
gobject.idle_add(setter_cb, screenshot_path)
361
def show_tip(self, hide_after=0):
362
if (not self.image.get_property('visible') or
363
self.tip_alpha >= 1.0):
366
if self._tip_fader: gobject.source_remove(self._tip_fader)
367
self._tip_fader = gobject.timeout_add(25, self._tip_fade_in)
371
gobject.source_remove(self._hide_after)
372
self._hide_after = gobject.timeout_add(hide_after, self.hide_tip)
376
if (not self.image.get_property('visible') or
377
self.tip_alpha <= 0.0):
381
gobject.source_remove(self._tip_fader)
382
self._tip_fader = gobject.timeout_add(25, self._tip_fade_out)
385
def get_is_actionable(self):
386
""" Returns true if there is a screenshot available and
387
the download has completed
389
return self.screenshot_available and self.ready
391
def set_screenshot_available(self, available):
392
""" Configures the ScreenshotView depending on whether there
393
is a screenshot available.
396
if not self.eventbox.parent:
397
self.remove(self.spinner_alignment)
399
self.add(self.eventbox)
401
if self.image.parent:
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)
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)
420
if self.get_property("visible"):
422
self.screenshot_available = available
425
def configure(self, app_details):
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.
432
acc = self.get_accessible()
433
acc.set_name(_('Fetching screenshot ...'))
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
445
""" All state trackers are set to their intitial states, and
446
the old screenshot is cleared from the view.
449
self.screenshot_available = True
453
if self.eventbox.parent:
455
self.remove(self.eventbox)
457
if not self.spinner_alignment.parent:
458
self.add(self.spinner_alignment)
460
self.spinner_alignment.set_size_request(*self.IDLE_SIZE)
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.
473
self.loader.download_file(self.thumbnail_url)
475
if self.get_property('visible'):
480
def draw(self, cr, a, expose_area):
481
""" Draws the thumbnail frame """
483
if mkit.not_overlapping(a, expose_area): return
485
if self.eventbox.get_property('visible'):
486
ia = self.eventbox.allocation
488
ia = self.spinner_alignment.allocation
490
x = a.x + (a.width - ia.width)/2
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)
497
if self.state == gtk.STATE_ACTIVE:
498
color = mkit.floats_from_gdkcolor(self.style.mid[self.state])
500
color = mkit.floats_from_gdkcolor(self.style.dark[gtk.STATE_SELECTED])
501
cr.set_source_rgb(*color)
504
cr.rectangle(x-3, y-3, ia.width+6, ia.height+6)
505
cr.set_source_rgb(1,1,1)
508
cr.translate(0.5, 0.5)
510
cr.rectangle(x-3, y-3, ia.width+5, ia.height+5)
512
dark = mkit.floats_from_gdkcolor(self.style.dark[self.state])
513
cr.set_source_rgb(*dark)
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]))
524
if __name__ == '__main__':
526
def testing_expose_handler(thumb, event):
527
cr = thumb.window.cairo_create()
528
thumb.draw(cr, thumb.allocation, event.area)
532
def testing_cycle_apps(thumb, apps, db):
534
if not thumb.pkgname or thumb.pkgname == "uace":
535
d = apps[0].get_details(db)
537
d = apps[1].get_details(db)
540
thumb.download_and_display()
545
logging.basicConfig(level=logging.DEBUG)
547
if len(sys.argv) > 1:
548
datadir = sys.argv[1]
549
elif os.path.exists("./data"):
552
datadir = "/usr/share/software-center"
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()
560
from softwarecenter.db.database import StoreDatabase
561
db = StoreDatabase(pathname, cache)
564
icons = gtk.icon_theme_get_default()
565
icons.append_search_path("/usr/share/app-install/icons/")
567
import softwarecenter.distro
568
distro = softwarecenter.distro.get_distro()
570
t = ScreenshotThumbnail(distro, icons)
571
t.connect('expose-event', testing_expose_handler)
574
w.set_border_width(10)
576
vb = gtk.VBox(spacing=6)
579
vb.pack_start(gtk.Button('A button for focus testing'))
583
w.connect('destroy', gtk.main_quit)
585
from softwarecenter.db.application import Application
586
apps = [Application("Movie Player", "totem"),
587
Application("ACE", "uace")]
589
testing_cycle_apps(t, apps, db)
591
gobject.timeout_add(6000, testing_cycle_apps, t, apps, db)