2
# Copyright 2009 Martin Owens
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program. If not, see <http://www.gnu.org/licenses/>
18
Wraps the gtk treeview in something a little nicer.
21
from GroundControl import GLADE_DIR, PIXMAP_DIR, __appname__
22
from GroundControl.base import Thread
32
class IconManager(object):
33
"""Manage a set of cached icons"""
34
def __init__(self, location):
35
self.location = os.path.join(PIXMAP_DIR, location)
37
self.get_icon('default')
39
def get_icon(self, name):
40
"""Simple method for getting a set of pix icons and caching them."""
43
if not self.cache.has_key(name):
44
icon_path = self.icon_path(name)
45
if os.path.exists(icon_path):
47
self.cache[name] = gtk.gdk.pixbuf_new_from_file(icon_path)
48
except glib.GError, msg:
49
logging.warn(_("No icon '%s',%s") % (icon_path, msg))
51
self.cache[name] = None
52
logging.warning(_("Can't find icon for %s in %s") % (
54
if not self.cache.has_key(name) or not self.cache[name]:
56
return self.cache.get(name, MISSING)
58
def icon_path(self, name):
59
"""Returns the icon path based on stored location"""
60
svg_path = os.path.join(self.location, '%s.svg' % name)
61
png_path = os.path.join(self.location, '%s.png' % name)
62
if os.path.exists(name) and os.path.isfile(name):
64
if os.path.exists(svg_path) and os.path.isfile(svg_path):
66
elif os.path.exists(png_path) and os.path.isfile(png_path):
68
return os.path.join(self.location, name)
73
This wraps gtk builder and allows for some extra functionality with
74
windows, especially the management of gtk main loops.
76
start_loop - If set to true will start a new gtk main loop.
77
*args **kwargs - Passed to primary window when loaded.
82
def __init__(self, *args, **kwargs):
83
self.main_loop = glib.main_depth()
84
start_loop = kwargs.pop('start_loop', False)
85
self.w_tree = gtk.Builder()
86
self.w_tree.set_translation_domain(__appname__)
87
self.w_tree.add_from_file(self.gapp_xml)
89
self.widget = self.w_tree.get_object
90
# Now start dishing out initalisation
91
self.init_gui(*args, **kwargs)
92
self.w_tree.connect_signals(self.signals)
94
# Start up a gtk main loop when requested
96
logging.debug("Starting new GTK Main Loop.")
101
"""Load any given gtk builder file from a standard location"""
102
path = os.path.join(GLADE_DIR, self.gtkfile)
103
if not os.path.exists(path):
104
raise Exception("Gtk Builder file is missing: %s" % path)
107
def init_gui(self, *args, **kwards):
108
"""Initalise all of our windows and load their signals"""
112
for cls in self.windows:
114
self._loaded[window.name] = window
116
if not self._primary:
118
window.load(*args, **kwards)
119
self._primary = window
121
logging.error(_("More than one window is set Primary!"))
123
def add_signal(self, name, function):
124
"""Add a signal to this gtk wtree object"""
125
if self.signals != None:
126
self.signals[name] = function
128
raise Exception("Unable to add signal '%s' - too late!" % name)
130
def load_window(self, name, *args, **kwargs):
131
"""Load a specific window from our group of windows"""
132
if self._loaded.has_key(name):
133
self._loaded[name].load(*args, **kwargs)
134
return self._loaded[name]
137
"""Exit our gtk application and kill gtk main if we have to"""
138
if self.main_loop < glib.main_depth():
139
# Quit Gtk loop if we started one.
140
logging.debug("Quit '%s' Main Loop." % self._primary.name)
142
# You have to return in order for the loop to exit
146
class Window(object):
148
This wraps gtk windows and allows for having parent windows as well
149
as callback events when done and optional quiting of the gtk stack.
151
name = 'name-of-the-window'
153
Should the window be the first loaded and control gtk loop:
160
def __init__(self, gapp):
162
# Set object defaults
168
# Setup the gtk app connection
169
self.widget = self.gapp.widget
170
# Setup the gtk builder window
171
self.window = self.widget(self.name)
173
raise Exception("Missing window '%s'" % (self.name))
174
# These are some generic convience signals
175
self.window.connect('destroy', self._exiting)
176
self.add_generic_signal('destroy', self.destroy_window)
177
self.add_generic_signal('close', self.destroy_window)
178
self.add_generic_signal('cancel', self.cancel_changes)
179
self.add_generic_signal('apply', self.apply_changes)
180
self.add_generic_signal('dblclk', self.double_click)
181
# Now load any custom signals
182
for name, value in self.signals().iteritems():
183
self.gapp.add_signal(name, value)
185
def load(self, parent=None, callback=None):
186
"""Show the window to the user and set callbacks"""
187
# If we have a parent window, then we expect not to quit
189
self.callback = callback
191
self.parent.set_sensitive(False)
195
"""Replace this with a method returning dict of signals"""
198
def add_generic_signal(self, name, function):
199
"""Adds a generic signal to the gapp"""
200
self.gapp.add_signal("%s_%s" % (self.name, name), function)
202
def load_window(self, name, *args, **kwargs):
203
"""Load child window, automatically sets parent"""
204
kwargs['parent'] = self.window
205
return self.gapp.load_window(name, *args, **kwargs)
208
"""Nothing to return"""
211
def double_click(self, widget, event):
212
"""This is the cope with gtk's rotten support for mouse events"""
213
if event.type == gtk.gdk._2BUTTON_PRESS:
214
return self.apply_changes(widget)
216
def apply_changes(self, widget=None):
217
"""Apply any changes as required by callbacks"""
218
valid = self.is_valid()
220
logging.debug("Applying changes")
222
self._args = self.get_args()
224
self.destroy_window()
226
self.is_valid_failed(valid)
228
def post_process(self):
229
"""Do anything internally that needs doing"""
233
"""Return true is all args are valid."""
236
def is_valid_failed(self, reason=None):
237
"""This is what happens when we're not valid"""
238
logging.error(_("Child arguments aren't valid: %s") % str(reason))
240
def cancel_changes(self, widget=None):
241
"""We didn't like what we did, so don't apply"""
243
self.destroy_window()
245
def destroy_window(self, widget=None):
246
"""We want to make sure that the window DOES quit"""
248
self.window.destroy()
251
self._exiting(widget)
253
def _exiting(self, widget=None):
254
"""Internal method for what to do when the window has died"""
255
if self.callback and self.done:
256
logging.debug("Calling callback function for window")
257
self.callback(**self._args)
260
logging.debug("Cancelled or no callback for window")
263
# We assume the parent didn't load another gtk loop
264
self.parent.set_sensitive(True)
265
# Exit our entire app if this is the primary window
269
self.sucess(**self._args)
272
def sucess(self, **kwargs):
273
"""A callback for when the gtk is finally gone and we're finished"""
277
class ChildWindow(Window):
279
Base class for child window objects, these child windows are typically
280
window objects in the same gtk builder file as their parents. If you just want
281
to make a window that interacts with a parent window, use the normal
282
Window class and call with the optional parent attribute.
287
anicons = IconManager('animation')
289
class ThreadedWindow(Window):
291
This class enables an extra status stream to cross call gtk
292
From a threaded process, allowing unfreezing of gtk apps.
294
def __init__(self, *args, **kwargs):
295
# Common variables for threading
298
self._calls = [] # Calls Stack
299
self._unique = {} # Unique calls stack
300
# Back to setting up a window
301
super(ThreadedWindow, self).__init__(*args, **kwargs)
303
def load(self, *args, **kwargs):
304
"""Load threads and kick off status"""
305
self._anistat = self.widget("astat")
307
# Kick off our initial thread
308
self.start_thread(self.inital_thread)
309
# Back to loading a window
310
super(ThreadedWindow, self).load(*args, **kwargs)
312
def start_thread(self, method, *args, **kwargs):
313
"""Kick off a thread and a status monitor timer"""
314
if not self._thread or not self._thread.isAlive() and not self._closed:
315
self._thread = Thread(target=method, args=args, kwargs=kwargs)
317
logging.debug("-- Poll Start %s --" % self.name)
318
# Show an animation to reflect the polling
321
# Kick off a polling service, after a delay
322
gobject.timeout_add( 300, self.thread_through )
324
raise Exception("Thread is already running!")
326
def thread_through(self):
327
"""Keep things up to date fromt he thread."""
329
if self._thread.isAlive():
330
#logging.debug("-- Poll Through %s --" % self.name)
331
# This will allow our poll to be visible to the user and
332
# Has the advantage of showing stuff is going on.
335
if self._anicount == 9:
337
image = anicons.get_icon(str(self._anicount))
338
self._anistat.set_from_pixbuf(image)
339
gobject.timeout_add( 100, self.thread_through )
341
logging.debug("-- Poll Quit %s --" % self.name)
342
# Hide the animation by default and exit the thread
347
def inital_thread(self):
348
"""Replace this method with your own app's setup thread"""
351
def thread_exited(self):
352
"""What is called when the thread exits"""
355
def call(self, name, *args, **kwargs):
356
"""Call a method outside of the thread."""
357
unique = kwargs.pop('unique_call', False)
358
if type(name) != str:
359
raise Exception("Call name must be a string not %s" % type(name))
360
call = (name, args, kwargs)
361
# Unique calls replace the previous calls to that method.
363
self._unique[name] = call
365
self._calls.append( call )
367
def process_calls(self):
368
"""Go through the calls stack and call them, return if required."""
370
(name, args, kwargs) = self._calls.pop(0)
371
logging.debug("Calling %s" % name)
372
ret = getattr(self, name)(*args, **kwargs)
373
for name in self._unique.keys():
374
(name, args, kwargs) = self._unique.pop(name)
375
ret = getattr(self, name)(*args, **kwargs)
377
def _exiting(self, widget=None):
378
if self._thread and self._thread.isAlive():
379
logging.warn(_("Your thread is still active."))
381
super(ThreadedWindow, self)._exiting(widget)
384
class BaseView(object):
385
"""Controls for tree and icon views, a base class"""
390
def __init__(self, dbobj, selected=None, unselected=None, name=None):
391
self.selected_signal = selected
392
self.unselected_signal = unselected
395
self.connect_signals()
397
super(BaseView, self).__init__()
399
def connect_signals(self):
400
"""Try and connect signals to and from the view control"""
401
raise NotImplementedError, "Signal connection should be elsewhere."
404
"""Setup any required aspects of the view"""
408
"""Clear all items from this treeview"""
409
for iter_index in range(0, len(self._model)):
413
logging.error(_("Could not delete item %d") % iter_index)
416
def add(self, target, parent=None):
417
"""Add all items from the target to the treeview"""
419
self.add_item(item, parent=parent)
421
def add_item(self, item, parent=None):
422
"""Add a single item image to the control"""
424
result = self._model.append(parent, [item])
425
# item.connect('update', self.updateItem)
428
raise Exception("Item can not be None.")
430
def replace(self, new_item, item_iter=None):
431
"""Replace all items, or a single item with object"""
433
self.remove_item(target_iter=item_iter)
434
self.add_item(new_item)
437
self._data = new_item
440
def item_selected(self, item=None):
441
"""Base method result, called as an item is selected"""
442
if self.selected != item:
444
if self.selected_signal and item:
445
self.selected_signal(item)
446
elif self.unselected_signal and not item:
447
self.unselected_signal(item)
449
def remove_item(self, item=None, target_iter=None):
450
"""Remove an item from this view"""
451
if target_iter and not item:
452
return self._model.remove(target_iter)
453
target_iter = self._model.get_iter_first()
454
for itemc in self._model:
456
return self._model.remove(target_iter)
457
target_iter = self._model.iter_next(target_iter)
459
def item_double_clicked(self, items):
460
"""What happens when you double click an item"""
461
return items # Nothing
463
def get_item(self, iter):
464
"""Return the object of attention from an iter"""
465
return self._model[iter][0]
468
class TreeView(BaseView):
469
"""Controls and operates a tree view."""
470
def connect_signals(self):
471
"""Attach the change cursor signal to the item selected"""
472
self._list.connect('cursor_changed', self.item_selected_signal)
474
def selected_items(self, treeview):
475
"""Return a list of selected item objects"""
476
# This may need more thought, only returns one item
477
item_iter = treeview.get_selection().get_selected()[1]
479
return [ self.get_item(item_iter) ]
480
except TypeError, msg:
481
logging.debug("Error %s" % msg)
483
def item_selected_signal(self, treeview):
484
"""Signal for selecting an item"""
485
items = self.selected_items(treeview)
487
return self.item_selected( items[0] )
489
def item_button_clicked(self, treeview, event):
490
"""Signal for mouse button click"""
491
if event.type == gtk.gdk.BUTTON_PRESS:
492
self.item_double_clicked( self.selected_items(treeview)[0] )
494
def expand_item(self, item):
495
"""Expand one of our nodes"""
496
self._list.expand_row(self._model.get_path(item), True)
499
"""Set up an icon view for showing gallery images"""
500
self._model = gtk.TreeStore(gobject.TYPE_PYOBJECT)
501
self._list.set_model(self._model)
505
class IconView(BaseView):
506
"""Allows a simpler IconView for DBus List Objects"""
507
def connect_signals(self):
508
"""Connect the selection changed signal up"""
509
self._list.connect('selection-changed', self.item_selected_signal)
512
"""Setup the icon view control view"""
513
self._model = gtk.ListStore(str, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT)
514
self._list.set_model(self._model)
517
def item_selected_signal(self, icon_view):
518
"""Item has been selected"""
519
self.selected = icon_view.get_selected_items()