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 _
7 import gtk
, gobject
, pango
10 from zeroinstall
import support
11 from zeroinstall
.gtkui
import icon
, xdgutils
12 from zeroinstall
.injector
import policy
, reader
, model
, namespaces
15 return s
.replace('&', '&').replace('<', '<')
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.
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."""
33 0: _("Run the application"),
34 1: _("Show documentation files"),
35 2: _("Upgrade or change versions"),
36 3: _("Remove launcher from the menu"),
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
):
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}
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
.set_translation_domain('zero-install')
56 builder
.add_from_file(builderfile
)
57 self
.window
= builder
.get_object('applist')
58 tv
= builder
.get_object('treeview')
60 self
.model
= gtk
.ListStore(gtk
.gdk
.Pixbuf
, str, str, str)
63 tv
.set_model(self
.model
)
64 tv
.get_selection().set_mode(gtk
.SELECTION_NONE
)
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
)
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
)
78 cell_actions
= ActionsRenderer(self
, tv
)
79 actions_column
= gtk
.TreeViewColumn('Actions', cell_actions
, uri
= AppListBox
.URI
)
80 tv
.append_column(actions_column
)
82 def redraw_actions(path
):
84 area
= tv
.get_cell_area(path
, actions_column
)
85 tv
.queue_draw_area(*area
)
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
)
92 new_hover
= (None, None, None)
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)
106 if new_hover
[2] is not None:
107 tooltip
.set_text(_tooltips
[cell_actions
.hover
[2]])
110 tv
.connect('query-tooltip', query_tooltip
)
112 def leave(widget
, lev
):
113 redraw_actions(cell_actions
.hover
[0])
114 cell_actions
.hover
= (None, None, None)
116 tv
.connect('leave-notify-event', leave
)
118 self
.model
.set_sort_column_id(AppListBox
.NAME
, gtk
.SORT_ASCENDING
)
120 show_cache
= builder
.get_object('show_cache')
121 self
.window
.action_area
.set_child_secondary(show_cache
, True)
123 def response(box
, resp
):
125 subprocess
.Popen(['0store', 'manage'])
128 self
.window
.connect('response', response
)
130 def populate_model(self
):
134 for uri
in self
.app_list
.get_apps():
136 m
[itr
][AppListBox
.URI
] = uri
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
, ex
:
147 summary
= unicode(ex
)
150 m
[itr
][AppListBox
.NAME
] = name
152 pixbuf
= self
.window
.render_icon(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_DIALOG
)
153 m
[itr
][AppListBox
.ICON
] = pixbuf
155 m
[itr
][AppListBox
.MARKUP
] = '<b>%s</b>\n<i>%s</i>' % (_pango_escape(name
), _pango_escape(summary
))
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 ['xterm', 'gnome-terminal', 'rxvt', 'konsole']:
162 exe
= support
.find_in_path(terminal
)
164 if terminal
== 'gnome-terminal':
168 subprocess
.Popen([terminal
, flag
, '0launch', '--', uri
])
171 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_ERROR
, gtk
.BUTTONS_OK
, _("Can't find a suitable terminal emulator"))
175 subprocess
.Popen(['0launch', '--', uri
])
177 def action_help(self
, uri
):
178 p
= policy
.Policy(uri
)
179 policy
.network_use
= model
.network_offline
180 if p
.need_download():
181 child
= subprocess
.Popen(['0launch', '-d', '--', uri
])
185 iface
= self
.iface_cache
.get_interface(uri
)
186 reader
.update_from_cache(iface
)
187 p
.solve_with_downloads()
188 impl
= p
.solver
.selections
[iface
]
189 assert impl
, "Failed to choose an implementation of %s" % uri
190 help_dir
= impl
.metadata
.get('doc-dir')
191 path
= p
.get_implementation_path(impl
)
192 assert path
, "Chosen implementation is not cached!"
194 path
= os
.path
.join(path
, help_dir
)
198 # Hack for ROX applications. They should be updated to
200 help_dir
= os
.path
.join(path
, os
.path
.dirname(main
), 'Help')
201 if os
.path
.isdir(help_dir
):
204 # xdg-open has no "safe" mode, so check we're not "opening" an application.
205 if os
.path
.exists(os
.path
.join(path
, 'AppRun')):
206 raise Exception(_("Documentation directory '%s' is an AppDir; refusing to open") % path
)
208 subprocess
.Popen(['xdg-open', path
])
210 def action_properties(self
, uri
):
211 subprocess
.Popen(['0launch', '--gui', '--', uri
])
213 def action_remove(self
, uri
):
215 name
= self
.iface_cache
.get_interface(uri
).get_name()
216 except model
.InvalidInterface
:
219 box
= gtk
.MessageDialog(self
.window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_QUESTION
, gtk
.BUTTONS_CANCEL
, "")
220 box
.set_markup(_("Remove <b>%s</b> from the menu?") % _pango_escape(name
))
221 box
.add_button(gtk
.STOCK_DELETE
, gtk
.RESPONSE_OK
)
222 box
.set_default_response(gtk
.RESPONSE_OK
)
225 if resp
== gtk
.RESPONSE_OK
:
227 self
.app_list
.remove_app(uri
)
228 except Exception, ex
:
229 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
})
232 self
.populate_model()
234 class ActionsRenderer(gtk
.GenericCellRenderer
):
236 "uri": (gobject
.TYPE_STRING
, "Text", "Text", "-", gobject
.PARAM_READWRITE
),
239 def __init__(self
, applist
, widget
):
240 "@param widget: widget used for style information"
241 gtk
.GenericCellRenderer
.__init
__(self
)
242 self
.set_property('mode', gtk
.CELL_RENDERER_MODE_ACTIVATABLE
)
245 self
.applist
= applist
248 def stock_lookup(name
):
249 pixbuf
= widget
.render_icon(name
, gtk
.ICON_SIZE_BUTTON
)
250 self
.size
= max(self
.size
, pixbuf
.get_width(), pixbuf
.get_height())
253 if hasattr(gtk
, 'STOCK_MEDIA_PLAY'):
254 self
.run
= stock_lookup(gtk
.STOCK_MEDIA_PLAY
)
256 self
.run
= stock_lookup(gtk
.STOCK_YES
)
257 self
.help = stock_lookup(gtk
.STOCK_HELP
)
258 self
.properties
= stock_lookup(gtk
.STOCK_PROPERTIES
)
259 self
.remove
= stock_lookup(gtk
.STOCK_DELETE
)
260 self
.hover
= (None, None, None) # Path, URI, action
262 def do_set_property(self
, prop
, value
):
263 setattr(self
, prop
.name
, value
)
265 def on_get_size(self
, widget
, cell_area
, layout
= None):
266 total_size
= self
.size
* 2 + self
.padding
* 4
267 return (0, 0, total_size
, total_size
)
269 def on_render(self
, window
, widget
, background_area
, cell_area
, expose_area
, flags
):
270 hovering
= self
.uri
== self
.hover
[1]
274 cx
= cell_area
.x
+ self
.padding
275 cy
= cell_area
.y
+ (cell_area
.height
/ 2) - s
- self
.padding
277 ss
= s
+ self
.padding
* 2
280 for (x
, y
), icon
in [((0, 0), self
.run
),
281 ((ss
, 0), self
.help),
282 ((0, ss
), self
.properties
),
283 ((ss
, ss
), self
.remove
)]:
284 if hovering
and b
== self
.hover
[2]:
285 widget
.style
.paint_box(window
, gtk
.STATE_NORMAL
, gtk
.SHADOW_OUT
,
286 expose_area
, widget
, None,
287 cx
+ x
- 2, cy
+ y
- 2, s
+ 4, s
+ 4)
289 window
.draw_pixbuf(widget
.style
.white_gc
, icon
,
294 def on_activate(self
, event
, widget
, path
, background_area
, cell_area
, flags
):
295 if event
.type != gtk
.gdk
.BUTTON_PRESS
:
297 action
= self
.get_action(cell_area
, event
.x
- cell_area
.x
, event
.y
- cell_area
.y
)
299 self
.applist
.action_run(self
.uri
)
301 self
.applist
.action_help(self
.uri
)
303 self
.applist
.action_properties(self
.uri
)
305 self
.applist
.action_remove(self
.uri
)
307 def get_action(self
, area
, x
, y
):
308 lower
= int(y
> (area
.height
/ 2)) * 2
310 s
= self
.size
+ self
.padding
* 2
313 return int(x
> s
) + lower