0desktop sets the Terminal flag correctly
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / gtkui / applistbox.py
blob86bdb3d10f5bb65784a6dcf2b4ff78123e9d971b
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.
5 from zeroinstall import _
6 import os
7 import gtk, gobject, pango
8 import subprocess
10 from zeroinstall import support
11 from zeroinstall.gtkui import icon, xdgutils, treetips
12 from zeroinstall.injector import policy, reader, model, namespaces
14 def _pango_escape(s):
15 return s.replace('&', '&amp;').replace('<', '&lt;')
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()
27 def remove_app(self, uri):
28 """Remove this application from the list."""
29 path = self.apps[uri]
30 os.unlink(path)
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"),
39 class AppListBox:
40 """A dialog box which lists applications already added to the menus."""
41 ICON, URI, NAME, MARKUP = range(4)
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
54 builder = gtk.Builder()
55 builder.add_from_file(builderfile)
56 self.window = builder.get_object('applist')
57 tv = builder.get_object('treeview')
59 self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str)
61 self.populate_model()
62 tv.set_model(self.model)
63 tv.get_selection().set_mode(gtk.SELECTION_NONE)
65 cell_icon = gtk.CellRendererPixbuf()
66 cell_icon.set_property('xpad', 4)
67 cell_icon.set_property('ypad', 4)
68 column = gtk.TreeViewColumn('Icon', cell_icon, pixbuf = AppListBox.ICON)
69 tv.append_column(column)
71 cell_text = gtk.CellRendererText()
72 cell_text.set_property('ellipsize', pango.ELLIPSIZE_END)
73 column = gtk.TreeViewColumn('Name', cell_text, markup = AppListBox.MARKUP)
74 column.set_expand(True)
75 tv.append_column(column)
77 cell_actions = ActionsRenderer(self, tv)
78 actions_column = gtk.TreeViewColumn('Actions', cell_actions, uri = AppListBox.URI)
79 tv.append_column(actions_column)
81 def redraw_actions(path):
82 if path is not None:
83 area = tv.get_cell_area(path, actions_column)
84 tv.queue_draw_area(*area)
86 tips = treetips.TreeTips()
87 def motion(widget, mev):
88 if mev.window == tv.get_bin_window():
89 new_hover = (None, None, None)
90 pos = tv.get_path_at_pos(int(mev.x), int(mev.y))
91 if pos:
92 path, col, x, y = pos
93 if col == actions_column:
94 area = tv.get_cell_area(path, col)
95 iface = self.model[path][AppListBox.URI]
96 action = cell_actions.get_action(area, x, y)
97 if action is not None:
98 new_hover = (path, iface, action)
99 if new_hover != cell_actions.hover:
100 redraw_actions(cell_actions.hover[0])
101 cell_actions.hover = new_hover
102 redraw_actions(cell_actions.hover[0])
103 tips.prime(tv, _tooltips.get(cell_actions.hover[2], None))
104 tv.connect('motion-notify-event', motion)
106 def leave(widget, lev):
107 redraw_actions(cell_actions.hover[0])
108 cell_actions.hover = (None, None, None)
109 tips.hide()
111 tv.connect('leave-notify-event', leave)
113 self.model.set_sort_column_id(AppListBox.NAME, gtk.SORT_ASCENDING)
115 show_cache = builder.get_object('show_cache')
116 self.window.action_area.set_child_secondary(show_cache, True)
118 def response(box, resp):
119 if resp == 0:
120 subprocess.Popen(['0store', 'manage'])
121 else:
122 box.destroy()
123 self.window.connect('response', response)
125 def populate_model(self):
126 model = self.model
127 model.clear()
129 default_icon = self.window.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_DIALOG)
131 for uri in self.app_list.get_apps():
132 itr = model.append()
133 model[itr][AppListBox.URI] = uri
135 iface = self.iface_cache.get_interface(uri)
136 name = iface.get_name()
137 summary = iface.summary or _('No information available')
138 summary = summary[:1].capitalize() + summary[1:]
140 model[itr][AppListBox.NAME] = name
141 pixbuf = icon.load_icon(self.iface_cache.get_icon_path(iface))
142 if not pixbuf:
143 pixbuf = default_icon
144 else:
145 # Cap icon size, some icons are really high resolution
146 pixbuf = self.cap_pixbuf_dimensions(pixbuf, default_icon.get_width())
147 model[itr][AppListBox.ICON] = pixbuf
149 model[itr][AppListBox.MARKUP] = '<b>%s</b>\n<i>%s</i>' % (_pango_escape(name), _pango_escape(summary))
151 def cap_pixbuf_dimensions(self, pixbuf, iconsize):
152 pixbuf_w = pixbuf.get_width()
153 pixbuf_h = pixbuf.get_height()
154 if (pixbuf_w > iconsize) or (pixbuf_h > iconsize):
155 if (pixbuf_w > pixbuf_h):
156 newheight = (pixbuf_w/pixbuf_h) * iconsize
157 newwidth = iconsize
158 else:
159 newwidth = (pixbuf_h/pixbuf_w) * iconsize
160 newheight = iconsize
161 return pixbuf.scale_simple(newwidth, newheight, gtk.gdk.INTERP_BILINEAR)
162 return pixbuf
164 def action_run(self, uri):
165 iface = self.iface_cache.get_interface(uri)
166 reader.update_from_cache(iface)
167 if len(iface.get_metadata(namespaces.XMLNS_IFACE, 'needs-terminal')):
168 for terminal in ['xterm', 'gnome-terminal', 'rxvt', 'konsole']:
169 exe = support.find_in_path(terminal)
170 if exe:
171 if terminal == 'gnome-terminal':
172 flag = '-x'
173 else:
174 flag = '-e'
175 subprocess.Popen([terminal, flag, '0launch', '--', uri])
176 break
177 else:
178 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, _("Can't find a suitable terminal emulator"))
179 box.run()
180 box.destroy()
181 else:
182 subprocess.Popen(['0launch', '--', uri])
184 def action_help(self, uri):
185 p = policy.Policy(uri)
186 policy.network_use = model.network_offline
187 if p.need_download():
188 child = subprocess.Popen(['0launch', '-d', '--', uri])
189 child.wait()
190 if child.returncode:
191 return
192 iface = self.iface_cache.get_interface(uri)
193 reader.update_from_cache(iface)
194 p.solve_with_downloads()
195 impl = p.solver.selections[iface]
196 assert impl, "Failed to choose an implementation of %s" % uri
197 help_dir = impl.metadata.get('doc-dir')
198 path = p.get_implementation_path(impl)
199 assert path, "Chosen implementation is not cached!"
200 if help_dir:
201 path = os.path.join(path, help_dir)
202 else:
203 main = impl.main
204 if main:
205 # Hack for ROX applications. They should be updated to
206 # set doc-dir.
207 help_dir = os.path.join(path, os.path.dirname(main), 'Help')
208 if os.path.isdir(help_dir):
209 path = help_dir
211 # xdg-open has no "safe" mode, so check we're not "opening" an application.
212 if os.path.exists(os.path.join(path, 'AppRun')):
213 raise Exception(_("Documentation directory '%s' is an AppDir; refusing to open") % path)
215 subprocess.Popen(['xdg-open', path])
217 def action_properties(self, uri):
218 subprocess.Popen(['0launch', '--gui', '--', uri])
220 def action_remove(self, uri):
221 name = self.iface_cache.get_interface(uri).get_name()
223 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, "")
224 box.set_markup(_("Remove <b>%s</b> from the menu?") % _pango_escape(name))
225 box.add_button(gtk.STOCK_DELETE, gtk.RESPONSE_OK)
226 box.set_default_response(gtk.RESPONSE_OK)
227 resp = box.run()
228 box.destroy()
229 if resp == gtk.RESPONSE_OK:
230 try:
231 self.app_list.remove_app(uri)
232 except Exception, ex:
233 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})
234 box.run()
235 box.destroy()
236 self.populate_model()
238 class ActionsRenderer(gtk.GenericCellRenderer):
239 __gproperties__ = {
240 "uri": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
243 def __init__(self, applist, widget):
244 "@param widget: widget used for style information"
245 gtk.GenericCellRenderer.__init__(self)
246 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
247 self.padding = 4
249 self.applist = applist
251 self.size = 10
252 def stock_lookup(name):
253 pixbuf = widget.render_icon(name, gtk.ICON_SIZE_BUTTON)
254 self.size = max(self.size, pixbuf.get_width(), pixbuf.get_height())
255 return pixbuf
257 if hasattr(gtk, 'STOCK_MEDIA_PLAY'):
258 self.run = stock_lookup(gtk.STOCK_MEDIA_PLAY)
259 else:
260 self.run = stock_lookup(gtk.STOCK_YES)
261 self.help = stock_lookup(gtk.STOCK_HELP)
262 self.properties = stock_lookup(gtk.STOCK_PROPERTIES)
263 self.remove = stock_lookup(gtk.STOCK_DELETE)
264 self.hover = (None, None, None) # Path, URI, action
266 def do_set_property(self, prop, value):
267 setattr(self, prop.name, value)
269 def on_get_size(self, widget, cell_area, layout = None):
270 total_size = self.size * 2 + self.padding * 4
271 return (0, 0, total_size, total_size)
273 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
274 hovering = self.uri == self.hover[1]
276 s = self.size
278 cx = cell_area.x + self.padding
279 cy = cell_area.y + (cell_area.height / 2) - s - self.padding
281 ss = s + self.padding * 2
283 b = 0
284 for (x, y), icon in [((0, 0), self.run),
285 ((ss, 0), self.help),
286 ((0, ss), self.properties),
287 ((ss, ss), self.remove)]:
288 if hovering and b == self.hover[2]:
289 widget.style.paint_box(window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
290 expose_area, widget, None,
291 cx + x - 2, cy + y - 2, s + 4, s + 4)
293 window.draw_pixbuf(widget.style.white_gc, icon,
294 0, 0, # Source x,y
295 cx + x, cy + y)
296 b += 1
298 def on_activate(self, event, widget, path, background_area, cell_area, flags):
299 if event.type != gtk.gdk.BUTTON_PRESS:
300 return False
301 action = self.get_action(cell_area, event.x - cell_area.x, event.y - cell_area.y)
302 if action == 0:
303 self.applist.action_run(self.uri)
304 elif action == 1:
305 self.applist.action_help(self.uri)
306 elif action == 2:
307 self.applist.action_properties(self.uri)
308 elif action == 3:
309 self.applist.action_remove(self.uri)
311 def get_action(self, area, x, y):
312 lower = int(y > (area.height / 2)) * 2
314 s = self.size + self.padding * 2
315 if x > s * 2:
316 return None
317 return int(x > s) + lower
319 if gtk.pygtk_version < (2, 8, 0):
320 # Note sure exactly which versions need this.
321 # 2.8.0 gives a warning if you include it, though.
322 gobject.type_register(ActionsRenderer)