Use a default icon if the app doesn't have one
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / gtkui / applistbox.py
blob631371a2eb071de12971c3268e0e1c85aca961c8
1 """A GTK dialog which displays a list of Zero Install applications in the menu."""
2 # Copyright (C) 2008, Thomas Leonard
3 # See the README file for details, or visit http://0install.net.
5 import os
6 import gtk, gobject
7 import gtk.glade
8 import subprocess
10 from zeroinstall.gtkui import icon, xdgutils
12 def _pango_escape(s):
13 return s.replace('&', '&amp;').replace('<', '&lt;')
15 class AppListBox:
16 """A dialog box which lists applications already added to the menus."""
17 ICON, URI, NAME, MARKUP = range(4)
19 def __init__(self, iface_cache):
20 gladefile = os.path.join(os.path.dirname(__file__), 'desktop.glade')
21 self.iface_cache = iface_cache
23 widgets = gtk.glade.XML(gladefile, 'applist')
24 self.window = widgets.get_widget('applist')
25 tv = widgets.get_widget('treeview')
27 self.model = gtk.ListStore(gtk.gdk.Pixbuf, str, str, str)
29 self.populate_model()
30 tv.set_model(self.model)
31 tv.get_selection().set_mode(gtk.SELECTION_NONE)
33 cell_icon = gtk.CellRendererPixbuf()
34 cell_icon.set_property('xpad', 4)
35 cell_icon.set_property('ypad', 4)
36 column = gtk.TreeViewColumn('Icon', cell_icon, pixbuf = AppListBox.ICON)
37 tv.append_column(column)
39 cell_text = gtk.CellRendererText()
40 column = gtk.TreeViewColumn('Name', cell_text, markup = AppListBox.MARKUP)
41 tv.append_column(column)
43 cell_actions = ActionsRenderer(self, tv)
44 actions_column = gtk.TreeViewColumn('Actions', cell_actions, uri = AppListBox.URI)
45 tv.append_column(actions_column)
47 def redraw_actions(path):
48 if path is not None:
49 area = tv.get_cell_area(path, actions_column)
50 tv.queue_draw_area(*area)
52 def motion(widget, mev):
53 if mev.window == tv.get_bin_window():
54 new_hover = (None, None, None)
55 pos = tv.get_path_at_pos(int(mev.x), int(mev.y))
56 if pos:
57 path, col, x, y = pos
58 if col == actions_column:
59 area = tv.get_cell_area(path, col)
60 iface = self.model[path][AppListBox.URI]
61 action = cell_actions.get_action(area, x, y)
62 if action is not None:
63 new_hover = (path, iface, action)
64 if new_hover != cell_actions.hover:
65 redraw_actions(cell_actions.hover[0])
66 cell_actions.hover = new_hover
67 redraw_actions(cell_actions.hover[0])
68 tv.connect('motion-notify-event', motion)
70 def leave(widget, lev):
71 redraw_actions(cell_actions.hover[0])
72 cell_actions.hover = (None, None, None)
74 tv.connect('leave-notify-event', leave)
76 self.model.set_sort_column_id(AppListBox.NAME, gtk.SORT_ASCENDING)
78 def response(box, resp):
79 box.destroy()
80 self.window.connect('response', response)
82 def populate_model(self):
83 self.apps = xdgutils.discover_existing_apps()
84 model = self.model
85 model.clear()
87 for uri in self.apps:
88 itr = model.append()
89 model[itr][AppListBox.URI] = uri
91 iface = self.iface_cache.get_interface(uri)
92 name = iface.get_name()
93 summary = iface.summary or 'No information available'
94 summary = summary[:1].capitalize() + summary[1:]
96 model[itr][AppListBox.NAME] = name
97 pixbuf = icon.load_icon(self.iface_cache.get_icon_path(iface))
98 if not pixbuf:
99 pixbuf = self.window.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_DIALOG)
100 model[itr][AppListBox.ICON] = pixbuf
102 model[itr][AppListBox.MARKUP] = '<b>%s</b>\n<i>%s</i>' % (_pango_escape(name), _pango_escape(summary))
104 def action_run(self, uri):
105 subprocess.Popen(['0launch', '--', uri])
107 def action_help(self, uri):
108 from zeroinstall.injector import policy, reader, model
109 p = policy.Policy(uri)
110 policy.network_use = model.network_offline
111 if p.need_download():
112 child = subprocess.Popen(['0launch', '-d', '--', uri])
113 child.wait()
114 if child.returncode:
115 return
116 iface = self.iface_cache.get_interface(uri)
117 reader.update_from_cache(iface)
118 p.solve_with_downloads()
119 impl = p.solver.selections[iface]
120 assert impl, "Failed to choose an implementation of " + uri
121 help_dir = impl.metadata.get('doc-dir')
122 path = p.get_implementation_path(impl)
123 assert path, "Chosen implementation is not cached!"
124 if help_dir:
125 path = os.path.join(path, help_dir)
126 subprocess.Popen(['xdg-open', path])
128 def action_properties(self, uri):
129 subprocess.Popen(['0launch', '--gui', '--', uri])
131 def action_remove(self, uri):
132 name = self.iface_cache.get_interface(uri).get_name()
134 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, "")
135 box.set_markup("Remove <b>%s</b> from the menu?" % _pango_escape(name))
136 box.add_button(gtk.STOCK_DELETE, gtk.RESPONSE_OK)
137 box.set_default_response(gtk.RESPONSE_OK)
138 resp = box.run()
139 box.destroy()
140 if resp == gtk.RESPONSE_OK:
141 path = self.apps[uri]
142 try:
143 os.unlink(path)
144 except Exception, ex:
145 box = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, "Failed to remove %s: %s" % (path, ex))
146 box.run()
147 box.destroy()
148 self.populate_model()
150 class ActionsRenderer(gtk.GenericCellRenderer):
151 __gproperties__ = {
152 "uri": (gobject.TYPE_STRING, "Text", "Text", "-", gobject.PARAM_READWRITE),
155 def __init__(self, applist, widget):
156 "@param widget: widget used for style information"
157 gtk.GenericCellRenderer.__init__(self)
158 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
159 self.padding = 4
161 self.applist = applist
163 self.size = 10
164 def stock_lookup(name):
165 pixbuf = widget.render_icon(name, gtk.ICON_SIZE_BUTTON)
166 self.size = max(self.size, pixbuf.get_width(), pixbuf.get_height())
167 return pixbuf
169 if hasattr(gtk, 'STOCK_MEDIA_PLAY'):
170 self.run = stock_lookup(gtk.STOCK_MEDIA_PLAY)
171 else:
172 self.run = stock_lookup(gtk.STOCK_YES)
173 self.help = stock_lookup(gtk.STOCK_HELP)
174 self.properties = stock_lookup(gtk.STOCK_PROPERTIES)
175 self.remove = stock_lookup(gtk.STOCK_DELETE)
176 self.hover = (None, None, None) # Path, URI, action
178 def do_set_property(self, prop, value):
179 setattr(self, prop.name, value)
181 def on_get_size(self, widget, cell_area, layout = None):
182 total_size = self.size * 2 + self.padding * 4
183 return (0, 0, total_size, total_size)
185 def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
186 hovering = self.uri == self.hover[1]
188 s = self.size
190 cx = cell_area.x + self.padding
191 cy = cell_area.y + (cell_area.height / 2) - s - self.padding
193 ss = s + self.padding * 2
195 b = 0
196 for (x, y), icon in [((0, 0), self.run),
197 ((ss, 0), self.help),
198 ((0, ss), self.properties),
199 ((ss, ss), self.remove)]:
200 if hovering and b == self.hover[2]:
201 widget.style.paint_box(window, gtk.STATE_NORMAL, gtk.SHADOW_OUT,
202 expose_area, widget, None,
203 cx + x - 2, cy + y - 2, s + 4, s + 4)
205 window.draw_pixbuf(widget.style.white_gc, icon,
206 0, 0, # Source x,y
207 cx + x, cy + y)
208 b += 1
210 def on_activate(self, event, widget, path, background_area, cell_area, flags):
211 if event.type != gtk.gdk.BUTTON_PRESS:
212 return False
213 action = self.get_action(cell_area, event.x - cell_area.x, event.y - cell_area.y)
214 if action == 0:
215 self.applist.action_run(self.uri)
216 elif action == 1:
217 self.applist.action_help(self.uri)
218 elif action == 2:
219 self.applist.action_properties(self.uri)
220 elif action == 3:
221 self.applist.action_remove(self.uri)
223 def get_action(self, area, x, y):
224 lower = int(y > (area.height / 2)) * 2
226 s = self.size + self.padding * 2
227 if x > s * 2:
228 return None
229 return int(x > s) + lower
231 if gtk.pygtk_version < (2, 8, 0):
232 # Note sure exactly which versions need this.
233 # 2.8.0 gives a warning if you include it, though.
234 gobject.type_register(ActionsRenderer)