~ubuntu-branches/debian/sid/groundcontrol/sid

« back to all changes in this revision

Viewing changes to GroundControl/gtkviews.py

  • Committer: Bazaar Package Importer
  • Author(s): Luke Faraone
  • Date: 2010-02-07 18:26:54 UTC
  • Revision ID: james.westby@ubuntu.com-20100207182654-u0n26lkazgfog4et
Tags: upstream-1.5
ImportĀ upstreamĀ versionĀ 1.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Copyright 2009 Martin Owens
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>
 
16
#
 
17
"""
 
18
Wraps the gtk treeview in something a little nicer.
 
19
"""
 
20
 
 
21
from GroundControl import GLADE_DIR, PIXMAP_DIR, __appname__
 
22
from GroundControl.base import Thread
 
23
 
 
24
import os
 
25
import gtk
 
26
import gobject
 
27
import glib
 
28
import logging
 
29
 
 
30
MISSING = None
 
31
 
 
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)
 
36
        self.cache    = {}
 
37
        self.get_icon('default')
 
38
 
 
39
    def get_icon(self, name):
 
40
        """Simple method for getting a set of pix icons and caching them."""
 
41
        if not name:
 
42
            name = 'default'
 
43
        if not self.cache.has_key(name):
 
44
            icon_path = self.icon_path(name)
 
45
            if os.path.exists(icon_path):
 
46
                try:
 
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))
 
50
            else:
 
51
                self.cache[name] = None
 
52
                logging.warning(_("Can't find icon for %s in %s") % (
 
53
                    name, self.location))
 
54
        if not self.cache.has_key(name) or not self.cache[name]:
 
55
            name = 'default'
 
56
        return self.cache.get(name, MISSING)
 
57
 
 
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):
 
63
            return name
 
64
        if os.path.exists(svg_path) and os.path.isfile(svg_path):
 
65
            return svg_path
 
66
        elif os.path.exists(png_path) and os.path.isfile(png_path):
 
67
            return png_path
 
68
        return os.path.join(self.location, name)
 
69
 
 
70
 
 
71
class GtkApp(object):
 
72
    """
 
73
    This wraps gtk builder and allows for some extra functionality with
 
74
    windows, especially the management of gtk main loops.
 
75
 
 
76
    start_loop - If set to true will start a new gtk main loop.
 
77
    *args **kwargs - Passed to primary window when loaded.
 
78
    """
 
79
    gtkfile = None
 
80
    windows = None
 
81
 
 
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)
 
88
        self.signals = {}
 
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)
 
93
        self.signals = None
 
94
        # Start up a gtk main loop when requested
 
95
        if start_loop:
 
96
            logging.debug("Starting new GTK Main Loop.")
 
97
            gtk.main()
 
98
 
 
99
    @property
 
100
    def gapp_xml(self):
 
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)
 
105
        return path
 
106
 
 
107
    def init_gui(self, *args, **kwards):
 
108
        """Initalise all of our windows and load their signals"""
 
109
        self._loaded = {}
 
110
        self._primary = None
 
111
        if self.windows:
 
112
            for cls in self.windows:
 
113
                window = cls(self)
 
114
                self._loaded[window.name] = window
 
115
                if window.primary:
 
116
                    if not self._primary:
 
117
                        
 
118
                        window.load(*args, **kwards)
 
119
                        self._primary = window
 
120
                    else:
 
121
                        logging.error(_("More than one window is set Primary!"))
 
122
 
 
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
 
127
        else:
 
128
            raise Exception("Unable to add signal '%s' - too late!" % name)
 
129
 
 
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]
 
135
 
 
136
    def exit(self):
 
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)
 
141
            gtk.main_quit()
 
142
            # You have to return in order for the loop to exit
 
143
            return 0
 
144
 
 
145
 
 
146
class Window(object):
 
147
    """
 
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.
 
150
 
 
151
    name = 'name-of-the-window'
 
152
 
 
153
    Should the window be the first loaded and control gtk loop:
 
154
 
 
155
    primary = True
 
156
    """
 
157
    name    = None
 
158
    primary = True
 
159
 
 
160
    def __init__(self, gapp):
 
161
        self.gapp = gapp
 
162
        # Set object defaults
 
163
        self.dead     = False
 
164
        self.done     = False
 
165
        self.parent   = None
 
166
        self.callback = None
 
167
        self._args    = {}
 
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)
 
172
        if not self.window:
 
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)
 
184
 
 
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
 
188
        self.parent = parent
 
189
        self.callback = callback
 
190
        if self.parent:
 
191
            self.parent.set_sensitive(False)
 
192
        self.window.show()
 
193
 
 
194
    def signals(self):
 
195
        """Replace this with a method returning dict of signals"""
 
196
        return {}
 
197
 
 
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)
 
201
 
 
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)
 
206
 
 
207
    def get_args(self):
 
208
        """Nothing to return"""
 
209
        return {}
 
210
 
 
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)
 
215
 
 
216
    def apply_changes(self, widget=None):
 
217
        """Apply any changes as required by callbacks"""
 
218
        valid = self.is_valid()
 
219
        if valid == True:
 
220
            logging.debug("Applying changes")
 
221
            self.done = True
 
222
            self._args = self.get_args()
 
223
            self.post_process()
 
224
            self.destroy_window()
 
225
        else:
 
226
            self.is_valid_failed(valid)
 
227
 
 
228
    def post_process(self):
 
229
        """Do anything internally that needs doing"""
 
230
        pass
 
231
 
 
232
    def is_valid(self):
 
233
        """Return true is all args are valid."""
 
234
        return True
 
235
 
 
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))
 
239
 
 
240
    def cancel_changes(self, widget=None):
 
241
        """We didn't like what we did, so don't apply"""
 
242
        self.done = False
 
243
        self.destroy_window()
 
244
 
 
245
    def destroy_window(self, widget=None):
 
246
        """We want to make sure that the window DOES quit"""
 
247
        if self.primary:
 
248
            self.window.destroy()
 
249
        else:
 
250
            self.window.hide()
 
251
            self._exiting(widget)
 
252
 
 
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)
 
258
            self.done = False
 
259
        else:
 
260
            logging.debug("Cancelled or no callback for window")
 
261
 
 
262
        if self.parent:
 
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
 
266
        if self.primary:
 
267
            self.gapp.exit()
 
268
            if self.done:
 
269
                self.sucess(**self._args)
 
270
        self.dead = True
 
271
 
 
272
    def sucess(self, **kwargs):
 
273
        """A callback for when the gtk is finally gone and we're finished"""
 
274
        pass
 
275
 
 
276
 
 
277
class ChildWindow(Window):
 
278
    """
 
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.
 
283
    """
 
284
    primary = False
 
285
 
 
286
 
 
287
anicons = IconManager('animation')
 
288
 
 
289
class ThreadedWindow(Window):
 
290
    """
 
291
    This class enables an extra status stream to cross call gtk
 
292
    From a threaded process, allowing unfreezing of gtk apps.
 
293
    """
 
294
    def __init__(self, *args, **kwargs):
 
295
        # Common variables for threading
 
296
        self._thread = None
 
297
        self._closed = False
 
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)
 
302
 
 
303
    def load(self, *args, **kwargs):
 
304
        """Load threads and kick off status"""
 
305
        self._anistat = self.widget("astat")
 
306
        self._anicount = 1
 
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)
 
311
 
 
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)
 
316
            self._thread.start()
 
317
            logging.debug("-- Poll Start %s --" % self.name)
 
318
            # Show an animation to reflect the polling
 
319
            if self._anistat:
 
320
                self._anistat.show()
 
321
            # Kick off a polling service, after a delay
 
322
            gobject.timeout_add( 300, self.thread_through )
 
323
        else:
 
324
            raise Exception("Thread is already running!")
 
325
 
 
326
    def thread_through(self):
 
327
        """Keep things up to date fromt he thread."""
 
328
        self.process_calls()
 
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.
 
333
            if self._anistat:
 
334
                self._anicount += 1
 
335
                if self._anicount == 9:
 
336
                    self._anicount = 1
 
337
                image = anicons.get_icon(str(self._anicount))
 
338
                self._anistat.set_from_pixbuf(image)
 
339
            gobject.timeout_add( 100, self.thread_through )
 
340
        else:
 
341
            logging.debug("-- Poll Quit %s --" % self.name)
 
342
            # Hide the animation by default and exit the thread
 
343
            if self._anistat:
 
344
                self._anistat.hide()
 
345
            self.thread_exited()
 
346
 
 
347
    def inital_thread(self):
 
348
        """Replace this method with your own app's setup thread"""
 
349
        pass
 
350
 
 
351
    def thread_exited(self):
 
352
        """What is called when the thread exits"""
 
353
        pass
 
354
 
 
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.
 
362
        if unique:
 
363
            self._unique[name] = call
 
364
        else:
 
365
            self._calls.append( call )
 
366
 
 
367
    def process_calls(self):
 
368
        """Go through the calls stack and call them, return if required."""
 
369
        while self._calls:
 
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)
 
376
 
 
377
    def _exiting(self, widget=None):
 
378
        if self._thread and self._thread.isAlive():
 
379
            logging.warn(_("Your thread is still active."))
 
380
        self._closed = True
 
381
        super(ThreadedWindow, self)._exiting(widget)
 
382
 
 
383
 
 
384
class BaseView(object):
 
385
    """Controls for tree and icon views, a base class"""
 
386
    _data = None
 
387
    _model = None
 
388
    selected = None
 
389
 
 
390
    def __init__(self, dbobj, selected=None, unselected=None, name=None):
 
391
        self.selected_signal = selected
 
392
        self.unselected_signal = unselected
 
393
        self._list = dbobj
 
394
        self._name = name
 
395
        self.connect_signals()
 
396
        self.setup()
 
397
        super(BaseView, self).__init__()
 
398
 
 
399
    def connect_signals(self):
 
400
        """Try and connect signals to and from the view control"""
 
401
        raise NotImplementedError, "Signal connection should be elsewhere."
 
402
 
 
403
    def setup(self):
 
404
        """Setup any required aspects of the view"""
 
405
        return self._list
 
406
 
 
407
    def clear(self):
 
408
        """Clear all items from this treeview"""
 
409
        for iter_index in range(0, len(self._model)):
 
410
            try:
 
411
                del(self._model[0])
 
412
            except IndexError:
 
413
                logging.error(_("Could not delete item %d") % iter_index)
 
414
                return
 
415
 
 
416
    def add(self, target, parent=None):
 
417
        """Add all items from the target to the treeview"""
 
418
        for item in target:
 
419
            self.add_item(item, parent=parent)
 
420
 
 
421
    def add_item(self, item, parent=None):
 
422
        """Add a single item image to the control"""
 
423
        if item:
 
424
            result = self._model.append(parent, [item])
 
425
            # item.connect('update', self.updateItem)
 
426
            return result
 
427
        else:
 
428
            raise Exception("Item can not be None.")
 
429
 
 
430
    def replace(self, new_item, item_iter=None):
 
431
        """Replace all items, or a single item with object"""
 
432
        if item_iter:
 
433
            self.remove_item(target_iter=item_iter)
 
434
            self.add_item(new_item)
 
435
        else:
 
436
            self.clear()
 
437
            self._data = new_item
 
438
            self.add(new_item)
 
439
 
 
440
    def item_selected(self, item=None):
 
441
        """Base method result, called as an item is selected"""
 
442
        if self.selected != item:
 
443
            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)
 
448
 
 
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:
 
455
            if itemc[0] == item:
 
456
                return self._model.remove(target_iter)
 
457
            target_iter = self._model.iter_next(target_iter)
 
458
 
 
459
    def item_double_clicked(self, items):
 
460
        """What happens when you double click an item"""
 
461
        return items # Nothing
 
462
 
 
463
    def get_item(self, iter):
 
464
        """Return the object of attention from an iter"""
 
465
        return self._model[iter][0]
 
466
 
 
467
 
 
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)
 
473
 
 
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]
 
478
        try:
 
479
            return [ self.get_item(item_iter) ]
 
480
        except TypeError, msg:
 
481
            logging.debug("Error %s" % msg)
 
482
 
 
483
    def item_selected_signal(self, treeview):
 
484
        """Signal for selecting an item"""
 
485
        items = self.selected_items(treeview)
 
486
        if items:
 
487
            return self.item_selected( items[0] )
 
488
 
 
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] )
 
493
 
 
494
    def expand_item(self, item):
 
495
        """Expand one of our nodes"""
 
496
        self._list.expand_row(self._model.get_path(item), True)
 
497
 
 
498
    def setup(self):
 
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)
 
502
        return self._list
 
503
 
 
504
 
 
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)
 
510
 
 
511
    def setup(self):
 
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)
 
515
        return self._list
 
516
 
 
517
    def item_selected_signal(self, icon_view):
 
518
        """Item has been selected"""
 
519
        self.selected = icon_view.get_selected_items()
 
520