#
# 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.
"""
try:
set
except AttributeError:
from sets import Set as set
import gobject
import gtk
from gtk import keysyms
from kiwi import ValueUnset
from kiwi.component import implements
from kiwi.datatypes import number
from kiwi.enums import ComboColumn, ComboMode
from kiwi.interfaces import IEasyCombo
from kiwi.python import deprecationwarn
from kiwi.ui.comboboxentry import BaseComboBoxEntry
from kiwi.ui.comboentry import ComboEntry
from kiwi.ui.gadgets import render_pixbuf
from kiwi.ui.proxywidget import ProxyWidgetMixin, ValidatableProxyWidgetMixin
from kiwi.ui.widgets.entry import ProxyEntry
from kiwi.utils import gsignal
class _EasyComboBoxHelper(object):
implements(IEasyCombo)
def __init__(self, combobox):
"""Call this constructor after the Combo one"""
if not isinstance(combobox, (gtk.ComboBox, ComboEntry)):
raise TypeError(
"combo needs to be a gtk.ComboBox or ComboEntry instance")
self._combobox = combobox
model = gtk.ListStore(str, object)
self._combobox.set_model(model)
self.mode = ComboMode.UNKNOWN
def get_mode(self):
return self.mode
def set_mode(self, mode):
if self.mode != ComboMode.UNKNOWN:
raise AssertionError
self.mode = mode
def clear(self):
"""Removes all items from list"""
model = self._combobox.get_model()
model.clear()
def prefill(self, itemdata, sort=False):
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 == ComboMode.UNKNOWN:
first = itemdata[0]
if isinstance(first, basestring):
self.set_mode(ComboMode.STRING)
elif isinstance(first, (tuple, list)):
self.set_mode(ComboMode.DATA)
else:
raise TypeError("Could not determine type, items must "
"be strings or tuple/list")
mode = self.mode
if mode not in (ComboMode.STRING, ComboMode.DATA):
raise TypeError("Incorrect format for itemdata; see "
"docstring for more information")
model = self._combobox.get_model()
values = set()
if mode == ComboMode.STRING:
if sort:
itemdata.sort()
for item in itemdata:
if item in values:
raise KeyError("Tried to insert duplicate value "
"%s into Combo!" % (item,))
values.add(item)
model.append((item, None))
elif mode == ComboMode.DATA:
if sort:
itemdata.sort(lambda x, y: cmp(x[0], y[0]))
for item in itemdata:
text, data = item
orig = text
count = 1
while text in values:
text = orig + ' (%d)' % count
count += 1
values.add(text)
model.append((text, data))
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 == ComboMode.UNKNOWN:
if data is not None:
self.set_mode(ComboMode.DATA)
else:
self.set_mode(ComboMode.STRING)
model = self._combobox.get_model()
if self.mode == ComboMode.STRING:
if data is not None:
raise TypeError("data can not be specified in string mode")
model.append((label, None))
elif self.mode == ComboMode.DATA:
if data is None:
raise TypeError("data must be specified in string mode")
model.append((label, data))
else:
raise AssertionError
def insert_item(self, position, label, data=None):
""" Inserts a single item at a position to the Combo.
:param position: position to insert the item at
:param label: a string with the text to be added
:param 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 == ComboMode.UNKNOWN:
if data is not None:
self.set_mode(ComboMode.DATA)
else:
self.set_mode(ComboMode.STRING)
model = self._combobox.get_model()
if self.mode == ComboMode.STRING:
if data is not None:
raise TypeError("data can not be specified in string mode")
model.insert(position, (label, None))
elif self.mode == ComboMode.DATA:
if data is None:
raise TypeError("data must be specified in string mode")
model.insert(position, (label, data))
else:
raise AssertionError
def select(self, data):
mode = self.mode
if self.mode == ComboMode.STRING:
self.select_item_by_label(data)
elif self.mode == ComboMode.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._combobox.set_active(pos)
def select_item_by_label(self, label):
model = self._combobox.get_model()
for row in model:
if row[ComboColumn.LABEL] == label:
self._combobox.set_active_iter(row.iter)
break
else:
raise KeyError("No item correspond to label %r in the combo %s"
% (label, self._combobox.name))
def select_item_by_data(self, data):
if self.mode != ComboMode.DATA:
raise TypeError("select_item_by_data can only be used in data mode")
model = self._combobox.get_model()
for row in model:
if row[ComboColumn.DATA] == data:
self._combobox.set_active_iter(row.iter)
break
else:
raise KeyError("No item correspond to data %r in the combo %s"
% (data, self._combobox.name))
def get_model_strings(self):
return [row[ComboColumn.LABEL] for row in self._combobox.get_model()]
def get_model_items(self):
if self.mode != ComboMode.DATA:
raise TypeError("get_model_items can only be used in data mode")
model = self._combobox.get_model()
items = {}
for row in model:
items[row[ComboColumn.LABEL]] = row[ComboColumn.DATA]
return items
def get_selected_label(self):
iter = self._combobox.get_active_iter()
if not iter:
return
model = self._combobox.get_model()
return model[iter][ComboColumn.LABEL]
def get_selected_data(self):
if self.mode != ComboMode.DATA:
raise TypeError("get_selected_data can only be used in data mode")
iter = self._combobox.get_active_iter()
if not iter:
return
model = self._combobox.get_model()
return model[iter][ComboColumn.DATA]
def get_selected(self):
mode = self.mode
if mode == ComboMode.STRING:
return self.get_selected_label()
elif mode == ComboMode.DATA:
return self.get_selected_data()
return None
[docs]class ProxyComboBox(gtk.ComboBox, ProxyWidgetMixin):
__gtype_name__ = 'ProxyComboBox'
allowed_data_types = (basestring, object) + number
data_type = gobject.property(
getter=ProxyWidgetMixin.get_data_type,
setter=ProxyWidgetMixin.set_data_type,
type=str, blurb='Data Type')
model_attribute = gobject.property(type=str, blurb='Model attribute')
gsignal('content-changed')
gsignal('validation-changed', bool)
gsignal('validate', object, retval=object)
def __init__(self):
self._color_attribute = None
gtk.ComboBox.__init__(self)
ProxyWidgetMixin.__init__(self)
self._helper = _EasyComboBoxHelper(self)
self.connect('changed', self._on__changed)
self._text_renderer = gtk.CellRendererText()
self.pack_start(self._text_renderer)
self.add_attribute(self._text_renderer, 'text', ComboColumn.LABEL)
def __len__(self):
# 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
return len(self.get_model())
def __nonzero__(self):
return True
# Callbacks
def _on__changed(self, combo):
self.emit('content-changed')
[docs] def set_color_attribute(self, value):
self._color_attribute = value
if not value:
return
def cell_data_func(view, renderer, model, treeiter):
category = model[treeiter][ComboColumn.DATA]
renderer.set_property('pixbuf',
render_pixbuf(category and category.color))
renderer = gtk.CellRendererPixbuf()
self.pack_start(renderer, False)
self.reorder(renderer, 0)
self.set_cell_data_func(renderer, cell_data_func)
self._text_renderer.set_padding(6, 0)
[docs] def get_color_attribute(self):
return self._color_attribute
color_attribute = gobject.property(
getter=get_color_attribute,
setter=set_color_attribute,
type=str, blurb='Color attribute')
# IProxyWidget
[docs] def read(self):
if self._helper.get_mode() == ComboMode.UNKNOWN:
return ValueUnset
data = self.get_selected()
if self._helper.get_mode() == ComboMode.STRING:
data = self._from_string(data)
return data
[docs] def update(self, data):
# We dont need validation because the user always
# choose a valid value
if data is None or data is ValueUnset:
return
if self._helper.get_mode() == ComboMode.STRING:
data = self._as_string(data)
self.select(data)
# IEasyCombo
[docs] def prefill(self, itemdata, sort=False):
"""
See :class:`kiwi.interfaces.IEasyCombo.prefill`
"""
self._helper.prefill(itemdata, sort)
# we always have something selected, by default the first item
self.set_active(0)
self.emit('content-changed')
[docs] def clear(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.clear`
"""
self._helper.clear()
self.emit('content-changed')
[docs] def append_item(self, label, data=None):
"""
See :class:`kiwi.interfaces.IEasyCombo.append_item`
"""
self._helper.append_item(label, data)
[docs] def insert_item(self, position, label, data=None):
"""
See :class:`kiwi.interfaces.IEasyCombo.insert_item`
"""
self._helper.insert_item(position, label, data)
[docs] def select(self, data):
"""
See :class:`kiwi.interfaces.IEasyCombo.select`
"""
self._helper.select(data)
[docs] def select_item_by_position(self, pos):
"""
See :class:`kiwi.interfaces.IEasyCombo.select`
"""
self._helper.select_item_by_position(pos)
[docs] def select_item_by_label(self, label):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_position`
"""
self._helper.select_item_by_label(label)
[docs] def select_item_by_data(self, data):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_label`
"""
self._helper.select_item_by_data(data)
[docs] def get_model_strings(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_data`
"""
return self._helper.get_model_strings()
[docs] def get_model_items(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_model_strings`
"""
return self._helper.get_model_items()
[docs] def get_selected_label(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_model_items`
"""
return self._helper.get_selected_label()
[docs] def get_selected_data(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_selected_label`
"""
return self._helper.get_selected_data()
[docs] def get_selected(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_selected_data`
"""
return self._helper.get_selected()
[docs]class ProxyComboBoxEntry(BaseComboBoxEntry,
ValidatableProxyWidgetMixin):
allowed_data_types = (basestring, object) + number
__gtype_name__ = 'ProxyComboBoxEntry'
data_type = gobject.property(
getter=ProxyWidgetMixin.get_data_type,
setter=ProxyWidgetMixin.set_data_type,
type=str, blurb='Data Type')
mandatory = gobject.property(type=bool, default=False)
model_attribute = gobject.property(type=str, blurb='Model attribute')
gsignal('content-changed')
gsignal('validation-changed', bool)
gsignal('validate', object, retval=object)
# 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.
def __init__(self, **kwargs):
deprecationwarn(
'ProxyComboBoxEntry is deprecated, use ProxyComboEntry instead',
stacklevel=3)
BaseComboBoxEntry.__init__(self)
ValidatableProxyWidgetMixin.__init__(self, widget=self.entry)
self._helper = _EasyComboBoxHelper(self)
for key, value in kwargs.items():
setattr(self.props, key, value)
self.set_text_column(ComboColumn.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 __nonzero__(self):
return True
def __len__(self):
return len(self.get_model())
# Properties
def _get_list_editable(self):
if self._helper.get_mode() == ComboMode.DATA:
return False
return self.entry.get_editable()
def _set_list_editable(self, value):
if self._helper.get_mode() != ComboMode.DATA:
self.entry.set_editable(value)
list_editable = gobject.property(getter=_get_list_editable,
setter=_set_list_editable,
type=bool, default=True,
nick="Editable")
# Private
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)
# Callbacks
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')
# IProxyWidget
[docs] def read(self):
if self._helper.get_mode() == ComboMode.UNKNOWN:
return ValueUnset
return self.get_selected()
[docs] def update(self, data):
if data is ValueUnset or data is None:
self.entry.set_text("")
else:
self.select(data)
# IEasyCombo
[docs] def prefill(self, itemdata, sort=False, clear_entry=True):
"""
See :class:`kiwi.interfaces.IEasyCombo.prefill`
"""
self._helper.prefill(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(ComboColumn.LABEL)
self.entry.set_completion(auto)
# we always have something selected, by default the first item
self.set_active(0)
self.emit('content-changed')
[docs] def clear(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.clear`
"""
self._helper.clear()
self.entry.set_text("")
[docs] def append_item(self, label, data=None):
"""
See :class:`kiwi.interfaces.IEasyCombo.append_item`
"""
self._helper.append_item(label, data)
[docs] def insert_item(self, position, label, data=None):
"""
See :class:`kiwi.interfaces.IEasyCombo.insert_item`
"""
self._helper.insert_item(position, label, data)
[docs] def select(self, data):
"""
See :class:`kiwi.interfaces.IEasyCombo.select`
"""
self._helper.select(data)
[docs] def select_item_by_position(self, pos):
"""
See :class:`kiwi.interfaces.IEasyCombo.select`
"""
self._helper.select_item_by_position(pos)
[docs] def select_item_by_label(self, label):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_position`
"""
self._helper.select_item_by_label(label)
[docs] def select_item_by_data(self, data):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_label`
"""
self._helper.select_item_by_data(data)
[docs] def get_model_strings(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.select_item_by_data`
"""
return self._helper.get_model_strings()
[docs] def get_model_items(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_model_strings`
"""
return self._helper.get_model_items()
[docs] def get_selected_label(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_model_items`
"""
return self._helper.get_selected_label()
[docs] def get_selected_data(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_selected_label`
"""
return self._helper.get_selected_data()
[docs] def get_selected(self):
"""
See :class:`kiwi.interfaces.IEasyCombo.get_selected_data`
"""
return self._helper.get_selected()
# Public API
[docs] def set_mode(self, mode):
# If we're in the transition to go from
# unknown->label set editable to False
if (self._helper.get_mode() == ComboMode.UNKNOWN and
mode == ComboMode.DATA):
self.entry.set_editable(False)
self._helper.set_mode(self, mode)
[docs]class ProxyComboEntry(ComboEntry, ValidatableProxyWidgetMixin):
__gtype_name__ = 'ProxyComboEntry'
allowed_data_types = (basestring, object) + number
data_type = gobject.property(
getter=ProxyWidgetMixin.get_data_type,
setter=ProxyWidgetMixin.set_data_type,
type=str, blurb='Data Type')
mandatory = gobject.property(type=bool, default=False)
model_attribute = gobject.property(type=str, blurb='Model attribute')
gsignal('content-changed')
gsignal('validation-changed', bool)
gsignal('validate', object, retval=object)
def __init__(self):
entry = ProxyEntry()
ComboEntry.__init__(self, entry=entry)
ValidatableProxyWidgetMixin.__init__(self)
entry.connect('content-changed', self._on_entry__content_changed)
entry.connect('validation-changed',
self._on_entry__validation_changed)
def __nonzero__(self):
return True
def __len__(self):
return len(self.get_model())
# Properties
def _get_list_editable(self):
return self.entry.get_editable()
def _set_list_editable(self, value):
self.entry.set_editable(value)
list_editable = gobject.property(getter=_get_list_editable,
setter=_set_list_editable,
type=bool, default=True,
nick="Editable")
# Callbacks
def _on_entry__content_changed(self, entry):
# We only need to listen for changes in the entry, it's updated
# even if you select something in the popup list
self.emit('content-changed')
def _on_entry__validation_changed(self, entry, value):
# Propagate entry's validity state
self.emit('validation-changed', value)
# IconEntry
[docs] def set_tooltip(self, text):
self.entry.set_tooltip(text)
# IProxyWidget
[docs] def read(self):
return self.get_selected()
[docs] def update(self, data):
if data is ValueUnset or data is None:
self.entry.set_text("")
else:
self.select(data)
#FIXME: This is really an ugly workaround. But for some dark and
# misterious force, we need to override this method because
# the method in superclass fails to retrieve the selected data.
[docs] def get_selected_data(self):
return self.entry.read()