Package flumotion :: Package admin :: Package gtk :: Module wizard
[hide private]

Source Code for Module flumotion.admin.gtk.wizard

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_greeter -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with th 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 21   
 22   
 23  import os 
 24   
 25  import sys 
 26   
 27  import gobject 
 28  import gtk 
 29  import gtk.glade 
 30   
 31  from flumotion.configure import configure 
 32   
 33  from flumotion.common import pygobject, log 
 34  from flumotion.common.pygobject import gsignal 
 35  from flumotion.ui.glade import GladeWidget, GladeWindow 
 36   
 37   
 38  # This file implements a generic wizard framework suitable for processes with 
 39  # few steps. Processes with 5 or more steps should use something more like the 
 40  # wizard in flumotion.wizard, because it has a history-sensitive navigation bar 
 41  # as well. For simple processes, this wizard is sufficient. 
 42  # 
 43  # You will first have to define the steps in your wizard. They are defined by 
 44  # subclassing WizardStep. You define the required class variables, the on_next 
 45  # method, and optionally the other methods documented in then WizardStep class. 
 46  # 
 47  # class FirstStep(WizardStep): 
 48  #     name = 'first-step' 
 49  #     title = 'First step' 
 50  #     text = 'Please fill in your bank details below.' 
 51  #     next_pages = ['second-step'] 
 52  # 
 53  #     def on_next(self, state): 
 54  #         state['bank-account'], self.bank_account_entry.get_text() 
 55  #         return '*finished*' 
 56  # 
 57  # The on_next method is expected to save any relevant information to the state 
 58  # object (a dict) and return the name of the next step, or '*finished*' if this 
 59  # is the last step. 
 60  # 
 61  # Besides control flow, the name of the step is also used to load up a glade 
 62  # file describing the step's contents. The wizard will look for it as 
 63  # WNAME-SNAME.glade, where WNAME is the name of the wizard and SNAME is the name 
 64  # of the step. The widget taken will be the direct child of the first toplevel 
 65  # window. Each widget in the glade file will be set as an attribute on the 
 66  # WizardStep, e.g. bank_account_entry in the example above. 
 67  # 
 68  # The "text" is shown above the widget created from the glade file. next_pages 
 69  # is a list of possible next steps. Before the widget is shown, the is_available 
 70  # method will be called on the steps listed in next_pages, and the names of 
 71  # those steps that are actually available will be put in the available_pages 
 72  # attribute on the current step. This is useful to allow early steps to show 
 73  # if a later step is not available, perhaps by desensitizing an option. 
 74  # 
 75  # Methods other than on_next and is_available are documented in the WizardStep 
 76  # class. 
 77  # 
 78  # To conjure up a new wizard, call Wizard(NAME, FIRST_PAGE_NAME, STEP1, 
 79  # STEP2...). NAME is the name of the wizard, for instance 'greeter'. FIRST_PAGE 
 80  # is the name of the first page that should be shown, for instance 'first-step' 
 81  # in the example above. STEP1... are the WizardStep specialized classes (not 
 82  # instances). 
 83  # 
 84   
85 -class WizardCancelled(Exception):
86 pass
87 88 # fixme: doc vmethods
89 -class WizardStep(GladeWidget, log.Loggable):
90 # all values filled in by subclasses 91 name = None 92 title = None 93 on_next = None 94 button_next = None 95 next_pages = None 96 # also, all widgets from the glade file will become attributes 97 page = None # filled from glade 98 wizard = None # set by wizard 99 logCategory = "wizardstep" 100
101 - def __init__(self, wizard, glade_prefix=''):
102 """ 103 @type wizard: L{Wizard} 104 @type glade_prefix: str 105 @param glade_prefix: prefix used for glade files for the step 106 """ 107 self.wizard = wizard 108 self.glade_file = glade_prefix + self.name + '.glade' 109 GladeWidget.__init__(self)
110
111 - def is_available(self):
112 return True
113
114 - def setup(self, state, available_pages):
115 # vmethod 116 pass
117
118 -class Wizard(GladeWindow):
119 """ 120 A generic wizard. 121 122 This wizard runs its own GObject main loop. 123 The wizard is run with the run() method. 124 125 Example: 126 w = Wizard('foo', 'first-step', FirstStep) 127 w.show() 128 w.run() => {'bank-account': 'foo'} 129 """ 130 # should by filled by subclasses 131 name = None 132 steps = [] 133 134 # private 135 glade_file = 'admin-wizard.glade' 136 page = None 137 page_stack = [] 138 pages = {} 139 state = {} 140 141 gsignal('finished') 142
143 - def __init__(self, initial_page):
144 """ 145 @type initial_page: str 146 @param initial_page: name of the WizardStep to start on 147 """ 148 GladeWindow.__init__(self) 149 150 # these should be filled by subclasses 151 assert self.name 152 assert self.steps 153 154 # instantiate steps 155 for cls in self.steps: 156 page = cls(self, self.name + '-') 157 self.pages[cls.name] = page 158 page.show() 159 160 self._setup_ui() 161 self.set_page(initial_page)
162
163 - def _setup_ui(self):
164 w = self.widgets 165 166 iconfile = os.path.join(configure.imagedir, 'fluendo.png') 167 self.window.set_icon_from_file(iconfile) 168 w['image_icon'].set_from_file(iconfile) 169 170 # have to get the style from the theme, but it's not really there until 171 # it's realized 172 w['label_title'].realize() 173 style = w['label_title'].get_style() 174 175 title_bg = style.bg[gtk.STATE_SELECTED] 176 title_fg = style.fg[gtk.STATE_SELECTED] 177 w['eventbox_top'].modify_bg(gtk.STATE_NORMAL, title_bg) 178 w['eventbox_top'].modify_bg(gtk.STATE_INSENSITIVE, title_bg) 179 w['label_title'].modify_fg(gtk.STATE_NORMAL, title_fg) 180 w['label_title'].modify_fg(gtk.STATE_INSENSITIVE, title_fg) 181 normal_bg = style.bg[gtk.STATE_NORMAL] 182 w['textview_text'].modify_base(gtk.STATE_INSENSITIVE, normal_bg) 183 w['textview_text'].modify_bg(gtk.STATE_INSENSITIVE, normal_bg) 184 w['eventbox_content'].modify_base(gtk.STATE_INSENSITIVE, normal_bg) 185 w['eventbox_content'].modify_bg(gtk.STATE_INSENSITIVE, normal_bg)
186
187 - def set_page(self, name):
188 try: 189 page = self.pages[name] 190 except KeyError: 191 raise AssertionError ('No page named %s in %r' % (name, self.pages)) 192 193 w = self.widgets 194 page.button_next = w['button_next'] 195 196 available_pages = [p for p in page.next_pages 197 if self.pages[p].is_available()] 198 199 w['button_prev'].set_sensitive(bool(self.page_stack)) 200 201 self.page = page 202 for x in w['page_bin'].get_children(): 203 w['page_bin'].remove(x) 204 w['page_bin'].add(page) 205 w['label_title'].set_markup('<big><b>%s</b></big>' % page.title) 206 w['textview_text'].get_buffer().set_text(page.text) 207 w['button_next'].set_sensitive(True) 208 page.setup(self.state, available_pages)
209
210 - def on_delete_event(self, *window):
211 self.state = None 212 self.emit('finished')
213
214 - def on_next(self, button):
215 button.set_sensitive(False) 216 next_page = self.page.on_next(self.state) 217 218 if not next_page: 219 # the input is incorrect 220 pass 221 elif next_page == '*finished*': 222 button.set_sensitive(True) 223 self.emit('finished') 224 elif next_page == '*signaled*': 225 # page wants to do more stuff and will signal us next_page when done 226 def on_finished(page, next_page): 227 button.set_sensitive(True) 228 self.page.disconnect(self._finished_id) 229 if next_page == '*finished*': 230 self.emit('finished') 231 else: 232 self.page_stack.append(self.page) 233 self.set_page(next_page)
234 self._finished_id = self.page.connect('finished', on_finished) 235 else: 236 button.set_sensitive(True) 237 self.page_stack.append(self.page) 238 self.set_page(next_page)
239
240 - def on_prev(self, button):
241 page = self.page_stack.pop() 242 self.set_page(page.name)
243
244 - def set_sensitive(self, is_sensitive):
245 self.window.set_sensitive(is_sensitive)
246
247 - def run(self):
248 """ 249 Run in a recursive main loop. Will block until the user finishes 250 or closes the wizard. 251 """ 252 loop = gobject.MainLoop() 253 d = self.run_async() 254 def async_done(_): 255 loop.quit()
256 d.addCallbacks(async_done, async_done) 257 loop.run() 258 return self.state 259
260 - def run_async(self):
261 """ 262 Show the wizard. Returns a deferred that fires when the user has 263 closed the wizard, either via completing the process or has 264 cancelled somehow. 265 266 @returns: a deferred that will fire the state dict accumulated 267 by the pages, or None on cancel 268 """ 269 assert self.window 270 self.set_sensitive(True) 271 self.show() 272 from twisted.internet import defer 273 d = defer.Deferred() 274 def finished(x): 275 self.disconnect(i) 276 if self.state: 277 d.callback(self.state) 278 else: 279 d.errback(WizardCancelled())
280 i = self.connect('finished', finished) 281 return d 282 283 pygobject.type_register(Wizard) 284