| Trees | Indices | Help |
|
|---|
|
|
1 '''
2 Defines a gtk dialog L{AEChooser} for configuring SUE scripts, devices,
3 profiles, and system settings. Uses a glade XML file to define the shell of the
4 dialog. Dynamically generates the contents of the script, device, and system
5 panes internal panes based on the available L{AEState} settings. Generates the
6 contents of the profile views using information from the
7 L{AccessEngine.AERegistrar}.
8
9 @author: Peter Parente
10 @organization: IBM Corporation
11 @copyright: Copyright (c) 2005, 2007 IBM Corporation
12 @license: The BSD License
13
14 @author: Frank Zenker
15 @organization: IT Science Center Ruegen gGmbH, Germany
16 @copyright: Copyright (c) 2007, 2008 ITSC Ruegen
17 @license: The BSD License
18
19 All rights reserved. This program and the accompanying materials are made
20 available under the terms of the BSD license which accompanies
21 this distribution, and is available at
22 U{http://www.opensource.org/licenses/bsd-license.php}
23 '''
24 import pygtk
25 pygtk.require('2.0')
26 import gtk, gobject, atk
27 from Tools import Atk
28 import gtk.glade
29 from AccessEngine import AEChooser
30 from AccessEngine import AEState
31 from Tools.i18n import _, DOMAIN
32 from GTKUIEView import *
33 import weakref, logging
34
35 __uie__ = dict(kind='chooser')
36
37 log = logging.getLogger('SettingsChooser')
38
40 '''
41 Widget factory for creating gtk views for L{AEState.Setting} objects. Has
42 one public method for creating new widgets. All other methods are protected
43 for internally handling widget events and updating their correspoding
44 settings.
45
46 @ivar last_setting: Last setting object to change. Used to prevent cyclic
47 notification between widgets and setting objects.
48 @type last_setting: L{AEState.Setting}
49 '''
52
54 '''
55 Detects duplicate notifications of setting changes. This can be caused by
56 a pygtk widget changing, updating a setting, and then getting renotified
57 by the setting about the change. It can also occur when the setting
58 changes, updates the widget, and the widget tries to update the setting
59 again.
60
61 @param setting: Setting which is about to change or has changed value
62 @type setting: L{AEState.Setting}
63 '''
64 if self.last_setting is setting:
65 # don't notify ourselves again
66 self.last_setting = None
67 return True
68 self.last_setting = setting
69 return False
70
74
78
82
86
88 if self._isCycle(setting): return
89 fn = widget.get_filename()
90 if fn is not None:
91 setting.value = fn
92
96
100
104
106 if self._isCycle(setting): return
107 index = widget.get_active()
108 setting.value = setting.values[index]
109
113
115 if self._isCycle(setting): return
116 color = widget.get_color()
117 if widget.get_use_alpha():
118 alpha = widget.get_alpha()
119 setting.value = (color.red, color.green, color.blue, alpha)
120 else:
121 setting.value = (color.red, color.green, color.blue)
122
124 if self._isCycle(setting): return
125 widget.set_color(gtk.gdk.Color(red=value[0], green=value[1],
126 blue=value[2]))
127 if len(value) > 3:
128 widget.set_alpha(value[3])
129
131 widget = None
132 if isinstance(setting, AEState.Group):
133 # another group
134 widget = gtk.Frame()
135 widget.set_shadow_type(gtk.SHADOW_NONE)
136 label = gtk.Label()
137 label.set_markup('<b>%s</b>' % setting.name)
138 widget.set_label_widget(label)
139 return widget, None
140 elif isinstance(setting, AEState.BoolSetting):
141 # boolean
142 widget = gtk.CheckButton(setting.label)
143 widget.set_active(setting.value)
144 id = widget.connect('toggled',
145 AEState.Proxy(self._boolWidgetChange), setting)
146 # register for setting value changes
147 setting.addObserver(self._boolSettingChange, widget)
148 return widget, None
149 elif isinstance(setting, AEState.FilenameSetting):
150 # filename
151 widget = gtk.FileChooserButton(setting.label)
152 widget.set_filename(setting.value)
153 widget.connect('selection-changed',
154 AEState.Proxy(self._filenameWidgetChange), setting)
155 # register for setting value changes
156 setting.addObserver(self._filenameSettingChange, widget)
157 elif isinstance(setting, AEState.StringSetting):
158 # string
159 widget = gtk.Entry()
160 widget.set_text(setting.value)
161 widget.connect('focus-out-event',
162 AEState.Proxy(self._stringWidgetChange), setting)
163 # register for setting value changes
164 setting.addObserver(self._stringSettingChange, widget)
165 elif isinstance(setting, AEState.NumericSetting):
166 # use a view object for a range
167 view = setting.getView()
168 # numeric or range
169 if view.precision == 0:
170 step = 1
171 else:
172 step = 0.1**view.precision
173 page = int((view.max-view.min)/10.0)
174 adj = gtk.Adjustment(view.value, view.min, view.max, step, page)
175 if isinstance(setting, (AEState.RangeSetting, AEState.RelRangeSetting)):
176 widget = gtk.HScale(adj)
177 else:
178 widget = gtk.SpinButton(adj)
179 widget.set_digits(view.precision)
180 widget.set_value(view.value)
181 adj.connect('value-changed', AEState.Proxy(self._numericWidgetChange)
182 , setting)
183 # register for setting value changes
184 setting.addObserver(self._numericSettingChange, widget)
185 elif isinstance(setting, AEState.EnumSetting):
186 # enum
187 widget = gtk.combo_box_new_text()
188 map(widget.append_text, setting.labels)
189 widget.set_active(setting.values.index(setting.value))
190 widget.connect('changed',
191 AEState.Proxy(self._choiceWidgetChange), setting)
192 # register for setting value changes
193 setting.addObserver(self._choiceSettingChange, widget)
194 elif isinstance(setting, AEState.ChoiceSetting):
195 # choice
196 widget = gtk.combo_box_new_text()
197 map(widget.append_text, setting.values)
198 widget.set_active(setting.values.index(setting.value))
199 widget.connect('changed',
200 AEState.Proxy(self._choiceWidgetChange), setting)
201 # register for setting value changes
202 setting.addObserver(self._choiceSettingChange, widget)
203 elif isinstance(setting, AEState.ColorSetting):
204 # color
205 widget = gtk.ColorButton(gtk.gdk.Color(red=setting.value[0],
206 green=setting.value[1],
207 blue=setting.value[2]))
208 if len(setting.value) > 3:
209 widget.set_use_alpha(True)
210 widget.set_alpha(setting.value[3])
211 else:
212 widget.set_use_alpha(False)
213 widget.connect('color-set',
214 AEState.Proxy(self._colorWidgetChange), setting)
215 # register for setting value changes
216 setting.addObserver(self._colorSettingChange, widget)
217 else:
218 raise NotImplementedError
219 # create a label
220 label = gtk.Label(setting.label)
221 label.set_alignment(0, 0.5)
222
223 return widget, label
224
226 '''
227 SUE settings dialog for configuring scripts, devices, profiles, and other
228 system settings.
229
230 @ivar factory: Factory used to create gtk widget for settings
231 @type factory: L{WidgetFactory}
232 @ivar dialog: Dialog widget for configuring SUE settings
233 @type dialog: gtk.Dialog
234 @ivar system: SUE system wide settings
235 @type system: L{AEState}
236 @ivar scripts: L{AccessEngine.AEScript} settings keyed by human readable
237 script name
238 @type scripts: dictionary of string : L{AEState}
239 @ivar devices: Per L{AEOutput} device settings keyed by human readable device
240 name
241 @type devices: dictionary of string : L{AEState}
242 @ivar installed: All UIE class names, human readable names, and
243 descriptions installed keyed by their type
244 @type installed: dictionary of string : 3-tuple of string
245 @ivar associated: All UIE class names associated with this profile
246 @type associated: list of string
247 @ivar section_nb: Main tabbed panel
248 @type section_nb: gtk.Notebook
249 @ivar script_tv: List of configurable L{AccessEngine.AEScript}s
250 @type script_tv: gtk.TreeView
251 @ivar device_tv: List of configurable L{AEInput} and L{AEOutput} devices
252 @type device_tv: gtk.TreeView
253 @ivar system_vp: Panel for system settings
254 @type system_vp: gtk.Viewport
255 @ivar profile_scripts: Aids selection of L{AccessEngine.AEScript}s to
256 associate with the profile
257 @type profile_scripts: L{GTKUIEView.UIEView}
258 @ivar profile_monitors: Aids selection of L{AEMonitor}s to associate with the
259 profile
260 @type profile_monitors: L{GTKUIEView.UIEView}
261 @ivar profile_choosers: View of L{AEChooser}s installed
262 @type profile_choosers: L{GTKUIEView.UIEView}
263 @ivar profile_devices: Aids selection of preferred L{AEOutput} and L{AEInput}
264 devices associated with this profile
265 @type profile_devices: L{GTKUIEView.UIEView}
266 @cvar FRAME_MARKUP: Pango markup for frame panels to make text larger
267 @type FRAME_MARKUP: string
268 @cvar SCRIPT: Tab number for configuring scripts
269 @type SCRIPT: integer
270 @cvar DEVICE: Tab number for configuring devices
271 @type DEVICE: integer
272 @cvar SYSTEM: Tab number for configuring system settings
273 @type SYSTEM: integer
274 @cvar PROFILE: Tab number for configuring the profile
275 @type PROFILE: integer
276 @cvar RAISE: Raise priority of a L{AEUserInterface} for handling events or getting
277 loaded
278 @type RAISE: integer
279 @cvar LOWER: Lower priority of a L{AEUserInterface} for handling events or getting
280 loaded
281 @type LOWER: integer
282 @cvar TO_LOAD: Indicates a L{AEUserInterface} is now set to be loaded
283 @type TO_LOAD: integer
284 @cvar TO_UNLOAD: Indicates a L{AEUserInterface} is now set to be unloaded
285 @type TO_UNLOAD: integer
286 '''
287 GLOBAL_SINGLETON = True
288 SCRIPT = 0
289 DEVICE = 1
290 SYSTEM = 2
291 PROFILE = 3
292 FRAME_MARKUP = '<span size="xx-large" weight="bold">%s</span>'
293
294 # signal constants pulled from GTKUIEView
295 RAISE = RAISE
296 LOWER = LOWER
297 TO_LOAD = TO_LOAD
298 TO_UNLOAD = TO_UNLOAD
299
300 - def init(self, system, scripts, installed, associated, profile, devices,
301 timestamp, **kwargs):
302 '''
303 Creates and shows the settings dialog and its components
304
305 @param system: SUE system wide settings
306 @type system: L{AEState}
307 @param scripts: L{AccessEngine.AEScript} settings keyed by script name
308 @type scripts: dictionary of string: L{AEState}
309 @param installed: All UIE class names, human readable names, and
310 descriptions installed keyed by their type
311 @type installed: dictionary of string: list of 3-tuple of string
312 @param associated: All UIE class names, human readable names, and
313 descriptions associated with this profile keyed by their type
314 @type associated: dictionary of string: list of 3-tuple of string
315 @param profile: Name of the profile
316 @type profile: string
317 @param devices: L{AEOutput}/L{AEInput} settings keyed by device name
318 @type devices: dictionary of string : L{AEState}
319 @param timestamp: Time at which input was given indicating the start of
320 this chooser
321 @type timestamp: float
322 '''
323 # create a widget factory
324 self.factory = WidgetFactory()
325
326 # store group objects
327 self.system = system
328 self.scripts = scripts
329 self.devices = devices
330
331 # load the glade file
332 gtk.glade.set_custom_handler(self._createCustomWidget)
333 source = gtk.glade.XML(self._getResource('sue_settings.glade'),
334 'main dialog', DOMAIN)
335
336 # get the dialog widget
337 self.dialog = source.get_widget('main dialog')
338
339
340 # get widget references
341 self.tips = gtk.Tooltips()
342 self.section_nb = source.get_widget('section notebook')
343 self.script_tv = source.get_widget('script treeview')
344 script_label = source.get_widget('script label')
345 script_vp = source.get_widget('script viewport')
346 self.device_tv = source.get_widget('device treeview')
347 device_label = source.get_widget('device label')
348 device_vp = source.get_widget('device viewport')
349 self.system_vp = source.get_widget('system viewport')
350 # put the name of the profile on the profile tab
351 pl = source.get_widget('profile label')
352 pl.set_text(pl.get_text() % profile.title())
353
354 # create widget models
355 self.script_tv.set_model(gtk.ListStore(gobject.TYPE_STRING))
356 self.script_tv.set_data('subsection label', script_label)
357 self.script_tv.set_data('subsection viewport', script_vp)
358
359 self.device_tv.set_model(gtk.ListStore(gobject.TYPE_STRING))
360 self.device_tv.set_data('subsection label', device_label)
361 self.device_tv.set_data('subsection viewport', device_vp)
362
363 # connect all signal handlers
364 source.signal_autoconnect(self)
365
366 # populate all sections
367 self._createScriptSettingsView()
368 self._createDeviceSettingsView()
369 self._createProfileView(associated, installed)
370 self._createSystemSettingsView()
371
372 self.activate(timestamp, present=False)
373
375 '''
376 Try to bring the window to the foreground.
377 '''
378 try:
379 # try to bring the window to the foreground if this feature is supported
380 self.dialog.window.set_user_time(timestamp)
381 except AttributeError:
382 pass
383 if present:
384 try:
385 self.dialog.present()
386 except gtk.Warning:
387 # ignore strange gtk warnings that prevent further building of the UI
388 pass
389
391 '''
392 Creates a custom widget. Invoked during processing by glade.
393
394 @param glade: glade XML parser
395 @type glade: gtk.glade.XML
396 @param widget_name: Name of the widget to create
397 @type widget_name: string
398 @param function_name: Name of the function to call to create this widget
399 @type function_name: string
400 @return: Custom widget
401 @rtype: gtk.Widget
402 '''
403 return getattr(self, function_name)()
404
406 '''
407 Creates a L{GTKUIEView.UIEView} instance to manage selection of
408 L{AccessEngine.AEScript}s.
409
410 @return: Root widget of the UIE view
411 @rtype: gtk.Widget
412 '''
413 self.profile_scripts = UIEView(self, ordered=False)
414 return self.profile_scripts.getWidget()
415
417 '''
418 Creates a L{GTKUIEView.UIEView} instance to manage selection of
419 L{AEMonitor}s.
420
421 @return: Root widget of the UIE view
422 @rtype: gtk.Widget
423 '''
424 self.profile_monitors = UIEView(self, ordered=False)
425 return self.profile_monitors.getWidget()
426
428 '''
429 Creates a L{GTKUIEView.UIEView} instance to manage selection of
430 L{AEChooser}s.
431
432 @return: Root widget of the UIE view
433 @rtype: gtk.Widget
434 '''
435 self.profile_choosers = UIEView(self, ordered=False,
436 activatable=False)
437 return self.profile_choosers.getWidget()
438
440 '''
441 Creates a L{GTKUIEView.UIEView} instance to manage selection of
442 L{AEInput} and L{AEOutput} devices.
443
444 @return: Root widget of the UIE view
445 @rtype: gtk.Widget
446 '''
447 self.profile_devices = UIEView(self, ordered=True)
448 return self.profile_devices.getWidget()
449
451 '''
452 Populates the system settings profile panel.
453 '''
454 layout = self._populateSection(self.system.getGroups(), self.system_vp)
455 self.system_vp.add(layout)
456 self.system_vp.show_all()
457
459 '''
460 Populates all of the profile view tab panels.
461 '''
462 order = (('scripts', self.profile_scripts),
463 ('devices', self.profile_devices),
464 ('monitors', self.profile_monitors),
465 ('choosers', self.profile_choosers))
466 for name, view in order:
467 loaded = associated[name]
468 unloaded = set(installed[name])-set(loaded)
469 view.setData(loaded, unloaded)
470
472 '''
473 Populates the list of configurable L{AccessEngine.AEScript}s and selects the
474 first to generate its settings panel.
475 '''
476 # reset the view completely
477 model = self.script_tv.get_model()
478 model.clear()
479 cols = self.script_tv.get_columns()
480 map(self.script_tv.remove_column, cols)
481
482 # configure the script list view
483 crt = gtk.CellRendererText()
484 tvc = gtk.TreeViewColumn('', crt, text=0)
485 self.script_tv.append_column(tvc)
486
487 # populate the list of scripts
488 if len(self.scripts) > 0:
489 keys = self.scripts.keys()
490 keys.sort()
491 for name in keys:
492 try:
493 self.scripts[name].getGroups()
494 except NotImplementedError:
495 # ignore states that have no configurable settings
496 continue
497 model.append([name])
498 # set the first one as selected to cause an update of the panel
499 self.script_tv.set_cursor((0,))
500
502 '''
503 Populates the list of configurable L{AEInput} and L{AEOutput} devices and
504 selects the first to generate its settings panel.
505 '''
506 # reset the view completely
507 model = self.device_tv.get_model()
508 model.clear()
509 cols = self.device_tv.get_columns()
510 map(self.device_tv.remove_column, cols)
511
512 # configure the script list view
513 crt = gtk.CellRendererText()
514 tvc = gtk.TreeViewColumn('', crt, text=0)
515 self.device_tv.append_column(tvc)
516
517 # populate the list of scripts with settings
518 if len(self.devices) > 0:
519 keys = self.devices.keys()
520 keys.sort()
521 for name in keys:
522 try:
523 self.devices[name].getGroups()
524 except NotImplementedError:
525 # ignore states that have no configurable settings
526 continue
527 model.append([name])
528 # set the first one as selected to cause an update of the panel
529 self.device_tv.set_cursor((0,))
530
532 '''
533 Scrolls a focused widget in a settings panel into view.
534
535 @param widget: Widget that has the focus
536 @type widget: gtk.Widget
537 @param direction: Direction constant, ignored
538 @type direction: integer
539 @param viewport: Viewport to scroll to bring the widget into view
540 @type viewport: gtk.Viewport
541 '''
542 x, y = widget.translate_coordinates(viewport, 0, 0)
543 w, h = widget.size_request()
544 vw, vh = viewport.window.get_geometry()[2:4]
545
546 adj = viewport.get_vadjustment()
547 if y+h > vh:
548 adj.value += (y+h) - vh - 3
549 elif y < 0:
550 adj.value = max(adj.value + y, adj.lower)
551
553 '''
554 Prepares a settings panel for viewing when the name of a
555 L{AccessEngine.AEScript} or L{AEInput}/L{AEOutput} device is selected from
556 the list of configurable elements.
557
558 @param tv: Treeview in which a configurable item is selected
559 @type tv: gtk.TreeView
560 '''
561 # get model data
562 model = tv.get_model()
563 label = tv.get_data('subsection label')
564 viewport = tv.get_data('subsection viewport')
565 # get selected category info
566 path, col = tv.get_cursor()
567
568 # MW: Vergleich (Zeilen 575 & 582) zwischen String aus po-File (Unicode, da
569 # ugettext()) und String aus Gtk-TreeView (UTF-8) -> Konvertierung aus UTF-8
570 # in Unicode noetig! (Vgl. Bug B4)
571 # z.B. u'Basisvergrößerung' (\xf6\xdf) /
572 # 'Basisvergr\xc3\xb6\xc3\x9ferung'
573 name = unicode(model[path][0])
574
575 # throw away the previous contents
576 child = viewport.get_child()
577 if child:
578 viewport.remove(child)
579
580 # set the label on the section frame
581 label.set_markup(self.FRAME_MARKUP % name)
582 # populate the subsection
583 if self.script_tv == tv:
584 group = self.scripts[name].getGroups()
585 try:
586 layout = self._populateSection(group, viewport)
587 viewport.add(layout)
588 except AssertionError:
589 log.warn('empty script settings group')
590 elif self.device_tv == tv:
591 group = self.devices[name].getGroups()
592 try:
593 layout = self._populateSection(group, viewport)
594 viewport.add(layout)
595 except AssertionError:
596 log.warn('empty device settings group')
597 viewport.show_all()
598
600 '''
601 Populate a section pane with widgets representing the L{AEState.Setting}s
602 in the group. The container is the top level widget in which other widgets
603 should be made. If recursive groups exist, their settings will be properly
604 represented by widgets in a widget sub-container.
605
606 @param group: Group organizing a subset of settings in the state
607 @type group: L{AEState.Setting.Group}
608 @param viewport: Reference to the viewport to populate with widgets
609 @type viewport: gtk.Viewport
610 @raise AssertionError: When there are no settings in the group
611 '''
612 n = len(group)
613 if n < 1:
614 # no settings in the group
615 raise AssertionError
616 # use a n by 2 table to order widgets nicely
617 table = gtk.Table(n, 2)
618 table.set_row_spacings(5)
619 table.set_col_spacings(5)
620 # nit: add in reverse order else controls show up backwards in acc. tree
621 for row, setting in group.iterSettings(reverse=True):
622 widget, label = self.factory.create(setting)
623 if isinstance(widget, gtk.Frame):
624 # if the widget is another group, recurse
625 layout = self._populateSection(setting, viewport)
626 layout.set_border_width(5)
627 # add the sublayout to the group widget
628 widget.add(layout)
629 # attach the group widget to the table across both columns
630 # nit: add widget on the right first then label on the left, else gail
631 # screws up the order in the acc. hierarchy
632 table.attach(widget, 0, 2, n-row-1, n-row, yoptions=gtk.FILL,
633 xpadding=5)
634 else:
635 if isinstance(widget, gtk.CheckButton):
636 # check buttons have a label already, so fill the row
637 table.attach(widget, 0, 2, n-row-1, n-row, yoptions=gtk.FILL)
638 else:
639 # relate the label to the widget
640 Atk.setRelation(widget, atk.RELATION_LABELLED_BY, label)
641 Atk.setRelation(label, atk.RELATION_LABEL_FOR, widget)
642 # attach to the table
643 # note: add widget on the right first then label on the left,
644 # else gail screws up the order in the acc. hierarchy
645 table.attach(widget, 1, 2, n-row-1, n-row, yoptions=gtk.FILL)
646 table.attach(label, 0, 1, n-row-1, n-row, gtk.FILL, gtk.FILL, 5, 0)
647 # watch for focus on the widget so we can scroll
648 if setting.description:
649 # set description as a tooltip on the widget itself; does not work
650 # with all widgets (e.g. combobox)
651 self.tips.set_tip(widget, setting.description)
652 # also set description as accessible description on the widget
653 Atk.setNameDesc(widget, description=setting.description)
654 # watch for focus so we can scroll the widget into view
655 widget.connect('focus', self._onScrollToFocus, viewport)
656 return table
657
659 '''
660 Gets lists of L{AEUserInterface} class names currently set to be associated
661 with the active profile.
662
663 @return: Dictionary of associated UIEs keyed by their kind
664 @rtype: dictionary of string : list of string
665 '''
666 d = {}
667 if self.profile_scripts.isDirty():
668 d['scripts'] = self.profile_scripts.getCurrentUIEs()[0]
669 if self.profile_monitors.isDirty():
670 d['monitors'] = self.profile_monitors.getCurrentUIEs()[0]
671 if self.profile_devices.isDirty():
672 d['devices'] = self.profile_devices.getCurrentUIEs()[0]
673 return d
674
676 '''
677 Closes the dialog and sends a signal indicating the OK action.
678
679 @param widget: Source of GUI event
680 @type widget: gtk.Widget
681 '''
682 self._signal(self.OK, system=self.system, scripts=self.scripts,
683 devices=self.devices, associated=self._getAssociated())
684 self.close()
685
687 '''
688 Closes the dialog and sends a signal indicating the Cancel action.
689
690 @param widget: Source of GUI event
691 @type widget: gtk.Widget
692 '''
693 self._signal(self.CANCEL, system=self.system, scripts=self.scripts,
694 devices=self.devices)
695 self.close()
696
698 '''
699 Sends a signal indicating the Apply action.
700
701 @param widget: Source of GUI event
702 @type widget: gtk.Widget
703 '''
704 # get all associated
705 self._signal(self.APPLY, system=self.system, scripts=self.scripts,
706 devices=self.devices, associated=self._getAssociated())
707
709 '''
710 Closes the chooser, preventing further chooser interaction with the user.
711 '''
712 self.tips = None
713 self.factory = None
714 gtk.glade.set_custom_handler(lambda x: None)
715 self.dialog.destroy()
716
718 '''
719 Gets the name of the chooser.
720
721 @return: Human readable name of the chooser
722 @rtype: string
723 '''
724 return _('SUE Settings')
725
727 '''
728 Updates the list of devices according to those that are now loaded.
729
730 @param devices: Per L{AEOutput}/L{AEInput} settings keyed by device name
731 @type devices: dictionary of string : L{AEState}
732 '''
733 self.devices = devices
734 self._createDeviceSettingsView()
735
| Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0beta1 on Mon Jun 30 13:06:14 2008 | http://epydoc.sourceforge.net |