Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / gtkui / applistbox.py
blob6c1bfd19635f5167cf36dc8ca1971ce1f8b66646
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 import os
6 import gtk, gobject, pango
7 import gtk.glade
8 import subprocess
10 from zeroinstall.gtkui import icon, xdgutils, treetips
12 def _pango_escape(s):
13 return s.replace('&', '&amp;').replace('<', '&lt;')
15 class AppList:
16 """A list of applications which can be displayed in an L{AppListBox}.
17 For example, a program might implement this to display a list of plugins.
18 This default implementation lists applications in the freedesktop.org menus.
19 """
20 def get_apps(self):
21 """Return a list of application URIs."""
22 self.apps = xdgutils.discover_existing_apps()
23 return self.apps.keys()
25 def remove_app(self, uri):
26 """Remove this application from the list."""
27 path = self.apps[uri]
28 os.unlink(path)
30 _tooltips = {
31 0: "Run the application",
32 1: "Show documentation files",
33 2: "Upgrade or change versions",
34 3: "Remove launcher from the menu",
37 class AppListBox:
38 """A dialog box which lists applications already added to the menus."""
39 ICON, URI, NAME, MARKUP = range(4)
41 def __init__(self, iface_cache, app_list):
42 """Constructor.
43 @param iface_cache: used to find extra information about programs
44 @type iface_cache: L{zeroinstall.injector.iface_cache.IfaceCache}
45 @param app_list: used to list or remove applications
46 @type app_list: L{AppList}
47 """
48 gladefile = os.path.join(os.path.dirname(__file__), 'desktop.glade')
49 self.iface_cache = iface_cache
50 self.app_list = app_list
52 widgets = gtk.glade.XML(gladefile, 'applist')
53 self.window = widgets.get_widget('applist')
54 tv = widgets.get_widget('treeview')
56 self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str)
58 self.populate_model()
59 tv.set_model(self.model)
60 tv.get_selection().set_mode(gtk.SELECTION_NONE)
62 cell_icon = gtk.CellRendererPixbuf()
63 cell_icon.set_property('xpad', 4)
64 cell_icon.set_property('ypad', 4)
65 column = gtk.TreeViewColumn('Icon', cell_icon, pixbuf = AppListBox.ICON)
66 tv.append_column(column)
68 cell_text = gtk.CellRendererText()
69 cell_text.set_property('ellipsize', pango.ELLIPSIZE_END)
70 column = gtk.TreeViewColumn('Name', cell_text, markup = AppListBox.MARKUP)
71 column.set_expand(True)
72 tv.append_column(column)
74 cell_actions = ActionsRenderer(self, tv)
75 actions_column = gtk.TreeViewColumn('Actions', cell_actions, uri = AppListBox.URI)
76 tv.append_column(actions_column)
78 def redraw_actions(path):
79 if path is not None:
80 area = tv.get_cell_area(path, actions_column)
81 tv.queue_draw_area(*area)
83 tips = treetips.TreeTips()
84 def motion(widget, mev):
85 if mev.window == tv.get_bin_window():
86 new_hover = (None, None, None)
87 pos = tv.get_path_at_pos(int(mev.x), int(mev.y))
88 if pos:
89 path, col, x, y = pos
90 if col == actions_column:
91 area = tv.get_cell_area(path, col)
92 iface = self.model[path][AppListBox.URI]
93 action = cell_actions.get_action(area, x, y)
94 if action is not None:
95 new_hover = (path, iface, action)
96 if new_hover != cell_actions.hover:
97 redraw_actions(cell_actions.hover[0])
98 cell_actions.hover = new_hover
99 redraw_actions(cell_actions.hover[0])
100 tips.prime(tv, _tooltips.get(cell_actions.hover[2], None))
101 tv.connect('motion-notify-event', motion)
103 def leave(widget, lev):
104 redraw_actions(cell_actions.hover[0])
105 cell_actions.hover = (None, None, None)
106 tips.hide()
108 tv.connect('leave-notify-event', leave)
110 self.model.set_sort_column_id(AppListBox.NAME, gtk.SORT_ASCENDING)
112 show_cache = widgets.get_widget('show_cache')
113 self.window.action_area.set_child_secondary(show_cache, True)
115 def response(box, resp):
116 if resp == 0:
117 subprocess.Popen(['0store', 'manage'])
118 else:
119 box.destroy()
120 self.window.connect('response', response)
122 def populate_model(self):
123 model = self.model
124 model.clear()
126 default_icon = self.window.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_DIALOG)
128 for uri in self.app_list.get_apps():
129 itr = model.append()
130 model[itr][AppListBox.URI] = uri
132 iface = self.iface_cache.get_interface(uri)
133 name = iface.get_name()
134 summary = iface.summary or 'No information available'
135 summary = summary[:1].capitalize() + summary[1:]
137 model[itr][AppListBox.NAME] = name
138 pixbuf = icon.load_icon(self.iface_cache.get_icon_path(iface))
139 if not pixbuf:
140 pixbuf = default_icon
141 else:
142 # Cap icon size, some icons are really high resolution
143 pixbuf = self.cap_pixbuf_dimensions(pixbuf, default_icon.get_width())
144 model[itr][AppListBox.ICON] = pixbuf
146 model[itr][AppListBox.MARKUP] = '<b>%s</b>\n<i>%s</i>' % (_pango_escape(name), _pango_escape(summary))
148 def cap_pixbuf_dimensions(self, pixbuf, iconsize):
149 pixbuf_w = pixbuf.get_width()
150 pixbuf_h = pixbuf.get_height()
151 if (pixbuf_w > iconsize) or (pixbuf_h > iconsize):
152 if (pixbuf_w > pixbuf_h):
153 newheight = (pixbuf_w/pixbuf_h) * iconsize
154 newwidth = iconsize
155 else:
156 newwidth = (pixbuf_h/pixbuf_w) * iconsize
157 newheight = iconsize
158 return pixbuf.scale_simple(newwidth, newheight, gtk.gdk.INTERP_BILINEAR)
159 return pixbuf
161 def action_run(self, uri):
162 subprocess.Popen(['0launch', '--', uri])
164 def action_help(self, uri):
165 from zeroinstall.injector import policy, reader, model
166 p = policy.Policy(uri)
167 policy.network_use = model.network_offline
168 if p.need_download():
169 child = subprocess.Popen(['0launch', '-d', '--', uri])
170 child.wait()
171 if child.returncode:
172 return
173 iface = self.iface_cache.get_interface(uri)
174 reader.update_from_cache(iface)
175 p.solve_with_downloads()
176 impl = p.solver.selections[iface]
177 assert impl, "Failed to choose an implementation of " + uri
178 help_dir = impl.metadata.get('doc-dir')
179 path = p.get_implementation_path(impl)
180 assert path, "Chosen implementation is not cached!"
181 if help_dir:
182 path = os.path.join(path, help_dir)
183 else:
184 main = impl.main
185 if main:
186 # Hack for ROX applications. They should be updated to
187 # set doc-dir.
188 help_dir = os.path.join(path, os.path.dirname(main), 'Help')
189 if os.path.isdir(help_dir):
190 path = help_dir
192 # xdg-open has no "safe" mode, so check we're not "opening" an application.
193 if os.path.exists(os.path.join(path, 'AppRun')):
194 raise Exception("Documentation directory '%s' is an AppDir; refusing to open" % path)
196 subprocess.Popen(['xdg-open', path])
198 def action_properties(self, uri):
199 subprocess.Popen(['0launch', '--gui', '--', uri])
201 def action_remove(self, uri):
202 name = self.iface_cache.get_interface(uri).get_name()
204 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, "")
205 box.set_markup("Remove <b>%s</b> from the menu?" % _pango_escape(name))
206 box.add_button(gtk.STOCK_DELETE, gtk.RESPONSE_OK)
207 box.set_default_response(gtk.RESPONSE_OK)
208 resp = box.run()
209 box.destroy()
210 if resp == gtk.RESPONSE_OK:
211 try:
212 self.app_list.remove_app(uri)
213 except Exception, ex:
214 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, "Failed to remove %s: %s" % (name, ex))
215 box.run()
216 box.destroy()
217 self.populate_model()
219 class ActionsRenderer(gtk.GenericCellRenderer):
220 __gproperties__ = {
221 "uri": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
224 def __init__(self, applist, widget):
225 "@param widget: widget used for style information"
226 gtk.GenericCellRenderer.__init__(self)
227 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
228 self.padding = 4
230 self.applist = applist
232 self.size = 10
233 def stock_lookup(name):
234 pixbuf = widget.render_icon(name, gtk.ICON_SIZE_BUTTON)
235 self.size = max(self.size, pixbuf.get_width(), pixbuf.get_height())
236 return pixbuf
238 if hasattr(gtk, 'STOCK_MEDIA_PLAY'):
239 self.run = stock_lookup(gtk.STOCK_MEDIA_PLAY)
240 else:
241 self.run = stock_lookup(gtk.STOCK_YES)
242 self.help = stock_lookup(gtk.STOCK_HELP)
243 self.properties = stock_lookup(gtk.STOCK_PROPERTIES)
244 self.remove = stock_lookup(gtk.STOCK_DELETE)
245 self.hover = (None, None, None) # Path, URI, action
247 def do_set_property(self, prop, value):
248 setattr(self, prop.name, value)
250 def on_get_size(self, widget, cell_area, layout = None):
251 total_size = self.size * 2 + self.padding * 4
252 return (0, 0, total_size, total_size)
254 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
255 hovering = self.uri == self.hover[1]
257 s = self.size
259 cx = cell_area.x + self.padding
260 cy = cell_area.y + (cell_area.height / 2) - s - self.padding
262 ss = s + self.padding * 2
264 b = 0
265 for (x, y), icon in [((0, 0), self.run),
266 ((ss, 0), self.help),
267 ((0, ss), self.properties),
268 ((ss, ss), self.remove)]:
269 if hovering and b == self.hover[2]:
270 widget.style.paint_box(window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
271 expose_area, widget, None,
272 cx + x - 2, cy + y - 2, s + 4, s + 4)
274 window.draw_pixbuf(widget.style.white_gc, icon,
275 0, 0, # Source x,y
276 cx + x, cy + y)
277 b += 1
279 def on_activate(self, event, widget, path, background_area, cell_area, flags):
280 if event.type != gtk.gdk.BUTTON_PRESS:
281 return False
282 action = self.get_action(cell_area, event.x - cell_area.x, event.y - cell_area.y)
283 if action == 0:
284 self.applist.action_run(self.uri)
285 elif action == 1:
286 self.applist.action_help(self.uri)
287 elif action == 2:
288 self.applist.action_properties(self.uri)
289 elif action == 3:
290 self.applist.action_remove(self.uri)
292 def get_action(self, area, x, y):
293 lower = int(y > (area.height / 2)) * 2
295 s = self.size + self.padding * 2
296 if x > s * 2:
297 return None
298 return int(x > s) + lower
300 if gtk.pygtk_version < (2, 8, 0):
301 # Note sure exactly which versions need this.
302 # 2.8.0 gives a warning if you include it, though.
303 gobject.type_register(ActionsRenderer)