summaryrefslogtreecommitdiff
path: root/kiwi/ui
diff options
context:
space:
mode:
Diffstat (limited to 'kiwi/ui')
-rw-r--r--kiwi/ui/__init__.py39
-rw-r--r--kiwi/ui/comboboxentry.py129
-rw-r--r--kiwi/ui/comboentry.py540
-rw-r--r--kiwi/ui/combomixin.py231
-rw-r--r--kiwi/ui/dateentry.py352
-rw-r--r--kiwi/ui/delegates.py103
-rw-r--r--kiwi/ui/dialogs.py326
-rw-r--r--kiwi/ui/entry.py603
-rw-r--r--kiwi/ui/gadgets.py182
-rw-r--r--kiwi/ui/gazpacholoader.py360
-rw-r--r--kiwi/ui/hyperlink.py255
-rw-r--r--kiwi/ui/icon.py280
-rw-r--r--kiwi/ui/libgladeloader.py76
-rw-r--r--kiwi/ui/objectlist.py1705
-rw-r--r--kiwi/ui/proxy.py373
-rw-r--r--kiwi/ui/proxywidget.py316
-rw-r--r--kiwi/ui/selectablebox.py186
-rw-r--r--kiwi/ui/test/__init__.py24
-rw-r--r--kiwi/ui/test/common.py198
-rw-r--r--kiwi/ui/test/listener.py458
-rw-r--r--kiwi/ui/test/main.py52
-rw-r--r--kiwi/ui/test/player.py250
-rw-r--r--kiwi/ui/tooltip.py118
-rw-r--r--kiwi/ui/views.py967
-rw-r--r--kiwi/ui/widgets/__init__.py20
-rw-r--r--kiwi/ui/widgets/checkbutton.py69
-rw-r--r--kiwi/ui/widgets/colorbutton.py46
-rw-r--r--kiwi/ui/widgets/combo.py254
-rw-r--r--kiwi/ui/widgets/combobox.py43
-rw-r--r--kiwi/ui/widgets/entry.py255
-rw-r--r--kiwi/ui/widgets/filechooser.py80
-rw-r--r--kiwi/ui/widgets/fontbutton.py47
-rw-r--r--kiwi/ui/widgets/label.py157
-rw-r--r--kiwi/ui/widgets/list.py62
-rw-r--r--kiwi/ui/widgets/radiobutton.py94
-rw-r--r--kiwi/ui/widgets/spinbutton.py124
-rw-r--r--kiwi/ui/widgets/textview.py70
-rw-r--r--kiwi/ui/wizard.py236
38 files changed, 9680 insertions, 0 deletions
diff --git a/kiwi/ui/__init__.py b/kiwi/ui/__init__.py
new file mode 100644
index 0000000..1946454
--- /dev/null
+++ b/kiwi/ui/__init__.py
@@ -0,0 +1,39 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+"""User interface: Framework and Widget support"""
+
+
+try:
+ import gtk
+ gtk
+except ImportError:
+ try:
+ import pygtk
+ pygtk.require('2.0')
+ except:
+ pass
+ try:
+ import gtk
+ gtk
+ except ImportError:
+ raise SystemExit(
+ "PyGTK is required by kiwi.ui")
diff --git a/kiwi/ui/comboboxentry.py b/kiwi/ui/comboboxentry.py
new file mode 100644
index 0000000..9ef89b1
--- /dev/null
+++ b/kiwi/ui/comboboxentry.py
@@ -0,0 +1,129 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+
+"""Reimplementation of GtkComboBoxEntry in Python.
+
+The main difference between the L{BaseComboBoxEntry} and GtkComboBoxEntry
+is that a {kiwi.ui.widgets.entry.Entry} is used instead of a GtkEntry."""
+
+import gobject
+import gtk
+
+from kiwi.python import deprecationwarn
+from kiwi.ui.widgets.entry import ProxyEntry
+
+class BaseComboBoxEntry(gtk.ComboBox):
+ def __init__(self, model=None, text_column=-1):
+ deprecationwarn(
+ 'ComboBoxEntry is deprecated, use ComboEntry instead',
+ stacklevel=3)
+
+ gtk.ComboBox.__init__(self)
+ self.entry = ProxyEntry()
+ # HACK: We need to set a private variable, this seems to
+ # be the only way of doing so
+ self.entry.start_editing(gtk.gdk.Event(gtk.gdk.BUTTON_PRESS))
+ self.add(self.entry)
+ self.entry.show()
+
+ self._text_renderer = gtk.CellRendererText()
+ self.pack_start(self._text_renderer, True)
+ self.set_active(-1)
+ self.entry_changed_id = self.entry.connect('changed',
+ self._on_entry__changed)
+ self._active_changed_id = self.connect("changed",
+ self._on_entry__active_changed)
+ self._has_frame_changed(None)
+ self.connect("notify::has-frame", self._has_frame_changed)
+
+ if not model:
+ model = gtk.ListStore(str)
+ text_column = 0
+ self.set_model(model)
+ self.set_text_column(text_column)
+
+ # Virtual methods
+ def do_mnemnoic_activate(self, group_cycling):
+ self.entry.grab_focus()
+ return True
+
+ def do_grab_focus(self):
+ self.entry.grab_focus()
+
+ # Signal handlers
+ def _on_entry__active_changed(self, combobox):
+ iter = combobox.get_active_iter()
+ if not iter:
+ return
+
+ self.entry.handler_block(self.entry_changed_id)
+ model = self.get_model()
+ self.entry.set_text(model[iter][self._text_column])
+ self.entry.handler_unblock(self.entry_changed_id)
+
+ def _has_frame_changed(self, pspec):
+ has_frame = self.get_property("has-frame")
+ self.entry.set_has_frame(has_frame)
+
+ def _on_entry__changed(self, entry):
+ self.handler_block(self._active_changed_id)
+ self.set_active(-1)
+ self.handler_unblock(self._active_changed_id)
+
+ # Public API
+ def set_text_column(self, text_column):
+ self._text_column = text_column
+ if text_column != -1:
+ self.set_attributes(self._text_renderer, text=text_column)
+
+ def get_text_column(self):
+ return self._text_column
+
+ # IconEntry
+ def set_pixbuf(self, pixbuf):
+ self.entry.set_pixbuf(pixbuf)
+
+ def update_background(self, color):
+ self.entry.update_background(color)
+
+ def get_icon_window(self):
+ return self.entry.get_icon_window()
+
+gobject.type_register(BaseComboBoxEntry)
+
+def test():
+ win = gtk.Window()
+ win.connect('delete-event', gtk.main_quit)
+
+ e = BaseComboBoxEntry()
+ win.add(e)
+
+ m = gtk.ListStore(str)
+ m.append(['foo'])
+ m.append(['bar'])
+ m.append(['baz'])
+ e.set_model(m)
+ win.show_all()
+ gtk.main()
+
+if __name__ == '__main__':
+ test()
diff --git a/kiwi/ui/comboentry.py b/kiwi/ui/comboentry.py
new file mode 100644
index 0000000..11ee92f
--- /dev/null
+++ b/kiwi/ui/comboentry.py
@@ -0,0 +1,540 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""Widget for displaying a list of objects"""
+
+import gtk
+from gtk import gdk, keysyms
+
+from kiwi.ui.entry import KiwiEntry
+from kiwi.utils import gsignal, type_register
+
+class _ComboEntryPopup(gtk.Window):
+ gsignal('text-selected', str)
+ def __init__(self, comboentry):
+ gtk.Window.__init__(self, gtk.WINDOW_POPUP)
+ self.add_events(gdk.BUTTON_PRESS_MASK)
+ self.connect('key-press-event', self._on__key_press_event)
+ self.connect('button-press-event', self._on__button_press_event)
+ self._comboentry = comboentry
+
+ # Number of visible rows in the popup window, sensible
+ # default value from other toolkits
+ self._visible_rows = 10
+ self._initial_text = None
+
+ frame = gtk.Frame()
+ frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT)
+ self.add(frame)
+ frame.show()
+
+ vbox = gtk.VBox()
+ frame.add(vbox)
+ vbox.show()
+
+ self._sw = gtk.ScrolledWindow()
+ self._sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_NEVER)
+ vbox.pack_start(self._sw)
+ self._sw.show()
+
+ self._model = gtk.ListStore(str)
+ self._treeview = gtk.TreeView(self._model)
+ self._treeview.set_enable_search(False)
+ self._treeview.connect('motion-notify-event',
+ self._on_treeview__motion_notify_event)
+ self._treeview.connect('button-release-event',
+ self._on_treeview__button_release_event)
+ self._treeview.add_events(gdk.BUTTON_PRESS_MASK)
+ self._selection = self._treeview.get_selection()
+ self._selection.set_mode(gtk.SELECTION_BROWSE)
+ self._treeview.append_column(
+ gtk.TreeViewColumn('Foo', gtk.CellRendererText(),
+ text=0))
+ self._treeview.set_headers_visible(False)
+ self._sw.add(self._treeview)
+ self._treeview.show()
+
+ self._label = gtk.Label()
+ vbox.pack_start(self._label, False, False)
+
+ self.set_resizable(False)
+ self.set_screen(comboentry.get_screen())
+
+ def popup(self, text=None):
+ """
+ Shows the list of options. And optionally selects an item
+ @param text: text to select
+ """
+ combo = self._comboentry
+ if not (combo.flags() & gtk.REALIZED):
+ return
+
+ treeview = self._treeview
+ toplevel = combo.get_toplevel()
+ if isinstance(toplevel, gtk.Window) and toplevel.group:
+ toplevel.group.add_window(self)
+
+ # width is meant for the popup window
+ # height is meant for the treeview, since it calculates using
+ # the height of the cells on the rows
+ x, y, width, height = self._get_position()
+ self.set_size_request(width, -1)
+ treeview.set_size_request(-1, height)
+ self.move(x, y)
+ self.show()
+
+ treeview.set_hover_expand(True)
+ selection = treeview.get_selection()
+ if text:
+ for row in treeview.get_model():
+ if text in row:
+ selection.select_iter(row.iter)
+ treeview.scroll_to_cell(row.path, use_align=True,
+ row_align=0.5)
+ treeview.set_cursor(row.path)
+ break
+ self.grab_focus()
+
+ if not (self._treeview.flags() & gtk.HAS_FOCUS):
+ self._treeview.grab_focus()
+
+ if not self._popup_grab_window():
+ self.hide()
+ return
+
+ self.grab_add()
+
+ def popdown(self):
+ combo = self._comboentry
+ if not (combo.flags() & gtk.REALIZED):
+ return
+
+ self.grab_remove()
+ self.hide()
+
+ def set_label_text(self, text):
+ if text is None:
+ text = ''
+ self._label.hide()
+ else:
+ self._label.show()
+ self._label.set_text(text)
+
+ def set_model(self, model):
+ self._treeview.set_model(model)
+ self._model = model
+
+ # Callbacks
+
+ def _on__key_press_event(self, window, event):
+ """
+ Mimics Combobox behavior
+
+ Escape or Alt+Up: Close
+ Enter, Return or Space: Select
+ """
+
+ keyval = event.keyval
+ state = event.state & gtk.accelerator_get_default_mod_mask()
+ if (keyval == keysyms.Escape or
+ ((keyval == keysyms.Up or keyval == keysyms.KP_Up) and
+ state == gdk.MOD1_MASK)):
+ self.popdown()
+ return True
+ elif keyval == keysyms.Tab:
+ self.popdown()
+ # XXX: private member of comboentry
+ self._comboentry._button.grab_focus()
+ return True
+ elif (keyval == keysyms.Return or
+ keyval == keysyms.space or
+ keyval == keysyms.KP_Enter or
+ keyval == keysyms.KP_Space):
+ model, treeiter = self._selection.get_selected()
+ self.emit('text-selected', model[treeiter][0])
+ return True
+
+ return False
+
+ def _on__button_press_event(self, window, event):
+ # If we're clicking outside of the window
+ # close the popup
+ if (event.window != self.window or
+ (tuple(self.allocation.intersect(
+ gdk.Rectangle(x=int(event.x), y=int(event.y),
+ width=1, height=1)))) == (0, 0, 0, 0)):
+ self.popdown()
+
+ def _on_treeview__motion_notify_event(self, treeview, event):
+ retval = treeview.get_path_at_pos(int(event.x),
+ int(event.y))
+ if not retval:
+ return
+ path, column, x, y = retval
+ self._selection.select_path(path)
+ self._treeview.set_cursor(path)
+
+ def _on_treeview__button_release_event(self, treeview, event):
+ retval = treeview.get_path_at_pos(int(event.x),
+ int(event.y))
+ if not retval:
+ return
+ path, column, x, y = retval
+
+ model = treeview.get_model()
+ self.emit('text-selected', model[path][0])
+
+ def _popup_grab_window(self):
+ activate_time = 0L
+ if gdk.pointer_grab(self.window, True,
+ (gdk.BUTTON_PRESS_MASK |
+ gdk.BUTTON_RELEASE_MASK |
+ gdk.POINTER_MOTION_MASK),
+ None, None, activate_time) == 0:
+ if gdk.keyboard_grab(self.window, True, activate_time) == 0:
+ return True
+ else:
+ self.window.get_display().pointer_ungrab(activate_time);
+ return False
+ return False
+
+ def _get_position(self):
+ treeview = self._treeview
+ treeview.realize()
+
+ sample = self._comboentry
+
+ # We need to fetch the coordinates of the entry window
+ # since comboentry itself does not have a window
+ x, y = sample.entry.window.get_origin()
+ width = sample.allocation.width
+
+ hpolicy = vpolicy = gtk.POLICY_NEVER
+ self._sw.set_policy(hpolicy, vpolicy)
+
+ pwidth = self.size_request()[0]
+ if pwidth > width:
+ self._sw.set_policy(gtk.POLICY_ALWAYS, vpolicy)
+ pwidth, pheight = self.size_request()
+
+ rows = len(self._model)
+ if rows > self._visible_rows:
+ rows = self._visible_rows
+ self._sw.set_policy(hpolicy, gtk.POLICY_ALWAYS)
+
+ focus_padding = treeview.style_get_property('focus-line-width') * 2
+ cell_height = treeview.get_column(0).cell_get_size()[4]
+ height = (cell_height + focus_padding) * rows
+
+ screen = self._comboentry.get_screen()
+ monitor_num = screen.get_monitor_at_window(sample.window)
+ monitor = screen.get_monitor_geometry(monitor_num)
+
+ if x < monitor.x:
+ x = monitor.x
+ elif x + width > monitor.x + monitor.width:
+ x = monitor.x + monitor.width - width
+
+ if y + sample.allocation.height + height <= monitor.y + monitor.height:
+ y += sample.allocation.height
+ elif y - height >= monitor.y:
+ y -= height
+ elif (monitor.y + monitor.height - (y + sample.allocation.height) >
+ y - monitor.y):
+ y += sample.allocation.height
+ height = monitor.y + monitor.height - y
+ else :
+ height = y - monitor.y
+ y = monitor.y
+
+ # Use half of the available screen space
+ max_height = monitor.height / 2
+ if height > max_height:
+ height = int(max_height)
+ elif height < 0:
+ height = 0
+
+ return x, y, width, height
+
+ def get_selected_iter(self):
+ return self._selection.get_selected()[1]
+
+ def set_selected_iter(self, iter):
+ self._selection.select_iter(iter)
+type_register(_ComboEntryPopup)
+
+class ComboEntry(gtk.HBox):
+ gsignal('changed')
+ gsignal('activate')
+ def __init__(self, entry=None):
+ """
+ @param entry: a gtk.Entry subclass to use
+ """
+ gtk.HBox.__init__(self)
+ self._popping_down = False
+
+ if not entry:
+ entry = KiwiEntry()
+
+ self.entry = entry
+ self.entry.connect('activate',
+ self._on_entry__activate)
+ self.entry.connect('changed',
+ self._on_entry__changed)
+ self.entry.connect('scroll-event',
+ self._on_entry__scroll_event)
+ self.entry.connect('key-press-event',
+ self._on_entry__key_press_event)
+ self.pack_start(self.entry, True, True)
+ self.entry.show()
+
+ self._button = gtk.ToggleButton()
+ self._button.connect('scroll-event', self._on_entry__scroll_event)
+ self._button.connect('toggled', self._on_button__toggled)
+ self._button.set_focus_on_click(False)
+ self.pack_end(self._button, False, False)
+ self._button.show()
+
+ arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)
+ self._button.add(arrow)
+ arrow.show()
+
+ self._popup = _ComboEntryPopup(self)
+ self._popup.connect('text-selected', self._on_popup__text_selected)
+ self._popup.connect('hide', self._on_popup__hide)
+ self._popup.set_size_request(-1, 24)
+
+ completion = gtk.EntryCompletion()
+ self.entry.set_completion(completion)
+ self.set_model(completion.get_model())
+
+ # Virtual methods
+
+ def do_grab_focus(self):
+ self.entry.grab_focus()
+
+ # Callbacks
+
+ def _on_entry_completion__match_selected(self, completion, model, iter):
+ # the iter we receive is specific to the tree model filter used
+ # In the entry completion, convert it to an iter in the real model
+ self.set_active_iter(model.convert_iter_to_child_iter(iter))
+
+ def _on_entry__activate(self, entry):
+ self.emit('activate')
+
+ def _on_entry__changed(self, entry):
+ self.emit('changed')
+
+ def _on_entry__scroll_event(self, entry, event):
+ model = self.get_model()
+ treeiter = self._popup.get_selected_iter()
+ # If nothing is selected, select the first one
+ if not treeiter:
+ self.set_active_iter(model[0].iter)
+ return
+
+ curr = model[treeiter].path[0]
+ # Scroll up, select the previous item
+ if event.direction == gdk.SCROLL_UP:
+ curr -= 1
+ if curr >= 0:
+ self.set_active_iter(model[curr].iter)
+ # Scroll down, select the next item
+ elif event.direction == gdk.SCROLL_DOWN:
+ curr += 1
+ if curr < len(model):
+ self.set_active_iter(model[curr].iter)
+
+ def _on_entry__key_press_event(self, entry, event):
+ """
+ Mimics Combobox behavior
+
+ Alt+Down: Open popup
+ """
+ keyval, state = event.keyval, event.state
+ state &= gtk.accelerator_get_default_mod_mask()
+ if ((keyval == keysyms.Down or keyval == keysyms.KP_Down) and
+ state == gdk.MOD1_MASK):
+ self.popup()
+ return True
+
+ def _on_popup__hide(self, popup):
+ self._popping_down = True
+ self._button.set_active(False)
+ self._popping_down = False
+
+ def _on_popup__text_selected(self, popup, text):
+ self.entry.set_text(text)
+ popup.popdown()
+ self.entry.grab_focus()
+ self.entry.set_position(len(self.entry.get_text()))
+ self.emit('changed')
+
+ def _on_button__toggled(self, button):
+ if self._popping_down:
+ return
+ self.popup()
+
+ # Private
+
+ def _update(self):
+ model = self._model
+ if not len(model):
+ return
+
+ iter = self._popup.get_selected_iter()
+ if not iter:
+ iter = model[0].iter
+ self._popup.set_selected_iter(iter)
+
+ # Public API
+
+ def clicked(self):
+ pass
+
+ def popup(self):
+ """
+ Hide the popup window
+ """
+ self._popup.popup(self.entry.get_text())
+
+ def popdown(self):
+ """
+ Show the popup window
+ """
+ self._popup.popdown()
+
+ # Entry interface
+
+ def set_text(self, text):
+ """
+ @param text:
+ """
+ self.entry.set_text(text)
+
+ def get_text(self):
+ """
+ @returns: current text
+ """
+ return self.entry.get_text()
+
+ # ComboMixin interface
+
+ def set_model(self, model):
+ """
+ Set the tree model to model
+ @param model: new model
+ @type model: gtk.TreeModel
+ """
+ self._model = model
+ self._popup.set_model(model)
+ completion = self.entry.get_completion()
+ completion.connect('match-selected',
+ self._on_entry_completion__match_selected)
+ completion.set_model(model)
+
+ self._update()
+
+ def get_model(self):
+ """
+ @returns: our model
+ @rtype: gtk.TreeModel
+ """
+ return self._model
+
+ def set_active_iter(self, iter):
+ """
+ @param iter: iter to select
+ @type iter: gtk.TreeIter
+ """
+ self._popup.set_selected_iter(iter)
+ self.set_text(self._model[iter][0])
+
+ def get_active_iter(self):
+ """
+ @returns: the selected iter
+ @rtype: gtk.TreeIter
+ """
+ return self._popup.get_selected_iter()
+
+ def prefill(self, itemdata, sort=False):
+ """
+ See L{kiwi.ui.widgets.entry}
+ """
+ self._model.clear()
+ self.entry.prefill(itemdata, sort)
+
+ def select_item_by_data(self, data):
+ """
+ @param data: object to select
+ """
+ treeiter = self.entry.get_iter_by_data(data)
+ self.set_active_iter(treeiter)
+
+ def select_item_by_label(self, text):
+ """
+ @param text: text to select
+ """
+ treeiter = self.entry.get_iter_by_label(text)
+ self.set_active_iter(treeiter)
+
+ def get_selected(self):
+ """
+ @returns: selected text or item or None if nothing
+ is selected
+ """
+ treeiter = self.get_active_iter()
+ if treeiter:
+ return self.entry.get_selected_by_iter(treeiter)
+
+ def get_selected_label(self):
+ """
+ @returns: the label of the currently selected item
+ """
+ treeiter = self.get_active_iter()
+ if treeiter:
+ return self.entry.get_selected_label(treeiter)
+
+ def select(self, obj):
+ """
+ @param obj: data or text to select
+ """
+ treeiter = self.entry.get_iter_from_obj(obj)
+ self.set_active_iter(treeiter)
+
+ def set_label_text(self, text):
+ self._popup.set_label_text(text)
+
+ # IconEntry
+
+ def set_pixbuf(self, pixbuf):
+ self.entry.set_pixbuf(pixbuf)
+
+ def update_background(self, color):
+ self.entry.update_background(color)
+
+ def get_icon_window(self):
+ return self.entry.get_icon_window()
+
+type_register(ComboEntry)
diff --git a/kiwi/ui/combomixin.py b/kiwi/ui/combomixin.py
new file mode 100644
index 0000000..d15e961
--- /dev/null
+++ b/kiwi/ui/combomixin.py
@@ -0,0 +1,231 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+#
+
+import gtk
+
+(COL_COMBO_LABEL,
+ COL_COMBO_DATA) = range(2)
+
+(COMBO_MODE_UNKNOWN,
+ COMBO_MODE_STRING,
+ COMBO_MODE_DATA) = range(3)
+
+class ComboMixin(object):
+ """Our combos always have one model with two columns, one for the string
+ that is displayed and one for the object it cames from.
+ """
+ def __init__(self):
+ """Call this constructor after the Combo one"""
+ model = gtk.ListStore(str, object)
+ self.set_model(model)
+ self.mode = COMBO_MODE_UNKNOWN
+
+ def set_mode(self, mode):
+ if self.mode != COMBO_MODE_UNKNOWN:
+ raise AssertionError
+ self.mode = mode
+
+ def __nonzero__(self):
+ return True
+
+ def __len__(self):
+ return len(self.get_model())
+
+ def prefill(self, itemdata, sort=False):
+ """Fills the Combo with listitems corresponding to the itemdata
+ provided.
+
+ Parameters:
+ - itemdata is a list of strings or tuples, each item corresponding
+ to a listitem. The simple list format is as follows::
+
+ >>> [ label0, label1, label2 ]
+
+ If you require a data item to be specified for each item, use a
+ 2-item tuple for each element. The format is as follows::
+
+ >>> [ ( label0, data0 ), (label1, data1), ... ]
+
+ - Sort is a boolean that specifies if the list is to be sorted by
+ label or not. By default it is not sorted
+ """
+ if not isinstance(itemdata, (list, tuple)):
+ raise TypeError("'data' parameter must be a list or tuple of item "
+ "descriptions, found %s") % type(itemdata)
+
+ self.clear()
+ if len(itemdata) == 0:
+ return
+
+ if self.mode == COMBO_MODE_UNKNOWN:
+ first = itemdata[0]
+ if isinstance(first, basestring):
+ self.set_mode(COMBO_MODE_STRING)
+ elif isinstance(first, (tuple, list)):
+ self.set_mode(COMBO_MODE_DATA)
+ else:
+ raise TypeError("Could not determine type, items must "
+ "be strings or tuple/list")
+
+ mode = self.mode
+ model = self.get_model()
+
+ values = {}
+ if mode == COMBO_MODE_STRING:
+ if sort:
+ itemdata.sort()
+
+ for item in itemdata:
+ if item in values:
+ raise KeyError("Tried to insert duplicate value "
+ "%s into Combo!" % item)
+ else:
+ values[item] = None
+
+ model.append((item, None))
+ elif mode == COMBO_MODE_DATA:
+ if sort:
+ itemdata.sort(lambda x, y: cmp(x[0], y[0]))
+
+ for item in itemdata:
+ text, data = item
+ if text in values:
+ raise KeyError("Tried to insert duplicate value "
+ "%s into Combo!" % item)
+ else:
+ values[text] = None
+ model.append((text, data))
+ else:
+ raise TypeError("Incorrect format for itemdata; see "
+ "docstring for more information")
+
+ def append_item(self, label, data=None):
+ """ Adds a single item to the Combo. Takes:
+ - label: a string with the text to be added
+ - data: the data to be associated with that item
+ """
+ if not isinstance(label, basestring):
+ raise TypeError("label must be string, found %s" % label)
+
+ if self.mode == COMBO_MODE_UNKNOWN:
+ if data is not None:
+ self.set_mode(COMBO_MODE_DATA)
+ else:
+ self.set_mode(COMBO_MODE_STRING)
+
+ model = self.get_model()
+ if self.mode == COMBO_MODE_STRING:
+ if data is not None:
+ raise TypeError("data can not be specified in string mode")
+ model.append((label, None))
+ elif self.mode == COMBO_MODE_DATA:
+ if data is None:
+ raise TypeError("data must be specified in string mode")
+ model.append((label, data))
+ else:
+ raise AssertionError
+
+ def clear(self):
+ """Removes all items from list"""
+ model = self.get_model()
+ model.clear()
+
+ def select(self, data):
+ mode = self.mode
+ if self.mode == COMBO_MODE_STRING:
+ self.select_item_by_label(data)
+ elif self.mode == COMBO_MODE_DATA:
+ self.select_item_by_data(data)
+ else:
+ # XXX: When setting the datatype to non string, automatically go to
+ # data mode
+ raise TypeError("unknown ComboBox mode. Did you call prefill?")
+
+ def select_item_by_position(self, pos):
+ self.set_active(pos)
+
+ def select_item_by_label(self, label):
+ model = self.get_model()
+ for row in model:
+ if row[COL_COMBO_LABEL] == label:
+ self.set_active_iter(row.iter)
+ break
+ else:
+ raise KeyError("No item correspond to label %r in the combo %s"
+ % (label, self.name))
+
+ def select_item_by_data(self, data):
+ if self.mode != COMBO_MODE_DATA:
+ raise TypeError("select_item_by_data can only be used in data mode")
+
+ model = self.get_model()
+ for row in model:
+ if row[COL_COMBO_DATA] == data:
+ self.set_active_iter(row.iter)
+ break
+ else:
+ raise KeyError("No item correspond to data %r in the combo %s"
+ % (data, self.name))
+
+ def get_model_strings(self):
+ return [row[COL_COMBO_LABEL] for row in self.get_model()]
+
+ def get_model_items(self):
+ if self.mode != COMBO_MODE_DATA:
+ raise TypeError("get_model_items can only be used in data mode")
+
+ model = self.get_model()
+ items = {}
+ for row in model:
+ items[row[COL_COMBO_LABEL]] = row[COL_COMBO_DATA]
+
+ return items
+
+ def get_selected_label(self):
+ iter = self.get_active_iter()
+ if not iter:
+ return
+
+ model = self.get_model()
+ return model[iter][COL_COMBO_LABEL]
+
+ def get_selected_data(self):
+ if self.mode != COMBO_MODE_DATA:
+ raise TypeError("get_selected_data can only be used in data mode")
+
+ iter = self.get_active_iter()
+ if not iter:
+ return
+
+ model = self.get_model()
+ return model[iter][COL_COMBO_DATA]
+
+ def get_selected(self):
+ mode = self.mode
+ if mode == COMBO_MODE_STRING:
+ return self.get_selected_label()
+ elif mode == COMBO_MODE_DATA:
+ return self.get_selected_data()
+ else:
+ raise AssertionError
diff --git a/kiwi/ui/dateentry.py b/kiwi/ui/dateentry.py
new file mode 100644
index 0000000..90d675e
--- /dev/null
+++ b/kiwi/ui/dateentry.py
@@ -0,0 +1,352 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+#
+# Based on date cell renderer in Planner written by Richard Hult
+# and Mikael Hallendal
+#
+
+import gettext
+import datetime
+
+import gtk
+from gtk import gdk, keysyms
+
+from kiwi.datatypes import converter, ValidationError
+from kiwi.utils import gsignal, type_register
+
+_ = lambda m: gettext.dgettext('kiwi', m)
+
+date_converter = converter.get_converter(datetime.date)
+
+class _DateEntryPopup(gtk.Window):
+ gsignal('date-selected', object)
+ def __init__(self, dateentry):
+ gtk.Window.__init__(self, gtk.WINDOW_POPUP)
+ self.add_events(gdk.BUTTON_PRESS_MASK)
+ self.connect('key-press-event', self._on__key_press_event)
+ self.connect('button-press-event', self._on__button_press_event)
+ self._dateentry = dateentry
+
+ frame = gtk.Frame()
+ frame.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+ self.add(frame)
+ frame.show()
+
+ vbox = gtk.VBox()
+ vbox.set_border_width(6)
+ frame.add(vbox)
+ vbox.show()
+ self._vbox = vbox
+
+ self.calendar = gtk.Calendar()
+ self.calendar.connect('day-selected-double-click',
+ self._on_calendar__day_selected_double_click)
+ vbox.pack_start(self.calendar, False, False)
+ self.calendar.show()
+
+ buttonbox = gtk.HButtonBox()
+ buttonbox.set_border_width(6)
+ buttonbox.set_layout(gtk.BUTTONBOX_SPREAD)
+ vbox.pack_start(buttonbox, False, False)
+ buttonbox.show()
+
+ for label, callback in [(_('_Today'), self._on_today__clicked),
+ (_('_Cancel'), self._on_cancel__clicked),
+ (_('_Select'), self._on_select__clicked)]:
+ button = gtk.Button(label, use_underline=True)
+ button.connect('clicked', callback)
+ buttonbox.pack_start(button)
+ button.show()
+
+ self.set_resizable(False)
+ self.set_screen(dateentry.get_screen())
+
+ self.realize()
+ self.height = self._vbox.size_request()[1]
+
+ def _on_calendar__day_selected_double_click(self, calendar):
+ self.emit('date-selected', self.get_date())
+
+ def _on__button_press_event(self, window, event):
+ # If we're clicking outside of the window close the popup
+ if tuple(self.allocation.intersect(
+ gdk.Rectangle(x=int(event.x), y=int(event.y),
+ width=1, height=1))) == (0, 0, 0, 0):
+ self.popdown()
+
+ # XXX: Clicking on button + entry
+
+ def _on__key_press_event(self, window, event):
+ """
+ Mimics Combobox behavior
+
+ Escape or Alt+Up: Close
+ Enter, Return or Space: Select
+ """
+
+ keyval = event.keyval
+ state = event.state & gtk.accelerator_get_default_mod_mask()
+ if (keyval == keysyms.Escape or
+ ((keyval == keysyms.Up or keyval == keysyms.KP_Up) and
+ state == gdk.MOD1_MASK)):
+ self.popdown()
+ return True
+ elif keyval == keysyms.Tab:
+ self.popdown()
+ # XXX: private member of dateentry
+ self._comboentry._button.grab_focus()
+ return True
+ elif (keyval == keysyms.Return or
+ keyval == keysyms.space or
+ keyval == keysyms.KP_Enter or
+ keyval == keysyms.KP_Space):
+ self.emit('date-selected', self.get_date())
+ return True
+
+ return False
+
+ def _on_select__clicked(self, button):
+ self.emit('date-selected', self.get_date())
+
+ def _on_cancel__clicked(self, button):
+ self.popdown()
+
+ def _on_today__clicked(self, button):
+ self.set_date(datetime.date.today())
+
+ def _popup_grab_window(self):
+ activate_time = 0L
+ if gdk.pointer_grab(self.window, True,
+ (gdk.BUTTON_PRESS_MASK |
+ gdk.BUTTON_RELEASE_MASK |
+ gdk.POINTER_MOTION_MASK),
+ None, None, activate_time) == 0:
+ if gdk.keyboard_grab(self.window, True, activate_time) == 0:
+ return True
+ else:
+ self.window.get_display().pointer_ungrab(activate_time);
+ return False
+ return False
+
+ def _get_position(self):
+ self.realize()
+ calendar = self
+
+ sample = self._dateentry
+
+ # We need to fetch the coordinates of the entry window
+ # since comboentry itself does not have a window
+ x, y = sample.entry.window.get_origin()
+ width, height = calendar.size_request()
+ height = self.height
+
+ screen = sample.get_screen()
+ monitor_num = screen.get_monitor_at_window(sample.window)
+ monitor = screen.get_monitor_geometry(monitor_num)
+
+ if x < monitor.x:
+ x = monitor.x
+ elif x + width > monitor.x + monitor.width:
+ x = monitor.x + monitor.width - width
+
+ if y + sample.allocation.height + height <= monitor.y + monitor.height:
+ y += sample.allocation.height
+ elif y - height >= monitor.y:
+ y -= height
+ elif (monitor.y + monitor.height - (y + sample.allocation.height) >
+ y - monitor.y):
+ y += sample.allocation.height
+ height = monitor.y + monitor.height - y
+ else :
+ height = y - monitor.y
+ y = monitor.y
+
+ return x, y, width, height
+
+ def popup(self, date):
+ """
+ Shows the list of options. And optionally selects an item
+ @param date: date to select
+ """
+ combo = self._dateentry
+ if not (combo.flags() & gtk.REALIZED):
+ return
+
+ treeview = self.calendar
+ if treeview.flags() & gtk.MAPPED:
+ return
+ toplevel = combo.get_toplevel()
+ if isinstance(toplevel, gtk.Window) and toplevel.group:
+ toplevel.group.add_window(self)
+
+ x, y, width, height = self._get_position()
+ self.set_size_request(width, height)
+ self.move(x, y)
+ self.show_all()
+
+ if date:
+ self.set_date(date)
+ self.grab_focus()
+
+ if not (self.calendar.flags() & gtk.HAS_FOCUS):
+ self.calendar.grab_focus()
+
+ if not self._popup_grab_window():
+ self.hide()
+ return
+
+ self.grab_add()
+
+ def popdown(self):
+ combo = self._dateentry
+ if not (combo.flags() & gtk.REALIZED):
+ return
+
+ self.grab_remove()
+ self.hide_all()
+
+ # month in gtk.Calendar is zero-based (i.e the allowed values are 0-11)
+ # datetime one-based (i.e. the allowed values are 1-12)
+ # So convert between them
+
+ def get_date(self):
+ y, m, d = self.calendar.get_date()
+ return datetime.date(y, m + 1, d)
+
+ def set_date(self, date):
+ self.calendar.select_month(date.month - 1, date.year)
+ self.calendar.select_day(date.day)
+ # FIXME: Only mark the day in the current month?
+ self.calendar.clear_marks()
+ self.calendar.mark_day(date.day)
+
+class DateEntry(gtk.HBox):
+ gsignal('changed')
+ gsignal('activate')
+ def __init__(self):
+ gtk.HBox.__init__(self)
+
+ self._popping_down = False
+ self._old_date = None
+
+ # bootstrap problems, kiwi.ui.widgets.entry imports dateentry
+ # we need to use a proxy entry because we want the mask
+ from kiwi.ui.widgets.entry import ProxyEntry
+ self.entry = ProxyEntry()
+ self.entry.connect('changed', self._on_entry__changed)
+ self.entry.set_mask_for_data_type(datetime.date)
+ mask = self.entry.get_mask()
+ if mask:
+ self.entry.set_width_chars(len(mask))
+ self.pack_start(self.entry, False, False)
+ self.entry.show()
+
+ self._button = gtk.ToggleButton()
+ self._button.connect('scroll-event', self._on_entry__scroll_event)
+ self._button.connect('toggled', self._on_button__toggled)
+ self._button.set_focus_on_click(False)
+ self.pack_start(self._button, False, False)
+ self._button.show()
+
+ arrow = gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)
+ self._button.add(arrow)
+ arrow.show()
+
+ self._popup = _DateEntryPopup(self)
+ self._popup.connect('date-selected', self._on_popup__date_selected)
+ self._popup.connect('hide', self._on_popup__hide)
+ self._popup.set_size_request(-1, 24)
+
+ # Virtual methods
+
+ def do_grab_focus(self):
+ self.entry.grab_focus()
+
+ # Callbacks
+
+ def _on_entry__changed(self, entry):
+ self._changed(self.get_date())
+
+ def _on_entry__activate(self, entry):
+ self.emit('activate')
+
+ def _on_entry__scroll_event(self, entry, event):
+ if event.direction == gdk.SCROLL_UP:
+ days = 1
+ elif event.direction == gdk.SCROLL_DOWN:
+ days = -1
+ else:
+ return
+
+ date = self.get_date()
+ if not date:
+ newdate = datetime.date.today()
+ else:
+ newdate = date + datetime.timedelta(days=days)
+ self.set_date(newdate)
+
+ def _on_button__toggled(self, button):
+ if self._popping_down:
+ return
+
+ self._popup.popup(self.get_date())
+
+ def _on_popup__hide(self, popup):
+ self._popping_down = True
+ self._button.set_active(False)
+ self._popping_down = False
+
+ def _on_popup__date_selected(self, popup, date):
+ self.set_date(date)
+ popup.popdown()
+ self.entry.grab_focus()
+ self.entry.set_position(len(self.entry.get_text()))
+ self._changed(date)
+
+ def _changed(self, date):
+ if self._old_date != date:
+ self.emit('changed')
+ self._old_date = date
+
+ # Public API
+
+ def set_date(self, date):
+ """
+ @param date: a datetime.date instance
+ """
+ if not isinstance(date, datetime.date):
+ raise TypeError("date must be a datetime.date instance")
+
+ self.entry.set_text(date_converter.as_string(date))
+
+ def get_date(self):
+ """
+ @returns: the currently selected day
+ """
+ try:
+ return date_converter.from_string(self.entry.get_text())
+ except ValidationError:
+ pass
+
+type_register(DateEntry)
diff --git a/kiwi/ui/delegates.py b/kiwi/ui/delegates.py
new file mode 100644
index 0000000..e183229
--- /dev/null
+++ b/kiwi/ui/delegates.py
@@ -0,0 +1,103 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2002, 2003 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""Defines the Delegate classes that are included in the Kiwi Framework."""
+
+from kiwi.ui.views import SlaveView, BaseView
+from kiwi.controllers import BaseController
+
+class SlaveDelegate(SlaveView, BaseController):
+ """A class that combines view and controller functionality into a
+ single package. It does not possess a top-level window, but is instead
+ intended to be plugged in to a View or Delegate using attach_slave().
+ """
+ def __init__(self, toplevel=None, widgets=(), gladefile=None,
+ gladename=None, toplevel_name=None, domain=None,
+ keyactions=None):
+ """
+ The keyactions parameter is sent to L{kiwi.controllers.BaseController},
+ the rest are sent to L{kiwi.ui.views.SlaveView}
+ """
+ SlaveView.__init__(self, toplevel, widgets, gladefile, gladename,
+ toplevel_name, domain)
+ BaseController.__init__(self, view=self, keyactions=keyactions)
+
+class Delegate(BaseView, BaseController):
+ """A class that combines view and controller functionality into a
+ single package. The Delegate class possesses a top-level window.
+ """
+ def __init__(self, toplevel=None, widgets=(), gladefile=None,
+ gladename=None, toplevel_name=None, domain=None,
+ delete_handler=None, keyactions=None):
+ """Creates a new Delegate.
+ The keyactions parameter is sent to L{kiwi.controllers.BaseController},
+ the rest are sent to L{kiwi.ui.views.BaseView}
+ """
+
+ BaseView.__init__(self, toplevel, widgets, gladefile,
+ gladename, toplevel_name, domain,
+ delete_handler)
+ BaseController.__init__(self, view=self, keyactions=keyactions)
+
+class ProxyDelegate(Delegate):
+ """A class that combines view, controller and proxy functionality into a
+ single package. The Delegate class possesses a top-level window.
+
+ @ivar model: the model
+ @ivar proxy: the proxy
+ """
+ def __init__(self, model, proxy_widgets=None, gladefile=None,
+ toplevel=None, widgets=(), gladename=None,
+ toplevel_name=None, domain=None, delete_handler=None,
+ keyactions=None):
+ """Creates a new Delegate.
+ @param model: instance to be attached
+ @param proxy_widgets:
+ The keyactions parameter is sent to L{kiwi.controllers.BaseController},
+ the rest are sent to L{kiwi.ui.views.BaseView}
+ """
+
+ BaseView.__init__(self, toplevel, widgets, gladefile,
+ gladename, toplevel_name, domain,
+ delete_handler)
+ self.model = model
+ self.proxy = self.add_proxy(model, proxy_widgets)
+ self.proxy.proxy_updated = self.proxy_updated
+
+ BaseController.__init__(self, view=self, keyactions=keyactions)
+
+ def set_model(self, model):
+ """
+ @param model:
+ """
+ self.proxy.set_model(model)
+ self.model = model
+
+ def proxy_updated(self, widget, attribute, value):
+ # Can be overriden in subclasses
+ pass
+
+ def update(self, attribute):
+ self.proxy.update(attribute)
diff --git a/kiwi/ui/dialogs.py b/kiwi/ui/dialogs.py
new file mode 100644
index 0000000..6ee8f8a
--- /dev/null
+++ b/kiwi/ui/dialogs.py
@@ -0,0 +1,326 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005,2006 Async Open Source
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+import os
+import gettext
+
+import atk
+import gtk
+
+__all__ = ['error', 'info', 'messagedialog', 'warning', 'yesno', 'save',
+ 'open', 'HIGAlertDialog', 'BaseDialog']
+
+_ = gettext.gettext
+
+_IMAGE_TYPES = {
+ gtk.MESSAGE_INFO: gtk.STOCK_DIALOG_INFO,
+ gtk.MESSAGE_WARNING : gtk.STOCK_DIALOG_WARNING,
+ gtk.MESSAGE_QUESTION : gtk.STOCK_DIALOG_QUESTION,
+ gtk.MESSAGE_ERROR : gtk.STOCK_DIALOG_ERROR,
+}
+
+_BUTTON_TYPES = {
+ gtk.BUTTONS_NONE: (),
+ gtk.BUTTONS_OK: (gtk.STOCK_OK, gtk.RESPONSE_OK,),
+ gtk.BUTTONS_CLOSE: (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE,),
+ gtk.BUTTONS_CANCEL: (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,),
+ gtk.BUTTONS_YES_NO: (gtk.STOCK_NO, gtk.RESPONSE_NO,
+ gtk.STOCK_YES, gtk.RESPONSE_YES),
+ gtk.BUTTONS_OK_CANCEL: (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OK, gtk.RESPONSE_OK)
+ }
+
+class HIGAlertDialog(gtk.Dialog):
+ def __init__(self, parent, flags,
+ type=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_NONE):
+ if not type in _IMAGE_TYPES:
+ raise TypeError(
+ "type must be one of: %s", ', '.join(_IMAGE_TYPES.keys()))
+ if not buttons in _BUTTON_TYPES:
+ raise TypeError(
+ "buttons be one of: %s", ', '.join(_BUTTON_TYPES.keys()))
+
+ gtk.Dialog.__init__(self, '', parent, flags)
+ self.set_border_width(5)
+ self.set_resizable(False)
+ self.set_has_separator(False)
+ # Some window managers (ION) displays a default title (???) if
+ # the specified one is empty, workaround this by setting it
+ # to a single space instead
+ self.set_title(" ")
+ self.set_skip_taskbar_hint(True)
+ self.vbox.set_spacing(14)
+ self.get_accessible().set_role(atk.ROLE_ALERT)
+
+ self._primary_label = gtk.Label()
+ self._secondary_label = gtk.Label()
+ self._details_label = gtk.Label()
+ self._image = gtk.image_new_from_stock(_IMAGE_TYPES[type],
+ gtk.ICON_SIZE_DIALOG)
+ self._image.set_alignment(0.5, 0.0)
+
+ self._primary_label.set_use_markup(True)
+ for label in (self._primary_label, self._secondary_label,
+ self._details_label):
+ label.set_line_wrap(True)
+ label.set_selectable(True)
+ label.set_alignment(0.0, 0.5)
+
+ hbox = gtk.HBox(False, 12)
+ hbox.set_border_width(5)
+ hbox.pack_start(self._image, False, False)
+
+ vbox = gtk.VBox(False, 0)
+ hbox.pack_start(vbox, False, False)
+ vbox.pack_start(self._primary_label, False, False)
+ vbox.pack_start(self._secondary_label, False, False)
+
+ self._expander = gtk.expander_new_with_mnemonic(
+ _("Show more _details"))
+ self._expander.set_spacing(6)
+ self._expander.add(self._details_label)
+ vbox.pack_start(self._expander, False, False)
+ self.vbox.pack_start(hbox, False, False)
+ hbox.show_all()
+ self._expander.hide()
+ self.add_buttons(*_BUTTON_TYPES[buttons])
+
+ def set_primary(self, text):
+ self._primary_label.set_markup(
+ "<span weight=\"bold\" size=\"larger\">%s</span>" % text)
+
+ def set_secondary(self, text):
+ self._secondary_label.set_markup(text)
+
+ def set_details(self, text):
+ self._details_label.set_text(text)
+ self._expander.show()
+
+ def set_details_widget(self, widget):
+ self._expander.remove(self._details_label)
+ self._expander.add(widget)
+ widget.show()
+ self._expander.show()
+
+class BaseDialog(gtk.Dialog):
+ def __init__(self, parent=None, title='', flags=0, buttons=()):
+ if parent and not isinstance(parent, gtk.Window):
+ raise TypeError("parent needs to be None or a gtk.Window subclass")
+
+ if not flags and parent:
+ flags &= (gtk.DIALOG_MODAL |
+ gtk.DIALOG_DESTROY_WITH_PARENT)
+
+ gtk.Dialog.__init__(self, title=title, parent=parent,
+ flags=flags, buttons=buttons)
+ self.set_border_width(6)
+ self.set_has_separator(False)
+ self.vbox.set_spacing(6)
+
+def messagedialog(dialog_type, short, long=None, parent=None,
+ buttons=gtk.BUTTONS_OK, default=-1):
+ """Create and show a MessageDialog.
+
+ @param dialog_type: one of constants
+ - gtk.MESSAGE_INFO
+ - gtk.MESSAGE_WARNING
+ - gtk.MESSAGE_QUESTION
+ - gtk.MESSAGE_ERROR
+ @param short: A header text to be inserted in the dialog.
+ @param long: A long description of message.
+ @param parent: The parent widget of this dialog
+ @type parent: a gtk.Window subclass
+ @param buttons: The button type that the dialog will be display,
+ one of the constants:
+ - gtk.BUTTONS_NONE
+ - gtk.BUTTONS_OK
+ - gtk.BUTTONS_CLOSE
+ - gtk.BUTTONS_CANCEL
+ - gtk.BUTTONS_YES_NO
+ - gtk.BUTTONS_OK_CANCEL
+ or a tuple or 2-sized tuples representing label and response. If label
+ is a stock-id a stock icon will be displayed.
+ @param default: optional default response id
+ """
+ if buttons in (gtk.BUTTONS_NONE, gtk.BUTTONS_OK, gtk.BUTTONS_CLOSE,
+ gtk.BUTTONS_CANCEL, gtk.BUTTONS_YES_NO,
+ gtk.BUTTONS_OK_CANCEL):
+ dialog_buttons = buttons
+ buttons = []
+ else:
+ if type(buttons) != tuple:
+ raise TypeError(
+ "buttons must be a GtkButtonsTypes constant or a tuple")
+ dialog_buttons = gtk.BUTTONS_NONE
+
+ if parent and not isinstance(parent, gtk.Window):
+ raise TypeError("parent must be a gtk.Window subclass")
+
+ d = HIGAlertDialog(parent=parent, flags=gtk.DIALOG_MODAL,
+ type=dialog_type, buttons=dialog_buttons)
+ for text, response in buttons:
+ d.add_buttons(text, response)
+
+ d.set_primary(short)
+
+ if long:
+ if isinstance(long, gtk.Widget):
+ d.set_details_widget(long)
+ elif isinstance(long, basestring):
+ d.set_details(long)
+ else:
+ raise TypeError("long must be a gtk.Widget or a string")
+
+ if default != -1:
+ d.set_default_response(default)
+
+ if parent:
+ d.set_transient_for(parent)
+ d.set_modal(True)
+
+ response = d.run()
+ d.destroy()
+ return response
+
+def _simple(type, short, long=None, parent=None, buttons=gtk.BUTTONS_OK,
+ default=-1):
+ if buttons == gtk.BUTTONS_OK:
+ default = gtk.RESPONSE_OK
+ return messagedialog(type, short, long,
+ parent=parent, buttons=buttons,
+ default=default)
+
+def error(short, long=None, parent=None, buttons=gtk.BUTTONS_OK, default=-1):
+ return _simple(gtk.MESSAGE_ERROR, short, long, parent=parent,
+ buttons=buttons, default=default)
+
+def info(short, long=None, parent=None, buttons=gtk.BUTTONS_OK, default=-1):
+ return _simple(gtk.MESSAGE_INFO, short, long, parent=parent,
+ buttons=buttons, default=default)
+
+def warning(short, long=None, parent=None, buttons=gtk.BUTTONS_OK, default=-1):
+ return _simple(gtk.MESSAGE_WARNING, short, long, parent=parent,
+ buttons=buttons, default=default)
+
+def yesno(text, parent=None, default=gtk.RESPONSE_YES):
+ return messagedialog(gtk.MESSAGE_WARNING, text, None, parent,
+ buttons=gtk.BUTTONS_YES_NO,
+ default=default)
+
+def open(title='', parent=None, patterns=[], folder=None):
+ """Displays an open dialog."""
+ filechooser = gtk.FileChooserDialog(title or _('Open'),
+ parent,
+ gtk.FILE_CHOOSER_ACTION_OPEN,
+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_OPEN, gtk.RESPONSE_OK))
+
+ if patterns:
+ file_filter = gtk.FileFilter()
+ for pattern in patterns:
+ file_filter.add_pattern(pattern)
+ filechooser.set_filter(file_filter)
+ filechooser.set_default_response(gtk.RESPONSE_OK)
+
+ if folder:
+ filechooser.set_current_folder(folder)
+
+ response = filechooser.run()
+ if response != gtk.RESPONSE_OK:
+ filechooser.destroy()
+ return
+
+ path = filechooser.get_filename()
+ if path and os.access(path, os.R_OK):
+ filechooser.destroy()
+ return path
+
+ abspath = os.path.abspath(path)
+
+ error(_('Could not open file "%s"') % abspath,
+ _('The file "%s" could not be opened. '
+ 'Permission denied.') % abspath)
+
+ filechooser.destroy()
+ return path
+
+def ask_overwrite(filename, parent=None):
+ submsg1 = _('A file named "%s" already exists') % os.path.abspath(filename)
+ submsg2 = _('Do you wish to replace it with the current one?')
+ text = ('<span weight="bold" size="larger">%s</span>\n\n%s\n'
+ % (submsg1, submsg2))
+ result = messagedialog(gtk.MESSAGE_ERROR, text, parent=parent,
+ buttons=((gtk.STOCK_CANCEL,
+ gtk.RESPONSE_CANCEL),
+ (_("Replace"),
+ gtk.RESPONSE_YES)))
+ return result == gtk.RESPONSE_YES
+
+def save(title='', parent=None, current_name='', folder=None):
+ """Displays a save dialog."""
+ filechooser = gtk.FileChooserDialog(title or _('Save'),
+ parent,
+ gtk.FILE_CHOOSER_ACTION_SAVE,
+ (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+ gtk.STOCK_SAVE, gtk.RESPONSE_OK))
+ if current_name:
+ filechooser.set_current_name(current_name)
+ filechooser.set_default_response(gtk.RESPONSE_OK)
+
+ if folder:
+ filechooser.set_current_folder(folder)
+
+ path = None
+ while True:
+ response = filechooser.run()
+ if response != gtk.RESPONSE_OK:
+ path = None
+ break
+
+ path = filechooser.get_filename()
+ if not os.path.exists(path):
+ break
+
+ if ask_overwrite(path, parent):
+ break
+ filechooser.destroy()
+ return path
+
+def _test():
+ yesno('Kill?', default=gtk.RESPONSE_NO)
+
+ info('Some information displayed not too long\nbut not too short',
+ long=('foobar ba asdjaiosjd oiadjoisjaoi aksjdasdasd kajsdhakjsdh\n'
+ 'askdjhaskjdha skjdhasdasdjkasldj alksdjalksjda lksdjalksdj\n'
+ 'asdjaslkdj alksdj lkasjdlkjasldkj alksjdlkasjd jklsdjakls\n'
+ 'ask;ldjaklsjdlkasjd alksdj laksjdlkasjd lkajs kjaslk jkl\n'),
+ default=gtk.RESPONSE_OK,
+ )
+
+ error('An error occurred', gtk.Button('Woho'))
+ error('Unable to mount the selected volume.',
+ 'mount: can\'t find /media/cdrom0 in /etc/fstab or /etc/mtab')
+ print open(title='Open a file', patterns=['*.py'])
+ print save(title='Save a file', current_name='foobar.py')
+
+if __name__ == '__main__':
+ _test()
diff --git a/kiwi/ui/entry.py b/kiwi/ui/entry.py
new file mode 100644
index 0000000..cd495b7
--- /dev/null
+++ b/kiwi/ui/entry.py
@@ -0,0 +1,603 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""
+An enchanced version of GtkEntry that supports icons and masks
+"""
+
+import gettext
+import string
+
+import gobject
+import pango
+import gtk
+
+from kiwi.ui.icon import IconEntry
+from kiwi.utils import PropertyObject, gsignal, gproperty, type_register
+
+class MaskError(Exception):
+ pass
+
+(INPUT_ASCII_LETTER,
+ INPUT_ALPHA,
+ INPUT_ALPHANUMERIC,
+ INPUT_DIGIT) = range(4)
+
+INPUT_FORMATS = {
+ '0': INPUT_DIGIT,
+ 'L': INPUT_ASCII_LETTER,
+ 'A': INPUT_ALPHANUMERIC,
+ 'a': INPUT_ALPHANUMERIC,
+ '&': INPUT_ALPHA,
+ }
+
+# Todo list: Other usefull Masks
+# 9 - Digit, optional
+# ? - Ascii letter, optional
+# C - Alpha, optional
+
+INPUT_CHAR_MAP = {
+ INPUT_ASCII_LETTER: lambda text: text in string.ascii_letters,
+ INPUT_ALPHA: unicode.isalpha,
+ INPUT_ALPHANUMERIC: unicode.isalnum,
+ INPUT_DIGIT: unicode.isdigit,
+ }
+
+
+(COL_TEXT,
+ COL_OBJECT) = range(2)
+
+(ENTRY_MODE_TEXT,
+ ENTRY_MODE_DATA) = range(2)
+
+_ = lambda msg: gettext.dgettext('kiwi', msg)
+
+class KiwiEntry(PropertyObject, gtk.Entry):
+ """
+ The KiwiEntry is a Entry subclass with the following additions:
+
+ - IconEntry, allows you to have an icon inside the entry
+ - Mask, force the input to meet certain requirements
+ - IComboMixin: Allows you work with objects instead of strings
+ Adds a number of convenience methods such as L{prefill}().
+ """
+ __gtype_name__ = 'KiwiEntry'
+
+ gproperty("completion", bool, False)
+ gproperty('exact-completion', bool, default=False)
+ gproperty("mask", str, default='')
+
+ def __init__(self):
+ gtk.Entry.__init__(self)
+ PropertyObject.__init__(self)
+
+ self.connect('insert-text', self._on_insert_text)
+ self.connect('delete-text', self._on_delete_text)
+
+ self._current_object = None
+ self._mode = ENTRY_MODE_TEXT
+ self._icon = IconEntry(self)
+
+ # List of validators
+ # str -> static characters
+ # int -> dynamic, according to constants above
+ self._mask_validators = []
+ self._mask = None
+ self._block_insert = False
+ self._block_delete = False
+
+ # Virtual methods
+
+ gsignal('size-allocate', 'override')
+ def do_size_allocate(self, allocation):
+ #gtk.Entry.do_size_allocate(self, allocation)
+ self.chain(allocation)
+
+ if self.flags() & gtk.REALIZED:
+ self._icon.resize_windows()
+
+ def do_expose_event(self, event):
+ gtk.Entry.do_expose_event(self, event)
+
+ if event.window == self.window:
+ self._icon.draw_pixbuf()
+
+ def do_realize(self):
+ gtk.Entry.do_realize(self)
+ self._icon.construct()
+
+ def do_unrealize(self):
+ self._icon.deconstruct()
+ gtk.Entry.do_unrealize(self)
+
+ # Properties
+
+ def prop_set_exact_completion(self, value):
+ self.set_exact_completion(value)
+ return value
+
+ def prop_set_completion(self, value):
+ if not self.get_completion():
+ self.set_completion(gtk.EntryCompletion())
+ return value
+
+ def prop_set_mask(self, value):
+ try:
+ self.set_mask(value)
+ return self.get_mask()
+ except MaskError, e:
+ pass
+ return ''
+
+ # Public API
+ def set_mask(self, mask):
+ """
+ Sets the mask of the Entry.
+ Supported format characters are:
+ - '0' digit
+ - 'L' ascii letter (a-z and A-Z)
+ - '&' alphabet, honors the locale
+ - 'a' alphanumeric, honors the locale
+ - 'A' alphanumeric, honors the locale
+
+ This is similar to MaskedTextBox:
+ U{http://msdn2.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask(VS.80).aspx}
+
+ Example mask for a ISO-8601 date
+ >>> entry.set_mask('0000-00-00')
+
+ @param mask: the mask to set
+ """
+
+ mask = unicode(mask)
+ if not mask:
+ self.modify_font(pango.FontDescription("sans"))
+ self._mask = mask
+ return
+
+ input_length = len(mask)
+ lenght = 0
+ pos = 0
+ while True:
+ if pos >= input_length:
+ break
+ if mask[pos] in INPUT_FORMATS:
+ self._mask_validators += [INPUT_FORMATS[mask[pos]]]
+ else:
+ self._mask_validators.append(mask[pos])
+ pos += 1
+
+ self.modify_font(pango.FontDescription("monospace"))
+
+ self.set_text("")
+ self._insert_mask(0, input_length)
+ self._mask = mask
+
+ def get_mask(self):
+ """
+ @returns: the mask
+ """
+ return self._mask
+
+ def get_field_text(self):
+ """
+ Get the fields assosiated with the entry.
+ A field is dynamic content separated by static.
+ For example, the format string 000-000 has two fields
+ separated by a dash.
+ if a field is empty it'll return an empty string
+ otherwise it'll include the content
+
+ @returns: fields
+ @rtype: list of strings
+ """
+ if not self._mask:
+ raise MaskError("a mask must be set before calling get_field_text")
+
+ def append_field(fields, field_type, s):
+ if s.count(' ') == len(s):
+ s = ''
+ if field_type == INPUT_DIGIT:
+ try:
+ s = int(s)
+ except ValueError:
+ s = None
+ fields.append(s)
+
+ fields = []
+ pos = 0
+ s = ''
+ field_type = -1
+ text = unicode(self.get_text())
+ validators = self._mask_validators
+ while True:
+ if pos >= len(validators):
+ append_field(fields, field_type, s)
+ break
+
+ validator = validators[pos]
+ if isinstance(validator, int):
+ try:
+ s += text[pos]
+ except IndexError:
+ s = ''
+ field_type = validator
+ else:
+ append_field(fields, field_type, s)
+ s = ''
+ field_type = -1
+ pos += 1
+
+ return fields
+
+ def get_empty_mask(self, start=None, end=None):
+ """
+ Gets the empty mask between start and end
+
+ @param start:
+ @param end:
+ @returns: mask
+ @rtype: string
+ """
+
+ if start is None:
+ start = 0
+ if end is None:
+ end = len(self._mask_validators)
+
+ s = ''
+ for validator in self._mask_validators[start:end]:
+ if isinstance(validator, int):
+ s += ' '
+ elif isinstance(validator, unicode):
+ s += validator
+ else:
+ raise AssertionError
+ return s
+
+ def set_exact_completion(self, value):
+ """
+ Enable exact entry completion.
+ Exact means it needs to start with the value typed
+ and the case needs to be correct.
+
+ @param value: enable exact completion
+ @type value: boolean
+ """
+
+ if value:
+ match_func = self._completion_exact_match_func
+ else:
+ match_func = self._completion_normal_match_func
+ completion = self._get_completion()
+ completion.set_match_func(match_func)
+
+ # Private
+
+ def _really_delete_text(self, start, end):
+ # A variant of delete_text() that never is blocked by us
+ self._block_delete = True
+ self.delete_text(start, end)
+ self._block_delete = False
+
+ def _really_insert_text(self, text, position):
+ # A variant of insert_text() that never is blocked by us
+ self._block_insert = True
+ self.insert_text(text, position)
+ self._block_insert = False
+
+ def _insert_mask(self, start, end):
+ text = self.get_empty_mask(start, end)
+ self._really_insert_text(text, position=start)
+
+ def _confirms_to_mask(self, position, text):
+ validators = self._mask_validators
+ if position >= len(validators):
+ return False
+
+ validator = validators[position]
+ if isinstance(validator, int):
+ if not INPUT_CHAR_MAP[validator](text):
+ return False
+ if isinstance(validator, unicode):
+ if validator == text:
+ return True
+ return False
+
+ return True
+
+ def _update_current_object(self, text):
+ if self._mode != ENTRY_MODE_DATA:
+ return
+
+ for row in self.get_completion().get_model():
+ if row[COL_TEXT] == text:
+ self._current_object = row[COL_OBJECT]
+ break
+ else:
+ # Customized validation
+ if text:
+ self.set_invalid(_("'%s' is not a valid object" % text))
+ elif self.mandatory:
+ self.set_blank()
+ else:
+ self.set_valid()
+ self._current_object = None
+
+ def _get_text_from_object(self, obj):
+ if self._mode != ENTRY_MODE_DATA:
+ return
+
+ for row in self.get_completion().get_model():
+ if row[COL_OBJECT] == obj:
+ return row[COL_TEXT]
+
+ def _get_completion(self):
+ # Check so we have completion enabled, not this does not
+ # depend on the property, the user can manually override it,
+ # as long as there is a completion object set
+ completion = self.get_completion()
+ if completion:
+ return completion
+
+ completion = gtk.EntryCompletion()
+ self.set_completion(completion)
+ return completion
+
+ def set_completion(self, completion):
+ gtk.Entry.set_completion(self, completion)
+ completion.set_model(gtk.ListStore(str, object))
+ completion.set_text_column(0)
+ self.set_exact_completion(False)
+ completion.connect("match-selected",
+ self._on_completion__match_selected)
+ self._current_object = None
+ return completion
+
+ def _completion_exact_match_func(self, completion, _, iter):
+ model = completion.get_model()
+ if not len(model):
+ return
+
+ content = model[iter][COL_TEXT]
+ return self.get_text().startswith(content)
+
+ def _completion_normal_match_func(self, completion, _, iter):
+ model = completion.get_model()
+ if not len(model):
+ return
+
+ content = model[iter][COL_TEXT].lower()
+ return self.get_text().lower() in content
+
+ def _on_completion__match_selected(self, completion, model, iter):
+ if not len(model):
+ return
+
+ # this updates current_object and triggers content-changed
+ self.set_text(model[iter][COL_TEXT])
+ self.set_position(-1)
+ # FIXME: Enable this at some point
+ #self.activate()
+
+ # Callbacks
+
+ def _on_insert_text(self, editable, new, length, position):
+ if not self._mask or self._block_insert:
+ return
+
+ position = self.get_position()
+ new = unicode(new)
+ for inc, c in enumerate(new):
+ if not self._confirms_to_mask(position + inc, c):
+ self.stop_emission('insert-text')
+ return
+
+ self._really_delete_text(position, position+1)
+
+ # If the next character is a static character and
+ # the one after the next is input, skip over
+ # the static character
+ next = position + 1
+ validators = self._mask_validators
+ if len(validators) > next + 1:
+ if (isinstance(validators[next], unicode) and
+ isinstance(validators[next+1], int)):
+ # Ugly: but it must be done after the entry
+ # inserts the text
+ gobject.idle_add(self.set_position, next+1)
+
+ def _on_delete_text(self, editable, start, end):
+ if not self._mask or self._block_delete:
+ return
+
+ # This is tricky, quite ugly but it works.
+ # We want to insert the mask after the delete is done
+ # Instead of using idle_add we delete the text first
+ # insert our mask afterwards and finally blocks the call
+ # from happing in the entry itself
+ self._really_delete_text(start, end)
+ self._insert_mask(start, end)
+
+ self.stop_emission('delete-text')
+
+ # IconEntry
+ def set_tooltip(self, text):
+ self._icon.set_tooltip(text)
+
+ def set_pixbuf(self, pixbuf):
+ self._icon.set_pixbuf(pixbuf)
+
+ def update_background(self, color):
+ self._icon.update_background(color)
+
+ def get_icon_window(self):
+ return self._icon.get_icon_window()
+
+ # IComboMixin
+
+ def prefill(self, itemdata, sort=False):
+ """Fills the Combo with listitems corresponding to the itemdata
+ provided.
+
+ Parameters:
+ - itemdata is a list of strings or tuples, each item corresponding
+ to a listitem. The simple list format is as follows::
+
+ >>> [ label0, label1, label2 ]
+
+ If you require a data item to be specified for each item, use a
+ 2-item tuple for each element. The format is as follows::
+
+ >>> [ ( label0, data0 ), (label1, data1), ... ]
+
+ - Sort is a boolean that specifies if the list is to be sorted by
+ label or not. By default it is not sorted
+ """
+ if not isinstance(itemdata, (list, tuple)):
+ raise TypeError("'data' parameter must be a list or tuple of item "
+ "descriptions, found %s") % type(itemdata)
+
+ completion = self._get_completion()
+ model = completion.get_model()
+
+ if len(itemdata) == 0:
+ model.clear()
+ return
+
+ if (len(itemdata) > 0 and
+ type(itemdata[0]) in (tuple, list) and
+ len(itemdata[0]) == 2):
+ mode = self._mode = ENTRY_MODE_DATA
+ else:
+ mode = self._mode
+
+ values = {}
+ if mode == ENTRY_MODE_TEXT:
+ if sort:
+ itemdata.sort()
+
+ for item in itemdata:
+ if item in values:
+ raise KeyError("Tried to insert duplicate value "
+ "%s into Combo!" % item)
+ else:
+ values[item] = None
+
+ model.append((item, None))
+ elif mode == ENTRY_MODE_DATA:
+ if sort:
+ itemdata.sort(lambda x, y: cmp(x[0], y[0]))
+
+ for item in itemdata:
+ text, data = item
+ if text in values:
+ raise KeyError("Tried to insert duplicate value "
+ "%s into Combo!" % item)
+ else:
+ values[text] = None
+ model.append((text, data))
+ else:
+ raise TypeError("Incorrect format for itemdata; see "
+ "docstring for more information")
+
+ def get_iter_by_data(self, data):
+ if self._mode != ENTRY_MODE_DATA:
+ raise TypeError(
+ "select_item_by_data can only be used in data mode")
+
+ completion = self._get_completion()
+ model = completion.get_model()
+
+ for row in model:
+ if row[COL_OBJECT] == data:
+ return row.iter
+ break
+ else:
+ raise KeyError("No item correspond to data %r in the combo %s"
+ % (data, self.name))
+
+ def get_iter_by_label(self, label):
+ completion = self._get_completion()
+ model = completion.get_model()
+ for row in model:
+ if row[COL_TEXT] == label:
+ return row.iter
+ else:
+ raise KeyError("No item correspond to label %r in the combo %s"
+ % (label, self.name))
+
+ def get_selected_by_iter(self, treeiter):
+ completion = self._get_completion()
+ model = completion.get_model()
+ mode = self._mode
+ text = model[treeiter][COL_TEXT]
+ if text != self.get_text():
+ return
+
+ if mode == ENTRY_MODE_TEXT:
+ return text
+ elif mode == ENTRY_MODE_DATA:
+ return model[treeiter][COL_OBJECT]
+ else:
+ raise AssertionError
+
+ def get_selected_label(self, treeiter):
+ completion = self._get_completion()
+ model = completion.get_model()
+ return model[treeiter][COL_TEXT]
+
+ def get_iter_from_obj(self, obj):
+ mode = self._mode
+ if mode == ENTRY_MODE_TEXT:
+ return self.get_iter_by_label(obj)
+ elif mode == ENTRY_MODE_DATA:
+ return self.get_iter_by_data(obj)
+ else:
+ # XXX: When setting the datatype to non string, automatically go to
+ # data mode
+ raise TypeError("unknown Entry mode. Did you call prefill?")
+
+type_register(KiwiEntry)
+
+def main(args):
+ win = gtk.Window()
+ win.set_title('gtk.Entry subclass')
+ def cb(window, event):
+ print 'fields', widget.get_field_text()
+ gtk.main_quit()
+ win.connect('delete-event', cb)
+
+ widget = KiwiEntry()
+ widget.set_mask('000.000.000.000')
+
+ win.add(widget)
+
+ win.show_all()
+
+ widget.select_region(0, 0)
+ gtk.main()
+
+if __name__ == '__main__':
+ import sys
+ sys.exit(main(sys.argv))
diff --git a/kiwi/ui/gadgets.py b/kiwi/ui/gadgets.py
new file mode 100644
index 0000000..442a962
--- /dev/null
+++ b/kiwi/ui/gadgets.py
@@ -0,0 +1,182 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""Graphical utilities: color management and eyecandy"""
+
+import gobject
+import gtk
+from gtk import gdk
+
+from kiwi.log import Logger
+from kiwi.utils import gsignal, type_register
+
+def gdk_color_to_string(color):
+ """Convert a color to a #AABBCC string"""
+ return "#%02X%02X%02X" % (int(color.red) >> 8,
+ int(color.green) >> 8,
+ int(color.blue) >> 8)
+
+def set_foreground(widget, color, state=gtk.STATE_NORMAL):
+ """
+ Set the foreground color of a widget:
+
+ - widget: the widget we are changing the color
+ - color: a hexadecimal code or a well known color name
+ - state: the state we are afecting, see gtk.STATE_*
+ """
+ widget.modify_fg(state, gdk.color_parse(color))
+
+def get_foreground(widget, state=gtk.STATE_NORMAL):
+ """Return the foreground color of the widget as a string"""
+ style = widget.get_style()
+ color = style.fg[state]
+ return gdk_color_to_string(color)
+
+def set_background(widget, color, state=gtk.STATE_NORMAL):
+ """
+ Set the background color of a widget:
+
+ - widget: the widget we are changing the color
+ - color: a hexadecimal code or a well known color name
+ - state: the state we are afecting, see gtk.STATE_*
+ """
+ if isinstance(widget, gtk.Entry):
+ widget.modify_base(state, gdk.color_parse(color))
+ else:
+ widget.modify_bg(state, gdk.color_parse(color))
+
+def get_background(widget, state=gtk.STATE_NORMAL):
+ """Return the background color of the widget as a string"""
+ style = widget.get_style()
+ color = style.bg[state]
+ return gdk_color_to_string(color)
+
+def quit_if_last(*args):
+ windows = [toplevel
+ for toplevel in gtk.window_list_toplevels()
+ if toplevel.get_property('type') == gtk.WINDOW_TOPLEVEL]
+ if len(windows) == 1:
+ gtk.main_quit()
+
+
+class FadeOut(gobject.GObject):
+ """I am a helper class to draw the fading effect of the background
+ Call my methods start() and stop() to control the fading.
+ """
+ gsignal('done')
+ gsignal('color-changed', gdk.Color)
+
+ # How long time it'll take before we start (in ms)
+ COMPLAIN_DELAY = 500
+
+ MERGE_COLORS_DELAY = 100
+
+ # XXX: Fetch the default value from the widget instead of hard coding it.
+ GOOD_COLOR = "white"
+ ERROR_COLOR = "#ffd5d5"
+
+ def __init__(self, widget):
+ gobject.GObject.__init__(self)
+ self._widget = widget
+ self._background_timeout_id = -1
+ self._countdown_timeout_id = -1
+ self._log = Logger('fade')
+ self._done = False
+
+ def _merge_colors(self, src_color, dst_color, steps=10):
+ """
+ Change the background of widget from src_color to dst_color
+ in the number of steps specified
+ """
+ self._log.debug('_merge_colors: %s -> %s' % (src_color, dst_color))
+
+ gdk_src = gdk.color_parse(src_color)
+ gdk_dst = gdk.color_parse(dst_color)
+ rs, gs, bs = gdk_src.red, gdk_src.green, gdk_src.blue
+ rd, gd, bd = gdk_dst.red, gdk_dst.green, gdk_dst.blue
+ rinc = (rd - rs) / float(steps)
+ ginc = (gd - gs) / float(steps)
+ binc = (bd - bs) / float(steps)
+ for dummy in xrange(steps):
+ rs += rinc
+ gs += ginc
+ bs += binc
+ col = gdk.color_parse("#%02X%02X%02X" % (int(rs) >> 8,
+ int(gs) >> 8,
+ int(bs) >> 8))
+ self.emit('color-changed', col)
+ yield True
+
+ self.emit('done')
+ self._background_timeout_id = -1
+ self._done = True
+ yield False
+
+ def _start_merging(self):
+ # If we changed during the delay
+ if self._background_timeout_id != -1:
+ self._log.debug('_start_merging: Already running')
+ return
+
+ self._log.debug('_start_merging: Starting')
+ func = self._merge_colors(FadeOut.GOOD_COLOR,
+ FadeOut.ERROR_COLOR).next
+ self._background_timeout_id = (
+ gobject.timeout_add(FadeOut.MERGE_COLORS_DELAY, func))
+ self._countdown_timeout_id = -1
+
+ def start(self):
+ """Schedules a start of the countdown.
+ @returns: True if we could start, False if was already in progress
+ """
+ if self._background_timeout_id != -1:
+ self._log.debug('start: Background change already running')
+ return False
+ if self._countdown_timeout_id != -1:
+ self._log.debug('start: Countdown already running')
+ return False
+ if self._done:
+ self._log.debug('start: Not running, already set')
+ return False
+
+ self._log.debug('start: Scheduling')
+ self._countdown_timeout_id = gobject.timeout_add(
+ FadeOut.COMPLAIN_DELAY, self._start_merging)
+
+ return True
+
+ def stop(self):
+ """Stops the fadeout and restores the background color"""
+ self._log.debug('Stopping')
+ if self._background_timeout_id != -1:
+ gobject.source_remove(self._background_timeout_id)
+ self._background_timeout_id = -1
+ if self._countdown_timeout_id != -1:
+ gobject.source_remove(self._countdown_timeout_id)
+ self._countdown_timeout_id = -1
+
+ self._widget.update_background(gdk.color_parse(FadeOut.GOOD_COLOR))
+ self._done = False
+
+type_register(FadeOut)
diff --git a/kiwi/ui/gazpacholoader.py b/kiwi/ui/gazpacholoader.py
new file mode 100644
index 0000000..b5e4426
--- /dev/null
+++ b/kiwi/ui/gazpacholoader.py
@@ -0,0 +1,360 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""Gazpacho integration: loader and extensions"""
+
+import datetime
+from decimal import Decimal
+import gettext
+import os
+import warnings
+
+import gobject
+import gtk
+from gazpacho.editor import PropertyCustomEditor
+from gazpacho.loader.loader import ObjectBuilder
+from gazpacho.loader.custom import Adapter, ComboBoxAdapter, \
+ PythonWidgetAdapter, adapter_registry
+from gazpacho.properties import prop_registry, CustomProperty, StringType
+from gazpacho.widgets.base.base import ContainerAdaptor
+from gazpacho.widgets.base.box import BoxAdaptor
+
+from kiwi.datatypes import currency
+from kiwi.environ import environ
+from kiwi.log import Logger
+from kiwi.python import disabledeprecationcall
+from kiwi.ui.hyperlink import HyperLink
+from kiwi.ui.objectlist import Column, ObjectList
+from kiwi.ui.widgets.checkbutton import ProxyCheckButton
+from kiwi.ui.widgets.combo import ProxyComboEntry, ProxyComboBox, \
+ ProxyComboBoxEntry
+from kiwi.ui.widgets.entry import ProxyDateEntry, ProxyEntry
+from kiwi.ui.widgets.label import ProxyLabel
+from kiwi.ui.widgets.radiobutton import ProxyRadioButton
+from kiwi.ui.widgets.spinbutton import ProxySpinButton
+from kiwi.ui.widgets.textview import ProxyTextView
+
+# Backwards compatibility + pyflakes
+from kiwi.ui.widgets.combobox import ComboBox, ComboBoxEntry
+from kiwi.ui.widgets.list import List
+HyperLink
+_ = gettext.gettext
+
+log = Logger('gazpacholoader')
+
+class Builder(ObjectBuilder):
+ def find_resource(self, filename):
+ return environ.find_resource("pixmaps", filename)
+
+class GazpachoWidgetTree:
+ """Example class of GladeAdaptor that uses Gazpacho loader to load the
+ glade files
+ """
+ def __init__(self, view, gladefile, widgets, gladename=None, domain=None):
+
+ if not gladefile:
+ raise ValueError("A gladefile wasn't provided.")
+ elif not isinstance(gladefile, basestring):
+ raise TypeError(
+ "gladefile should be a string, found %s" % type(gladefile))
+ filename = os.path.splitext(os.path.basename(gladefile))[0]
+ self._filename = filename + '.glade'
+ self._view = view
+ self._gladefile = environ.find_resource("glade", self._filename)
+ self._widgets = (widgets or view.widgets or [])[:]
+ self.gladename = gladename or filename
+ self._showwarning = warnings.showwarning
+ warnings.showwarning = self._on_load_warning
+ self._tree = Builder(self._gladefile, domain=domain)
+ warnings.showwarning = self._showwarning
+ if not self._widgets:
+ self._widgets = [w.get_data("gazpacho::object-id")
+ for w in self._tree.get_widgets()]
+ self._attach_widgets()
+
+ def _on_load_warning(self, warning, category, file, line):
+ self._showwarning('while loading glade file: %s' % warning,
+ category, self._filename, '???')
+
+ def _attach_widgets(self):
+ # Attach widgets in the widgetlist to the view specified, so
+ # widgets = [label1, button1] -> view.label1, view.button1
+ for w in self._widgets:
+ widget = self._tree.get_widget(w)
+ if widget is not None:
+ setattr(self._view, w, widget)
+ else:
+ log.warn("Widget %s was not found in glade widget tree." % w)
+
+ def get_widget(self, name):
+ """Retrieves the named widget from the View (or glade tree)"""
+ name = name.replace('.', '_')
+ name = name.replace('-', '_')
+ widget = self._tree.get_widget(name)
+ if widget is None:
+ raise AttributeError(
+ "Widget %s not found in view %s" % (name, self._view))
+ return widget
+
+ def get_widgets(self):
+ return self._tree.get_widgets()
+
+ def signal_autoconnect(self, dic):
+ self._tree.signal_autoconnect(dic)
+
+ def get_sizegroups(self):
+ return self._tree.sizegroups
+
+# Normal widgets
+for prop in ('normal-color', 'normal-underline', 'normal-bold',
+ 'hover-color', 'hover-underline', 'hover-bold',
+ 'active-color', 'active-underline', 'active-bold'):
+ prop_registry.override_simple('HyperLink::%s' % prop, editable=False)
+
+class HyperLinkAdaptor(ContainerAdaptor):
+ def fill_empty(self, context, widget):
+ pass
+
+ def post_create(self, context, widget, interactive):
+ widget.set_text(widget.get_name())
+
+class ComboEntryAdaptor(BoxAdaptor):
+ def get_children(self, context, comboentry):
+ return []
+
+class DateEntryAdaptor(BoxAdaptor):
+ def get_children(self, context, comboentry):
+ return []
+
+class KiwiColumnAdapter(Adapter):
+ object_type = Column
+ def construct(self, name, gtype, properties):
+ return Column(name)
+adapter_registry.register_adapter(KiwiColumnAdapter)
+
+class ObjectListAdapter(PythonWidgetAdapter):
+ object_type = ObjectList
+ def construct(self, name, gtype, properties):
+ if gtype == List:
+ gtype == ObjectList
+ return super(ObjectListAdapter, self).construct(name, gtype,
+ properties)
+adapter_registry.register_adapter(ObjectListAdapter)
+
+# Framework widgets
+
+class DataTypeAdaptor(PropertyCustomEditor):
+ def __init__(self):
+ super(DataTypeAdaptor, self).__init__()
+ self._input = self.create_editor()
+
+ def get_editor_widget(self):
+ return self._input
+
+ def get_data_types(self):
+ """
+ Subclasses should override this.
+ Expected to return a list of 2 sized tuples with
+ name of type and type, to be used in a combo box.
+ """
+ raise NotImplementedError
+
+ def create_editor(self):
+ model = gtk.ListStore(str, object)
+ for datatype in self.get_data_types():
+ model.append(datatype)
+ combo = gtk.ComboBox(model)
+ renderer = gtk.CellRendererText()
+ combo.pack_start(renderer)
+ combo.add_attribute(renderer, 'text', 0)
+ combo.set_active(0)
+ combo.set_data('connection-id', -1)
+ return combo
+
+ def update(self, context, kiwiwidget, proxy):
+ combo = self._input
+ connection_id = combo.get_data('connection-id')
+ if (connection_id != -1):
+ combo.disconnect(connection_id)
+ model = combo.get_model()
+ connection_id = combo.connect('changed', self._editor_edit,
+ proxy, model)
+ combo.set_data('connection-id', connection_id)
+ value = kiwiwidget.get_property('data-type')
+ for row in model:
+ if row[1] == value:
+ combo.set_active_iter(row.iter)
+ break
+
+ def _editor_edit(self, combo, proxy, model):
+ active_iter = combo.get_active_iter()
+ proxy.set_value(model[active_iter][1])
+
+class SpinBtnDataType(DataTypeAdaptor):
+ def get_data_types(self):
+ return [
+ (_('Integer'), int),
+ (_('Float'), float),
+ (_('Decimal'), Decimal),
+ (_('Currency'), currency)
+ ]
+
+class EntryDataType(DataTypeAdaptor):
+ def get_data_types(self):
+ return [
+ (_('String'), str),
+ (_('Unicode'), unicode),
+ (_('Integer'), int),
+ (_('Float'), float),
+ (_('Decimal'), Decimal),
+ (_('Currency'), currency),
+ (_('Date'), datetime.date),
+ (_('Date and Time'), datetime.datetime),
+ (_('Time'), datetime.time),
+ (_('Object'), object)
+ ]
+
+class TextViewDataType(DataTypeAdaptor):
+ def get_data_types(self):
+ return [
+ (_('String'), str),
+ (_('Unicode'), unicode),
+ (_('Integer'), int),
+ (_('Float'), float),
+ (_('Decimal'), Decimal),
+ (_('Date'), datetime.date),
+ ]
+
+class ComboBoxDataType(DataTypeAdaptor):
+ def get_data_types(self):
+ return [
+ (_('String'), str),
+ (_('Unicode'), unicode),
+ (_('Boolean'), bool),
+ (_('Integer'), int),
+ (_('Float'), float),
+ (_('Decimal'), Decimal),
+ (_('Object'), object)
+ ]
+
+class LabelDataType(DataTypeAdaptor):
+ def get_data_types(self):
+ return [
+ (_('String'), str),
+ (_('Unicode'), unicode),
+ (_('Boolean'), bool),
+ (_('Integer'), int),
+ (_('Float'), float),
+ (_('Decimal'), Decimal),
+ (_('Date'), datetime.date),
+ (_('Date and Time'), datetime.datetime),
+ (_('Time'), datetime.time),
+ (_('Currency'), currency)
+ ]
+
+class DataType(CustomProperty, StringType):
+ translatable = False
+ def save(self):
+ value = self.get()
+ if value is not None:
+ return value.__name__
+
+class BoolOnlyDataType(CustomProperty, StringType):
+ translatable = False
+ editable = False
+ def save(self):
+ return 'bool'
+
+class DateOnlyDataType(CustomProperty, StringType):
+ translatable = False
+ editable = False
+ def save(self):
+ return 'date'
+
+class ModelProperty(CustomProperty, StringType):
+ translatable = False
+
+class DataValueProperty(CustomProperty, StringType):
+ translatable = False
+
+# Register widgets which have data-type and model-attributes
+# ComboBox is a special case, it needs to inherit from another
+# adapter and need to support two types.
+class KiwiComboBoxAdapter(ComboBoxAdapter):
+ object_type = ProxyComboBox, ProxyComboBoxEntry
+ def construct(self, name, gtype, properties):
+ if gtype in (ProxyComboBox.__gtype__,
+ ComboBox.__gtype__):
+ object_type = ProxyComboBox
+ elif gtype in (ProxyComboBoxEntry.__gtype__,
+ ComboBoxEntry.__gtype__):
+ object_type = ProxyComboBoxEntry
+ else:
+ raise AssertionError("Unknown ComboBox GType: %r" % gtype)
+
+ obj = disabledeprecationcall(object_type)
+ obj.set_name(name)
+ return obj
+adapter_registry.register_adapter(KiwiComboBoxAdapter)
+
+def register_widgets():
+ for gobj, editor, data_type in [
+ (ProxyEntry, EntryDataType, DataType),
+ (ProxyDateEntry, None, DateOnlyDataType),
+ (ProxyCheckButton, None, BoolOnlyDataType),
+ (ProxyLabel, LabelDataType, DataType),
+ (ProxyComboBox, ComboBoxDataType, DataType),
+ (ProxyComboBoxEntry, ComboBoxDataType, DataType),
+ (ProxyComboEntry, ComboBoxDataType, DataType),
+ (ProxySpinButton, SpinBtnDataType, DataType),
+ (ProxyRadioButton, None, BoolOnlyDataType),
+ (ProxyTextView, TextViewDataType, DataType)
+ ]:
+ # Property overrides, used in the editor
+ type_name = gobject.type_name(gobj)
+
+ data_name = type_name + '::data-type'
+ if editor:
+ prop_registry.override_simple(data_name, data_type, editor=editor)
+ else:
+ prop_registry.override_simple(data_name, data_type)
+
+ prop_registry.override_simple(type_name + '::model-attribute',
+ ModelProperty)
+
+ if issubclass(gobj, ProxyRadioButton):
+ prop_registry.override_simple(type_name + '::data-value',
+ DataValueProperty)
+ # Register custom adapters, since gobject.new is broken in 2.6
+ # Used by loader, eg in gazpacho and in applications
+ # ComboBox is registered above
+ if gobj == ProxyComboBox:
+ continue
+
+ klass = type('Kiwi%sAdapter', (PythonWidgetAdapter,),
+ dict(object_type=gobj))
+ adapter_registry.register_adapter(klass)
+
+if not environ.epydoc:
+ register_widgets()
diff --git a/kiwi/ui/hyperlink.py b/kiwi/ui/hyperlink.py
new file mode 100644
index 0000000..988cef3
--- /dev/null
+++ b/kiwi/ui/hyperlink.py
@@ -0,0 +1,255 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): (C) Ali Afshar <aafshar@gmail.com>
+#
+# Contact Ali if you require release under a different license.
+
+
+"""A hyper link widget."""
+
+
+from cgi import escape
+
+import gtk
+
+from kiwi.utils import gsignal, gproperty, PropertyObject, type_register
+
+class HyperLink(PropertyObject, gtk.EventBox):
+ __gtype_name__ = 'HyperLink'
+ """
+ A hyperlink widget.
+
+ This widget behaves much like a hyperlink from a browser. The markup that
+ will be displayed is contained in the properties normal-markup
+ hover-markup and active-markup. There is a clicked signal which is fired
+ when hyperlink is clicked with the left mouse button.
+
+ Additionally, the user may set a menu that will be popped up when the user
+ right clicks the hyperlink.
+ """
+
+ gproperty('text', str, '')
+ gproperty('normal-color', str, '#0000c0')
+ gproperty('normal-underline', bool, False)
+ gproperty('normal-bold', bool, False)
+ gproperty('hover-color', str, '#0000c0')
+ gproperty('hover-underline', bool, True)
+ gproperty('hover-bold', bool, False)
+ gproperty('active-color', str, '#c00000')
+ gproperty('active-underline', bool, True)
+ gproperty('active-bold', bool, False)
+
+ gsignal('clicked')
+ gsignal('right-clicked')
+
+ def __init__(self, text=None, menu=None):
+ """
+ Create a new hyperlink.
+
+ @param text: The text of the hyperlink.
+ @type text: str
+ """
+ gtk.EventBox.__init__(self)
+ PropertyObject.__init__(self)
+ self._gproperties = {}
+ if text is not None:
+ self.set_property('text', text)
+ self._is_active = False
+ self._is_hover = False
+ self._menu = menu
+ self._label = gtk.Label()
+ self.add(self._label)
+ self.add_events(gtk.gdk.BUTTON_PRESS_MASK |
+ gtk.gdk.BUTTON_RELEASE_MASK |
+ gtk.gdk.ENTER_NOTIFY_MASK |
+ gtk.gdk.LEAVE_NOTIFY_MASK)
+ self.connect('button-press-event', self._on_button_press_event)
+ self.connect('button-release-event', self._on_button_release_event)
+ self.connect('enter-notify-event', self._on_hover_changed, True)
+ self.connect('leave-notify-event', self._on_hover_changed, False)
+ self.connect('map-event', self._on_map_event)
+ self.connect('notify', self._on_notify)
+ self.set_text(text)
+
+ # public API
+
+ def get_text(self):
+ """
+ Return the hyperlink text.
+ """
+ return self.text
+
+ def set_text(self, text):
+ """
+ Set the text of the hyperlink.
+
+ @param text: The text to set the hyperlink to.
+ @type text: str
+ """
+ self.text = text
+ self._update_look()
+
+ def set_menu(self, menu):
+ """
+ Set the menu to be used for popups.
+
+ @param menu: the gtk.Menu to be used.
+ @type menu: gtk.Menu
+ """
+ self._menu = menu
+
+ def has_menu(self):
+ """
+ Return whether the widget has a menu set.
+
+ @return: a boolean value indicating whether the internal menu has been
+ set.
+ """
+ return self._menu is not None
+
+ def popup(self, menu=None, button=3, etime=0L):
+ """
+ Popup the menu and emit the popup signal.
+
+ @param menu: The gtk.Menu to be popped up. This menu will be
+ used instead of the internally set menu. If this parameter is not
+ passed or None, the internal menu will be used.
+ @type menu: gtk.Menu
+ @param button: An integer representing the button number pressed to
+ cause the popup action.
+ @type button: int
+ @param etime: The time that the popup event was initiated.
+ @type etime: long
+ """
+ if menu is None:
+ menu = self._menu
+ if menu is not None:
+ menu.popup(None, None, None, button, etime)
+ self.emit('right-clicked')
+
+ def clicked(self):
+ """
+ Fire a clicked signal.
+ """
+ self.emit('clicked')
+
+ def get_label(self):
+ """
+ Get the internally stored widget.
+ """
+ return self._label
+
+ # private API
+
+ def _update_look(self):
+ """
+ Update the look of the hyperlink depending on state.
+ """
+ if self._is_active:
+ state = 'active'
+ elif self._is_hover:
+ state = 'hover'
+ else:
+ state = 'normal'
+ color = self.get_property('%s-color' % state)
+ underline = self.get_property('%s-underline' % state)
+ bold = self.get_property('%s-bold' % state)
+ markup_string = self._build_markup(self.get_text() or '',
+ color, underline, bold)
+ self._label.set_markup(markup_string)
+
+ def _build_markup(self, text, color, underline, bold):
+ """
+ Build a marked up string depending on parameters.
+ """
+ out = '<span color="%s">%s</span>' % (color, escape(text))
+ if underline:
+ out = '<u>%s</u>' % out
+ if bold:
+ out = '<b>%s</b>' % out
+ return out
+
+ # signal callbacks
+
+ def _on_button_press_event(self, eventbox, event):
+ """
+ Called on mouse down.
+
+ Behaves in 2 ways.
+ 1. if left-button, register the start of a click and grab the
+ mouse.
+ 1. if right-button, emit a right-clicked signal +/- popup the
+ menu.
+ """
+ if event.button == 1:
+ self.grab_add()
+ self._is_active = True
+ self._update_look()
+ elif event.button == 3:
+ if event.type == gtk.gdk.BUTTON_PRESS:
+ self.popup(button=event.button, etime=event.time)
+
+ def _on_button_release_event(self, eventbox, event):
+ """
+ Called on mouse up.
+
+ If the left-button is released and the widget was earlier activated by
+ a mouse down event a clicked signal is fired.
+ """
+ if event.button == 1:
+ self.grab_remove()
+ if self._is_active:
+ self.clicked()
+ self._is_active = False
+ self._update_look()
+
+ def _on_hover_changed(self, eb, event, hover):
+ """
+ Called when the mouse pinter enters or leaves the widget.
+
+ @param hover: Whether the mouse has entered the widget.
+ @type hover: boolean
+ """
+ self._is_hover = hover
+ self._update_look()
+
+ def _on_notify(self, eventbox, param):
+ """
+ Called on property notification.
+
+ Ensure that the look is up to date with the properties
+ """
+ if (param.name == 'text' or
+ param.name.endswith('-color') or
+ param.name.endswith('-underline') or
+ param.name.endswith('-bold')):
+ self._update_look()
+
+ def _on_map_event(self, eventbox, event):
+ """
+ Called on initially mapping the widget.
+
+ Used here to set the cursor type.
+ """
+ cursor = gtk.gdk.Cursor(gtk.gdk.HAND1)
+ self.window.set_cursor(cursor)
+
+type_register(HyperLink)
diff --git a/kiwi/ui/icon.py b/kiwi/ui/icon.py
new file mode 100644
index 0000000..9ff8794
--- /dev/null
+++ b/kiwi/ui/icon.py
@@ -0,0 +1,280 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+#
+# This is tricky and contains quite a few hacks:
+# An entry contains 2 GdkWindows, one for the background and one for
+# the text area. The normal one, on which the (normally white) background
+# is drawn can be accessed through entry.window (after realization)
+# The other window is the one where the cursor and the text is drawn upon,
+# it's refered to as "text area" inside the GtkEntry code and it is called
+# the same here. It can only be accessed through window.get_children()[0],
+# since it's considered private to the entry.
+#
+# +-------------------------------------+
+# | (1) | (1) parent widget (grey)
+# |+----------------(2)----------------+|
+# || |-- /-\ | || (2) entry.window (white)
+# || |- | | |(4) (3) ||
+# || | \-/ | || (3) text area (transparent)
+# |+-----------------------------------+|
+# |-------------------------------------| (4) cursor, black
+# | |
+# +-------------------------------------|
+#
+# So, now we want to put an icon in the edge:
+# An earlier approached by Lorzeno drew the icon directly on the text area,
+# which is not desired since if the text is using the whole width of the
+# entry the icon will be drawn on top of the text.
+# Now what we want to do is to resize the text area and create a
+# new window upon which we can draw the icon.
+#
+# +-------------------------------------+
+# | | (5) icon window
+# |+----------------------------++-----+|
+# || |-- /-\ | || ||
+# || |- | | | || (5) ||
+# || | \-/ | || ||
+# |+----------------------------++-----+|
+# |-------------------------------------|
+# | |
+# +-------------------------------------+
+#
+# When resizing the text area the cursor and text is not moved into the
+# correct position, it'll still be off by the width of the icon window
+# To fix this we need to call a private function, gtk_entry_recompute,
+# a workaround is to call set_visiblity() which calls recompute()
+# internally.
+#
+
+"""Provides a helper classes for displaying icons in widgets.
+
+Currently only a L{kiwi.ui.widgets.entry.Entry} and widgets
+which embed them, L{kiwi.ui.widgets.spinbutton.SpinButton} and
+L{kiwi.ui.comboboxentry.BaseComboBoxEntry}.
+"""
+
+import gtk
+from gtk import gdk
+from kiwi.ui.tooltip import Tooltip
+
+class IconEntry(object):
+ """
+ Helper object for rendering an icon in a GtkEntry
+ """
+
+ def __init__(self, entry):
+ if not isinstance(entry, gtk.Entry):
+ raise TypeError("entry must be a gtk.Entry")
+ self._constructed = False
+ self._pixbuf = None
+ self._pixw = 1
+ self._pixh = 1
+ self._text_area = None
+ self._text_area_pos = (0, 0)
+ self._icon_win = None
+ self._entry = entry
+ self._tooltip = Tooltip(self)
+ entry.connect('enter-notify-event',
+ self._on_entry__enter_notify_event)
+ entry.connect('leave-notify-event',
+ self._on_entry__leave_notify_event)
+ entry.connect('notify::xalign',
+ self._on_entry__notify_xalign)
+ self._update_position()
+
+ def _on_entry__notify_xalign(self, entry, pspec):
+ self._update_position()
+
+ def _on_entry__enter_notify_event(self, entry, event):
+ icon_win = self.get_icon_window()
+ if event.window != icon_win:
+ return
+
+ self._tooltip.display(entry)
+
+ def _on_entry__leave_notify_event(self, entry, event):
+ if event.window != self.get_icon_window():
+ return
+
+ self._tooltip.hide()
+
+ def set_tooltip(self, text):
+ self._tooltip.set_text(text)
+
+ def get_icon_window(self):
+ return self._icon_win
+
+ def set_pixbuf(self, pixbuf):
+ """
+ @param pixbuf: a gdk.Pixbuf or None
+ """
+ entry = self._entry
+ if not isinstance(entry.get_toplevel(), gtk.Window):
+ # For widgets in SlaveViews, wait until they're attached
+ # to something visible, then set the pixbuf
+ entry.connect_object('realize', self.set_pixbuf, pixbuf)
+ return
+
+ if pixbuf:
+ if not isinstance(pixbuf, gdk.Pixbuf):
+ raise TypeError("pixbuf must be a GdkPixbuf")
+ else:
+ # Turning of the icon should also restore the background
+ entry.modify_base(gtk.STATE_NORMAL, None)
+ if not self._pixbuf:
+ return
+ self._pixbuf = pixbuf
+
+ if pixbuf:
+ self._pixw = pixbuf.get_width()
+ self._pixh = pixbuf.get_height()
+ else:
+ self._pixw = self._pixh = 0
+
+ win = self._icon_win
+ if not win:
+ self.construct()
+ win = self._icon_win
+
+ self.resize_windows()
+
+ # XXX: Why?
+ if win:
+ if not pixbuf:
+ win.hide()
+ else:
+ win.show()
+
+ # Hack: This triggers a .recompute() which is private
+ entry.set_visibility(entry.get_visibility())
+ entry.queue_draw()
+
+ def construct(self):
+ if self._constructed:
+ return
+
+ entry = self._entry
+ if not entry.flags() & gtk.REALIZED:
+ entry.realize()
+
+ # Hack: Save a reference to the text area, now when its created
+ self._text_area = entry.window.get_children()[0]
+ self._text_area_pos = self._text_area.get_position()
+
+ # PyGTK should allow default values for most of the values here.
+ win = gtk.gdk.Window(entry.window,
+ self._pixw, self._pixh,
+ gtk.gdk.WINDOW_CHILD,
+ (gtk.gdk.ENTER_NOTIFY_MASK |
+ gtk.gdk.LEAVE_NOTIFY_MASK),
+ gtk.gdk.INPUT_OUTPUT,
+ 'icon window',
+ 0, 0,
+ entry.get_visual(),
+ entry.get_colormap(),
+ gtk.gdk.Cursor(entry.get_display(), gdk.LEFT_PTR),
+ '', '', True)
+ self._icon_win = win
+ win.set_user_data(entry)
+ win.set_background(entry.style.base[entry.state])
+ self._constructed = True
+
+ def deconstruct(self):
+ if self._icon_win:
+ # This is broken on PyGTK 2.6.x
+ try:
+ self._icon_win.set_user_data(None)
+ except:
+ pass
+ # Destroy not needed, called by the GC.
+ self._icon_win = None
+
+ def update_background(self, color):
+ if not self._icon_win:
+ return
+
+ self._entry.modify_base(gtk.STATE_NORMAL, color)
+
+ self.draw_pixbuf()
+
+ def resize_windows(self):
+ if not self._pixbuf:
+ return
+
+ icony = iconx = 4
+
+ # Make space for the icon, both windows
+ winw = self._entry.window.get_size()[0]
+ textw, texth = self._text_area.get_size()
+ textw = winw - self._pixw - (iconx + icony)
+
+ if self._pos == gtk.POS_LEFT:
+ textx, texty = self._text_area_pos
+ textx += iconx + self._pixw
+
+ # FIXME: Why is this needed. Focus padding?
+ # The text jumps without this
+ textw -= 2
+ self._text_area.move_resize(textx, texty, textw, texth)
+ elif self._pos == gtk.POS_RIGHT:
+ self._text_area.resize(textw, texth)
+ iconx += textw
+
+ icon_win = self._icon_win
+ # XXX: Why?
+ if not icon_win:
+ return
+
+ # If the size of the window is large enough, resize and move it
+ # Otherwise just move it to the right side of the entry
+ if icon_win.get_size() != (self._pixw, self._pixh):
+ icon_win.move_resize(iconx, icony, self._pixw, self._pixh)
+ else:
+ icon_win.move(iconx, icony)
+
+ def draw_pixbuf(self):
+ if not self._pixbuf:
+ return
+
+ win = self._icon_win
+ # XXX: Why?
+ if not win:
+ return
+
+ # Draw background first
+ color = self._entry.style.base_gc[self._entry.state]
+ win.draw_rectangle(color, True,
+ 0, 0, self._pixw, self._pixh)
+
+ # If sensitive draw the icon, regardless of the window emitting the
+ # event since makes it a bit smoother on resize
+ if self._entry.flags() & gtk.SENSITIVE:
+ win.draw_pixbuf(None, self._pixbuf, 0, 0, 0, 0,
+ self._pixw, self._pixh)
+
+ def _update_position(self):
+ if self._entry.get_property('xalign') > 0.5:
+ self._pos = gtk.POS_LEFT
+ else:
+ self._pos = gtk.POS_RIGHT
diff --git a/kiwi/ui/libgladeloader.py b/kiwi/ui/libgladeloader.py
new file mode 100644
index 0000000..d3c2872
--- /dev/null
+++ b/kiwi/ui/libgladeloader.py
@@ -0,0 +1,76 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+import os
+
+from gtk.glade import XML
+
+from kiwi.environ import environ
+from kiwi.log import Logger
+
+log = Logger('libgladeloader')
+
+class LibgladeWidgetTree(XML):
+ def __init__(self, view, gladefile, widgets, gladename=None, domain=None):
+
+ if not gladefile:
+ raise ValueError("A gladefile wasn't provided.")
+ elif not isinstance(gladefile, basestring):
+ raise TypeError(
+ "gladefile should be a string, found %s" % type(gladefile))
+ filename = os.path.splitext(os.path.basename(gladefile))[0]
+
+ self._view = view
+ self._gladefile = environ.find_resource("glade", filename + ".glade")
+ self._widgets = (widgets or view.widgets or [])[:]
+ self.gladename = gladename or filename
+ XML.__init__(self, self._gladefile, domain)
+ if not self._widgets:
+ self._widgets = [w.get_name() for w in self.get_widget_prefix('')]
+ self._attach_widgets()
+
+ def _attach_widgets(self):
+ # Attach widgets in the widgetlist to the view specified, so
+ # widgets = [label1, button1] -> view.label1, view.button1
+ for w in self._widgets:
+ widget = XML.get_widget(self, w)
+ if widget is not None:
+ setattr(self._view, w, widget)
+ else:
+ log.warn("Widget %s was not found in glade widget tree." % w)
+
+ def get_widget(self, name):
+ """Retrieves the named widget from the View (or glade tree)"""
+ name = name.replace('.', '_')
+ widget = XML.get_widget(self, name)
+
+ if widget is None:
+ raise AttributeError(
+ "Widget %s not found in view %s" % (name, self._view))
+ return widget
+
+ def get_widgets(self):
+ return self.get_widget_prefix('')
+
+ def get_sizegroups(self):
+ return []
diff --git a/kiwi/ui/objectlist.py b/kiwi/ui/objectlist.py
new file mode 100644
index 0000000..d6b1720
--- /dev/null
+++ b/kiwi/ui/objectlist.py
@@ -0,0 +1,1705 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2001-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""High level wrapper for GtkTreeView"""
+
+import datetime
+import gettext
+
+import gobject
+import gtk
+from gtk import gdk
+
+from kiwi.accessor import kgetattr
+from kiwi.datatypes import converter, currency, number
+from kiwi.log import Logger
+from kiwi.python import slicerange
+from kiwi.utils import PropertyObject, gsignal, gproperty, type_register
+
+_ = gettext.gettext
+
+log = Logger('objectlist')
+
+str2type = converter.str_to_type
+
+def str2enum(value_name, enum_class):
+ "converts a string to a enum"
+ for _, enum in enum_class.__enum_values__.items():
+ if value_name in (enum.value_name, enum.value_nick):
+ return enum
+
+def str2bool(value, from_string=converter.from_string):
+ "converts a boolean to a enum"
+ return from_string(bool, value)
+
+class Column(PropertyObject, gobject.GObject):
+ """Specifies a column for an L{ObjectList}"""
+ gproperty('title', str)
+ gproperty('data-type', object)
+ gproperty('visible', bool, default=True)
+ gproperty('justify', gtk.Justification, default=gtk.JUSTIFY_LEFT)
+ gproperty('format', str)
+ gproperty('width', int, maximum=2**16)
+ gproperty('sorted', bool, default=False)
+ gproperty('order', gtk.SortType, default=gtk.SORT_ASCENDING)
+ gproperty('expand', bool, default=False)
+ gproperty('tooltip', str)
+ gproperty('format_func', object)
+ gproperty('editable', bool, default=False)
+ gproperty('searchable', bool, default=False)
+ gproperty('radio', bool, default=False)
+ gproperty('cache', bool, default=False)
+ gproperty('use-stock', bool, default=False)
+ gproperty('icon-size', gtk.IconSize, default=gtk.ICON_SIZE_MENU)
+ gproperty('editable_attribute', str)
+ #gproperty('title_pixmap', str)
+
+ # This can be set in subclasses, to be able to allow custom
+ # cell_data_functions, used by SequentialColumn
+ cell_data_func = None
+
+ # This is called after the renderer property is set, to allow
+ # us to set custom rendering properties
+ renderer_func = None
+
+ # This is called when the renderer is created, so we can set/fetch
+ # initial properties
+ on_attach_renderer = None
+
+ def __init__(self, attribute, title=None, data_type=None, **kwargs):
+ """
+ Creates a new Column, which describes how a column in a
+ ObjectList should be rendered.
+
+ @param attribute: a string with the name of the instance attribute the
+ column represents.
+ @param title: the title of the column, defaulting to the capitalized
+ form of the attribute.
+ @param data_type: the type of the attribute that will be inserted
+ into the column.
+
+ @keyword visible: a boolean specifying if it is initially hidden or
+ shown.
+ @keyword justify: one of gtk.JUSTIFY_LEFT, gtk.JUSTIFY_RIGHT or
+ gtk.JUSTIFY_CENTER or None. If None, the justification will be
+ determined by the type of the attribute value of the first
+ instance to be inserted in the ObjectList (numbers will be
+ right-aligned).
+ @keyword format: a format string to be applied to the attribute
+ value upon insertion in the list.
+ @keyword width: the width in pixels of the column, if not set, uses the
+ default to ObjectList. If no Column specifies a width,
+ columns_autosize() will be called on the ObjectList upon append()
+ or the first add_list().
+ @keyword sorted: whether or not the ObjectList is to be sorted by this
+ column.
+ If no Columns are sorted, the ObjectList will be created unsorted.
+ @keyword order: one of gtk.SORT_ASCENDING or gtk.SORT_DESCENDING or
+ -1. The value -1 is used internally when the column is not sorted.
+ @keyword expand: if set column will expand. Note: this space is shared
+ equally amongst all columns that have the expand set to True.
+ @keyword tooltip: a string which will be used as a tooltip for
+ the column header
+ @keyword format_func: a callable which will be used to format
+ the output of a column. The function will take one argument
+ which is the value to convert and is expected to return a string.
+ Note that you cannot use format and format_func at the same time,
+ if you provide a format function you'll be responsible for
+ converting the value to a string.
+ @keyword editable: if true the field is editable and when you modify
+ the contents of the cell the model will be updated.
+ @keyword searchable: if true the attribute values of the column can
+ be searched using type ahead search. Only string attributes are
+ currently supported.
+ @keyword radio: If true render the column as a radio instead of toggle.
+ Only applicable for columns with boolean data types.
+ @keyword cache: If true, the value will only be fetched once,
+ and the same value will be reused for futher access.
+ @keyword use_stock: If true, this will be rendered as pixbuf from the
+ value which should be a stock id.
+ @keyword icon_size: a gtk.IconSize constant, gtk.ICON_SIZE_MENU if not
+ specified.
+ @keyword editable_attribute: a string which is the attribute
+ which should decide if the cell is editable or not.
+ @keyword title_pixmap: (TODO) if set to a filename a pixmap will be
+ used *instead* of the title set. The title string will still be
+ used to identify the column in the column selection and in a
+ tooltip, if a tooltip is not set.
+ """
+
+ # XXX: filter function?
+ if ' ' in attribute:
+ msg = ("The attribute can not contain spaces, otherwise I can"
+ " not find the value in the instances: %s" % attribute)
+ raise AttributeError(msg)
+
+ self.attribute = attribute
+ self.compare = None
+ self.from_string = None
+
+ kwargs['title'] = title or attribute.capitalize()
+ if not data_type:
+ data_type = str
+ kwargs['data_type'] = data_type
+
+ # If we don't specify a justification, right align it for int/float
+ # center for bools and left align it for everything else.
+ if "justify" not in kwargs:
+ if data_type:
+ if issubclass(data_type, bool):
+ kwargs['justify'] = gtk.JUSTIFY_CENTER
+ elif issubclass(data_type, (number, currency)):
+ kwargs['justify'] = gtk.JUSTIFY_RIGHT
+
+ format_func = kwargs.get('format_func')
+ if format_func:
+ if not callable(format_func):
+ raise TypeError("format_func must be callable")
+ if 'format' in kwargs:
+ raise TypeError(
+ "format and format_func can not be used at the same time")
+
+ # editable_attribute always turns on editable
+ if 'editable_attribute' in kwargs:
+ if not kwargs.get('editable', True):
+ raise TypeError(
+ "editable cannot be disabled when using editable_attribute")
+ kwargs['editable'] = True
+
+ PropertyObject.__init__(self, **kwargs)
+ gobject.GObject.__init__(self)
+
+ # This is meant to be subclassable, we're using kgetattr, as
+ # a staticmethod as an optimization, so we can avoid a function call.
+ get_attribute = staticmethod(kgetattr)
+
+ def prop_set_data_type(self, data):
+ if data is not None:
+ conv = converter.get_converter(data)
+ self.compare = conv.get_compare_function()
+ self.from_string = conv.from_string
+ return data
+
+ def __repr__(self):
+ namespace = self.__dict__.copy()
+ attr = namespace['attribute']
+ del namespace['attribute']
+ return "<%s %s: %s>" % (self.__class__.__name__, attr, namespace)
+
+ # XXX: Replace these two with a gazpacho loader adapter
+ def __str__(self):
+ if self.data_type is None:
+ data_type = ''
+ else:
+ data_type = self.data_type.__name__
+
+ return "%s|%s|%s|%s|%d|%s|%s|%d|%s|%d" % \
+ (self.attribute, self.title, data_type, self.visible,
+ self.justify, self.tooltip, self.format, self.width,
+ self.sorted, self.order)
+
+ def as_string(self, data):
+ data_type = self.data_type
+ if (self.format or
+ data_type == datetime.date or
+ data_type == datetime.datetime or
+ data_type == datetime.time):
+ conv = converter.get_converter(data_type)
+ text = conv.as_string(data, format=self.format or None)
+ elif self.format_func:
+ text = self.format_func(data)
+ else:
+ text = data
+
+ return text
+
+ def from_string(cls, data_string):
+ fields = data_string.split('|')
+ if len(fields) != 10:
+ msg = 'every column should have 10 fields, not %d' % len(fields)
+ raise ValueError(msg)
+
+ # the attribute is mandatory
+ if not fields[0]:
+ raise TypeError
+
+ column = cls(fields[0])
+ column.title = fields[1] or ''
+ column.data_type = str2type(fields[2])
+ column.visible = str2bool(fields[3])
+ column.justify = str2enum(fields[4], gtk.JUSTIFY_LEFT)
+ column.tooltip = fields[5]
+ column.format = fields[6]
+
+ try:
+ column.width = int(fields[7])
+ except ValueError:
+ pass
+
+ column.sorted = str2bool(fields[8])
+ column.order = str2enum(fields[9], gtk.SORT_ASCENDING) \
+ or gtk.SORT_ASCENDING
+
+ # XXX: expand, remember to sync with __str__
+
+ return column
+ from_string = classmethod(from_string)
+
+class SequentialColumn(Column):
+ """I am a column which will display a sequence of numbers, which
+ represent the row number. The value is independent of the data in
+ the other columns, so no matter what I will always display 1 in
+ the first column, unless you reverse it by clicking on the column
+ header.
+
+ If you don't give me any argument I'll have the title of a hash (#) and
+ right justify the sequences."""
+ def __init__(self, title='#', justify=gtk.JUSTIFY_RIGHT, **kwargs):
+ Column.__init__(self, '_kiwi_sequence_id',
+ title=title, justify=justify, data_type=int, **kwargs)
+
+ def cell_data_func(self, tree_column, renderer, model, treeiter,
+ (column, renderer_prop)):
+ reversed = tree_column.get_sort_order() == gtk.SORT_DESCENDING
+
+ row = model[treeiter]
+ if reversed:
+ sequence_id = len(model) - row.path[0]
+ else:
+ sequence_id = row.path[0] + 1
+
+ row[COL_MODEL]._kiwi_sequence_id = sequence_id
+
+ try:
+ renderer.set_property(renderer_prop, sequence_id)
+ except TypeError:
+ raise TypeError("%r does not support parameter %s" %
+ (renderer, renderer_prop))
+
+class ColoredColumn(Column):
+ """
+ I am a column which can colorize the text of columns under
+ certain circumstances. I take a color and an extra function
+ which will be called for each row
+
+ Example, to colorize negative values to red:
+
+ >>> def colorize(value):
+ ... return value < 0
+ ...
+ ... ColoredColumn('age', data_type=int, color='red',
+ ... data_func=colorize),
+ """
+
+ def __init__(self, attribute, title=None, data_type=None,
+ color=None, data_func=None, **kwargs):
+ if not issubclass(data_type, number):
+ raise TypeError("data type must be a number")
+ if not callable(data_func):
+ raise TypeError("data func must be callable")
+
+ self._color = gdk.color_parse(color)
+ self._color_normal = None
+
+ self._data_func = data_func
+
+ Column.__init__(self, attribute, title, data_type, **kwargs)
+
+ def on_attach_renderer(self, renderer):
+ renderer.set_property('foreground-set', True)
+ self._color_normal = renderer.get_property('foreground-gdk')
+
+ def renderer_func(self, renderer, data):
+ if self._data_func(data):
+ color = self._color
+ else:
+ color = self._color_normal
+
+ renderer.set_property('foreground-gdk', color)
+
+class _ContextMenu(gtk.Menu):
+
+ """
+ ContextMenu is a wrapper for the menu that's displayed when right
+ clicking on a column header. It monitors the treeview and rebuilds
+ when columns are added, removed or moved.
+ """
+
+ def __init__(self, treeview):
+ gtk.Menu.__init__(self)
+
+ self._dirty = True
+ self._signal_ids = []
+ self._treeview = treeview
+ self._treeview.connect('columns-changed',
+ self._on_treeview__columns_changed)
+ self._create()
+
+ def clean(self):
+ for child in self.get_children():
+ self.remove(child)
+
+ for menuitem, signal_id in self._signal_ids:
+ menuitem.disconnect(signal_id)
+ self._signal_ids = []
+
+ def popup(self, event):
+ self._create()
+ gtk.Menu.popup(self, None, None, None,
+ event.button, event.time)
+
+ def _create(self):
+ if not self._dirty:
+ return
+
+ self.clean()
+
+ for column in self._treeview.get_columns():
+ header_widget = column.get_widget()
+ if not header_widget:
+ continue
+ title = header_widget.get_text()
+
+ menuitem = gtk.CheckMenuItem(title)
+ menuitem.set_active(column.get_visible())
+ signal_id = menuitem.connect("activate",
+ self._on_menuitem__activate,
+ column)
+ self._signal_ids.append((menuitem, signal_id))
+ menuitem.show()
+ self.append(menuitem)
+
+ self._dirty = False
+
+ def _on_treeview__columns_changed(self, treeview):
+ self._dirty = True
+
+ def _on_menuitem__activate(self, menuitem, column):
+ active = menuitem.get_active()
+ column.set_visible(active)
+
+ # The width or height of some of the rows might have
+ # changed after changing the visibility of the column,
+ # so we have to re-measure all the rows, this can be done
+ # using row_changed.
+ model = self._treeview.get_model()
+ for row in model:
+ model.row_changed(row.path, row.iter)
+
+ children = self.get_children()
+ if active:
+ # Make sure all items are selectable
+ for child in children:
+ child.set_sensitive(True)
+ else:
+ # Protect so we can't hide all the menu items
+ # If there's only one menuitem less to select, set
+ # it to insensitive
+ active_children = [child for child in children
+ if child.get_active()]
+ if len(active_children) == 1:
+ active_children[0].set_sensitive(False)
+
+COL_MODEL = 0
+
+_marker = object()
+
+class ObjectList(PropertyObject, gtk.ScrolledWindow):
+ """
+ An enhanced version of GtkTreeView, which provides pythonic wrappers
+ for accessing rows, and optional facilities for column sorting (with
+ types) and column selection.
+
+ Items in an ObjectList is stored in objects. Each row represents an object
+ and each column represents an attribute in the object.
+ The column description object must be a subclass of L{Column}.
+ Simple example
+
+ >>> class Fruit:
+ >>> pass
+
+ >>> apple = Fruit()
+ >>> apple.name = 'Apple'
+ >>> apple.description = 'Worm house'
+
+ >>> banana = Fruit()
+ >>> banana.name = 'Banana'
+ >>> banana.description = 'Monkey food'
+
+ >>> list = ObjectList([Column('name'),
+ >>> Column('description')])
+ >>> list.append(apple)
+ >>> list.append(banana)
+ """
+
+ __gtype_name__ = 'ObjectList'
+
+ # row activated
+ gsignal('row-activated', object)
+
+ # selected row(s)
+ gsignal('selection-changed', object)
+
+ # row double-clicked
+ gsignal('double-click', object)
+
+ # edited object, attribute name
+ gsignal('cell-edited', object, str)
+
+ # emitted when empty or non-empty status changes
+ gsignal('has-rows', bool)
+
+ # this property is used to serialize the columns of a ObjectList. The format
+ # is a big string with '^' as the column separator and '|' as the field
+ # separator
+ gproperty('column-definitions', str, nick="ColumnDefinitions")
+ gproperty('selection-mode', gtk.SelectionMode,
+ default=gtk.SELECTION_BROWSE, nick="SelectionMode")
+
+ def __init__(self, columns=[],
+ instance_list=None,
+ mode=gtk.SELECTION_BROWSE):
+ """
+ @param columns: a list of L{Column}s
+ @param instance_list: a list of objects to be inserted or None
+ @param mode: selection mode
+ """
+ # allow to specify only one column
+ if isinstance(columns, Column):
+ columns = [columns]
+ elif not isinstance(columns, list):
+ raise TypeError("columns must be a list or a Column")
+
+ if not isinstance(mode, gtk.SelectionMode):
+ raise TypeError("mode must be an gtk.SelectionMode enum")
+ # gtk.SELECTION_EXTENDED & gtk.SELECTION_MULTIPLE are both 3.
+ # so we can't do this check.
+ #elif mode == gtk.SELECTION_EXTENDED:
+ # raise TypeError("gtk.SELECTION_EXTENDED is deprecated")
+
+ # Mapping of instance id -> treeiter
+ self._iters = {}
+ self._cell_data_caches = {}
+ self._columns_configured = False
+ self._autosize = True
+ self._vscrollbar = None
+ # by default we are unordered. This index points to the column
+ # definition of the column that dictates the order, in case there is
+ # any
+ self._sort_column_index = -1
+
+ gtk.ScrolledWindow.__init__(self)
+
+ # we always want a vertical scrollbar. Otherwise the button on top
+ # of it doesn't make sense. This button is used to display the popup
+ # menu
+ self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+ self.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+
+ self._model = gtk.ListStore(object)
+ self._model.connect('row-inserted', self._on_model__row_inserted)
+ self._model.connect('row-deleted', self._on_model__row_deleted)
+ self._treeview = gtk.TreeView(self._model)
+ self._treeview.connect('button-press-event',
+ self._on_treeview__button_press_event)
+ self._treeview.connect_after('row-activated',
+ self._after_treeview__row_activated)
+ self._treeview.set_rules_hint(True)
+ self._treeview.show()
+ self.add(self._treeview)
+
+ # these tooltips are used for the columns
+ self._tooltips = gtk.Tooltips()
+
+ # create a popup menu for showing or hiding columns
+ self._popup = _ContextMenu(self._treeview)
+
+ # when setting the column definition the columns are created
+ self.set_columns(columns)
+
+ if instance_list:
+ self._treeview.freeze_notify()
+ self._load(instance_list, clear=True)
+ self._treeview.thaw_notify()
+
+ if self._sort_column_index != -1:
+ column = self._columns[self._sort_column_index]
+ self._model.set_sort_column_id(self._sort_column_index,
+ column.order)
+
+ # Set selection mode last to avoid spurious events
+ selection = self._treeview.get_selection()
+ selection.connect("changed", self._on_selection__changed)
+
+ # Select the first item if no items are selected
+ if mode != gtk.SELECTION_NONE and instance_list:
+ selection.select_iter(self._model[COL_MODEL].iter)
+
+ # Depends on treeview and selection being set up
+ PropertyObject.__init__(self)
+
+ self.set_selection_mode(mode)
+
+ # Python list object implementation
+ # These methods makes the kiwi list behave more or less
+ # like a normal python list
+ #
+ # TODO:
+ # operators
+ # __add__, __eq__, __ge__, __gt__, __iadd__,
+ # __imul__, __le__, __lt__, __mul__, __ne__,
+ # __rmul__
+ #
+ # misc
+ # __delitem__, __hash__, __reduce__, __reduce_ex__
+ # __reversed__
+
+ def __len__(self):
+ "len(list)"
+ return len(self._model)
+
+ def __nonzero__(self):
+ "if list"
+ return True
+
+ def __contains__(self, instance):
+ "item in list"
+ for row in self._model:
+ if row[COL_MODEL] == instance:
+ return True
+ return False
+
+ def __iter__(self):
+ "for item in list"
+ class ModelIterator:
+ def __init__(self):
+ self._index = -1
+
+ def __iter__(self):
+ return self
+
+ def next(self, model=self._model):
+ try:
+ self._index += 1
+ return model[self._index][COL_MODEL]
+ except IndexError:
+ raise StopIteration
+
+ return ModelIterator()
+
+ def __getitem__(self, arg):
+ "list[n]"
+ if isinstance(arg, (int, gtk.TreeIter, str)):
+ item = self._model[arg][COL_MODEL]
+ elif isinstance(arg, slice):
+ model = self._model
+ return [model[item][COL_MODEL]
+ for item in slicerange(arg, len(self._model))]
+ else:
+ raise TypeError("argument arg must be int, gtk.Treeiter or "
+ "slice, not %s" % type(arg))
+ return item
+
+ def __setitem__(self, arg, item):
+ "list[n] = m"
+ if isinstance(arg, (int, gtk.TreeIter, str)):
+ self._model[arg] = (item,)
+ elif isinstance(arg, slice):
+ raise NotImplementedError("slices for list are not implemented")
+ else:
+ raise TypeError("argument arg must be int or gtk.Treeiter,"
+ " not %s" % type(arg))
+
+ # append and remove are below
+
+ def extend(self, iterable):
+ """
+ Extend list by appending elements from the iterable
+
+ @param iterable:
+ """
+
+ return self.add_list(iterable, clear=False)
+
+ def index(self, item, start=None, stop=None):
+ """
+ Return first index of value
+
+ @param item:
+ @param start:
+ @param stop
+ """
+
+ if start is not None or stop is not None:
+ raise NotImplementedError("start and stop")
+
+ treeiter = self._iters.get(id(item), _marker)
+ if treeiter is _marker:
+ raise ValueError("item %r is not in the list" % item)
+
+ return self._model[treeiter].path[0]
+
+ def count(self, item):
+ "L.count(item) -> integer -- return number of occurrences of value"
+
+ count = 0
+ for row in self._model:
+ if row[COL_MODEL] == item:
+ count += 1
+ return count
+
+ def insert(self, index, item):
+ "L.insert(index, item) -- insert object before index"
+ raise NotImplementedError
+
+ def pop(self, index):
+ """
+ Remove and return item at index (default last)
+ @param index:
+ """
+ raise NotImplementedError
+
+ def reverse(self, pos, item):
+ "L.reverse() -- reverse *IN PLACE*"
+ raise NotImplementedError
+
+ def sort(self, pos, item):
+ """L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
+ cmp(x, y) -> -1, 0, 1"""
+ raise NotImplementedError
+
+ # Properties
+
+ def prop_set_column_definition(self, value):
+ self.set_columns(value)
+ return value
+
+ def prop_set_selection_mode(self, mode):
+ self.set_selection_mode(mode)
+
+ def prop_get_selection_mode(self):
+ return self.get_selection_mode()
+
+ # Model
+
+ def _on_model__row_inserted(self, model, path, iter):
+ if len(model) == 1:
+ self.emit('has-rows', True)
+
+ def _on_model__row_deleted(self, model, path):
+ if not len(model):
+ self.emit('has-rows', False)
+
+ # Columns handling
+ def _load(self, instances, clear):
+ # do nothing if empty list or None provided
+ model = self._model
+ if clear:
+ if not instances:
+ self.unselect_all()
+ self.clear()
+ return
+
+ model = self._model
+ iters = self._iters
+
+ old_instances = [row[COL_MODEL] for row in model]
+
+ # Save selection
+ selected_instances = []
+ if old_instances:
+ selection = self._treeview.get_selection()
+ _, paths = selection.get_selected_rows()
+ if paths:
+ selected_instances = [model[path][COL_MODEL]
+ for (path,) in paths]
+
+ iters = self._iters
+ prev = None
+ # Do not always just clear the list, check if we have the same
+ # instances in the list we want to insert and merge in the new
+ # items
+ if clear:
+ for instance in iter(instances):
+ objid = id(instance)
+ # If the instance is not in the list insert it after
+ # the previous inserted object
+ if not objid in iters:
+ if prev is None:
+ prev = model.append((instance,))
+ else:
+ prev = model.insert_after(prev, (instance,))
+ iters[objid] = prev
+ else:
+ prev = iters[objid]
+
+ # Optimization when we were empty, we wont need to remove anything
+ # nor restore selection
+ if old_instances:
+ # Remove
+ objids = [id(instance) for instance in instances]
+ for instance in old_instances:
+ objid = id(instance)
+ if objid in objids:
+ continue
+ self._remove(objid)
+ else:
+ for instance in iter(instances):
+ iters[id(instance)] = model.append((instance,))
+
+ # Restore selection
+ for instance in selected_instances:
+ objid = id(instance)
+ if objid in iters:
+ selection.select_iter(iters[objid])
+
+ # As soon as we have data for that list, we can autosize it, and
+ # we don't want to autosize again, or we may cancel user
+ # modifications.
+ if self._autosize:
+ self._treeview.columns_autosize()
+ self._autosize = False
+
+ def _setup_columns(self):
+ if self._columns_configured:
+ return
+
+ searchable = None
+ sorted = None
+ expand = False
+ for column in self._columns:
+ if column.searchable:
+ if searchable:
+ raise ValueError("Can't make column %s searchable, column"
+ " %s is already set as searchable" % (
+ column.attribute, searchable.attribute))
+ searchable = column.searchable
+ if column.sorted:
+ if sorted:
+ raise ValueError("Can't make column %s sorted, column"
+ " %s is already set as sortable" % (
+ column.attribute, sorted.attribute))
+ sorted = column.sorted
+ if column.expand:
+ expand = True
+
+ for column in self._columns:
+ self._setup_column(column)
+
+ if not expand:
+ column = gtk.TreeViewColumn()
+ self._treeview.append_column(column)
+
+ self._columns_configured = True
+
+ def _setup_column(self, column):
+ # You can't subclass bool, so this is okay
+ if (column.data_type is bool and column.format):
+ raise TypeError("format is not supported for boolean columns")
+
+ index = self._columns.index(column)
+ self._model.set_sort_func(index, self._sort_function)
+ treeview_column = self._treeview.get_column(index)
+ if treeview_column is None:
+ treeview_column = self._create_column(column)
+
+ renderer, renderer_prop = self._guess_renderer_for_type(column)
+ if column.on_attach_renderer:
+ column.on_attach_renderer(renderer)
+ justify = column.justify
+ if justify == gtk.JUSTIFY_RIGHT:
+ xalign = 1.0
+ elif justify == gtk.JUSTIFY_CENTER:
+ xalign = 0.5
+ elif justify in (gtk.JUSTIFY_LEFT,
+ gtk.JUSTIFY_FILL):
+ xalign = 0.0
+ else:
+ raise AssertionError
+ renderer.set_property("xalign", xalign)
+ treeview_column.set_property("alignment", xalign)
+
+ if column.use_stock:
+ cell_data_func = self._cell_data_pixbuf_func
+ else:
+ cell_data_func = self._cell_data_text_func
+
+ if column.cell_data_func:
+ cell_data_func = column.cell_data_func
+ elif column.cache:
+ self._cell_data_caches[column.attribute] = {}
+
+ treeview_column.pack_start(renderer)
+ treeview_column.set_cell_data_func(renderer, cell_data_func,
+ (column, renderer_prop))
+ treeview_column.set_visible(column.visible)
+
+ treeview_column.connect("clicked", self._on_column__clicked, column)
+ if column.width:
+ treeview_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
+ treeview_column.set_fixed_width(column.width)
+ if column.tooltip:
+ widget = self._get_column_button(treeview_column)
+ if widget is not None:
+ self._tooltips.set_tip(widget, column.tooltip)
+
+ if column.expand:
+ # Default is False
+ treeview_column.set_expand(True)
+
+ if column.sorted:
+ self._sort_column_index = index
+ treeview_column.set_sort_indicator(True)
+
+ if column.width:
+ self._autosize = False
+
+ if column.searchable:
+ if not issubclass(column.data_type, basestring):
+ raise TypeError("Unsupported data type for "
+ "searchable column: %s" % column.data_type)
+ self._treeview.set_search_column(index)
+ self._treeview.set_search_equal_func(self._search_equal_func,
+ column)
+
+ if column.radio:
+ if not issubclass(column.data_type, bool):
+ raise TypeError("You can only use radio for boolean columns")
+
+ # typelist here may be none. It's okay; justify_columns will try
+ # and use the specified justifications and if not present will
+ # not touch the column. When typelist is not set,
+ # append/add_list have a chance to fix up the remaining
+ # justification by looking at the first instance's data.
+# self._justify_columns(columns, typelist)
+
+ def _create_column(self, column):
+ treeview_column = gtk.TreeViewColumn()
+ # we need to set our own widget because otherwise
+ # __get_column_button won't work
+
+ label = gtk.Label(column.title)
+ label.show()
+ treeview_column.set_widget(label)
+ treeview_column.set_resizable(True)
+ treeview_column.set_clickable(True)
+ treeview_column.set_reorderable(True)
+ self._treeview.append_column(treeview_column)
+
+ # setup the button to show the popup menu
+ button = self._get_column_button(treeview_column)
+ button.connect('button-release-event',
+ self._on_header__button_release_event)
+ return treeview_column
+
+ def _on_renderer_toggle_check__toggled(self, renderer, path, model, attr):
+ obj = model[path][COL_MODEL]
+ value = not getattr(obj, attr, None)
+ setattr(obj, attr, value)
+ self.emit('cell-edited', obj, attr)
+
+ def _on_renderer_toggle_radio__toggled(self, renderer, path, model, attr):
+ # Deactive old one
+ old = renderer.get_data('kiwilist::radio-active')
+
+ # If we don't have the radio-active set it means we're doing
+ # This for the first time, so scan and see which one is currently
+ # active, so we can deselect it
+ if not old:
+ # XXX: Handle multiple values set to True, this
+ # algorithm just takes the first one it finds
+ for row in self._model:
+ obj = row[COL_MODEL]
+ value = getattr(obj, attr)
+ if value == True:
+ old = obj
+ break
+ else:
+ raise TypeError("You need an initial attribute value set "
+ "to true when using radio")
+
+ setattr(old, attr, False)
+
+ # Active new and save a reference to the object of the
+ # previously selected row
+ new = model[path][COL_MODEL]
+ setattr(new, attr, True)
+ renderer.set_data('kiwilist::radio-active', new)
+ self.emit('cell-edited', new, attr)
+
+ def _on_renderer_text__edited(self, renderer, path, text,
+ model, attr, column, from_string):
+ obj = model[path][COL_MODEL]
+ value = from_string(text)
+ setattr(obj, attr, value)
+ self.emit('cell-edited', obj, attr)
+
+ def _guess_renderer_for_type(self, column):
+ """Gusses which CellRenderer we should use for a given type.
+ It also set the property of the renderer that depends on the model,
+ in the renderer.
+ """
+
+ # TODO: Move to column
+ data_type = column.data_type
+ if data_type is bool:
+ renderer = gtk.CellRendererToggle()
+ if column.editable:
+ renderer.set_property('activatable', True)
+ # Boolean can be either a radio or a checkbox.
+ # Changes are handled by the toggled callback, which
+ # should only be connected if the column is editable.
+ if column.radio:
+ renderer.set_radio(True)
+ cb = self._on_renderer_toggle_radio__toggled
+ else:
+ cb = self._on_renderer_toggle_check__toggled
+ renderer.connect('toggled', cb, self._model, column.attribute)
+ prop = 'active'
+ elif column.use_stock or data_type == gdk.Pixbuf:
+ renderer = gtk.CellRendererPixbuf()
+ prop = 'pixbuf'
+ if column.editable:
+ raise TypeError("use-stock columns cannot be editable")
+ elif issubclass(data_type, (datetime.date, datetime.time,
+ basestring, number,
+ currency)):
+ renderer = gtk.CellRendererText()
+ prop = 'text'
+ if column.editable:
+ renderer.set_property('editable', True)
+ renderer.connect('edited', self._on_renderer_text__edited,
+ self._model, column.attribute, column,
+ column.from_string)
+
+ else:
+ raise ValueError("the type %s is not supported yet" % data_type)
+
+ return renderer, prop
+
+ def _search_equal_func(self, model, tree_column, key, treeiter, column):
+ data = column.get_attribute(model[treeiter][COL_MODEL],
+ column.attribute, None)
+ if data.startswith(key):
+ return False
+ return True
+
+ def _cell_data_text_func(self, tree_column, renderer, model, treeiter,
+ (column, renderer_prop)):
+
+ row = model[treeiter]
+ if column.editable_attribute:
+ data = column.get_attribute(row[COL_MODEL],
+ column.editable_attribute, None)
+ data_type = column.data_type
+ if isinstance(renderer, gtk.CellRendererToggle):
+ renderer.set_property('activatable', data)
+ elif isinstance(renderer, gtk.CellRendererText):
+ renderer.set_property('editable', data)
+ else:
+ raise AssertionError
+
+ if column.cache:
+ cache = self._cell_data_caches[column.attribute]
+ path = row.path[0]
+ if path in cache:
+ data = cache[path]
+ else:
+ data = column.get_attribute(row[COL_MODEL],
+ column.attribute, None)
+ cache[path] = data
+ else:
+ data = column.get_attribute(row[COL_MODEL],
+ column.attribute, None)
+
+ text = column.as_string(data)
+
+ renderer.set_property(renderer_prop, text)
+
+ if column.renderer_func:
+ column.renderer_func(renderer, data)
+
+ def _cell_data_pixbuf_func(self, tree_column, renderer, model, treeiter,
+ (column, renderer_prop)):
+ row = model[treeiter]
+ data = column.get_attribute(row[COL_MODEL],
+ column.attribute, None)
+ pixbuf = self.render_icon(data, column.icon_size)
+ renderer.set_property(renderer_prop, pixbuf)
+
+ def _on_header__button_release_event(self, button, event):
+ if event.button == 3:
+ self._popup.popup(event)
+ return False
+
+ return False
+
+ def _on_renderer__edited(self, renderer, path, value, column):
+ data_type = column.data_type
+ if data_type in number:
+ value = data_type(value)
+
+ # XXX convert new_text to the proper data type
+ setattr(self._model[path][COL_MODEL], column.attribute, value)
+
+ def _on_renderer__toggled(self, renderer, path, column):
+ setattr(self._model[path][COL_MODEL], column.attribute,
+ not renderer.get_active())
+
+ def _clear_columns(self):
+ while self._treeview.get_columns():
+ self._treeview.remove_column(self._treeview.get_column(COL_MODEL))
+
+ self._popup.clean()
+
+ self._columns_configured = False
+
+ # selection methods
+ def _select_and_focus_row(self, row_iter):
+ self._treeview.set_cursor(self._model[row_iter].path)
+
+ def _sort_function(self, model, iter1, iter2):
+ column = self._columns[self._sort_column_index]
+ attr = column.attribute
+ return column.compare(
+ column.get_attribute(model[iter1][COL_MODEL], attr),
+ column.get_attribute(model[iter2][COL_MODEL], attr))
+
+ def _on_column__clicked(self, treeview_column, column):
+ # this means we are not sorting at all
+ if self._sort_column_index == -1:
+ return
+
+ old_treeview_column = self._treeview.get_column(
+ self._sort_column_index)
+
+ if treeview_column is old_treeview_column:
+ # same column, so reverse the order
+ if column.order == gtk.SORT_ASCENDING:
+ new_order = gtk.SORT_DESCENDING
+ elif column.order == gtk.SORT_DESCENDING:
+ new_order = gtk.SORT_ASCENDING
+ else:
+ raise AssertionError
+ else:
+ # new column, sort ascending
+ new_order = gtk.SORT_ASCENDING
+ self._sort_column_index = self._columns.index(column)
+ # cosmetic changes
+ old_treeview_column.set_sort_indicator(False)
+ treeview_column.set_sort_indicator(True)
+
+ column.order = new_order
+ treeview_column.set_sort_order(new_order)
+
+ # This performs the actual ordering
+ self._model.set_sort_column_id(self._sort_column_index, new_order)
+
+ # handlers
+ def _after_treeview__row_activated(self, treeview, path, view_column):
+ try:
+ row = self._model[path]
+ except IndexError:
+ print 'path %s was not found in model: %s' % (
+ path, map(list, self._model))
+ return
+ item = row[COL_MODEL]
+ self.emit('row-activated', item)
+
+ def _on_selection__changed(self, selection):
+ mode = selection.get_mode()
+ if mode == gtk.SELECTION_MULTIPLE:
+ item = self.get_selected_rows()
+ elif mode in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
+ item = self.get_selected()
+ else:
+ raise AssertionError
+ self.emit('selection-changed', item)
+
+ def _on_treeview__button_press_event(self, treeview, event):
+ if event.type == gtk.gdk._2BUTTON_PRESS:
+ selection = self._treeview.get_selection()
+ mode = selection.get_mode()
+ if mode == gtk.SELECTION_MULTIPLE:
+ item = self.get_selected_rows()
+ else:
+ item = self.get_selected()
+ self.emit('double-click', item)
+
+ # hacks
+ def _get_column_button(self, column):
+ """Return the button widget of a particular TreeViewColumn.
+
+ This hack is needed since that widget is private of the TreeView but
+ we need access to them for Tooltips, right click menus, ...
+
+ Use this function at your own risk
+ """
+
+ button = column.get_widget()
+ assert button is not None, ("You must call column.set_widget() "
+ "before calling _get_column_button")
+
+ while not isinstance(button, gtk.Button):
+ button = button.get_parent()
+
+ return button
+
+ # start of the hack to put a button on top of the vertical scrollbar
+ def _setup_popup_button(self):
+ """Put a button on top of the vertical scrollbar to show the popup
+ menu.
+ Internally it uses a POPUP window so you can tell how *Evil* is this.
+ """
+ self._popup_window = gtk.Window(gtk.WINDOW_POPUP)
+ self._popup_button = gtk.Button('*')
+ self._popup_window.add(self._popup_button)
+ self._popup_window.show_all()
+
+ self.forall(self._find_vertical_scrollbar)
+ self.connect('size-allocate', self._on_scrolled_window__size_allocate)
+ self.connect('realize', self._on_scrolled_window__realize)
+
+ def _find_vertical_scrollbar(self, widget):
+ """This method is called from a .forall() method in the ScrolledWindow.
+ It just save a reference to the vertical scrollbar for doing evil
+ things later.
+ """
+ if isinstance(widget, gtk.VScrollbar):
+ self._vscrollbar = widget
+
+ def _get_header_height(self):
+ treeview_column = self._treeview.get_column(0)
+ button = self._get_column_button(treeview_column)
+ alloc = button.get_allocation()
+ return alloc.height
+
+ def _on_scrolled_window__realize(self, widget):
+ toplevel = widget.get_toplevel()
+ self._popup_window.set_transient_for(toplevel)
+ self._popup_window.set_destroy_with_parent(True)
+
+ def _on_scrolled_window__size_allocate(self, widget, allocation):
+ """Resize the Vertical Scrollbar to make it smaller and let space
+ for the popup button. Also put that button there.
+ """
+ old_alloc = self._vscrollbar.get_allocation()
+ height = self._get_header_height()
+ new_alloc = gtk.gdk.Rectangle(old_alloc.x, old_alloc.y + height,
+ old_alloc.width,
+ old_alloc.height - height)
+ self._vscrollbar.size_allocate(new_alloc)
+ # put the popup_window in its position
+ gdk_window = self.window
+ if gdk_window:
+ winx, winy = gdk_window.get_origin()
+ self._popup_window.move(winx + old_alloc.x,
+ winy + old_alloc.y)
+
+ # end of the popup button hack
+
+ #
+ # Public API
+ #
+ def get_model(self):
+ "Return treemodel of the current list"
+ return self._model
+
+ def get_treeview(self):
+ "Return treeview of the current list"
+ return self._treeview
+
+ def get_columns(self):
+ return self._columns
+
+ def get_column_by_name(self, name):
+ """Returns the name of a column"""
+ for column in self._columns:
+ if column.attribute == name:
+ return column
+
+ raise LookupError("There is no column called %s" % name)
+
+ def get_treeview_column(self, column):
+ """
+ @param column: a @Column
+ """
+ if not isinstance(column, Column):
+ raise TypeError
+
+ if not column in self._columns:
+ raise ValueError
+
+ index = self._columns.index(column)
+ tree_columns = self._treeview.get_columns()
+ return tree_columns[index]
+
+ def set_columns(self, value):
+ """This function can be called in two different ways:
+ - value is a string with the column definitions in a special format
+ (see column-definitions property at the beginning of this class)
+
+ - value is a list/tuple of Column objects
+ """
+
+ if isinstance(value, basestring):
+ self._columns_string = value
+ self._columns = []
+ for col in value.split('^'):
+ if not col:
+ continue
+ self._columns.append(Column.from_string(col))
+ elif isinstance(value, (list, tuple)):
+ self._columns = value
+ self._columns_string = '^'.join([str(col) for col in value])
+ else:
+ raise ValueError("value should be a string of a list of columns")
+
+ self._clear_columns()
+ self._setup_columns()
+
+ def append(self, instance, select=False):
+ """Adds an instance to the list.
+ - instance: the instance to be added (according to the columns spec)
+ - select: whether or not the new item should appear selected.
+ """
+
+ # Freeze and save original selection mode to avoid blinking
+ self._treeview.freeze_notify()
+
+ row_iter = self._model.append((instance,))
+ self._iters[id(instance)] = row_iter
+
+ if self._autosize:
+ self._treeview.columns_autosize()
+
+ if select:
+ self._select_and_focus_row(row_iter)
+ self._treeview.thaw_notify()
+
+ def _remove(self, objid):
+ # linear search for the instance to remove
+ treeiter = self._iters.pop(objid)
+ if not treeiter:
+ return False
+
+ # Remove any references to this path
+ self._clear_cache_for_iter(treeiter)
+
+ # All references to the iter gone, now it can be removed
+ self._model.remove(treeiter)
+
+ return True
+
+ def _clear_cache_for_iter(self, treeiter):
+ # Not as inefficent as it looks
+ path = self._model[treeiter].path[0]
+ for cache in self._cell_data_caches.values():
+ if path in cache:
+ del cache[path]
+
+ def remove(self, instance, select=False):
+ """Remove an instance from the list.
+ If the instance is not in the list it returns False. Otherwise it
+ returns True.
+
+ @param instance:
+ @param select: if true, the previous item will be selected
+ if there is one.
+ """
+
+ objid = id(instance)
+ if not objid in self._iters:
+ raise ValueError("instance %r is not in the list" % instance)
+
+
+ if select:
+ prev = self.get_previous(instance)
+ rv = self._remove(objid)
+ if prev != instance:
+ self.select(prev)
+ else:
+ rv = self._remove(objid)
+ return rv
+
+ def update(self, instance):
+ objid = id(instance)
+ if not objid in self._iters:
+ raise ValueError("instance %r is not in the list" % instance)
+ treeiter = self._iters[objid]
+ self._clear_cache_for_iter(treeiter)
+ self._model.row_changed(self._model[treeiter].path, treeiter)
+
+ def refresh(self):
+ """
+ Reloads the values from all objects.
+ """
+ # XXX: Optimize this to only reload items, no need to remove/readd
+ model = self._model
+ instances = [row[COL_MODEL] for row in model]
+ model.clear()
+ self.add_list(instances)
+
+ def set_column_visibility(self, column_index, visibility):
+ treeview_column = self._treeview.get_column(column_index)
+ treeview_column.set_visible(visibility)
+
+ def get_selection_mode(self):
+ selection = self._treeview.get_selection()
+ if selection:
+ return selection.get_mode()
+
+ def set_selection_mode(self, mode):
+ selection = self._treeview.get_selection()
+ if selection:
+ self.notify('selection-mode')
+ return selection.set_mode(mode)
+
+ def unselect_all(self):
+ selection = self._treeview.get_selection()
+ if selection:
+ selection.unselect_all()
+
+ def select_paths(self, paths):
+ """
+ Selects a number of rows corresponding to paths
+
+ @param paths: rows to be selected
+ """
+
+ selection = self._treeview.get_selection()
+ if selection.get_mode() == gtk.SELECTION_NONE:
+ raise TypeError("Selection not allowed")
+
+ selection.unselect_all()
+ for path in paths:
+ selection.select_path(path)
+
+ def select(self, instance, scroll=True):
+ selection = self._treeview.get_selection()
+ if selection.get_mode() == gtk.SELECTION_NONE:
+ raise TypeError("Selection not allowed")
+
+ objid = id(instance)
+ if not objid in self._iters:
+ raise ValueError("instance %s is not in the list" % repr(instance))
+
+ treeiter = self._iters[objid]
+
+ selection.select_iter(treeiter)
+
+ if scroll:
+ self._treeview.scroll_to_cell(self._model[treeiter].path,
+ None, True, 0.5, 0)
+
+ def get_selected(self):
+ """Returns the currently selected object
+ If an object is not selected, None is returned
+ """
+ selection = self._treeview.get_selection()
+ if not selection:
+ # AssertionError ?
+ return
+
+ mode = selection.get_mode()
+ if mode == gtk.SELECTION_NONE:
+ raise TypeError("Selection not allowed in %r mode" % mode)
+ elif mode not in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
+ log.warn('get_selected() called when multiple rows '
+ 'can be selected')
+
+ model, treeiter = selection.get_selected()
+ if treeiter:
+ return model[treeiter][COL_MODEL]
+
+ def get_selected_rows(self):
+ """Returns a list of currently selected objects
+ If no objects are selected an empty list is returned
+ """
+ selection = self._treeview.get_selection()
+ if not selection:
+ # AssertionError ?
+ return
+
+ mode = selection.get_mode()
+ if mode == gtk.SELECTION_NONE:
+ raise TypeError("Selection not allowed in %r mode" % mode)
+ elif mode in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
+ log.warn('get_selected_rows() called when only a single row '
+ 'can be selected')
+
+ model, paths = selection.get_selected_rows()
+ if paths:
+ return [model[path][COL_MODEL] for (path,) in paths]
+ return []
+
+ def add_list(self, instances, clear=True):
+ """
+ Allows a list to be loaded, by default clearing it first.
+ freeze() and thaw() are called internally to avoid flashing.
+
+ @param instances: a list to be added
+ @param clear: a boolean that specifies whether or not to
+ clear the list
+ """
+
+ self._treeview.freeze_notify()
+
+ ret = self._load(instances, clear)
+
+ self._treeview.thaw_notify()
+
+ return ret
+
+ def clear(self):
+ """Removes all the instances of the list"""
+ self._model.clear()
+ self._iters = {}
+
+ # Don't clear the whole cache, just the
+ # individual column caches
+ for key in self._cell_data_caches:
+ self._cell_data_caches[key] = {}
+
+ def get_next(self, instance):
+ """
+ Returns the item after instance in the list.
+ Note that the instance must be inserted before this can be called
+ If there are no instances after, the first item will be returned.
+
+ @param instance: the instance
+ """
+
+ objid = id(instance)
+ if not objid in self._iters:
+ raise ValueError("instance %r is not in the list" % instance)
+
+ treeiter = self._iters[objid]
+
+ model = self._model
+ pos = model[treeiter].path[0]
+ if pos >= len(model) - 1:
+ pos = 0
+ else:
+ pos += 1
+ return model[pos][COL_MODEL]
+
+ def get_previous(self, instance):
+ """
+ Returns the item before instance in the list.
+ Note that the instance must be inserted before this can be called
+ If there are no instances before, the last item will be returned.
+
+ @param instance: the instance
+ """
+
+ objid = id(instance)
+ if not objid in self._iters:
+ raise ValueError("instance %r is not in the list" % instance)
+ treeiter = self._iters[objid]
+
+ model = self._model
+ pos = model[treeiter].path[0]
+ if pos == 0:
+ pos = len(model) - 1
+ else:
+ pos -= 1
+ return model[pos][COL_MODEL]
+
+ def get_selected_row_number(self):
+ """
+ @returns: the selected row number or None if no rows were selected
+ """
+ selection = self._treeview.get_selection()
+ if selection.get_mode() == gtk.SELECTION_MULTIPLE:
+ model, paths = selection.get_selected_rows()
+ if paths:
+ return paths[0][0]
+ else:
+ model, iter = selection.get_selected()
+ if iter:
+ return model[iter].path[0]
+
+ def double_click(self, rowno):
+ """
+ Same as double clicking on the row rowno
+
+ @param rowno: integer
+ """
+ columns = self._treeview.get_columns()
+ if not columns:
+ raise AssertionError(
+ "%s has no columns" % self.get_name())
+
+ self._treeview.row_activated(rowno, columns[0])
+
+ def set_headers_visible(self, value):
+ """
+ @param value: if true, shows the headers, if false hide then
+ """
+ self._treeview.set_headers_visible(value)
+
+type_register(ObjectList)
+
+class ListLabel(gtk.HBox):
+ """I am a subclass of a GtkHBox which you can use if you want
+ to vertically align a label with a column
+ """
+
+ def __init__(self, klist, column, label='', value_format='%s'):
+ """
+ @param klist: list to follow
+ @type klist: kiwi.ui.objectlist.ObjectList
+ @param column: name of a column in a klist
+ @type column: string
+ @param label: label
+ @type label: string
+ @param value_format: format string used to format value
+ @type value_format: string
+ """
+ self._label = label
+ self._label_width = -1
+ if not isinstance(klist, ObjectList):
+ raise TypeError("list must be a kiwi list and not %r" %
+ type(klist).__name__)
+ self._klist = klist
+ if not isinstance(column, str):
+ raise TypeError("column must be a string and not %r" %
+ type(column).__name__)
+ self._column = klist.get_column_by_name(column)
+ self._value_format = value_format
+
+ gtk.HBox.__init__(self)
+
+ self._create_ui()
+
+ # Public API
+
+ def set_value(self, value):
+ """Sets the value of the label.
+ Note that it needs to be of the same type as you specified in
+ value_format in the constructor.
+ I also support the GMarkup syntax, so you can use "<b>%d</b>" if
+ you want."""
+ self._value_widget.set_markup(self._value_format % value)
+
+ def get_value_widget(self):
+ return self._value_widget
+
+ def get_label_widget(self):
+ return self._label_widget
+
+ # Private
+
+ def _create_ui(self):
+
+ # When tracking the position/size of a column, we need to pay
+ # attention to the following two things:
+ # * treeview_column::width
+ # * size-allocate of treeview_columns header widget
+ #
+ tree_column = self._klist.get_treeview_column(self._column)
+ tree_column.connect('notify::width',
+ self._on_treeview_column__notify_width)
+
+ button = self._klist._get_column_button(tree_column)
+ button.connect('size-allocate',
+ self._on_treeview_column_button__size_allocate)
+
+ self._label_widget = gtk.Label()
+ self._label_widget.set_markup(self._label)
+
+ layout = self._label_widget.get_layout()
+ self._label_width = layout.get_pixel_size()[0]
+ self._label_widget.set_alignment(1.0, 0.5)
+ self.pack_start(self._label_widget, False, False, padding=6)
+ self._label_widget.show()
+
+ self._value_widget = gtk.Label()
+ xalign = tree_column.get_property('alignment')
+ self._value_widget.set_alignment(xalign, 0.5)
+ self.pack_start(self._value_widget, False, False)
+ self._value_widget.show()
+
+ def _resize(self, position=-1, width=-1):
+ if position != -1:
+ if position != 0:
+ if self._label_width > position:
+ self._label_widget.set_text('')
+ else:
+ self._label_widget.set_markup(self._label)
+
+ # XXX: Replace 12 with a constant
+ if position >= 12:
+ self._label_widget.set_size_request(position - 12, -1)
+
+ if width != -1:
+ self._value_widget.set_size_request(width, -1)
+
+ # Callbacks
+
+ def _on_treeview_column_button__size_allocate(self, label, rect):
+ self._resize(position=rect[0])
+
+ def _on_treeview_column__notify_width(self, treeview, pspec):
+ value = treeview.get_property(pspec.name)
+ self._resize(width=value)
+
+ def _on_list__size_allocate(self, list, rect):
+ self._resize(position=rect[0], width=rect[2])
+
+
+class SummaryLabel(ListLabel):
+ """I am a subclass of ListLabel which you can use if you want
+ to summarize all the values of a specific column.
+ Please note that I only know how to handle number column
+ data types and I will complain if you give me something else."""
+
+ def __init__(self, klist, column, label=_('Total:'), value_format='%s'):
+ ListLabel.__init__(self, klist, column, label, value_format)
+ if not issubclass(self._column.data_type, number):
+ raise TypeError("data_type of column must be a number, not %r",
+ self._column.data_type)
+ klist.connect('cell-edited', self._on_klist__cell_edited)
+ self.update_total()
+
+ # Public API
+
+ def update_total(self):
+ """Recalculate the total value of all columns"""
+ column = self._column
+ attr = column.attribute
+ get_attribute = column.get_attribute
+
+ value = sum([get_attribute(obj, attr) for obj in self._klist],
+ column.data_type('0'))
+
+ self.set_value(column.as_string(value))
+
+ # Callbacks
+
+ def _on_klist__cell_edited(self, klist, object, attribute):
+ self.update_total()
diff --git a/kiwi/ui/proxy.py b/kiwi/ui/proxy.py
new file mode 100644
index 0000000..fb80138
--- /dev/null
+++ b/kiwi/ui/proxy.py
@@ -0,0 +1,373 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2002-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""This module defines the Proxy class, which is a facility that can be used
+to keep the state of a model object synchronized with a View.
+"""
+
+import gobject
+import gtk
+
+from kiwi import ValueUnset
+from kiwi.accessor import kgetattr, ksetattr, clear_attr_cache
+from kiwi.decorators import deprecated
+from kiwi.interfaces import IProxyWidget, IValidatableProxyWidget
+from kiwi.log import Logger
+
+class ProxyError(Exception):
+ pass
+
+log = Logger('proxy')
+
+def block_widget(widget):
+ """Blocks the signal handler of the 'content-changed' signal on widget"""
+ connection_id = widget.get_data('content-changed-id')
+ if connection_id:
+ widget.handler_block(connection_id)
+
+def unblock_widget(widget):
+ """Unblocks the signal handler of the 'content-changed' signal on widget"""
+ connection_id = widget.get_data('content-changed-id')
+ if connection_id:
+ widget.handler_unblock(connection_id)
+
+class Proxy:
+ """ A Proxy is a class that 'attaches' an instance to an interface's
+ widgets, and transparently manipulates that instance's attributes as
+ the user alters the content of the widgets.
+
+ The Proxy takes the widget list and detects what widgets are to be
+ attached to the model by looking if it is a KiwiWidget and if it
+ has the model-attribute set.
+ """
+
+ def __init__(self, view, model=None, widgets=()):
+ """
+ @param view: view attched to the slave
+ @type view: a L{kiwi.ui.views.BaseView} subclass
+ @param model: model attached to proxy
+ @param widgets: the widget names
+ @type widgets: list of strings
+ """
+ self._view = view
+ self._model = model
+ self._model_attributes = {}
+
+ for widget_name in widgets:
+ widget = getattr(self._view, widget_name, None)
+ if widget is None:
+ raise AttributeError("The widget %s was not found in the "
+ "view %s" % (
+ widget_name, self._view.__class__.__name__))
+
+ self._setup_widget(widget_name, widget)
+
+ # Private API
+
+ def _reset_widget(self, attribute, widget):
+ if self._model is None:
+ # if we have no model, leave value unset so we pick up
+ # the widget default below.
+ value = ValueUnset
+ else:
+ # if we have a model, grab its value to update the widgets
+ self._register_proxy_in_model(attribute)
+ value = kgetattr(self._model, attribute, ValueUnset)
+
+ self.update(attribute, value, block=True)
+
+ # The initial value of the model is set, at this point
+ # do a read, it'll trigger a validation for widgets who
+ # supports it.
+ if not IValidatableProxyWidget.providedBy(widget):
+ return
+
+ widget.validate(force=True)
+
+ def _setup_widget(self, widget_name, widget):
+ if not IProxyWidget.providedBy(widget):
+ raise ProxyError("The widget %s (%r), in view %s is not "
+ "a kiwi widget and cannot be added to a proxy"
+ % (widget_name, widget,
+ self._view.__class__.__name__))
+
+ data_type = widget.get_property('data-type')
+ if data_type is None:
+ raise ProxyError("The kiwi widget %s (%r) in view %s should "
+ "have a data type set" % (
+ widget_name, widget, self._view.__class__.__name__))
+
+ attribute = widget.get_property('model-attribute')
+ if not attribute:
+ raise ProxyError(
+ "The widget %s (%s) in view %s is a kiwi widget but does "
+ "not have a model attribute set so it will not be "
+ "associated with the model" % (
+ widget_name, widget, self._view.__class__.__name__))
+
+ # Do a isinstance here instead of in the callback,
+ # as an optimization, it'll never change in runtime anyway
+ connection_id = widget.connect(
+ 'content-changed',
+ self._on_widget__content_changed,
+ attribute,
+ IValidatableProxyWidget.providedBy(widget))
+ widget.set_data('content-changed-id', connection_id)
+
+ model_attributes = self._model_attributes
+ # save this widget in our map
+ if (attribute in model_attributes and
+ # RadioButtons are allowed several times
+ not gobject.type_is_a(widget, 'GtkRadioButton')):
+ old_widget = model_attributes[attribute]
+ raise KeyError("The widget %s (%r) in view %s is already in "
+ "the proxy, defined by widget %s (%r)" % (
+ widget_name, widget, self._view.__class__.__name__,
+ old_widget.name, old_widget))
+
+ model_attributes[attribute] = widget
+ self._reset_widget(attribute, widget)
+
+ def _register_proxy_in_model(self, attribute):
+ model = self._model
+ if not hasattr(model, "register_proxy_for_attribute"):
+ return
+ try:
+ model.register_proxy_for_attribute(attribute, self)
+ except AttributeError:
+ msg = ("Failed to run register_proxy() on Model %s "
+ "(that was supplied to %s. \n"
+ "(Hint: if this model also inherits from ZODB's "
+ "Persistent class, this problem occurs if you haven't "
+ "set __setstate__() up correctly. __setstate__() "
+ "should call Model.__init__() (and "
+ "Persistent.__setstate__() of course) to rereset "
+ "things properly.)")
+ raise TypeError(msg % (model, self))
+
+ def _unregister_proxy_in_model(self):
+ if self._model and hasattr(self._model, "unregister_proxy"):
+ self._model.unregister_proxy(self)
+
+ # Callbacks
+
+ def _on_widget__content_changed(self, widget, attribute, validate):
+ """This is called as soon as the content of one of the widget
+ changes, the widgets tries fairly hard to not emit when it's not
+ neccessary"""
+
+ # skip updates for model if there is none, right?
+ if self._model is None:
+ return
+
+ if validate:
+ value = widget.validate()
+ else:
+ value = widget.read()
+
+ log('%s.%s = %r' % (self._model.__class__.__name__,
+ attribute, value))
+
+ # only update the model if the data is correct
+ if value is ValueUnset:
+ return
+
+ model = self._model
+ # XXX: one day we might want to queue and unique updates?
+ if hasattr(model, "block_proxy"):
+ model.block_proxy(self)
+ ksetattr(model, attribute, value)
+ model.unblock_proxy(self)
+ else:
+ ksetattr(model, attribute, value)
+
+ # Call global update hook
+ self.proxy_updated(widget, attribute, value)
+
+ # Properties
+
+ def _get_model(self):
+ return self._model
+ model = property(_get_model)
+
+ # Public API
+
+ def proxy_updated(self, widget, attribute, value):
+ """ This is a hook that is called whenever the proxy updates the
+ model. Implement it in the inherited class to perform actions that
+ should be done each time the user changes something in the interface.
+ This hook by default does nothing.
+ @param widget:
+ @param attribute:
+ @param value:
+ """
+
+ def update_many(self, attributes, value=ValueUnset, block=False):
+ """
+ Like L{update} but takes a sequence of attributes
+
+ @param attributes: sequence of attributes to update
+ @param value: see L{update}
+ @param block: see L{update}
+ """
+
+ for attribute in attributes:
+ self.update(attribute, value, block)
+
+ def update(self, attribute, value=ValueUnset, block=False):
+ """ Generic frontend function to update the contentss of a widget based
+ on its model attribute name using the internal update functions.
+
+ @param attribute: the name of the attribute whose widget we wish to
+ updated. If accessing a radiobutton, specify its group
+ name.
+ @param value: specifies the value to set in the widget. If
+ unspecified, it defaults to the current model's value
+ (through an accessor, if it exists, or getattr).
+ @param block: defines if we are to block cascading proxy updates
+ triggered by this update. You should use block if you are
+ calling update on *the same attribute that is currently
+ being updated*.
+ This means if you have hooked to a signal of the widget
+ associated to that attribute, and you call update() for
+ the *same attribute*, use block=True. And pray. 8). If
+ block is set to False, the normal update mechanism will
+ occur (the model being updated in the end, hopefully).
+ """
+
+ if value is ValueUnset:
+ # We want to obtain a value from our model
+ if self._model is None:
+ # We really want to avoid trying to update our UI if our
+ # model doesn't exist yet and no value was provided.
+ # update() is also called by user code, but it should be
+ # safe to return here since you shouldn't need to code
+ # around the lack of a model in your callbacks if you
+ # can help it.
+ value = None
+ else:
+ value = kgetattr(self._model, attribute, ValueUnset)
+
+ widget = self._model_attributes.get(attribute, None)
+
+ if widget is None:
+ raise AttributeError("Called update for `%s', which isn't "
+ "attached to the proxy %s. Valid "
+ "attributes are: %s (you may have "
+ "forgetten to add `:' to the name in "
+ "the widgets list)"
+ % (attribute, self,
+ self._model_attributes.keys()))
+
+
+ # The type of value should match the data-type property. The two
+ # exceptions to this rule are ValueUnset and None
+ if not (value is ValueUnset or value is None):
+ data_type = widget.get_property('data-type')
+ value_type = type(value)
+ if not isinstance(value, data_type):
+ raise TypeError(
+ "attribute %s of model %r requires a value of "
+ "type %s, not %s" % (
+ attribute, self._model,
+ data_type.__name__,
+ value_type.__name__))
+
+ if block:
+ block_widget(widget)
+ self._view.handler_block(widget)
+ widget.update(value)
+ self._view.handler_unblock(widget)
+ unblock_widget(widget)
+ else:
+ widget.update(value)
+ return True
+
+ def set_model(self, model, relax_type=False):
+ """
+ Updates the model instance of the proxy.
+ Allows a proxy interface to change model without the need to destroy
+ and recreate the UI (which would cause flashing, at least)
+
+ @param model:
+ @param relax_type:
+ """
+ if self._model is not None and model is not None:
+ if (not relax_type and
+ type(model) != type(self._model) and
+ not isinstance(model, self._model.__class__)):
+ raise TypeError("model has wrong type %s, expected %s"
+ % (type(model), type(self._model)))
+
+ # the following isn't strictly necessary, but it currently works
+ # around a bug with reused ids in the attribute cache and also
+ # makes a lot of sense for most applications (don't want a huge
+ # eternal cache pointing to models that you're not using anyway)
+ clear_attr_cache()
+
+ # unregister previous proxy
+ self._unregister_proxy_in_model()
+
+ self._model = model
+
+ for attribute, widget in self._model_attributes.items():
+ self._reset_widget(attribute, widget)
+
+ def add_widget(self, name, widget):
+ """
+ Adds a new widget to the proxy
+
+ @param name: name of the widget
+ @param widget: widget, must be a gtk.Widget subclass
+ """
+ if name in self._model_attributes:
+ raise TypeError("there is already a widget called %s" % name)
+
+ if not isinstance(widget, gtk.Widget):
+ raise TypeError("%r must be a gtk.Widget subclass" % widget)
+
+ self._setup_widget(name, widget)
+
+ def remove_widget(self, name):
+ """
+ Removes a widget from the proxy
+
+ @param name: the name of the widget to remove
+ """
+ if not name in self._model_attributes:
+ raise TypeError("there is no widget called %s" % name)
+
+ widget = self._model_attributes.pop(name)
+ if IValidatableProxyWidget.providedBy(widget):
+ connection_id = widget.get_data('content-changed-id')
+ widget.disconnect(connection_id)
+
+ # Backwards compatibility
+
+ def new_model(self, model, relax_type=False):
+ self.set_model(model)
+ new_model = deprecated('set_model', log)(new_model)
+
diff --git a/kiwi/ui/proxywidget.py b/kiwi/ui/proxywidget.py
new file mode 100644
index 0000000..717e832
--- /dev/null
+++ b/kiwi/ui/proxywidget.py
@@ -0,0 +1,316 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+# Daniel Saran R. da Cunha <daniel@async.com.br>
+#
+
+"""Basic classes for widget support for the Kiwi Framework"""
+
+import gettext
+
+import gtk
+from gtk import gdk
+
+from kiwi import ValueUnset
+from kiwi.component import implements
+from kiwi.datatypes import ValidationError, converter
+from kiwi.environ import environ
+from kiwi.interfaces import IProxyWidget, IValidatableProxyWidget
+from kiwi.log import Logger
+from kiwi.ui.gadgets import FadeOut
+from kiwi.utils import gsignal, gproperty
+
+log = Logger('widget proxy')
+
+_ = gettext.gettext
+
+class ProxyWidgetMixin(object):
+ """This class is a mixin that provide a common interface for KiwiWidgets.
+
+ Usually the Proxy class need to set and get data from the widgets. It also
+ need a validation framework.
+
+ @cvar allowed_data_types: A list of types which we are allowed to use
+ in this class.
+ """
+
+ implements(IProxyWidget)
+
+ gsignal('content-changed')
+ gsignal('validation-changed', bool)
+ gsignal('validate', object, retval=object)
+
+ gproperty('data-type', object, blurb='Data Type')
+ gproperty('model-attribute', str, blurb='Model attribute')
+
+ allowed_data_types = object,
+
+ # To be able to call the as/from_string without setting the data_type
+ # property and still receiving a good warning.
+ _converter = None
+
+ def __init__(self):
+ if not type(self.allowed_data_types) == tuple:
+ raise TypeError("%s.allowed_data_types must be a tuple" % (
+ self.allowed_data_types))
+ self._data_format = None
+
+ # Properties
+
+ def prop_set_data_type(self, data_type):
+ """Set the data type for the widget
+
+ @param data_type: can be None, a type object or a string with the
+ name of the type object, so None, "<type 'str'>"
+ or 'str'
+ """
+ if data_type is None:
+ return data_type
+
+ # This may convert from string to type,
+ # A type object will always be returned
+ data_type = converter.check_supported(data_type)
+
+ if not issubclass(data_type, self.allowed_data_types):
+ raise TypeError(
+ "%s only accept %s types, not %r"
+ % (self,
+ ' or '.join([t.__name__ for t in self.allowed_data_types]),
+ data_type))
+
+ self._converter = converter.get_converter(data_type)
+ return data_type
+
+ # Public API
+ def set_data_format(self, format):
+ self._data_format = format
+
+ def read(self):
+ """Get the content of the widget.
+ The type of the return value
+ @returns: None if the user input a invalid value
+ @rtype: Must matche the data-type property.
+ """
+ raise NotImplementedError
+
+ def update(self, value):
+ """
+ @param value:
+ """
+ raise NotImplementedError
+
+ # Private
+
+ def _as_string(self, data):
+ """Convert a value to a string
+ @param data: data to convert
+ """
+ conv = self._converter
+ if conv is None:
+ raise AssertionError(
+ "You need to set a data type before calling _as_string")
+
+ return conv.as_string(data, format=self._data_format)
+
+ def _from_string(self, data):
+ """Convert a string to the data type of the widget
+ This may raise a L{kiwi.datatypes.ValidationError} if conversion
+ failed
+ @param data: data to convert
+ """
+ conv = self._converter
+ if conv is None:
+ raise AssertionError(
+ "You need to set a data type before calling _from_string")
+
+ return conv.from_string(data)
+
+MANDATORY_ICON = gtk.STOCK_EDIT
+ERROR_ICON = gdk.pixbuf_new_from_file(
+ environ.find_resource('pixmap', 'validation-error-16.png'))
+
+class ValidatableProxyWidgetMixin(ProxyWidgetMixin):
+ """Class used by some Kiwi Widgets that need to support mandatory
+ input and validation features such as custom validation and data-type
+ validation.
+
+ Mandatory support provides a way to warn the user when input is necessary.
+ The validatation feature provides a way to check the data entered and to
+ display information about what is wrong.
+ """
+
+ implements(IValidatableProxyWidget)
+
+ gproperty('mandatory', bool, default=False)
+
+ def __init__(self, widget=None):
+ ProxyWidgetMixin.__init__(self)
+
+ self._valid = True
+ self._fade = FadeOut(self)
+ self._fade.connect('color-changed', self._on_fadeout__color_changed)
+
+ # Override in subclass
+
+ def update_background(self, color):
+ "Implement in subclass"
+
+ def set_pixbuf(self, pixbuf):
+ "Implement in subclass"
+
+ def get_icon_window(self):
+ "Implement in subclass"
+
+ def set_tooltip(self, text):
+ "Implement in subclass"
+
+ # Public API
+
+ def is_valid(self):
+ """
+ @returns: True if the widget is in validated state
+ """
+ return self._valid
+
+ def validate(self, force=False):
+ """Checks if the data is valid.
+ Validates data-type and custom validation.
+
+ @param force: if True, force validation
+ @returns: validated data or ValueUnset if it failed
+ """
+
+ try:
+ data = self.read()
+ log.debug('Read %r for %s' % (data, self.model_attribute))
+
+ # check if we should draw the mandatory icon
+ # this need to be done before any data conversion because we
+ # we don't want to end drawing two icons
+ if self.mandatory and (data == None or
+ data == '' or
+ data == ValueUnset):
+ self.set_blank()
+ return ValueUnset
+ else:
+
+ # The widgets themselves have now valid the data
+ # Next step is to call the application specificed
+ # checks, which are found in the view.
+ if data is not None and data is not ValueUnset:
+ # this signal calls the on_widgetname__validate method
+ # of the view class and gets the exception (if any).
+ error = self.emit("validate", data)
+ if error:
+ raise error
+
+ self.set_valid()
+ return data
+ except ValidationError, e:
+ self.set_invalid(str(e))
+ return ValueUnset
+
+ def set_valid(self):
+ """Changes the validation state to valid, which will remove icons and
+ reset the background color
+ """
+
+ log.debug('Setting state for %s to VALID' % self.model_attribute)
+ self._set_valid_state(True)
+
+ self._fade.stop()
+ self.set_pixbuf(None)
+
+ def set_invalid(self, text=None, fade=True):
+ """Changes the validation state to invalid.
+ @param text: text of tooltip of None
+ @param fade: if we should fade the background
+ """
+ log.debug('Setting state for %s to INVALID' % self.model_attribute)
+
+ self._set_valid_state(False)
+
+ if not fade:
+ return
+
+ # If there is no error text, set a generic one so the error icon
+ # still have a tooltip
+ if not text:
+ text = _("'%s' is not a valid value for this field") % self.read()
+
+ self.set_tooltip(text)
+
+ # When the fading animation is finished, set the error icon
+ # We don't need to check if the state is valid, since stop()
+ # (which removes this timeout) is called as soon as the user
+ # types valid data.
+ def done(fadeout, c):
+ self.set_pixbuf(ERROR_ICON)
+ self.queue_draw()
+ fadeout.disconnect(c.signal_id)
+
+ class SignalContainer:
+ pass
+ c = SignalContainer()
+ c.signal_id = self._fade.connect('done', done, c)
+
+ if self._fade.start():
+ self.set_pixbuf(None)
+
+ def set_blank(self):
+ """Changes the validation state to blank state, this only applies
+ for mandatory widgets, draw an icon and set a tooltip"""
+
+ log.debug('Setting state for %s to BLANK' % self.model_attribute)
+
+ if self.mandatory:
+ self._draw_stock_icon(MANDATORY_ICON)
+ self.set_tooltip(_('This field is mandatory'))
+ self._fade.stop()
+ valid = False
+ else:
+ valid = True
+
+ self._set_valid_state(valid)
+
+ # Private
+
+ def _set_valid_state(self, state):
+ """Updates the validation state and emits a signal iff it changed"""
+
+ if self._valid == state:
+ return
+
+ self.emit('validation-changed', state)
+ self._valid = state
+
+ def _draw_stock_icon(self, stock_id):
+ icon = self.render_icon(stock_id, gtk.ICON_SIZE_MENU)
+ self.set_pixbuf(icon)
+ self.queue_draw()
+
+ # Callbacks
+
+ def _on_fadeout__color_changed(self, fadeout, color):
+ self.update_background(color)
diff --git a/kiwi/ui/selectablebox.py b/kiwi/ui/selectablebox.py
new file mode 100644
index 0000000..33d2085
--- /dev/null
+++ b/kiwi/ui/selectablebox.py
@@ -0,0 +1,186 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""
+A box which you can select and will have a border around it when
+you click on any widgets in it
+"""
+
+import gtk
+from gtk import gdk
+
+class SelectableBox(object):
+ def __init__(self, width=4):
+ self._selected = None
+ self._draw_gc = None
+ self._selection_width = width
+ self.unset_flags(gtk.NO_WINDOW)
+ self.set_redraw_on_allocate(True)
+ self.set_spacing(width)
+ self.set_border_width(width)
+
+ # Public API
+
+ def get_selected(self):
+ """
+ @returns: the currently selected widget
+ """
+
+ return self._selected
+
+ def set_selected(self, widget):
+ """
+ @param widget: widget to select, must be a children of self
+ """
+
+ if not widget in self.get_children():
+ raise ValueError("widget must be a child of %r" % self)
+
+ old_selected = self._selected
+ self._selected = widget
+ if old_selected != widget:
+ self.queue_draw()
+
+ def pack_start(self, child, expand=True, fill=True, padding=0):
+ """
+ Identical to gtk.Box.pack_start
+ """
+ super(SelectableBox, self).pack_start(child, expand=expand,
+ fill=fill, padding=padding)
+ self._child_added(child)
+
+ def pack_end(self, child, expand=True, fill=True, padding=0):
+ """
+ Identical to gtk.Box.pack_end
+ """
+ super(SelectableBox, self).pack_end(child, expand=expand,
+ fill=fill, padding=padding)
+ self._child_added(child)
+
+ def add(self, child):
+ """
+ Identical to gtk.Container.add
+ """
+ super(SelectableBox, self).add(child)
+ self._child_added(child)
+
+ def update_selection(self):
+ selected = self._selected
+ if not selected:
+ return
+
+ border = self._selection_width
+ x, y, w, h = selected.allocation
+ self.window.draw_rectangle(self._draw_gc, False,
+ x - (border / 2), y - (border / 2),
+ w + border, h + border)
+
+ # GtkWidget
+
+ def do_realize(self):
+ assert not (self.flags() & gtk.NO_WINDOW)
+ self.set_flags(self.flags() | gtk.REALIZED)
+ self.window = gdk.Window(self.get_parent_window(),
+ width=self.allocation.width,
+ height=self.allocation.height,
+ window_type=gdk.WINDOW_CHILD,
+ wclass=gdk.INPUT_OUTPUT,
+ event_mask=(self.get_events() |
+ gdk.EXPOSURE_MASK |
+ gdk.BUTTON_PRESS_MASK))
+ self.window.set_user_data(self)
+ self.style.attach(self.window)
+ self.style.set_background(self.window, gtk.STATE_NORMAL)
+
+ self._draw_gc = gdk.GC(self.window,
+ line_width=self._selection_width,
+ line_style=gdk.SOLID,
+ foreground=self.style.bg[gtk.STATE_SELECTED])
+
+ def do_button_press_event(self, event):
+ selected = self._get_child_at_pos(int(event.x), int(event.y))
+ if selected:
+ self.set_selected(selected)
+
+ # Private
+
+ def _get_child_at_pos(self, x, y):
+ """
+ @param x: x coordinate
+ @type x: integer
+ @param y: y coordinate
+ @type y: integer
+ """
+ toplevel = self.get_toplevel()
+ for child in self.get_children():
+ coords = toplevel.translate_coordinates(child, x, y)
+ if not coords:
+ continue
+
+ child_x, child_y = coords
+ if (0 <= child_x < child.allocation.width and
+ 0 <= child_y < child.allocation.height and
+ child.flags() & (gtk.MAPPED | gtk.VISIBLE)):
+ return child
+
+ def _child_added(self, child):
+ child.connect('button-press-event',
+ lambda child, e: self.set_selected(child))
+
+class SelectableHBox(SelectableBox, gtk.HBox):
+ __gtype_name__ = 'SelectableHBox'
+
+ def __init__(self, width=4):
+ gtk.HBox.__init__(self)
+ SelectableBox.__init__(self, width=width)
+
+ do_realize = SelectableBox.do_realize
+ do_button_press_event = SelectableBox.do_button_press_event
+
+ def do_size_allocate(self, allocation):
+ gtk.HBox.do_size_allocate(self, allocation)
+ if self.flags() & gtk.REALIZED:
+ self.window.move_resize(*allocation)
+
+ def do_expose_event(self, event):
+ gtk.HBox.do_expose_event(self, event)
+ self.update_selection()
+
+class SelectableVBox(SelectableBox, gtk.VBox):
+ __gtype_name__ = 'SelectableVBox'
+
+ def __init__(self, width=4):
+ gtk.VBox.__init__(self)
+ SelectableBox.__init__(self, width=width)
+
+ do_realize = SelectableBox.do_realize
+ do_button_press_event = SelectableBox.do_button_press_event
+
+ def do_size_allocate(self, allocation):
+ gtk.VBox.do_size_allocate(self, allocation)
+ if self.flags() & gtk.REALIZED:
+ self.window.move_resize(*allocation)
+
+ def do_expose_event(self, event):
+ gtk.VBox.do_expose_event(self, event)
+ self.update_selection()
diff --git a/kiwi/ui/test/__init__.py b/kiwi/ui/test/__init__.py
new file mode 100644
index 0000000..436680f
--- /dev/null
+++ b/kiwi/ui/test/__init__.py
@@ -0,0 +1,24 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br
+#
+
+"""User interface: Testing"""
diff --git a/kiwi/ui/test/common.py b/kiwi/ui/test/common.py
new file mode 100644
index 0000000..37474e7
--- /dev/null
+++ b/kiwi/ui/test/common.py
@@ -0,0 +1,198 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br
+#
+
+"""
+Common routines used by L{kiwi.ui.test.listener.Listener} and
+L{kiwi.ui.test.player.Player}
+"""
+
+import sets
+
+import gobject
+import gtk
+
+class Base(object):
+ def __init__(self):
+ self._windows = {}
+ self._window_list = self._list_windows()
+ gobject.timeout_add(25, self._check_windows)
+ self._objects = {}
+
+ # Public API
+
+ def parse_one(self, toplevel, gobj):
+ """
+ @param toplevel:
+ @param gobj:
+ """
+
+ if not isinstance(gobj, gobject.GObject):
+ raise TypeError
+
+ gtype = gobj
+ while True:
+ name = gobject.type_name(gtype)
+ func = getattr(self, name, None)
+ if func:
+ if func(toplevel, gobj):
+ break
+ if gtype == gobject.GObject.__gtype__:
+ break
+
+ gtype = gobject.type_parent(gtype)
+
+ def get_object(self, attr):
+ """
+ @param attr: name of toplevel object to get
+ @returns: toplevel object
+ """
+ return self._objects[attr]
+
+ # Override in subclass
+
+ def window_added(self, window):
+ """
+ This will be called when a window is displayed
+ @param window:
+ """
+
+ def window_removed(self, window):
+ """
+ This will be called when a window is destroyed
+ @param window:
+ """
+
+ # Private
+
+ def _on_window_name_change(self, window, pspec, old_name):
+ # Update datastructures, no need to notify that the dialog
+ # was added, we already know about it and all its children
+ self._windows[window.get_name()] = self._windows.pop(old_name)
+
+ def _list_windows(self):
+ # We're only interested in toplevels for now, tooltip windows are
+ # popups for example
+ rv = []
+ for window in gtk.window_list_toplevels():
+ if window.type != gtk.WINDOW_TOPLEVEL:
+ if not isinstance(window.child, gtk.Menu):
+ continue
+
+ # Hack to get all the entries of a popup menu in
+ # the same namespace as the window they were launched
+ # in.
+ parent_menu = window.child.get_data('parent-menu')
+ if parent_menu:
+ main = parent_menu.get_toplevel()
+ rv.append((main.get_name(), window))
+ else:
+ rv.append((window.get_name(), window))
+
+ return sets.Set(rv)
+
+ def _check_windows(self):
+ new_windows = self._list_windows()
+ if self._windows != new_windows:
+ for name, window in new_windows.difference(self._window_list):
+ # Popup window, eg menu popups needs to be treated
+ # specially, only parse the contained widgets, do not
+ # add it or listen to name changes, we don't care about them
+ if window.type == gtk.WINDOW_POPUP:
+ # XXX: This is trigged by stoq.test.gui.salewizard
+ if name in self._windows:
+ toplevel = self._windows[name]
+ self.parse_one(toplevel, window)
+ else:
+ self.parse_one(window, window)
+
+ window.connect('notify::name', self._on_window_name_change,
+ window.get_name())
+ self.window_added(window)
+ self._windows[name] = window
+
+ for name, window in self._window_list.difference(new_windows):
+ # We don care about popup windows, see above
+ if window.type == gtk.WINDOW_POPUP:
+ continue
+
+ self.window_removed(window)
+ del self._windows[name]
+
+ self._window_list = new_windows
+ return True
+
+ def ignore(self, toplevel, gobj):
+ pass
+
+ GtkSeparatorMenuItem = GtkTearoffMenuItem = ignore
+
+ def _add_widget(self, toplevel, widget, name):
+ toplevel_widgets = self._objects.setdefault(toplevel.get_name(), {})
+ if not name in toplevel_widgets:
+ toplevel_widgets[name] = widget
+
+ def GtkWidget(self, toplevel, widget):
+ """
+ Called when a GtkWidget is about to be traversed
+ """
+ self._add_widget(toplevel, widget, widget.get_name())
+
+ def GtkContainer(self, toplevel, container):
+ """
+ Called when a GtkContainer is about to be traversed
+
+ Parsers all the children and listens for new children, which
+ may be added at a later point.
+ """
+ for child in container.get_children():
+ self.parse_one(toplevel, child)
+
+ def _on_container_add(container, widget):
+ self.parse_one(toplevel, widget)
+ container.connect('add', _on_container_add)
+
+ def GtkDialog(self, toplevel, dialog):
+ """
+ Called when a GtkDialog is about to be traversed
+
+ Just parses the widgets embedded in the dialogs.
+ """
+ self.parse_one(toplevel, dialog.action_area)
+ self.parse_one(toplevel, dialog.vbox)
+
+ def GtkMenuItem(self, toplevel, item):
+ """
+ Called when a GtkMenuItem is about to be traversed
+
+ It does some magic to tie a stronger connection between toplevel
+ menuitems and submenus, which later will be used.
+ """
+ submenu = item.get_submenu()
+ if submenu:
+ submenu.set_data('parent-menu', item)
+ for child_item in submenu.get_children():
+ child_item.set_data('parent-menu', item)
+ self.parse_one(toplevel, submenu)
+
+ def GtkToolButton(self, toplevel, item):
+ item.child.set_name(item.get_name())
diff --git a/kiwi/ui/test/listener.py b/kiwi/ui/test/listener.py
new file mode 100644
index 0000000..bfb4583
--- /dev/null
+++ b/kiwi/ui/test/listener.py
@@ -0,0 +1,458 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br
+#
+
+"""
+User interface event listener and serializer.
+
+This module provides an interface for creating, listening to
+and saving events.
+It uses the gobject introspection base class
+L{kiwi.ui.test.common.Base} to gather widgets, windows and other objects.
+
+The user interfaces are saved in a format so they can easily be played
+back by simply executing the script through a standard python interpreter.
+"""
+
+import atexit
+
+from gtk import gdk
+import gtk
+
+from kiwi.ui.test.common import Base
+from kiwi.ui.combomixin import ComboMixin
+from kiwi.ui.objectlist import ObjectList
+
+_events = []
+
+def register_event_type(event_type):
+ """
+ Add an event type to a list of event types.
+
+ @param event_type: a L{Event} subclass
+ """
+ if event_type in _events:
+ raise AssertionError("event %s already registered" % event_type)
+ _events.append(event_type)
+
+def get_event_types():
+ """
+ Returns the collection of event types.
+ @returns: the event types.
+ """
+ return _events
+
+class SkipEvent(Exception):
+ pass
+
+class Event(object):
+ """
+ Event is a base class for all events.
+ An event represent a user change of an interactive widget.
+ @cvar object_type: subclass for type, L{Listener} uses this to
+ automatically attach events to objects when they appear
+ """
+ object_type = None
+ def __init__(self, object, name=None):
+ """
+ @param object: a gobject subclass
+ @param name: name of the object, if None, the
+ method get_name() will be called
+ """
+ self.object = object
+ if name is None:
+ name = object.get_name()
+ self.name = name
+ self.toplevel_name = self.get_toplevel(object).get_name()
+
+ # Override in subclass
+ def get_toplevel(self, widget):
+ """
+ This fetches the toplevel widget for a specific object,
+ by default it assumes it's a wiget subclass and calls
+ get_toplevel() for the widget
+
+ Override this in a subclass.
+ """
+ return widget.get_toplevel()
+
+ def serialize(self):
+ """
+ Serialize the widget, write the code here which is
+ used to reproduce the event, for a button which is clicked
+ the implementation looks like this:
+
+ >>> def serialize(self):
+ >>> ... return '%s.clicked' % self.name
+
+ @returns: string to reproduce event
+ Override this in a subclass.
+ """
+ pass
+
+class SignalEvent(Event):
+ """
+ A SignalEvent is an L{Event} which is tied to a GObject signal,
+ L{Listener} uses this to automatically attach itself to a signal
+ at which point this object will be instantiated.
+
+ @cvar signal_name: signal to listen to
+ """
+ signal_name = None
+ def __init__(self, object, name, args):
+ """
+ @param object:
+ @param name:
+ @param args:
+ """
+ Event.__init__(self, object, name)
+ self.args = args
+
+ def connect(cls, object, signal_name, cb):
+ """
+ Calls connect on I{object} for signal I{signal_name}.
+
+ @param object: object to connect on
+ @param signal_name: signal name to listen to
+ @param cb: callback
+ """
+ object.connect(signal_name, cb, cls, object)
+ connect = classmethod(connect)
+
+class WindowDeleteEvent(SignalEvent):
+ """
+ This event represents a user click on the close button in the
+ window manager.
+ """
+
+ signal_name = 'delete-event'
+ object_type = gtk.Window
+
+ def serialize(self):
+ return 'delete_window("%s")' % self.name
+
+register_event_type(WindowDeleteEvent)
+
+class MenuItemActivateEvent(SignalEvent):
+ """
+ This event represents a user click on a menu item.
+ It could be a toplevel or a normal entry in a submenu.
+ """
+ signal_name = 'activate'
+ object_type = gtk.MenuItem
+
+ def serialize(self):
+ return '%s.activate()' % self.name
+register_event_type(MenuItemActivateEvent)
+
+class ImageMenuItemButtonReleaseEvent(SignalEvent):
+ """
+ This event represents a click on a normal menu entry
+ It's sort of a hack to use button-press-event, instea
+ of listening to activate, but we'll get the active callback
+ after the user specified callbacks are called, at which point
+ it is already too late.
+ """
+ signal_name = 'button-release-event'
+ object_type = gtk.ImageMenuItem
+
+ def get_toplevel(self, widget):
+ parent = widget
+ while True:
+ widget = parent.get_data('parent-menu')
+ if not widget:
+ break
+ parent = widget
+ toplevel = parent.get_toplevel()
+ return toplevel
+
+ def serialize(self):
+ return '%s.activate()' % self.name
+register_event_type(ImageMenuItemButtonReleaseEvent)
+
+class ToolButtonReleaseEvent(SignalEvent):
+ """
+ This event represents a click on a normal toolbar button
+ Hackish, see L{ImageMenuItemButtonReleaseEvent} for more details.
+ """
+ signal_name = 'button-release-event'
+ object_type = gtk.Button
+
+ def serialize(self):
+ return '%s.activate()' % self.name
+register_event_type(ToolButtonReleaseEvent)
+
+class EntrySetTextEvent(SignalEvent):
+ """
+ This event represents a content modification of a GtkEntry.
+ When the user deletes, clears, adds, modifies the text this
+ event will be created.
+ """
+ signal_name = 'notify::text'
+ object_type = gtk.Entry
+
+ def __init__(self, object, name, args):
+ SignalEvent.__init__(self, object, name, args)
+ self.text = self.object.get_text()
+
+ def serialize(self):
+ return '%s.set_text("%s")' % (self.name, self.text)
+register_event_type(EntrySetTextEvent)
+
+class EntryActivateEvent(SignalEvent):
+ """
+ This event represents an activate event for a GtkEntry, eg when
+ the user presses enter in a GtkEntry.
+ """
+
+ signal_name = 'activate'
+ object_type = gtk.Entry
+
+ def serialize(self):
+ return '%s.activate()' % (self.name)
+register_event_type(EntryActivateEvent)
+
+# Also works for Toggle, Radio and Check
+class ButtonClickedEvent(SignalEvent):
+ """
+ This event represents a button click.
+ Note that this will also work for GtkToggleButton, GtkRadioButton
+ and GtkCheckButton.
+ """
+ signal_name = 'clicked'
+ object_type = gtk.Button
+
+ def serialize(self):
+ return '%s.clicked()' % self.name
+register_event_type(ButtonClickedEvent)
+
+# Kiwi widget support
+class ObjectListSelectionChanged(SignalEvent):
+ """
+ This event represents a selection change on a
+ L{kiwi.ui.objectlist.ObjectList},
+ eg when the user selects or unselects a row.
+ It is actually tied to the signal changed on GtkTreeSelection object.
+ """
+ object_type = ObjectList
+ signal_name = 'changed'
+ def __init__(self, objectlist, name, args):
+ self._objectlist = objectlist
+ SignalEvent.__init__(self, objectlist, name=objectlist.get_name(),
+ args=args)
+ self.rows = self._get_rows()
+
+ def _get_rows(self):
+ selection = self._objectlist.get_treeview().get_selection()
+
+ if selection.get_mode() == gtk.SELECTION_MULTIPLE:
+ # get_selected_rows() returns a list of paths
+ iters = selection.get_selected_rows()[1]
+ if iters:
+ return iters
+ else:
+ # while get_selected returns an iter, yay.
+ model, iter = selection.get_selected()
+ if iter is not None:
+ # so convert it to a path and put it in an empty list
+ return [model.get_string_from_iter(iter)]
+
+ return []
+
+ def connect(cls, orig, signal_name, cb):
+ object = orig.get_treeview().get_selection()
+ object.connect(signal_name, cb, cls, orig)
+ connect = classmethod(connect)
+
+ def get_toplevel(self, widget):
+ return self._objectlist.get_toplevel()
+
+ def serialize(self):
+ return '%s.select_paths(%s)' % (self.name, self.rows)
+register_event_type(ObjectListSelectionChanged)
+
+class ObjectListDoubleClick(SignalEvent):
+ """
+ This event represents a double click on a row in objectlist
+ """
+ signal_name = 'button-press-event'
+ object_type = ObjectList
+
+ def __init__(self, objectlist, name, args):
+ event, = args
+ if event.type != gdk._2BUTTON_PRESS:
+ raise SkipEvent
+
+ SignalEvent.__init__(self, objectlist, name, args)
+ self.row = objectlist.get_selected_row_number()
+
+ def connect(cls, orig, signal_name, cb):
+ object = orig.get_treeview()
+ object.connect(signal_name, cb, cls, orig)
+ connect = classmethod(connect)
+
+ def serialize(self):
+ return '%s.double_click(%s)' % (self.name, self.row)
+register_event_type(ObjectListDoubleClick)
+
+class KiwiComboBoxChangedEvent(SignalEvent):
+ """
+ This event represents a a selection of an item
+ in a L{kiwi.ui.widgets.combobox.ComboBoxEntry} or
+ L{kiwi.ui.widgets.combobox.ComboBox}.
+ """
+ signal_name = 'changed'
+ object_type = ComboMixin
+ def __init__(self, combo, name, args):
+ SignalEvent.__init__(self, combo, name, args)
+ self.label = combo.get_selected_label()
+
+ def serialize(self):
+ return '%s.select_item_by_label("%s")' % (self.name, self.label)
+
+register_event_type(KiwiComboBoxChangedEvent)
+
+class Listener(Base):
+ """
+ Listener takes care of attaching events to widgets, when the appear,
+ and creates the events when the user is interacting with some widgets.
+ When the tracked program is closed the events are serialized into
+ a script which can be played back with help of
+ L{kiwi.ui.test.player.Player}.
+ """
+
+ def __init__(self, filename, args):
+ """
+ @param filename: name of the script
+ @param args: command line used to run the script
+ """
+ Base.__init__(self)
+ self._filename = filename
+ self._args = args
+ self._events = []
+ self._listened_objects = []
+ self._event_types = self._configure_event_types()
+
+ atexit.register(self.save)
+
+ def _configure_event_types(self):
+ event_types = {}
+ for event_type in get_event_types():
+ if event_type.object_type is None:
+ raise AssertionError
+ elist = event_types.setdefault(event_type.object_type, [])
+ elist.append(event_type)
+
+ return event_types
+
+ def _add_event(self, event):
+ self._events.append(event)
+
+ def _listen_event(self, object, event_type):
+ if not issubclass(event_type, SignalEvent):
+ raise TypeError("Can only listen to SignalEvents, not %r"
+ % event_type)
+
+ if event_type.signal_name is None:
+ raise ValueError("signal_name cannot be None")
+
+ # This is horrible, but there's no good way of passing in
+ # more than one variable to the script and we really want to be
+ # able to connect it to any kind of signal, regardless of
+ # the number of parameters the signal has
+ def on_signal(object, *args):
+ event_type, orig = args[-2:]
+ try:
+ self._add_event(event_type(orig, None, args[:-2]))
+ except SkipEvent:
+ pass
+ event_type.connect(object, event_type.signal_name, on_signal)
+
+ def window_removed(self, window):
+ self._add_event(WindowDeleteEvent(window, None, []))
+
+ def parse_one(self, toplevel, gobj):
+ Base.parse_one(self, toplevel, gobj)
+
+ # mark the object as "listened" to ensure we'll always
+ # receive unique objects
+ if gobj in self._listened_objects:
+ return
+ self._listened_objects.append(gobj)
+
+ for object_type, event_types in self._event_types.items():
+ if not isinstance(gobj, object_type):
+ continue
+
+ for event_type in event_types:
+ # These 3 hacks should move into the event class itself
+ if event_type == MenuItemActivateEvent:
+ if not isinstance(gobj.get_parent(), gtk.MenuBar):
+ continue
+ elif event_type == ToolButtonReleaseEvent:
+ if not isinstance(gobj.get_parent(), gtk.ToolButton):
+ continue
+ elif event_type == ButtonClickedEvent:
+ if isinstance(gobj.get_parent(), gtk.ToolButton):
+ continue
+
+ if issubclass(event_type, SignalEvent):
+ self._listen_event(gobj, event_type)
+
+ def save(self):
+ """
+ Collect events and serialize them into a script and save
+ the script.
+ This should be called when the tracked program has
+ finished executing.
+ """
+
+ try:
+ fd = open(self._filename, 'w')
+ except IOError:
+ raise SystemExit("Could not write: %s" % self._filename)
+ fd.write("from kiwi.ui.test.player import Player\n"
+ "\n"
+ "player = Player(%s)\n"
+ "app = player.get_app()\n" % repr(self._args))
+
+ windows = {}
+
+ for event in self._events:
+ toplevel = event.toplevel_name
+ if not toplevel in windows:
+ fd.write('\n'
+ 'player.wait_for_window("%s")\n' % toplevel)
+ windows[toplevel] = True
+
+ if isinstance(event, WindowDeleteEvent):
+ fd.write("player.%s\n\n" % (event.serialize()))
+ if not event.name in windows:
+ # Actually a bug
+ continue
+ del windows[event.name]
+ else:
+ fd.write("app.%s.%s\n" % (toplevel,
+ event.serialize()))
+
+ fd.write('player.finish()\n')
+ fd.close()
diff --git a/kiwi/ui/test/main.py b/kiwi/ui/test/main.py
new file mode 100644
index 0000000..b1c1d37
--- /dev/null
+++ b/kiwi/ui/test/main.py
@@ -0,0 +1,52 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005,2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+
+"""
+Kiwi UI Test: command line interface
+"""
+
+import optparse
+import sys
+
+def _play(filename, args):
+ from kiwi.ui.test.player import play_file
+ play_file(filename, args)
+
+def _record(filename, args):
+ from kiwi.ui.test.listener import Listener
+
+ Listener(filename, args[1:])
+
+ sys.argv = args[1:]
+ execfile(sys.argv[0])
+
+def main(args):
+ parser = optparse.OptionParser()
+ parser.add_option('', '--record', action="store",
+ dest="record")
+ options, args = parser.parse_args(args)
+
+ if options.record:
+ _record(options.record, args)
+ else:
+ if len(args) < 2:
+ raise SystemExit("Error: needs a filename to play")
+ _play(args[1], args[2:])
diff --git a/kiwi/ui/test/player.py b/kiwi/ui/test/player.py
new file mode 100644
index 0000000..0e17e16
--- /dev/null
+++ b/kiwi/ui/test/player.py
@@ -0,0 +1,250 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005,2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br
+#
+
+"""
+Test script playback system and infrastructure.
+"""
+import os
+import sys
+import threading
+import time
+
+import gobject
+gobject.threads_init()
+import gtk
+from gtk import gdk
+gdk.threads_init()
+
+from kiwi.log import Logger
+from kiwi.ui.test.common import Base
+
+WINDOW_TIMEOUT = 10
+WINDOW_WAIT = 0.5
+WIDGET_TIMEOUT = 2
+
+# This is pretty important, it gives the application 2 seconds
+# to finish closing the dialog, eg write stuff to the database and
+# yada yada
+DELETE_WINDOW_WAIT = 4
+
+class TimeOutError(Exception):
+ """
+ Exception that will be raised when a widget cannot be found,
+ which will happen after a few seconds depending on the type
+ of widget
+ """
+ pass
+
+log = Logger('uitest')
+
+class ThreadSafeFunction:
+ """
+ A function which is safe thread in the mainloop context
+ All widgets and object functions will be wrapped by this.
+ """
+
+ def __init__(self, func, obj_name):
+ self._func = func
+ self._obj_name = obj_name
+
+ def _invoke(self, *args, **kwargs):
+ gdk.threads_enter()
+ log('Calling %s.%s(%s)' % (self._obj_name,
+ self._func.__name__,
+ ', '.join(map(repr, args))))
+ self._func(*args, **kwargs)
+ gdk.threads_leave()
+ return False
+
+ def __call__(self, *args, **kwargs):
+ # dialog.run locks us out
+ #rv = self._func(*args, **kwargs)
+ gobject.idle_add(self._invoke, *args, **kwargs)
+
+class ThreadSafeObject:
+ """
+ A wrapper around a gobject which replaces all callable
+ objects which wraps all callable objects uses L{ThreadSafeFunction}.
+ """
+ def __init__(self, gobj):
+ """
+ @param gobj:
+ """
+ self._gobj = gobj
+
+ def __getattr__(self, name):
+ attr = getattr(self._gobj, name, None)
+ if attr is None:
+ raise KeyError(name)
+ if callable(attr):
+ return ThreadSafeFunction(attr, self._gobj.get_name())
+ return attr
+
+class DictWrapper(object):
+ def __init__(self, dict, name):
+ self._dict = dict
+ self._name = name
+
+ def __getattr__(self, attr):
+ start = time.time()
+ while True:
+ if (time.time() - start) > WIDGET_TIMEOUT:
+ raise TimeOutError("no %s called %s" % (self._name, attr))
+
+ if attr in self._dict:
+ return ThreadSafeObject(self._dict[attr])
+
+ time.sleep(0.1)
+
+class App(DictWrapper):
+ def __init__(self, player):
+ self._player = player
+
+ def __getattr__(self, attr):
+ return DictWrapper(self._player.get_object(attr), 'widget')
+
+class Player(Base):
+ """
+ Event playback object. Usually used inside a scripted generated by
+ L{kiwi.ui.test.listener.Listener}.
+
+ The application script will be exectured in a different thread,
+ so to be able to conveniently use it a number of tricks are used
+ to avoid making the user worry about threadsafety.
+ """
+ def __init__(self, args):
+ """
+ @param args:
+ """
+ Base.__init__(self)
+
+ self._app = App(self)
+
+ if not os.path.exists(args[0]):
+ print >> sys.stderr, \
+ "ERROR: %s: No such a file or directory" % args[0]
+ os._exit(1)
+
+ # Send notification to main thread
+ gobject.idle_add(self._start_app, args)
+
+ def _start_app(self, args):
+ sys.argv = args[:]
+ execfile(sys.argv[0])
+
+ # Run all pending events, such as idle adds
+ while gtk.events_pending():
+ gtk.main_iteration()
+
+ def get_app(self):
+ """
+ Returns a virtual application object, which is a special object
+ where you can access the windows as attributes and widget in the
+ windows as attributes on the window, examples:
+
+ >>> app = player.get_app()
+ >>> app.WindowName.WidgetName.method()
+
+ @return: virtual application object
+ """
+ return self._app
+
+ def wait_for_window(self, name, timeout=WINDOW_TIMEOUT):
+ """
+ Waits for a window with name I{name} to appear.
+
+ @param name: the name of the window to wait for
+ @param timeout: number of seconds to wait after the window appeared.
+ """
+
+ log('waiting for %s (%d)' % (name, timeout))
+
+ # XXX: No polling!
+ start_time = time.time()
+ while True:
+ if name in self._objects:
+ window = self.get_object(name)
+ time.sleep(WINDOW_WAIT)
+ return window
+
+ if time.time() - start_time > timeout:
+ raise TimeOutError("could not find window %s" % name)
+ time.sleep(0.05)
+
+ def delete_window(self, window_name):
+ """
+ Deletes a window, creates a delete-event and sends it to the window
+ """
+
+ log('deleting window %s' % window_name)
+
+ if window_name in self._objects:
+ del self._objects[window_name]
+
+ start_time = time.time()
+ while True:
+ window = self._windows.get(window_name)
+ # If the window is already removed, skip
+ if (not window_name in self._windows or
+ window is None or
+ window.window is None):
+ return False
+
+ if time.time() - start_time > DELETE_WINDOW_WAIT:
+ event = gdk.Event(gdk.DELETE)
+ event.window = window.window
+ event.put()
+ return True
+ time.sleep(0.1)
+
+ def finish(self):
+ pass
+
+def play_file(filename, args=None):
+ """
+ Plays a recorded script file
+
+ @param filename: name to play
+ @param args: additional arguments to put in sys.argv
+ """
+ if not os.path.exists(filename):
+ raise SystemExit("%s: No such a file or directory" % filename)
+
+ if not args:
+ args = []
+
+ sys.argv = [filename] + args
+
+ def _thread(filename):
+ try:
+ execfile(filename)
+ except:
+ import traceback
+ etype, value, tb = sys.exc_info()
+ traceback.print_exception(etype, value, tb.tb_next)
+ os._exit(1)
+
+ t = threading.Thread(target=_thread, args=[filename])
+ t.start()
+
+ gobject.MainLoop().run()
diff --git a/kiwi/ui/tooltip.py b/kiwi/ui/tooltip.py
new file mode 100644
index 0000000..23ad395
--- /dev/null
+++ b/kiwi/ui/tooltip.py
@@ -0,0 +1,118 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""A tooltip popup window which only pop ups on demand, which
+makes it possible for us to tie it to a specific gtk.gdk.Window
+"""
+
+import gobject
+import gtk
+
+DEFAULT_DELAY = 500
+BORDER_WIDTH = 4
+
+class Tooltip(gtk.Window):
+ def __init__(self, widget):
+ gtk.Window.__init__(self, gtk.WINDOW_POPUP)
+ # from gtktooltips.c:gtk_tooltips_force_window
+ self.set_app_paintable(True)
+ self.set_resizable(False)
+ self.set_name("gtk-tooltips")
+ self.set_border_width(BORDER_WIDTH)
+ self.connect('expose-event', self._on__expose_event)
+
+ self._label = gtk.Label()
+ self.add(self._label)
+ self._show_timeout_id = -1
+
+ # from gtktooltips.c:gtk_tooltips_draw_tips
+ def _calculate_pos(self, widget):
+ screen = widget.get_screen()
+
+ w, h = self.size_request()
+
+ x, y = widget.window.get_origin()
+
+ if widget.flags() & gtk.NO_WINDOW:
+ x += widget.allocation.x
+ y += widget.allocation.y
+
+ x = screen.get_root_window().get_pointer()[0]
+ x -= (w / 2 + BORDER_WIDTH)
+
+ pointer_screen, px, py, _ = screen.get_display().get_pointer()
+ if pointer_screen != screen:
+ px = x
+ py = y
+
+ monitor_num = screen.get_monitor_at_point(px, py)
+ monitor = screen.get_monitor_geometry(monitor_num)
+
+ if (x + w) > monitor.x + monitor.width:
+ x -= (x + w) - (monitor.x + monitor.width);
+ elif x < monitor.x:
+ x = monitor.x
+
+ if ((y + h + widget.allocation.height + BORDER_WIDTH) >
+ monitor.y + monitor.height):
+ y = y - h - BORDER_WIDTH
+ else:
+ y = y + widget.allocation.height + BORDER_WIDTH
+
+ return x, y
+
+ # from gtktooltips.c:gtk_tooltips_paint_window
+ def _on__expose_event(self, window, event):
+ w, h = window.size_request()
+ window.style.paint_flat_box(window.window,
+ gtk.STATE_NORMAL, gtk.SHADOW_OUT,
+ None, window, "tooltip",
+ 0, 0, w, h)
+ return False
+
+ def _real_display(self, widget):
+ x, y = self._calculate_pos(widget)
+
+ self.move(x, y)
+ self.show_all()
+
+ # Public API
+
+ def set_text(self, text):
+ self._label.set_text(text)
+
+ def hide(self):
+ gtk.Window.hide(self)
+ gobject.source_remove(self._show_timeout_id)
+ self._show_timeout_id = -1
+
+ def display(self, widget):
+ if not self._label.get_text():
+ return
+
+ if self._show_timeout_id != -1:
+ return
+
+ self._show_timeout_id = gobject.timeout_add(DEFAULT_DELAY,
+ self._real_display,
+ widget)
diff --git a/kiwi/ui/views.py b/kiwi/ui/views.py
new file mode 100644
index 0000000..0c68d4e
--- /dev/null
+++ b/kiwi/ui/views.py
@@ -0,0 +1,967 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2001-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Jon Nelson <jnelson@securepipe.com>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Johan Dahlin <jdahlin@async.com.br>
+# Henrique Romano <henrique@async.com.br>
+#
+
+"""
+Defines the View classes that are included in the Kiwi Framework, which
+are the base of Delegates and Proxies.
+"""
+
+import re
+import string
+
+import gobject
+import gtk
+from gtk import gdk
+
+from kiwi.environ import is_gazpacho_required
+from kiwi.interfaces import IValidatableProxyWidget
+from kiwi.log import Logger
+from kiwi.utils import gsignal, type_register
+from kiwi.ui.gadgets import quit_if_last
+from kiwi.ui.proxy import Proxy
+
+log = Logger('view')
+
+_non_interactive = (
+ gtk.Label,
+ gtk.Alignment,
+ gtk.AccelLabel,
+ gtk.Arrow,
+ gtk.EventBox,
+ gtk.Fixed,
+ gtk.Frame,
+ gtk.HBox,
+ gtk.HButtonBox,
+ gtk.HPaned,
+ gtk.HSeparator,
+ gtk.Layout,
+ gtk.Progress,
+ gtk.ProgressBar,
+ gtk.ScrolledWindow,
+ gtk.Table,
+ gtk.VBox,
+ gtk.VButtonBox,
+ gtk.VPaned,
+ gtk.VSeparator,
+ gtk.Window,
+)
+
+color_red = gdk.color_parse('red')
+color_black = gdk.color_parse('black')
+
+#
+# Signal brokers
+#
+
+method_regex = re.compile(r'^(on|after)_(\w+)__(\w+)$')
+
+class SignalBroker(object):
+ def __init__(self, view, controller):
+ methods = controller._get_all_methods()
+ self._do_connections(view, methods)
+
+ def _do_connections(self, view, methods):
+ """This method allows subclasses to add more connection mechanism"""
+ self._autoconnect_by_method_name(view, methods)
+
+ def _autoconnect_by_method_name(self, view, methods):
+ """
+ Offers autoconnection of widget signals based on function names.
+ You simply need to define your controller method in the format::
+
+ def on_widget_name__signal_name(self, widget, *args):
+
+ In other words, start the method by "on_", followed by the
+ widget name, followed by two underscores ("__"), followed by the
+ signal name. Note: If more than one double underscore sequences
+ are in the string, the last one is assumed to separate the
+ signal name.
+ """
+ self._autoconnected = {}
+
+ for fname in methods.keys():
+ # `on_x__y' has 7 chars and is the smallest possible handler
+ # (though illegal, of course, since the signal name x is bogus)
+ if len(fname) < 7:
+ continue
+ match = method_regex.match(fname)
+ if match is None:
+ continue
+ after, w_name, signal = match.groups()
+ widget = getattr(view, w_name, None)
+ if widget is None:
+ raise AttributeError("couldn't find widget %s in %s"
+ % (w_name, view))
+ if not isinstance(widget, gobject.GObject):
+ raise AttributeError("%s (%s) is not a widget or an action "
+ "and can't be connected to"
+ % (w_name, widget))
+ # Must use getattr; using the class method ends up with it
+ # being called unbound and lacking, thus, "self".
+ try:
+ if after:
+ signal_id = widget.connect_after(signal, methods[fname])
+ else:
+ signal_id = widget.connect(signal, methods[fname])
+ except TypeError:
+ log.warn("Widget %s doesn't provide a signal %s" % (
+ widget.__class__, signal))
+ continue
+ self._autoconnected.setdefault(widget, []).append((
+ signal, signal_id))
+
+ def handler_block(self, widget, signal_name):
+ signals = self._autoconnected
+ if not widget in signals:
+ return
+
+ for signal, signal_id in signals[widget]:
+ if signal_name is None or signal == signal_name:
+ widget.handler_block(signal_id)
+
+ def handler_unblock(self, widget, signal_name):
+ signals = self._autoconnected
+ if not widget in signals:
+ return
+
+ for signal, signal_id in signals[widget]:
+ if signal_name is None or signal == signal_name:
+ widget.handler_unblock(signal_id)
+
+ def disconnect_autoconnected(self):
+ for widget, signals in self._autoconnected.items():
+ for signal in signals:
+ widget.disconnect(signal[1])
+
+class GladeSignalBroker(SignalBroker):
+ def _do_connections(self, view, methods):
+ super(GladeSignalBroker, self)._do_connections(view, methods)
+ self._connect_glade_signals(view, methods)
+
+ def _connect_glade_signals(self, view, methods):
+ # mainly because the two classes cannot have a common base
+ # class. studying the class layout carefully or using
+ # composition may be necessary.
+
+ # called by framework.basecontroller. takes a controller, and
+ # creates the dictionary to attach to the signals in the tree.
+ if not methods:
+ raise AssertionError("controller must be provided")
+
+ dict = {}
+ for name, method in methods.items():
+ if callable(method):
+ dict[name] = method
+ view._glade_adaptor.signal_autoconnect(dict)
+
+
+class SlaveView(gobject.GObject):
+ """
+ Base class for all View classes. Defines the essential class
+ attributes (controller, toplevel, widgets) and handles
+ initialization of toplevel and widgets. Once
+ AbstractView.__init__() has been called, you can be sure
+ self.toplevel and self.widgets are sane and processed.
+
+ When a controller is associated with a View (the view should be
+ passed in to its constructor) it will try and call a hook in the
+ View called _attach_callbacks. See AbstractGladeView for an example
+ of this method.
+ """
+ controller = None
+ toplevel = None
+ widgets = []
+ toplevel_name = None
+ gladefile = None
+ gladename = None
+ domain = None
+
+ # This signal is emited when the view wants to return a result value
+ gsignal("result", object)
+
+ # This is emitted when validation changed for a view
+ # Used by parents views to know when child slaves changes
+ gsignal('validation-changed', bool)
+
+ def __init__(self, toplevel=None, widgets=None, gladefile=None,
+ gladename=None, toplevel_name=None, domain=None):
+ """ Creates a new SlaveView. Sets up self.toplevel and self.widgets
+ and checks for reserved names.
+ """
+ gobject.GObject.__init__(self)
+
+ self._broker = None
+ self.slaves = {}
+ self._proxies = []
+ self._valid = True
+
+ # slave/widget name -> validation status
+ self._validation = {}
+
+ # stores the function that will be called when widgets
+ # validity is checked
+ self._validate_function = None
+
+ # setup the initial state with the value of the arguments or the
+ # class variables
+ klass = type(self)
+ self.toplevel = toplevel or klass.toplevel
+ self.widgets = widgets or klass.widgets
+ self.gladefile = gladefile or klass.gladefile
+ self.gladename = gladename or klass.gladename
+ self.toplevel_name = (toplevel_name or
+ klass.toplevel_name or
+ self.gladefile or
+ self.gladename)
+ self.domain = domain or klass.domain
+
+ self._check_reserved()
+ self._glade_adaptor = self.get_glade_adaptor()
+ self.toplevel = self._get_toplevel()
+
+ # grab the accel groups
+ self._accel_groups = gtk.accel_groups_from_object(self.toplevel)
+
+ # XXX: support normal widgets
+ # notebook page label widget ->
+ # dict (slave name -> validation status)
+ self._notebook_validation = {}
+ self._notebooks = self._get_notebooks()
+
+ def _get_notebooks(self):
+ if not self._glade_adaptor:
+ return []
+
+ return [widget for widget in self._glade_adaptor.get_widgets()
+ if isinstance(widget, gtk.Notebook)]
+
+ def _check_reserved(self):
+ for reserved in ["widgets", "toplevel", "gladefile",
+ "gladename", "tree", "model", "controller"]:
+ # XXX: take into account widget constructor?
+ if reserved in self.widgets:
+ raise AttributeError(
+ "The widgets list for %s contains a widget named `%s', "
+ "which is a reserved. name""" % (self, reserved))
+
+ def _get_toplevel(self):
+ toplevel = self.toplevel
+ if not toplevel and self.toplevel_name:
+ if self._glade_adaptor:
+ toplevel = self._glade_adaptor.get_widget(self.toplevel_name)
+ else:
+ toplevel = getattr(self, self.toplevel_name, None)
+
+ if not toplevel:
+ raise TypeError("A View requires an instance variable "
+ "called toplevel that specifies the "
+ "toplevel widget in it")
+
+ if isinstance(toplevel, gtk.Window):
+ if toplevel.flags() & gtk.VISIBLE:
+ log.warn("Toplevel widget %s (%s) is visible; that's probably "
+ "wrong" % (toplevel, toplevel.get_name()))
+
+ return toplevel
+
+ def get_glade_adaptor(self):
+ """Special init code that subclasses may want to override."""
+ if not self.gladefile:
+ return
+
+ glade_adaptor = _open_glade(self, self.gladefile,
+ self.widgets, self.gladename,
+ self.domain)
+
+ container_name = self.toplevel_name
+ if not container_name:
+ raise ValueError(
+ "You provided a gladefile %s to grab the widgets from "
+ "but you didn't give me a toplevel/container name!" %
+ self.gladefile)
+
+ # a SlaveView inside a glade file needs to come inside a toplevel
+ # window, so we pull our slave out from it, grab its groups and
+ # muerder it later
+ shell = glade_adaptor.get_widget(container_name)
+ if not isinstance(shell, gtk.Window):
+ raise TypeError("Container %s should be a Window, found %s" % (
+ container_name, type(shell)))
+
+ self.toplevel = shell.get_child()
+ shell.remove(self.toplevel)
+ shell.destroy()
+
+ return glade_adaptor
+
+ #
+ # Hooks
+ #
+
+ def on_attach(self, parent):
+ """ Hook function called when attach_slave is performed on slave views.
+ """
+ pass
+
+ def on_startup(self):
+ """
+ This is a virtual method that can be customized by classes that
+ want to perform additional initalization after a controller has
+ been set for it. If you need this, add this method to your View
+ subclass and BaseController will call it when the controller is
+ set to the proxy."""
+ pass
+
+ #
+ # Accessors
+ #
+
+ def get_toplevel(self):
+ """Returns the toplevel widget in the view"""
+ return self.toplevel
+
+ def get_widget(self, name):
+ """Retrieves the named widget from the View"""
+ name = string.replace(name, '.', '_')
+ widget = getattr(self, name, None)
+ if widget is None:
+ raise AttributeError("Widget %s not found in view %s"
+ % (name, self))
+ if not isinstance(widget, gtk.Widget):
+ raise TypeError("%s in view %s is not a Widget"
+ % (name, self))
+ return widget
+
+ def set_controller(self, controller):
+ """
+ Sets the view's controller, checking to see if one has already
+ been set before."""
+ # Only one controller per view, please
+ if self.controller:
+ raise AssertionError("This view already has a controller: %s"
+ % self.controller)
+ self.controller = controller
+
+ #
+ # GTK+ proxies and convenience functions
+ #
+
+ def show_and_loop(self, parent=None):
+ """
+ Runs show() and runs the GTK+ event loop. If the parent
+ argument is supplied and is a valid view, this view is set as a
+ transient for the parent view
+
+ @param parent:
+ """
+
+ self.show()
+ if parent:
+ self.set_transient_for(parent)
+ gtk.main()
+
+ def show(self, *args):
+ """Shows the toplevel widget"""
+ self.toplevel.show()
+
+ def show_all(self, *args):
+ """Shows all widgets attached to the toplevel widget"""
+ if self._glade_adaptor is not None:
+ raise AssertionError("You don't want to call show_all on a "
+ "SlaveView. Use show() instead.")
+ self.toplevel.show_all()
+
+ def focus_toplevel(self):
+ """Focuses the toplevel widget in the view"""
+ # XXX: warn if there is no GdkWindow
+ if self.toplevel and self.toplevel.window is not None:
+ self.toplevel.grab_focus()
+
+ def focus_topmost(self, widgets=None):
+ """
+ Looks through widgets specified (if no widgets are specified,
+ look through all widgets attached to the view and sets focus to
+ the widget that is rendered in the position closest to the view
+ window's top and left
+
+ - widgets: a list of widget names to be searched through
+ """
+ widget = self.get_topmost_widget(widgets, can_focus=True)
+ if widget is not None:
+ widget.grab_focus()
+ # So it can be idle_added safely
+ return False
+
+ def get_topmost_widget(self, widgets=None, can_focus=False):
+ """
+ A real hack; returns the widget that is most to the left and
+ top of the window.
+
+ - widgets: a list of widget names. If widgets is supplied,
+ it only checks in the widgets in the list; otherwise, it
+ looks at the widgets named in self.widgets, or, if
+ self.widgets is None, looks through all widgets attached
+ to the view.
+
+ - can_focus: boolean, if set only searches through widget
+ that can be focused
+ """
+ # XXX: recurse through containers from toplevel widget, better
+ # idea and will work.
+ widgets = widgets or self.widgets or self.__dict__.keys()
+ top_widget = None
+ for widget_name in widgets:
+ widget = getattr(self, widget_name)
+ if not isinstance(widget, gtk.Widget):
+ continue
+ if not widget.flags() & gtk.REALIZED:
+ # If widget isn't realized but we have a toplevel
+ # window, it's safe to realize it. If this check isn't
+ # performed, we get a crash as per
+ # http://bugzilla.gnome.org/show_bug.cgi?id=107872
+ if isinstance(widget.get_toplevel(), gtk.Window):
+ widget.realize()
+ else:
+ log.warn("get_topmost_widget: widget %s was not realized"
+ % widget_name)
+ continue
+ if can_focus:
+ # Combos don't focus, but their entries do
+ if isinstance(widget, gtk.Combo):
+ widget = widget.entry
+ if not widget.flags() & gtk.CAN_FOCUS or \
+ isinstance(widget, (gtk.Label, gtk.HSeparator,
+ gtk.VSeparator, gtk.Window)):
+ continue
+
+ if top_widget:
+ allocation = widget.allocation
+ top_allocation = getattr(top_widget, 'allocation', None)
+ assert top_allocation != None
+ if (top_allocation[0] + top_allocation[1] >
+ allocation[0] + allocation[1]):
+ top_widget = widget
+ else:
+ top_widget = widget
+ return top_widget
+
+ #
+ # Callback handling
+ #
+
+ def _attach_callbacks(self, controller):
+ if self._glade_adaptor is None:
+ brokerclass = SignalBroker
+ else:
+ brokerclass = GladeSignalBroker
+
+ self._broker = brokerclass(self, controller)
+
+# def _setup_keypress_handler(self, keypress_handler):
+# # Only useful in BaseView and derived classes
+# # XXX: support slaveview correctly
+# log.warn("Tried to setup a keypress handler for %s "
+# "but no toplevel window exists to attach to" % self)
+
+ #
+ # Slave handling
+ #
+
+ def attach_slave(self, name, slave):
+ """Attaches a slaveview to the current view, substituting the
+ widget specified by name. the widget specified *must* be a
+ eventbox; its child widget will be removed and substituted for
+ the specified slaveview's toplevel widget::
+
+ .-----------------------. the widget that is indicated in the diagram
+ |window/view (self.view)| as placeholder will be substituted for the
+ | .----------------. | slaveview's toplevel.
+ | | eventbox (name)| | .-----------------.
+ | |.--------------.| |slaveview (slave)|
+ | || placeholder <----. |.---------------.|
+ | |'--------------'| \___ toplevel ||
+ | '----------------' | ''---------------'|
+ '-----------------------' '-----------------'
+
+ the original way of attachment (naming the *child* widget
+ instead of the eventbox) is still supported for compatibility
+ reasons but will print a warning.
+ """
+ log('%s: Attaching slave %s of type %s' % (self.__class__.__name__,
+ name,
+ slave.__class__.__name__))
+
+ if name in self.slaves:
+ # XXX: TypeError
+ log.warn("A slave with name %s is already attached to %r" % (
+ name, self))
+ self.slaves[name] = slave
+
+ if not isinstance(slave, SlaveView):
+ raise TypeError("slave must be a SlaveView, not a %s" %
+ type(slave))
+
+ shell = slave.get_toplevel()
+
+ if isinstance(shell, gtk.Window): # view with toplevel window
+ new_widget = shell.get_child()
+ shell.remove(new_widget) # remove from window to allow reparent
+ else: # slaveview
+ new_widget = shell
+
+ # if our widgets are in a glade file get the placeholder from them
+ # or take it from the view itself otherwise
+ if self._glade_adaptor:
+ placeholder = self._glade_adaptor.get_widget(name)
+ else:
+ placeholder = getattr(self, name, None)
+
+ if not placeholder:
+ raise AttributeError(
+ "slave container widget `%s' not found" % name)
+ parent = placeholder.get_parent()
+
+ if slave._accel_groups:
+ # take care of accelerator groups; attach to parent window if we
+ # have one; if embedding a slave into another slave, store its
+ # accel groups; otherwise complain if we're dropping the
+ # accelerators
+ win = parent.get_toplevel()
+ if isinstance(win, gtk.Window):
+ # use idle_add to be sure we attach the groups as late
+ # as possible and avoid reattaching groups -- see
+ # comment in _attach_groups.
+ gtk.idle_add(self._attach_groups, win, slave._accel_groups)
+ elif isinstance(self, SlaveView):
+ self._accel_groups.extend(slave._accel_groups)
+ else:
+ log.warn("attached slave %s to parent %s, but parent lacked "
+ "a window and was not a slave view" % (slave, self))
+ slave._accel_groups = []
+
+ # Merge the sizegroups of the slave that is being attached with the
+ # sizegroups of where it is being attached to. Only the sizegroups
+ # with the same name will be merged.
+ if slave._glade_adaptor:
+ sizegroups = slave._glade_adaptor.get_sizegroups()
+ for sizegroup in sizegroups:
+ self._merge_sizegroup(sizegroup)
+
+ if isinstance(placeholder, gtk.EventBox):
+ # standard mechanism
+ child = placeholder.get_child()
+ if child is not None:
+ placeholder.remove(child)
+ placeholder.set_visible_window(False)
+ placeholder.add(new_widget)
+ elif isinstance(parent, gtk.EventBox):
+ # backwards compatibility
+ log.warn("attach_slave's api has changed: read docs, update code!")
+ parent.remove(placeholder)
+ parent.add(new_widget)
+ else:
+ raise TypeError(
+ "widget to be replaced must be wrapped in eventbox")
+
+ # when attaching a slave we usually want it visible
+ parent.show()
+ # call slave's callback
+ slave.on_attach(self)
+
+ slave.connect_object('validation-changed',
+ self._on_child__validation_changed,
+ name)
+
+ for notebook in self._notebooks:
+ for child in notebook.get_children():
+ if not shell.is_ancestor(child):
+ continue
+
+ label = notebook.get_tab_label(child)
+ slave.connect('validation-changed',
+ self._on_notebook_slave__validation_changed,
+ name, label)
+ self._notebook_validation[label] = {}
+
+ # Fire of an initial notification
+ slave.check_and_notify_validity(force=True)
+
+ # return placeholder we just removed
+ return placeholder
+
+ def _merge_sizegroup(self, other_sizegroup):
+ # Merge sizegroup from other with self that have the same name.
+ # Actually, no merging is being done, since the old group is preserved
+
+ name = other_sizegroup.get_data('gazpacho::object-id')
+ sizegroup = getattr(self, name, None)
+ if not sizegroup:
+ return
+
+ widgets = other_sizegroup.get_data('gazpacho::sizegroup-widgets')
+ if not widgets:
+ return
+
+ for widget in widgets:
+ sizegroup.add_widget(widget)
+
+ def detach_slave(self, name):
+ """
+ Detatch a slave called name from view
+ """
+ if not name in self.slaves:
+ raise LookupError("There is no slaved called %s attached to %r" %
+ (name, self))
+ del self.slaves[name]
+
+ def _attach_groups(self, win, accel_groups):
+ # get groups currently attached to the window; we use them
+ # to avoid reattaching an accelerator to the same window, which
+ # generates messages like:
+ #
+ # gtk-critical **: file gtkaccelgroup.c: line 188
+ # (gtk_accel_group_attach): assertion `g_slist_find
+ # (accel_group->attach_objects, object) == null' failed.
+ #
+ # interestingly, this happens many times with notebook,
+ # because libglade creates and attaches groups in runtime to
+ # its toplevel window.
+ current_groups = gtk.accel_groups_from_object(win)
+ for group in accel_groups:
+ if group in current_groups:
+ # skip group already attached
+ continue
+ win.add_accel_group(group)
+
+ def get_slave(self, holder):
+ return self.slaves.get(holder)
+
+
+
+ #
+ # Signal connection
+ #
+
+ def connect_multiple(self, widgets, signal, handler, after=False):
+ """
+ Connect the same handler to the specified signal for a number of
+ widgets.
+ - widgets: a list of GtkWidgets
+ - signal: a string specifying the signals
+ - handler: a callback method
+ - after: a boolean; if TRUE, we use connect_after(), otherwise,
+ connect()
+ """
+ if not isinstance(widgets, (list, tuple)):
+ raise TypeError("widgets must be a list, found %s" % widgets)
+ for widget in widgets:
+ if not isinstance(widget, gtk.Widget):
+ raise TypeError(
+ "Only Gtk widgets may be passed in list, found\n%s" % widget)
+ if after:
+ widget.connect_after(signal, handler)
+ else:
+ widget.connect(signal, handler)
+
+ def disconnect_autoconnected(self):
+ """
+ Disconnect handlers previously connected with
+ autoconnect_signals()"""
+ self._broker.disconnect_autoconnected()
+
+ def handler_block(self, widget, signal_name=None):
+ # XXX: Warning, or bail out?
+ if not self._broker:
+ return
+ self._broker.handler_block(widget, signal_name)
+
+ def handler_unblock(self, widget, signal_name=None):
+ if not self._broker:
+ return
+ self._broker.handler_unblock(widget, signal_name)
+
+ #
+ # Proxies
+ #
+
+ def add_proxy(self, model=None, widgets=None):
+ """
+ Add a proxy to this view that automatically update a model when
+ the view changes. Arguments:
+
+ - model. the object we are proxing. It can be None if we don't have
+ a model yet and we want to display the interface and set it up with
+ future models.
+ - widgets. the list of widgets that contains model attributes to be
+ proxied. If it is None (or not specified) it will be the whole list
+ of widgets this View has.
+
+ This method return a Proxy object that you may want to use to force
+ updates or setting new models. Keep a reference to it since there is
+ no way to get that proxy later on. You have been warned (tm)
+ """
+ log('%s: adding proxy for %s' % (
+ self.__class__.__name__,
+ model and model.__class__.__name__))
+
+ widgets = widgets or self.widgets
+
+ for widget_name in widgets:
+ widget = getattr(self, widget_name, None)
+ if widget is None:
+ continue
+
+ if not IValidatableProxyWidget.providedBy(widget):
+ continue
+
+ try:
+ widget.connect_object('validation-changed',
+ self._on_child__validation_changed,
+ widget_name)
+ except TypeError:
+ raise AssertionError("%r does not have a validation-changed "
+ "signal." % widget)
+
+ proxy = Proxy(self, model, widgets)
+ self._proxies.append(proxy)
+ return proxy
+
+ #
+ # Validation
+ #
+
+ def _on_child__validation_changed(self, name, value):
+ # Children of the view, eg slaves or widgets are connected to
+ # this signal. When validation changes of a validatable child
+ # this callback is called
+ self._validation[name] = value
+
+ self.check_and_notify_validity()
+
+ def _on_notebook_slave__validation_changed(self, slave, value, name,
+ label):
+ validation = self._notebook_validation[label]
+ validation[name] = value
+
+ is_valid = True
+ if False in validation.values():
+ is_valid = False
+
+ if is_valid:
+ color = color_black
+ else:
+ color = color_red
+
+ # Only modify active state, since that's the (somewhat badly named)
+ # state used for the pages which are not selected.
+ label.modify_fg(gtk.STATE_ACTIVE, color)
+ label.modify_fg(gtk.STATE_NORMAL, color)
+
+ def check_and_notify_validity(self, force=False):
+ # Current view is only valid if we have no invalid children
+ # their status are stored as values in the dictionary
+ is_valid = True
+ if False in self._validation.values():
+ is_valid = False
+
+ # Check if validation really changed
+ if self._valid == is_valid and force == False:
+ return
+
+ self._valid = is_valid
+ self.emit('validation-changed', is_valid)
+
+ # FIXME: Remove and update all callsites to use validation-changed
+ if self._validate_function:
+ self._validate_function(is_valid)
+
+ def force_validation(self):
+ self.check_and_notify_validity(force=True)
+
+ def register_validate_function(self, function):
+ """The signature of the validate function is:
+
+ def function(is_valid):
+
+ or, if it is a method:
+
+ def function(self, is_valid):
+
+ where the 'is_valid' parameter is True if all the widgets have
+ valid data or False otherwise.
+ """
+ self._validate_function = function
+
+type_register(SlaveView)
+
+class BaseView(SlaveView):
+ """A view with a toplevel window."""
+
+ def __init__(self, toplevel=None, widgets=None, gladefile=None,
+ gladename=None, toplevel_name=None, domain=None,
+ delete_handler=None):
+ SlaveView.__init__(self, toplevel, widgets, gladefile, gladename,
+ toplevel_name, domain)
+
+ if not isinstance(self.toplevel, gtk.Window):
+ raise TypeError("toplevel widget must be a Window "
+ "(or inherit from it),\nfound `%s' %s"
+ % (toplevel, self.toplevel))
+ self.toplevel.set_name(self.__class__.__name__)
+
+ if delete_handler:
+ id = self.toplevel.connect("delete-event", delete_handler)
+ if not id:
+ raise ValueError(
+ "Invalid delete handler provided: %s" % delete_handler)
+
+ def get_glade_adaptor(self):
+ if not self.gladefile:
+ return
+
+ return _open_glade(self, self.gladefile, self.widgets,
+ self.gladename, self.domain)
+
+ #
+ # Hook for keypress handling
+ #
+
+ def _attach_callbacks(self, controller):
+ super(BaseView, self)._attach_callbacks(controller)
+ self._setup_keypress_handler(controller.on_key_press)
+
+ def _setup_keypress_handler(self, keypress_handler):
+ self.toplevel.connect_after("key_press_event", keypress_handler)
+
+ #
+ # Proxying for self.toplevel
+ #
+ def set_transient_for(self, view):
+ """Makes the view a transient for another view; this is commonly done
+ for dialogs, so the dialog window is managed differently than a
+ top-level one.
+ """
+ if hasattr(view, 'toplevel') and isinstance(view.toplevel, gtk.Window):
+ self.toplevel.set_transient_for(view.toplevel)
+ # In certain cases, it is more convenient to send in a window;
+ # for instance, in a deep slaveview hierarchy, getting the
+ # top view is difficult. We used to print a warning here, I
+ # removed it for convenience; we might want to put it back when
+ # http://bugs.async.com.br/show_bug.cgi?id=682 is fixed
+ elif isinstance(view, gtk.Window):
+ self.toplevel.set_transient_for(view)
+ else:
+ raise TypeError("Parameter to set_transient_for should "
+ "be View (found %s)" % view)
+
+ def set_title(self, title):
+ """Sets the view's window title"""
+ self.toplevel.set_title(title)
+
+ #
+ # Focus handling
+ #
+
+ def get_focus_widget(self):
+ """Returns the currently focused widget in the window"""
+ return self.toplevel.focus_widget
+
+ def check_focus(self):
+ """ Tests the focus in the window and prints a warning if no
+ widget is focused.
+ """
+ focus = self.toplevel.focus_widget
+ if focus:
+ return
+ values = self.__dict__.values()
+ interactive = None
+ # Check if any of the widgets is interactive
+ for v in values:
+ if (isinstance(v, gtk.Widget) and not
+ isinstance(v, _non_interactive)):
+ interactive = v
+ if interactive:
+ log.warn("No widget is focused in view %s but you have an "
+ "interactive widget in it: %s""" % (self, interactive))
+
+ #
+ # Window show/hide and mainloop manipulation
+ #
+
+ def hide(self, *args):
+ """Hide the view's window"""
+ self.toplevel.hide()
+
+ def show_all(self, parent=None, *args):
+ self.toplevel.show_all()
+ self.show(parent, *args)
+
+ def show(self, parent=None, *args):
+ """Show the view's window.
+ If the parent argument is supplied and is a valid view, this view
+ is set as a transient for the parent view.
+ """
+ # Uniconize window if minimized
+ self.toplevel.present() # this call win.show() for us
+ self.check_focus()
+ if parent is not None:
+ self.set_transient_for(parent)
+
+ def quit_if_last(self, *args):
+ quit_if_last(*args)
+
+ def hide_and_quit(self, *args):
+ """Hides the current window and breaks the GTK+ event loop if this
+ is the last window.
+ Its method signature allows it to be used as a signal handler.
+ """
+ self.toplevel.hide()
+ self.quit_if_last()
+
+WidgetTree = None
+
+def _open_glade(view, gladefile, widgets, name, domain):
+ global WidgetTree
+ if not WidgetTree:
+ try:
+ from gazpacho.loader import loader
+ loader # pyflakes
+ except ImportError, e:
+ if is_gazpacho_required():
+ raise RuntimeError(
+ "Gazpacho is required, but could not be found: %s" % e)
+ else:
+ try:
+ from kiwi.ui.libgladeloader import LibgladeWidgetTree as WT
+ WidgetTree = WT
+ except ImportError:
+ raise RuntimeError("Could not find a glade parser library")
+ else:
+ from kiwi.ui.gazpacholoader import GazpachoWidgetTree as WT
+ WidgetTree = WT
+
+ return WidgetTree(view, gladefile, widgets, name, domain)
diff --git a/kiwi/ui/widgets/__init__.py b/kiwi/ui/widgets/__init__.py
new file mode 100644
index 0000000..1c8b70f
--- /dev/null
+++ b/kiwi/ui/widgets/__init__.py
@@ -0,0 +1,20 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
diff --git a/kiwi/ui/widgets/checkbutton.py b/kiwi/ui/widgets/checkbutton.py
new file mode 100644
index 0000000..f7bf5c1
--- /dev/null
+++ b/kiwi/ui/widgets/checkbutton.py
@@ -0,0 +1,69 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Gustavo Rahal <gustavo@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+#
+
+"""GtkCheckButton support for the Kiwi Framework"""
+
+import gtk
+
+from kiwi.python import deprecationwarn
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal, type_register
+
+class ProxyCheckButton(PropertyObject, gtk.CheckButton, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyCheckButton'
+
+ # changed allowed data types because checkbuttons can only
+ # accept bool values
+ allowed_data_types = bool,
+
+ def __init__(self):
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self, data_type=bool)
+ gtk.CheckButton.__init__(self)
+
+ gsignal('toggled', 'override')
+ def do_toggled(self):
+ self.emit('content-changed')
+ self.chain()
+
+ def read(self):
+ return self.get_active()
+
+ def update(self, data):
+ if data is None:
+ self.set_active(False);
+ return
+
+ # No conversion to string needed, we only accept bool
+ self.set_active(data)
+
+class CheckButton(ProxyCheckButton):
+ def __init__(self):
+ deprecationwarn(
+ 'CheckButton is deprecated, use ProxyCheckButton instead',
+ stacklevel=3)
+ ProxyCheckButton.__init__(self)
+type_register(CheckButton)
diff --git a/kiwi/ui/widgets/colorbutton.py b/kiwi/ui/widgets/colorbutton.py
new file mode 100644
index 0000000..7b17e78
--- /dev/null
+++ b/kiwi/ui/widgets/colorbutton.py
@@ -0,0 +1,46 @@
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Ali Afshar <aafshar@gmail.com>
+#
+
+"""ColorButton proxy for the kiwi framework"""
+
+import gtk
+
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal, type_register
+
+
+class ProxyColorButton(PropertyObject, gtk.ColorButton, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyColorButton'
+
+ allowed_data_types = object,
+
+ def __init__(self, color=gtk.gdk.Color(0, 0, 0)):
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self, data_type=object)
+ gtk.ColorButton.__init__(self, color)
+
+ gsignal('color-set', 'override')
+ def do_color_set(self):
+ self.emit('content-changed')
+ self.chain()
+
+ def read(self):
+ return self.get_color()
+
+ def update(self, data):
+ self.set_color(data)
+
+
+type_register(ProxyColorButton)
diff --git a/kiwi/ui/widgets/combo.py b/kiwi/ui/widgets/combo.py
new file mode 100644
index 0000000..491008f
--- /dev/null
+++ b/kiwi/ui/widgets/combo.py
@@ -0,0 +1,254 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Johan Dahlin <jdahlin@async.com.br>
+# Gustavo Rahal <gustavo@async.com.br>
+# Daniel Saran R. da Cunha <daniel@async.com.br>
+# Evandro Vale Miquelito <evandro@async.com.br>
+#
+
+"""GtkComboBox and GtkComboBoxEntry support for the Kiwi Framework.
+
+The GtkComboBox and GtkComboBoxEntry classes here are also slightly extended
+they contain methods to easily insert and retrieve data from combos.
+"""
+
+import gobject
+import gtk
+from gtk import keysyms
+
+from kiwi import ValueUnset
+from kiwi.python import deprecationwarn
+from kiwi.ui.comboboxentry import BaseComboBoxEntry
+from kiwi.ui.comboentry import ComboEntry
+from kiwi.ui.combomixin import COL_COMBO_LABEL, COMBO_MODE_STRING, \
+ COMBO_MODE_DATA, COMBO_MODE_UNKNOWN, ComboMixin
+from kiwi.ui.proxywidget import ProxyWidgetMixin, ValidatableProxyWidgetMixin
+from kiwi.ui.widgets.entry import ProxyEntry
+from kiwi.utils import PropertyObject, gproperty
+
+class ProxyComboBox(PropertyObject, gtk.ComboBox, ComboMixin, ProxyWidgetMixin):
+
+ __gtype_name__ = 'ProxyComboBox'
+
+ def __init__(self):
+ gtk.ComboBox.__init__(self)
+ ComboMixin.__init__(self)
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self)
+ self.connect('changed', self._on__changed)
+
+ renderer = gtk.CellRendererText()
+ self.pack_start(renderer)
+ self.add_attribute(renderer, 'text', COL_COMBO_LABEL)
+
+ # GtkComboBox is a GtkContainer subclass which implements __len__ in
+ # PyGTK in 2.8 and higher. Therefor we need to provide our own
+ # implementation to be backwards compatible and override the new
+ # behavior in 2.8
+ def __len__(self):
+ return len(self.get_model())
+
+ def _on__changed(self, combo):
+ self.emit('content-changed')
+
+ def read(self):
+ if self.mode == COMBO_MODE_UNKNOWN:
+ return ValueUnset
+
+ data = self.get_selected()
+ if self.mode == COMBO_MODE_STRING:
+ data = self._from_string(data)
+
+ return data
+
+ def update(self, data):
+ # We dont need validation because the user always
+ # choose a valid value
+
+ if data is None:
+ return
+
+ if self.mode == COMBO_MODE_STRING:
+ data = self._as_string(data)
+
+ self.select(data)
+
+ def prefill(self, itemdata, sort=False):
+ ComboMixin.prefill(self, itemdata, sort)
+
+ # we always have something selected, by default the first item
+ self.set_active(0)
+ self.emit('content-changed')
+
+ def clear(self):
+ ComboMixin.clear(self)
+ self.emit('content-changed')
+
+class ProxyComboBoxEntry(PropertyObject, BaseComboBoxEntry, ComboMixin,
+ ValidatableProxyWidgetMixin):
+ __gtype_name__ = 'ProxyComboBoxEntry'
+ # it doesn't make sense to connect to this signal
+ # because we want to monitor the entry of the combo
+ # not the combo box itself.
+
+ gproperty("list-editable", bool, True, "Editable")
+
+ def __init__(self, **kwargs):
+ deprecationwarn(
+ 'ProxyComboBoxEntry is deprecated, use ProxyComboEntry instead',
+ stacklevel=3)
+ BaseComboBoxEntry.__init__(self)
+ ComboMixin.__init__(self)
+ ValidatableProxyWidgetMixin.__init__(self, widget=self.entry)
+ PropertyObject.__init__(self, **kwargs)
+
+ self.set_text_column(COL_COMBO_LABEL)
+
+ # here we connect the expose-event signal directly to the entry
+ self.child.connect('changed', self._on_child_entry__changed)
+
+ # HACK! we force a queue_draw because when the window is first
+ # displayed the icon is not drawn.
+ gobject.idle_add(self.queue_draw)
+
+ self.set_events(gtk.gdk.KEY_RELEASE_MASK)
+ self.connect("key-release-event", self._on__key_release_event)
+
+ def prop_set_list_editable(self, value):
+ if self.mode == COMBO_MODE_DATA:
+ return
+
+ self.entry.set_editable(value)
+
+ return value
+
+ def _update_selection(self, text=None):
+ if text is None:
+ text = self.entry.get_text()
+
+ self.select_item_by_label(text)
+
+ def _add_text_to_combo_list(self):
+ text = self.entry.get_text()
+ if not text.strip():
+ return
+
+ if text in self.get_model_strings():
+ return
+
+ self.entry.set_text('')
+ self.append_item(text)
+ self._update_selection(text)
+
+ def _on__key_release_event(self, widget, event):
+ """Checks for "Enter" key presses and add the entry text to
+ the combo list if the combo list is set as editable.
+ """
+ if not self.list_editable:
+ return
+
+ if event.keyval in (keysyms.KP_Enter,
+ keysyms.Return):
+ self._add_text_to_combo_list()
+
+ def _on_child_entry__changed(self, widget):
+ """Called when something on the entry changes"""
+ if not widget.get_text():
+ return
+
+ self.emit('content-changed')
+
+ def set_mode(self, mode):
+ # If we're in the transition to go from
+ # unknown->label set editable to False
+ if (self.mode == COMBO_MODE_UNKNOWN and mode == COMBO_MODE_DATA):
+ self.entry.set_editable(False)
+
+ ComboMixin.set_mode(self, mode)
+
+ def read(self):
+ if self.mode == COMBO_MODE_UNKNOWN:
+ return ValueUnset
+ return self.get_selected()
+
+ def update(self, data):
+ if data is ValueUnset or data is None:
+ self.entry.set_text("")
+ else:
+ self.select(data)
+
+ def prefill(self, itemdata, sort=False, clear_entry=False):
+ ComboMixin.prefill(self, itemdata, sort)
+ if clear_entry:
+ self.entry.set_text("")
+
+ # setup the autocompletion
+ auto = gtk.EntryCompletion()
+ auto.set_model(self.get_model())
+ auto.set_text_column(COL_COMBO_LABEL)
+ self.entry.set_completion(auto)
+
+ def clear(self):
+ """Removes all items from list and erases entry"""
+ ComboMixin.clear(self)
+ self.entry.set_text("")
+
+class ProxyComboEntry(PropertyObject, ComboEntry,
+ ValidatableProxyWidgetMixin):
+ __gtype_name__ = 'ProxyComboEntry'
+
+ gproperty("list-editable", bool, True, "Editable")
+
+ def __init__(self):
+ entry = ProxyEntry()
+ ComboEntry.__init__(self, entry=entry)
+ ValidatableProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self)
+ entry.connect('content-changed', self._on_entry__content_changed)
+
+ # We only need to listen for changes in the entry, it's updated
+ # even if you select something in the popup list
+ def _on_entry__content_changed(self, entry):
+ self.emit('content-changed')
+
+ def prop_set_list_editable(self, value):
+ self.entry.set_editable(value)
+ return value
+
+ def read(self):
+ return self.get_selected()
+
+ def update(self, data):
+ if data is ValueUnset or data is None:
+ self.entry.set_text("")
+ else:
+ self.select(data)
+
+ def clear(self):
+ """Removes all items from list and erases entry"""
+ ComboMixin.clear(self)
+ self.entry.set_text("")
+
+ def set_tooltip(self, text):
+ self.entry.set_tooltip(text)
+
diff --git a/kiwi/ui/widgets/combobox.py b/kiwi/ui/widgets/combobox.py
new file mode 100644
index 0000000..7bfcc60
--- /dev/null
+++ b/kiwi/ui/widgets/combobox.py
@@ -0,0 +1,43 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2001-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""GtkComboBox and GtkComboBoxEntry support for the Kiwi Framework.
+backwards compatibility layer"""
+
+from kiwi.ui.widgets.combo import ProxyComboBox
+from kiwi.ui.widgets.combo import ProxyComboBoxEntry
+from kiwi.ui.combomixin import COL_COMBO_LABEL, COL_COMBO_DATA, \
+ COMBO_MODE_STRING, COMBO_MODE_DATA, COMBO_MODE_UNKNOWN
+
+class ComboBox(ProxyComboBox):
+ pass
+
+class ComboBoxEntry(ProxyComboBoxEntry):
+ pass
+
+# pyflakes
+(COL_COMBO_LABEL, COL_COMBO_DATA, COMBO_MODE_STRING,
+ COMBO_MODE_DATA, COMBO_MODE_UNKNOWN)
diff --git a/kiwi/ui/widgets/entry.py b/kiwi/ui/widgets/entry.py
new file mode 100644
index 0000000..9ab8f0f
--- /dev/null
+++ b/kiwi/ui/widgets/entry.py
@@ -0,0 +1,255 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+#
+
+"""GtkEntry support for the Kiwi Framework"""
+
+import datetime
+
+import gtk
+
+from kiwi.datatypes import ValidationError, converter, number
+from kiwi.decorators import deprecated
+from kiwi.python import deprecationwarn
+from kiwi.ui.entry import MaskError, KiwiEntry, ENTRY_MODE_TEXT, \
+ ENTRY_MODE_DATA
+from kiwi.ui.dateentry import DateEntry
+from kiwi.ui.proxywidget import ValidatableProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal, type_register
+
+DATE_MASK_TABLE = {
+ '%m': '00',
+ '%y': '00',
+ '%d': '00',
+ '%Y': '0000',
+ '%H': '00',
+ '%M': '00',
+ '%S': '00',
+ '%T': '00:00:00',
+ # FIXME: locale specific
+ '%r': '00:00:00 LL',
+ }
+
+class ProxyEntry(KiwiEntry, ValidatableProxyWidgetMixin):
+ """The Kiwi Entry widget has many special features that extend the basic
+ gtk entry.
+
+ First of all, as every Kiwi Widget, it implements the Proxy protocol.
+ As the users types the entry can interact with the application model
+ automatically.
+ Kiwi Entry also implements interesting UI additions. If the input data
+ does not match the data type of the entry the background nicely fades
+ to a light red color. As the background changes an information icon
+ appears. When the user passes the mouse over the information icon a
+ tooltip is displayed informing the user how to correctly fill the
+ entry. When dealing with date and float data-type the information on
+ how to fill these entries is displayed according to the current locale.
+ """
+
+ __gtype_name__ = 'ProxyEntry'
+
+ def __init__(self, data_type=None):
+ self._block_changed = False
+ KiwiEntry.__init__(self)
+ ValidatableProxyWidgetMixin.__init__(self)
+ self.set_property('data-type', data_type)
+
+ # Virtual methods
+ gsignal('changed', 'override')
+ def do_changed(self):
+ """Called when the content of the entry changes.
+
+ Sets an internal variable that stores the last time the user
+ changed the entry
+ """
+
+ self.chain()
+
+ self._update_current_object(self.get_text())
+ self.emit('content-changed')
+
+ def prop_set_data_type(self, data_type):
+ data_type = super(ProxyEntry, self).prop_set_data_type(data_type)
+
+ # Numbers should be right aligned
+ if data_type and issubclass(data_type, number):
+ self.set_property('xalign', 1.0)
+
+ # Apply a mask for the data types, some types like
+ # dates has a default mask
+ try:
+ self.set_mask_for_data_type(data_type)
+ except MaskError:
+ pass
+ return data_type
+
+ # Public API
+
+ def set_mask_for_data_type(self, data_type):
+ """
+ @param data_type:
+ """
+
+ if not data_type in (datetime.datetime, datetime.date, datetime.time):
+ return
+ conv = converter.get_converter(data_type)
+ mask = conv.get_format()
+
+ # For win32, skip mask
+ # FIXME: How can we figure out the real locale specific string?
+ if mask == '%X':
+ mask = '%H:%M:%S'
+ elif mask == '%x':
+ mask = '%d/%m/%Y'
+ elif mask == '%c':
+ mask = '%d/%m/%Y %H:%M:%S'
+
+ for format_char, mask_char in DATE_MASK_TABLE.items():
+ mask = mask.replace(format_char, mask_char)
+
+ self.set_mask(mask)
+
+ #@deprecated('prefill')
+ def set_completion_strings(self, strings=[], values=[]):
+ """
+ Set strings used for entry completion.
+ If values are provided, each string will have an additional
+ data type.
+
+ @param strings:
+ @type strings: list of strings
+ @param values:
+ @type values: list of values
+ """
+
+ completion = self._get_completion()
+ model = completion.get_model()
+ model.clear()
+
+ if values:
+ self._mode = ENTRY_MODE_DATA
+ self.prefill(zip(strings, values))
+ else:
+ self._mode = ENTRY_MODE_TEXT
+ self.prefill(strings)
+ set_completion_strings = deprecated('prefill')(set_completion_strings)
+
+ def set_text(self, text):
+ """
+ Sets the text of the entry
+
+ @param text:
+ """
+
+ self._update_current_object(text)
+
+ # If content isn't empty set_text emitts changed twice.
+ # Protect content-changed from being updated and issue
+ # a manual emission afterwards
+ self._block_changed = True
+ gtk.Entry.set_text(self, text)
+ self._block_changed = False
+ self.emit('content-changed')
+
+ self.set_position(-1)
+
+ def do_changed(self):
+ if self._block_changed:
+ self.emit_stop_by_name('changed')
+ return
+ self.emit('content-changed')
+
+ # ProxyWidgetMixin implementation
+
+ def read(self):
+ mode = self._mode
+ if mode == ENTRY_MODE_TEXT:
+ text = self.get_text()
+ try:
+ return self._from_string(text)
+ except ValidationError:
+ # Do not consider masks which only displays static
+ # characters invalid, instead return an empty string
+ if self.get_mask() and text == self.get_empty_mask():
+ return ""
+ else:
+ raise
+ elif mode == ENTRY_MODE_DATA:
+ return self._current_object
+ else:
+ raise AssertionError
+
+ def update(self, data):
+ if data is None:
+ text = ""
+ else:
+ mode = self._mode
+ if mode == ENTRY_MODE_DATA:
+ new = self._get_text_from_object(data)
+ if new is None:
+ raise TypeError("%r is not a data object" % data)
+ text = new
+ elif mode == ENTRY_MODE_TEXT:
+ text = self._as_string(data)
+
+ self.set_text(text)
+
+type_register(ProxyEntry)
+
+class Entry(ProxyEntry):
+ def __init__(self, data_type=None):
+ deprecationwarn('Entry is deprecated, use ProxyEntry instead',
+ stacklevel=3)
+ ProxyEntry.__init__(self, data_type)
+type_register(Entry)
+
+class ProxyDateEntry(PropertyObject, DateEntry, ValidatableProxyWidgetMixin):
+ __gtype_name__ = 'ProxyDateEntry'
+
+ # changed allowed data types because checkbuttons can only
+ # accept bool values
+ allowed_data_types = datetime.date,
+
+ def __init__(self):
+ DateEntry.__init__(self)
+ ValidatableProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self)
+
+ gsignal('changed', 'override')
+ def do_changed(self):
+ self.chain()
+ self.emit('content-changed')
+
+ # ProxyWidgetMixin implementation
+
+ def read(self):
+ return self.get_date()
+
+ def update(self, data):
+ if data is None:
+ self.entry.set_text("")
+ else:
+ self.set_date(data)
+
+type_register(ProxyDateEntry)
diff --git a/kiwi/ui/widgets/filechooser.py b/kiwi/ui/widgets/filechooser.py
new file mode 100644
index 0000000..bbfdda1
--- /dev/null
+++ b/kiwi/ui/widgets/filechooser.py
@@ -0,0 +1,80 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Ali Afshar <aafshar@gmail.com>
+#
+
+
+"""Filechooser widgets for the kiwi framework"""
+
+import gtk
+
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal
+
+class _FileChooserMixin(object):
+ """Mixin to use common methods of the FileChooser interface"""
+
+ allowed_data_types = str,
+
+ gsignal('selection_changed', 'override')
+ def do_selection_changed(self):
+ self.emit('content-changed')
+ self.chain()
+
+ def read(self):
+ return self.get_filename()
+
+ def update(self, data):
+ if data is None:
+ return
+ self.set_filename(data)
+
+class ProxyFileChooserButton(_FileChooserMixin, PropertyObject,
+ gtk.FileChooserButton, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyFileChooserButton'
+ def __init__(self, title=None, parent=None,
+ action=gtk.FILE_CHOOSER_ACTION_OPEN,
+ buttons=None, backend=None):
+ """
+ @param title:
+ @param parent:
+ @param action:
+ @param buttons:
+ @param backend:
+ """
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self, data_type=str)
+ gtk.FileChooserButton.__init__(self, title=title,
+ parent=parent, action=action,
+ buttons=buttons, backend=backend)
+
+class ProxyFileChooserWidget(_FileChooserMixin, PropertyObject,
+ gtk.FileChooserWidget, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyFileChooserWidget'
+ def __init__(self, title, backend=None):
+ """
+ @param title:
+ @param backend:
+ """
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self, data_type=str)
+ gtk.FileChooserWidget.__init__(self, title=title, backend=backend)
+
diff --git a/kiwi/ui/widgets/fontbutton.py b/kiwi/ui/widgets/fontbutton.py
new file mode 100644
index 0000000..5852c02
--- /dev/null
+++ b/kiwi/ui/widgets/fontbutton.py
@@ -0,0 +1,47 @@
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Ali Afshar <aafshar@gmail.com>
+#
+
+"""FontButton proxy for the kiwi framework"""
+
+import gtk
+
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal, type_register
+
+
+class ProxyFontButton(PropertyObject, gtk.FontButton, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyFontButton'
+
+ allowed_data_types = str,
+
+ def __init__(self, fontname=None):
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self, data_type=str)
+ gtk.FontButton.__init__(self, fontname)
+
+ gsignal('font-set', 'override')
+ def do_font_set(self):
+ self.emit('content-changed')
+ self.chain()
+
+ def read(self):
+ return self.get_font_name()
+
+ def update(self, data):
+ self.set_font_name(data)
+
+
+type_register(ProxyFontButton)
+
diff --git a/kiwi/ui/widgets/label.py b/kiwi/ui/widgets/label.py
new file mode 100644
index 0000000..b91cc76
--- /dev/null
+++ b/kiwi/ui/widgets/label.py
@@ -0,0 +1,157 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+#
+
+"""GtkLabel support for the Kiwi Framework
+
+The L{Label} is also extended to support some basic markup like
+L{Label.set_bold}"""
+
+import gtk
+
+from kiwi.python import deprecationwarn
+from kiwi.ui.gadgets import set_foreground
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+from kiwi.utils import PropertyObject, type_register
+
+class ProxyLabel(PropertyObject, gtk.Label, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyLabel'
+ def __init__(self, label='', data_type=None):
+ """
+ @param label: initial text
+ @param data_type: data type of label
+ """
+ gtk.Label.__init__(self, label)
+ PropertyObject.__init__(self, data_type=data_type)
+ ProxyWidgetMixin.__init__(self)
+ self.set_use_markup(True)
+ self._attr_dic = { "style": None,
+ "weight": None,
+ "size": None,
+ "underline": None}
+ self._size_list = ('xx-small', 'x-small',
+ 'small', 'medium',
+ 'large', 'x-large',
+ 'xx-large')
+
+ self.connect("notify::label", self._on_label_changed)
+
+ def _on_label_changed(self, label, param):
+ # Since most of the time labels do not have a model attached to it
+ # we should just emit a signal if a model is defined
+ if self.model_attribute:
+ self.emit('content-changed')
+
+ def read(self):
+ return self._from_string(self.get_text())
+
+ def update(self, data):
+ if data is None:
+ text = ""
+ else:
+ text = self._as_string(data)
+ self.set_text(text)
+
+ def _apply_attributes(self):
+ # sorting is been done so we can be sure of the order of the
+ # attributes. Helps writing tests cases
+ attrs = self._attr_dic
+ keys = attrs.keys()
+ keys.sort()
+
+ attr_pairs = ['%s="%s"' % (key, attrs[key]) for key in keys
+ if attrs[key]]
+ self.set_markup('<span %s>%s</span>' % (' '.join(attr_pairs),
+ self.get_text()))
+
+ def _set_text_attribute(self, attribute_name, attr, value):
+ if value:
+ if self._attr_dic[attribute_name] is None:
+ self._attr_dic[attribute_name] = attr
+ self._apply_attributes()
+ else:
+ if self._attr_dic[attribute_name] is not None:
+ self._attr_dic[attribute_name] = None
+ self._apply_attributes()
+
+ def set_bold(self, value):
+ """ If True set the text to bold. False sets the text to normal """
+ self._set_text_attribute("weight", "bold", value)
+
+ def set_italic(self, value):
+ """ Enable or disable italic text
+ @param value: Allowed values:
+ - True: enable Italic attribute
+ - False: disable Italic attribute
+ """
+ self._set_text_attribute("style", "italic", value)
+
+ def set_underline(self, value):
+ """ Enable or disable underlined text
+ @param value: Allowed values:
+ - True: enable Underline attribute
+ - Fase: disable Underline attribute
+ """
+ self._set_text_attribute("underline", "single", value)
+
+ def set_size(self, size=None):
+ """ Set the size of the label. If size is empty the label will be
+ set to the default size.
+ @param size: Allowed values:
+ - xx-small
+ - x-small
+ - small
+ - medium,
+ - large
+ - x-large
+ - xx-large
+ @type size: string
+ """
+ if (size is not None and
+ size not in self._size_list):
+ raise ValueError('Size of "%s" label is not valid' %
+ self.get_text())
+
+ self._attr_dic["size"] = size
+ self._apply_attributes()
+
+ def set_text(self, text):
+ """ Overrides gtk.Label set_text method. Sets the new text of
+ the label but keeps the formating
+ @param text: label
+ @type text: string
+ """
+ gtk.Label.set_text(self, text)
+ self._apply_attributes()
+
+ def set_color(self, color):
+ set_foreground(self, color)
+type_register(ProxyLabel)
+
+class Label(ProxyLabel):
+ def __init__(self, label='', data_type=None):
+ deprecationwarn(
+ 'Label is deprecated, use ProxyLabel instead',
+ stacklevel=3)
+ ProxyLabel.__init__(self, label=label, data_type=data_type)
+type_register(Label)
diff --git a/kiwi/ui/widgets/list.py b/kiwi/ui/widgets/list.py
new file mode 100644
index 0000000..e57d412
--- /dev/null
+++ b/kiwi/ui/widgets/list.py
@@ -0,0 +1,62 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2001-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin@async.com.br>
+#
+
+"""High level wrapper for GtkTreeView: backwards compatibility layer"""
+
+import gtk
+
+from kiwi.decorators import deprecated
+from kiwi.python import deprecationwarn
+from kiwi.ui.objectlist import Column, SequentialColumn, ColoredColumn, \
+ ListLabel, SummaryLabel
+from kiwi.ui.objectlist import ObjectList, log
+
+# pyflakes
+Column, SequentialColumn, ColoredColumn, ListLabel, SummaryLabel
+
+class List(ObjectList):
+ def __init__(self, columns=[],
+ instance_list=None,
+ mode=gtk.SELECTION_BROWSE):
+ deprecationwarn(
+ 'List is deprecated, use ObjectList instead',
+ stacklevel=3)
+ ObjectList.__init__(self, columns, instance_list, mode)
+
+ # Backwards compat
+ def add_instance(self, *args, **kwargs):
+ return self.append(*args, **kwargs)
+ add_instance = deprecated('append', log)(add_instance)
+
+ def remove_instance(self, *args, **kwargs):
+ return self.remove(*args, **kwargs)
+ remove_instance = deprecated('remove', log)(remove_instance)
+
+ def update_instance(self, *args, **kwargs):
+ return self.update(*args, **kwargs)
+ update_instance = deprecated('update', log)(update_instance)
+
+ def select_instance(self, *args, **kwargs):
+ return self.select(*args, **kwargs)
+ select_instance = deprecated('select', log)(select_instance)
+
diff --git a/kiwi/ui/widgets/radiobutton.py b/kiwi/ui/widgets/radiobutton.py
new file mode 100644
index 0000000..147613f
--- /dev/null
+++ b/kiwi/ui/widgets/radiobutton.py
@@ -0,0 +1,94 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Daniel Saran R. da Cunha <daniel@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Gustavo Rahal <gustavo@async.com.br>
+#
+
+"""GtkRadioButton support for the Kiwi Framework"""
+
+import gtk
+
+from kiwi import ValueUnset
+from kiwi.python import deprecationwarn
+from kiwi.utils import PropertyObject, gproperty, type_register
+from kiwi.ui.proxywidget import ProxyWidgetMixin
+
+class ProxyRadioButton(PropertyObject, gtk.RadioButton, ProxyWidgetMixin):
+ __gtype_name__ = 'ProxyRadioButton'
+ gproperty('data-value', str, nick='Data Value')
+
+ def __init__(self, group=None, label=None, use_underline=True):
+ gtk.RadioButton.__init__(self, None, label, use_underline)
+ if group:
+ self.set_group(group)
+ ProxyWidgetMixin.__init__(self)
+ PropertyObject.__init__(self)
+ self.connect('group-changed', self._on_group_changed)
+
+ def _on_radio__toggled(self, radio):
+ self.emit('content-changed')
+
+ def _on_group_changed(self, radio):
+ for radio in radio.get_group():
+ radio.connect('toggled', self._on_radio__toggled)
+
+ def get_selected(self):
+ """
+ Get the currently selected radiobutton.
+
+ @returns: The selected L{RadioButton} or None if there are no
+ selected radiobuttons.
+ """
+
+ for button in self.get_group():
+ if button.get_active():
+ return button
+
+ def read(self):
+ button = self.get_selected()
+ if button is None:
+ return ValueUnset
+
+ return self._from_string(button.data_value)
+
+ def update(self, data):
+ if data is None:
+ # In a group of radiobuttons, the only widget which is in
+ # the proxy is ourself, the other buttons do not get their
+ # update() method called, so the default value is activate
+ # ourselves when the model is empty
+ self.set_active(True)
+ return
+
+ data = self._as_string(data)
+ for rb in self.get_group():
+ if rb.get_property('data-value') == data:
+ rb.set_active(True)
+
+class RadioButton(ProxyRadioButton):
+ def __init__(self):
+ deprecationwarn(
+ 'RadioButton is deprecated, use ProxyRadioButton instead',
+ stacklevel=3)
+ ProxyRadioButton.__init__(self)
+type_register(RadioButton)
diff --git a/kiwi/ui/widgets/spinbutton.py b/kiwi/ui/widgets/spinbutton.py
new file mode 100644
index 0000000..1c7c78e
--- /dev/null
+++ b/kiwi/ui/widgets/spinbutton.py
@@ -0,0 +1,124 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Gustavo Rahal <gustavo@async.com.br>
+# Lorenzo Gil Sanchez <lgs@sicem.biz>
+# Evandro Vale Miquelito <evandro@async.com.br>
+#
+
+"""GtkSpinButton support for the Kiwi Framework
+
+L{SpinButton} is also enhanced to display an icon using
+L{kiwi.ui.icon.IconEntry}
+"""
+
+import gtk
+
+from kiwi.datatypes import number
+from kiwi.python import deprecationwarn
+from kiwi.ui.icon import IconEntry
+from kiwi.ui.proxywidget import ValidatableProxyWidgetMixin
+from kiwi.utils import PropertyObject, gsignal, type_register
+
+class ProxySpinButton(PropertyObject, gtk.SpinButton, ValidatableProxyWidgetMixin):
+ """
+ A SpinButton subclass which adds supports for the Kiwi Framework.
+ This widget supports validation
+ The only allowed types for spinbutton are int and float.
+
+ """
+ __gtype_name__ = 'ProxySpinButton'
+ allowed_data_types = number
+
+ def __init__(self):
+ # since the default data_type is str we need to set it to int
+ # or float for spinbuttons
+ gtk.SpinButton.__init__(self)
+ PropertyObject.__init__(self, data_type=int)
+ ValidatableProxyWidgetMixin.__init__(self)
+ self._icon = IconEntry(self)
+ self.set_property('xalign', 1.0)
+
+ gsignal('changed', 'override')
+ def do_changed(self):
+ """Called when the content of the spinbutton changes.
+ """
+ # This is a work around, because GtkEditable.changed is called too
+ # often, as reported here: http://bugzilla.gnome.org/show_bug.cgi?id=64998
+ if self.get_text() != '':
+ self.emit('content-changed')
+ self.chain()
+
+ def read(self):
+ return self._from_string(self.get_text())
+
+ def update(self, data):
+ if data is None:
+ self.set_text("")
+ else:
+ # set_value accepts a float or int, no as_string conversion needed,
+ # and since we accept only int and float just send it in.
+ self.set_value(data)
+
+ def do_expose_event(self, event):
+ # This gets called when any of our three windows needs to be redrawn
+ gtk.SpinButton.do_expose_event(self, event)
+
+ if event.window == self.window:
+ self._icon.draw_pixbuf()
+
+ gsignal('size-allocate', 'override')
+ def do_size_allocate(self, allocation):
+
+ self.chain(allocation)
+
+ if self.flags() & gtk.REALIZED:
+ self._icon.resize_windows()
+
+ def do_realize(self):
+ gtk.SpinButton.do_realize(self)
+ self._icon.construct()
+
+ def do_unrealize(self):
+ self._icon.deconstruct()
+ gtk.SpinButton.do_unrealize(self)
+
+ # IconEntry
+
+ def set_tooltip(self, text):
+ self._icon.set_tooltip(text)
+
+ def set_pixbuf(self, pixbuf):
+ self._icon.set_pixbuf(pixbuf)
+
+ def update_background(self, color):
+ self._icon.update_background(color)
+
+ def get_icon_window(self):
+ return self._icon.get_icon_window()
+
+class SpinButton(ProxySpinButton):
+ def __init__(self):
+ deprecationwarn(
+ 'SpinButton is deprecated, use ProxySpinButton instead',
+ stacklevel=3)
+ ProxySpinButton.__init__(self)
+type_register(SpinButton)
diff --git a/kiwi/ui/widgets/textview.py b/kiwi/ui/widgets/textview.py
new file mode 100644
index 0000000..d35b945
--- /dev/null
+++ b/kiwi/ui/widgets/textview.py
@@ -0,0 +1,70 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2003-2005 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Christian Reis <kiko@async.com.br>
+# Gustavo Rahal <gustavo@async.com.br>
+# Evandro Vale Miquelito <evandro@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+
+"""GtkTextView support for the Kiwi Framework"""
+
+import gtk
+
+from kiwi.python import deprecationwarn
+from kiwi.ui.proxywidget import ValidatableProxyWidgetMixin
+from kiwi.utils import PropertyObject, type_register
+
+class ProxyTextView(PropertyObject, gtk.TextView, ValidatableProxyWidgetMixin):
+ __gtype_name__ = 'ProxyTextView'
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ PropertyObject.__init__(self, data_type=str)
+ ValidatableProxyWidgetMixin.__init__(self)
+
+ self._textbuffer = gtk.TextBuffer()
+ self._textbuffer.connect('changed',
+ self._on_textbuffer__changed)
+ self.set_buffer(self._textbuffer)
+
+ def _on_textbuffer__changed(self, textbuffer):
+ self.emit('content-changed')
+ self.read()
+
+ def read(self):
+ textbuffer = self._textbuffer
+ data = textbuffer.get_text(textbuffer.get_start_iter(),
+ textbuffer.get_end_iter())
+ return self._from_string(data)
+
+ def update(self, data):
+ if data is None:
+ text = ""
+ else:
+ text = self._as_string(data)
+
+ self._textbuffer.set_text(text)
+
+class TextView(ProxyTextView):
+ def __init__(self):
+ deprecationwarn(
+ 'TextView is deprecated, use ProxyTextView instead',
+ stacklevel=3)
+ ProxyTextView.__init__(self)
+type_register(TextView)
diff --git a/kiwi/ui/wizard.py b/kiwi/ui/wizard.py
new file mode 100644
index 0000000..26e72b8
--- /dev/null
+++ b/kiwi/ui/wizard.py
@@ -0,0 +1,236 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2005-2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+# Author(s): Gustavo Rahal <gustavo@async.com.br>
+# Evandro Vale Miquelito <evandro@async.com.br>
+# Johan Dahlin <jdahlin@async.com.br>
+#
+
+import gettext
+
+import gtk
+
+from kiwi.ui.delegates import Delegate
+
+_ = lambda m: gettext.dgettext('kiwi', m)
+
+class WizardStep:
+ """ This class must be inherited by the steps """
+ def __init__(self, previous=None, header=None):
+ self.previous = previous
+ self.header = header
+
+ def next_step(self):
+ # This is a virtual method, which must be redefined on children
+ # classes. It should not be called by the last step (in this case,
+ # has_next_step should return 0).
+ raise NotImplementedError
+
+ def post_init(self):
+ """A virtual method that must be defined on child when it's
+ necessary. This method will be called right after the change_step
+ method on PluggableWizard is concluded for the current step.
+ """
+
+ def has_next_step(self):
+ # This method should return False on last step classes
+ return True
+
+ def has_previous_step(self):
+ # This method should return False on first step classes; since
+ # self.previous is normally None for them, we can get away with
+ # this simplified check. Redefine as necessary.
+ return self.previous is not None
+
+ def previous_step(self):
+ return self.previous
+
+ def validate_step(self):
+ """A hook called always when changing steps. If it returns False
+ we can not go forward.
+ """
+ return True
+
+class PluggableWizard(Delegate):
+ """ Wizard controller and view class """
+ gladefile = 'PluggableWizard'
+ retval = None
+
+ def __init__(self, title, first_step, size=None, edit_mode=False):
+ """
+ @param title:
+ @param first_step:
+ @param size:
+ @param edit_mode:
+ """
+ Delegate.__init__(self, delete_handler=self.quit_if_last,
+ gladefile=self.gladefile,
+ widgets=self.widgets)
+ if not isinstance(first_step, WizardStep):
+ raise TypeError("first_step must be a WizardStep instance")
+
+ self.set_title(title)
+ self._current = None
+ self._first_step = first_step
+ self.edit_mode = edit_mode
+ if size:
+ self.get_toplevel().set_default_size(size[0], size[1])
+
+ self._change_step(first_step)
+ if not self.edit_mode:
+ self.ok_button.hide()
+
+ # Callbacks
+
+ def on_next_button__clicked(self, button):
+ if not self._current.validate_step():
+ return
+
+ if not self._current.has_next_step():
+ # This is the last step
+ self._change_step()
+ return
+
+ self._change_step(self._current.next_step())
+
+ def on_ok_button__clicked(self, button):
+ self._change_step()
+
+ def on_previous_button__clicked(self, button):
+ self._change_step(self._current.previous_step())
+
+ def on_cancel_button__clicked(self, button):
+ self.cancel()
+
+ # Private API
+
+ def _change_step(self, step=None):
+ if step is None:
+ # This is the last step and we can finish the job here
+ self.finish()
+ return
+ step.show()
+ self._current = step
+ self._refresh_slave()
+ if step.header:
+ self.header_lbl.show()
+ self.header_lbl.set_text(step.header)
+ else:
+ self.header_lbl.hide()
+ self.update_view()
+ self._current.post_init()
+
+ def _refresh_slave(self):
+ holder_name = 'slave_area'
+ if self.get_slave(holder_name):
+ self.detach_slave(holder_name)
+ self.attach_slave(holder_name, self._current)
+
+ def _show_first_page(self):
+ self.enable_next()
+ self.disable_back()
+ self.disable_finish()
+ self.notification_lbl.hide()
+
+ def _show_page(self):
+ self.enable_back()
+ self.enable_next()
+ self.disable_finish()
+ self.notification_lbl.hide()
+
+ def _show_last_page(self):
+ self.enable_back()
+ self.notification_lbl.show()
+ if self.edit_mode:
+ self.disable_next()
+ else:
+ self.enable_next()
+ self.enable_finish()
+
+ # Public API
+ def update_view(self):
+ if self.edit_mode:
+ self.ok_button.set_sensitive(True)
+
+ if not self._current.has_previous_step():
+ self._show_first_page()
+ elif self._current.has_next_step():
+ self._show_page()
+ else:
+ self._show_last_page()
+
+ def enable_next(self):
+ """
+ Enables the next button in the wizard.
+ """
+ self.next_button.set_sensitive(True)
+
+ def disable_next(self):
+ """
+ Enables the next button in the wizard.
+ """
+ self.next_button.set_sensitive(False)
+
+ def enable_back(self):
+ """
+ Enables the back button in the wizard.
+ """
+ self.previous_button.set_sensitive(True)
+
+ def disable_back(self):
+ """
+ Enables the back button in the wizard.
+ """
+ self.previous_button.set_sensitive(False)
+
+ def enable_finish(self):
+ """
+ Enables the finish button in the wizard.
+ """
+ if self.edit_mode:
+ button = self.ok_button
+ else:
+ button = self.next_button
+ button.set_label(_('Finish'))
+
+ def disable_finish(self):
+ """
+ Disables the finish button in the wizard.
+ """
+ if self.edit_mode:
+ self.ok_button.set_label(gtk.STOCK_OK)
+ else:
+ self.next_button.set_label(gtk.STOCK_GO_FORWARD)
+
+ def set_message(self, message):
+ """
+ @param message:
+ """
+ self.notification_lbl.set_text(message)
+
+ def cancel(self, *args):
+ # Redefine this method if you want something done when cancelling the
+ # wizard.
+ self.retval = None
+
+ def finish(self):
+ # Redefine this method if you want something done when finishing the
+ # wizard.
+ pass