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