Package zeroinstall :: Package gtkui :: Module applistbox
[frames] | no frames]

Source Code for Module zeroinstall.gtkui.applistbox

  1  """A GTK dialog which displays a list of Zero Install applications in the menu.""" 
  2  # Copyright (C) 2009, Thomas Leonard 
  3  # See the README file for details, or visit http://0install.net. 
  4   
  5  from zeroinstall import _, gobject 
  6  import os, sys 
  7  import gtk, pango 
  8  import subprocess 
  9   
 10  from zeroinstall import support 
 11  from zeroinstall.gtkui import icon, xdgutils 
 12  from zeroinstall.injector import reader, model, namespaces 
 13   
 14  gtk2 = sys.version_info[0] < 3 
 15   
16 -def _pango_escape(s):
17 return s.replace('&', '&amp;').replace('<', '&lt;')
18
19 -class AppList(object):
20 """A list of applications which can be displayed in an L{AppListBox}. 21 For example, a program might implement this to display a list of plugins. 22 This default implementation lists applications in the freedesktop.org menus. 23 """
24 - def get_apps(self):
25 """Return a list of application URIs.""" 26 self.apps = xdgutils.discover_existing_apps() 27 return self.apps.keys()
28
29 - def remove_app(self, uri):
30 """Remove this application from the list.""" 31 path = self.apps[uri] 32 os.unlink(path)
33 34 _tooltips = { 35 0: _("Run the application"), 36 1: _("Show documentation files"), 37 2: _("Upgrade or change versions"), 38 3: _("Remove launcher from the menu"), 39 } 40
41 -class AppListBox(object):
42 """A dialog box which lists applications already added to the menus.""" 43 ICON, URI, NAME, MARKUP = range(4) 44
45 - def __init__(self, iface_cache, app_list):
46 """Constructor. 47 @param iface_cache: used to find extra information about programs 48 @type iface_cache: L{zeroinstall.injector.iface_cache.IfaceCache} 49 @param app_list: used to list or remove applications 50 @type app_list: L{AppList} 51 """ 52 builderfile = os.path.join(os.path.dirname(__file__), 'desktop.ui') 53 self.iface_cache = iface_cache 54 self.app_list = app_list 55 56 builder = gtk.Builder() 57 builder.set_translation_domain('zero-install') 58 builder.add_from_file(builderfile) 59 self.window = builder.get_object('applist') 60 tv = builder.get_object('treeview') 61 62 self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str) 63 64 self.populate_model() 65 tv.set_model(self.model) 66 tv.get_selection().set_mode(gtk.SELECTION_NONE) 67 68 cell_icon = gtk.CellRendererPixbuf() 69 cell_icon.set_property('xpad', 4) 70 cell_icon.set_property('ypad', 4) 71 column = gtk.TreeViewColumn('Icon', cell_icon, pixbuf = AppListBox.ICON) 72 tv.append_column(column) 73 74 cell_text = gtk.CellRendererText() 75 cell_text.set_property('ellipsize', pango.ELLIPSIZE_END) 76 column = gtk.TreeViewColumn('Name', cell_text, markup = AppListBox.MARKUP) 77 column.set_expand(True) 78 tv.append_column(column) 79 80 cell_actions = ActionsRenderer(self, tv) 81 actions_column = gtk.TreeViewColumn('Actions', cell_actions, uri = AppListBox.URI) 82 tv.append_column(actions_column) 83 84 def redraw_actions(path): 85 if path is not None: 86 area = tv.get_cell_area(path, actions_column) 87 if gtk2: 88 tv.queue_draw_area(*area) 89 else: 90 tv.queue_draw_area(area.x, area.y, area.width, area.height)
91 92 tv.set_property('has-tooltip', True) 93 def query_tooltip(widget, x, y, keyboard_mode, tooltip): 94 x, y = tv.convert_widget_to_bin_window_coords(x, y) 95 pos = tv.get_path_at_pos(x, y) 96 if pos: 97 new_hover = (None, None, None) 98 path, col, x, y = pos 99 if col == actions_column: 100 area = tv.get_cell_area(path, col) 101 iface = self.model[path][AppListBox.URI] 102 action = cell_actions.get_action(area, x, y) 103 if action is not None: 104 new_hover = (path, iface, action) 105 if new_hover != cell_actions.hover: 106 redraw_actions(cell_actions.hover[0]) 107 cell_actions.hover = new_hover 108 redraw_actions(cell_actions.hover[0]) 109 tv.set_tooltip_cell(tooltip, pos[0], pos[1], None) 110 111 if new_hover[2] is not None: 112 tooltip.set_text(_tooltips[cell_actions.hover[2]]) 113 return True 114 return False
115 tv.connect('query-tooltip', query_tooltip) 116 117 def leave(widget, lev): 118 redraw_actions(cell_actions.hover[0]) 119 cell_actions.hover = (None, None, None) 120 121 tv.connect('leave-notify-event', leave) 122 123 self.model.set_sort_column_id(AppListBox.NAME, gtk.SORT_ASCENDING) 124 125 show_cache = builder.get_object('show_cache') 126 self.window.action_area.set_child_secondary(show_cache, True) 127 128 def response(box, resp): 129 if resp == 0: # Show Cache 130 subprocess.Popen(['0store', 'manage']) 131 elif resp == 1: # Add 132 from zeroinstall.gtkui.addbox import AddBox 133 box = AddBox() 134 box.window.connect('destroy', lambda dialog: self.populate_model()) 135 box.window.show() 136 else: 137 box.destroy() 138 self.window.connect('response', response) 139
140 - def populate_model(self):
141 m = self.model 142 m.clear() 143 144 for uri in self.app_list.get_apps(): 145 itr = m.append() 146 m[itr][AppListBox.URI] = uri 147 148 try: 149 iface = self.iface_cache.get_interface(uri) 150 feed = self.iface_cache.get_feed(uri) 151 if feed: 152 name = feed.get_name() 153 summary = feed.summary or _('No information available') 154 summary = summary[:1].capitalize() + summary[1:] 155 else: 156 name = iface.get_name() 157 summary = _('No information available') 158 # (GTK3 returns an extra boolean at the start) 159 icon_width, icon_height = gtk.icon_size_lookup(gtk.ICON_SIZE_DIALOG)[-2:] 160 pixbuf = icon.load_icon(self.iface_cache.get_icon_path(iface), icon_width, icon_height) 161 except model.InvalidInterface as ex: 162 name = uri 163 summary = unicode(ex) 164 pixbuf = None 165 166 m[itr][AppListBox.NAME] = name 167 if pixbuf is None: 168 pixbuf = self.window.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_DIALOG) 169 m[itr][AppListBox.ICON] = pixbuf 170 171 m[itr][AppListBox.MARKUP] = '<b>%s</b>\n<i>%s</i>' % (_pango_escape(name), _pango_escape(summary))
172
173 - def action_run(self, uri):
174 iface = self.iface_cache.get_interface(uri) 175 reader.update_from_cache(iface) 176 if len(iface.get_metadata(namespaces.XMLNS_IFACE, 'needs-terminal')): 177 if gtk.pygtk_version >= (2,16,0) and gtk.gdk.WINDOWING == 'quartz': 178 script = ['0launch', '--', uri] 179 osascript = support.find_in_path('osascript') 180 subprocess.Popen([osascript, '-e', 'tell app "Terminal"', '-e', 'activate', 181 '-e', 'do script "%s"' % ' '.join(script), '-e', 'end tell']) 182 return 183 for terminal in ['x-terminal-emulator', 'xterm', 'gnome-terminal', 'rxvt', 'konsole']: 184 exe = support.find_in_path(terminal) 185 if exe: 186 if terminal == 'gnome-terminal': 187 flag = '-x' 188 else: 189 flag = '-e' 190 subprocess.Popen([terminal, flag, '0launch', '--', uri]) 191 break 192 else: 193 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("Can't find a suitable terminal emulator")) 194 box.run() 195 box.destroy() 196 else: 197 subprocess.Popen(['0launch', '--', uri])
198
199 - def action_help(self, uri):
200 from zeroinstall.injector.config import load_config 201 from zeroinstall import helpers 202 c = load_config() 203 sels = helpers.ensure_cached(uri, config = c) 204 if not sels: 205 return 206 207 impl = sels.selections[uri] 208 assert impl, "Failed to choose an implementation of %s" % uri 209 help_dir = impl.attrs.get('doc-dir') 210 211 if impl.id.startswith('package:'): 212 assert os.path.isabs(help_dir), "Package doc-dir must be absolute!" 213 path = help_dir 214 else: 215 path = impl.local_path or c.stores.lookup_any(impl.digests) 216 217 assert path, "Chosen implementation is not cached!" 218 if help_dir: 219 path = os.path.join(path, help_dir) 220 else: 221 main = impl.main 222 if main: 223 # Hack for ROX applications. They should be updated to 224 # set doc-dir. 225 help_dir = os.path.join(path, os.path.dirname(main), 'Help') 226 if os.path.isdir(help_dir): 227 path = help_dir 228 229 # xdg-open has no "safe" mode, so check we're not "opening" an application. 230 if os.path.exists(os.path.join(path, 'AppRun')): 231 raise Exception(_("Documentation directory '%s' is an AppDir; refusing to open") % path) 232 233 subprocess.Popen(['xdg-open', path])
234
235 - def action_properties(self, uri):
236 subprocess.Popen(['0launch', '--gui', '--', uri])
237
238 - def action_remove(self, uri):
239 try: 240 name = self.iface_cache.get_interface(uri).get_name() 241 except model.InvalidInterface: 242 name = uri 243 244 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, "") 245 box.set_markup(_("Remove <b>%s</b> from the menu?") % _pango_escape(name)) 246 box.add_button(gtk.STOCK_DELETE, gtk.RESPONSE_OK) 247 box.set_default_response(gtk.RESPONSE_OK) 248 resp = box.run() 249 box.destroy() 250 if resp == gtk.RESPONSE_OK: 251 try: 252 self.app_list.remove_app(uri) 253 except Exception as ex: 254 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("Failed to remove %(interface_name)s: %(exception)s") % {'interface_name': name, 'exception': ex}) 255 box.run() 256 box.destroy() 257 self.populate_model()
258
259 -class ActionsRenderer(gtk.GenericCellRenderer):
260 __gproperties__ = { 261 "uri": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE), 262 } 263
264 - def __init__(self, applist, widget):
265 "@param widget: widget used for style information" 266 gtk.GenericCellRenderer.__init__(self) 267 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE) 268 self.padding = 4 269 270 self.applist = applist 271 272 self.size = 10 273 def stock_lookup(name): 274 pixbuf = widget.render_icon(name, gtk.ICON_SIZE_BUTTON) 275 self.size = max(self.size, pixbuf.get_width(), pixbuf.get_height()) 276 return pixbuf
277 278 if hasattr(gtk, 'STOCK_MEDIA_PLAY'): 279 self.run = stock_lookup(gtk.STOCK_MEDIA_PLAY) 280 else: 281 self.run = stock_lookup(gtk.STOCK_YES) 282 self.help = stock_lookup(gtk.STOCK_HELP) 283 self.properties = stock_lookup(gtk.STOCK_PROPERTIES) 284 self.remove = stock_lookup(gtk.STOCK_DELETE) 285 self.hover = (None, None, None) # Path, URI, action
286
287 - def do_set_property(self, prop, value):
288 setattr(self, prop.name, value)
289
290 - def do_get_size(self, widget, cell_area, layout = None):
291 total_size = self.size * 2 + self.padding * 4 292 return (0, 0, total_size, total_size)
293 on_get_size = do_get_size # GTK 2 294
295 - def render(self, cr, widget, background_area, cell_area, flags, expose_area = None):
296 hovering = self.uri == self.hover[1] 297 298 s = self.size 299 300 cx = cell_area.x + self.padding 301 cy = cell_area.y + (cell_area.height / 2) - s - self.padding 302 303 ss = s + self.padding * 2 304 305 b = 0 306 for (x, y), icon in [((0, 0), self.run), 307 ((ss, 0), self.help), 308 ((0, ss), self.properties), 309 ((ss, ss), self.remove)]: 310 if gtk2: 311 if hovering and b == self.hover[2]: 312 widget.style.paint_box(cr, gtk.STATE_NORMAL, gtk.SHADOW_OUT, 313 expose_area, widget, None, 314 cx + x - 2, cy + y - 2, s + 4, s + 4) 315 316 cr.draw_pixbuf(widget.style.white_gc, icon, 317 0, 0, # Source x,y 318 cx + x, cy + y) 319 else: 320 if hovering and b == self.hover[2]: 321 gtk.render_focus(widget.get_style_context(), cr, 322 cx + x - 2, cy + y - 2, s + 4, s + 4) 323 gtk.gdk.cairo_set_source_pixbuf(cr, icon, cx + x, cy + y) 324 cr.paint() 325 b += 1
326 327 if gtk2:
328 - def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
329 self.render(window, widget, background_area, cell_area, flags, expose_area = expose_area)
330 331 else: 332 do_render = render 333
334 - def on_activate(self, event, widget, path, background_area, cell_area, flags):
335 if event.type != gtk.gdk.BUTTON_PRESS: 336 return False 337 action = self.get_action(cell_area, event.x - cell_area.x, event.y - cell_area.y) 338 if action == 0: 339 self.applist.action_run(self.uri) 340 elif action == 1: 341 self.applist.action_help(self.uri) 342 elif action == 2: 343 self.applist.action_properties(self.uri) 344 elif action == 3: 345 self.applist.action_remove(self.uri)
346 do_activate = on_activate 347
348 - def get_action(self, area, x, y):
349 lower = int(y > (area.height / 2)) * 2 350 351 s = self.size + self.padding * 2 352 if x > s * 2: 353 return None 354 return int(x > s) + lower
355