diff --git a/src/widgets/Makefile.am b/src/widgets/Makefile.am index 17f567c52..46bfbbc7e 100644 --- a/src/widgets/Makefile.am +++ b/src/widgets/Makefile.am @@ -8,6 +8,8 @@ pkgdatadir = $(datadir)/@PACKAGE@/widgets pkgdata_PYTHON = \ __init__.py \ grampswidgets.py \ + multitypecomboentry.py \ + toolbarwidgets.py \ styledtextbuffer.py \ styledtexteditor.py diff --git a/src/widgets/__init__.py b/src/widgets/__init__.py index 8986ec7c3..ef694e949 100644 --- a/src/widgets/__init__.py +++ b/src/widgets/__init__.py @@ -23,6 +23,8 @@ """Custom widgets.""" from grampswidgets import * +from multitypecomboentry import MultiTypeComboEntry +from toolbarwidgets import * from styledtextbuffer import * from styledtexteditor import * diff --git a/src/widgets/multitypecomboentry.py b/src/widgets/multitypecomboentry.py new file mode 100644 index 000000000..6b46727ff --- /dev/null +++ b/src/widgets/multitypecomboentry.py @@ -0,0 +1,200 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Zsolt Foldvari +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU 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 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 +# + +# $Id$ + +"The MultiTypeComboEntry widget class." + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".widgets.multitypecomboentry") + +#------------------------------------------------------------------------- +# +# GTK modules +# +#------------------------------------------------------------------------- +import gtk + +#------------------------------------------------------------------------- +# +# MultiTypeComboEntry class +# +#------------------------------------------------------------------------- +class MultiTypeComboEntry(gtk.ComboBox, gtk.CellLayout): + """A ComboBoxEntry widget with validation. + + MultiTypeComboEntry may have data type other then string (tbd.). + + Its behaviour is different from gtk.ComboBoxEntry in the way how + the entry part of the widget is handled. While gtk.ComboBoxEntry + emits the 'changed' signal immediatelly the text in the entry is + changed, MultiTypeComboEntry emits the signal only after the text is + activated (enter is pressed, the focus is moved out) and validated. + + Validation function is an optional feature and activated only if a + validator function is given at instantiation. + + The entry can be set as editable or not editable using the + L{set_entry_editable} method. + + """ + __gtype_name__ = "MultiTypeComboEntry" + + def __init__(self, model=None, column=-1, validator=None): + gtk.ComboBox.__init__(self, model) + + self._entry = gtk.Entry() + # + dummy_event = gtk.gdk.Event(gtk.gdk.NOTHING) + self._entry.start_editing(dummy_event) + # + self.add(self._entry) + self._entry.show() + + self._text_renderer = gtk.CellRendererText() + self.pack_start(self._text_renderer, False) + + self._text_column = -1 + self.set_text_column(column) + self._active_text = '' + self.set_active(-1) + + self._validator = validator + + self._entry.connect('activate', self._on_entry_activate) + self._entry.connect('focus-out-event', self._on_entry_focus_out_event) + self._entry.connect('key-press-event', self._on_entry_key_press_event) + self.changed_cb_id = self.connect('changed', self._on_changed) + + self._has_frame_changed() + self.connect('notify', self._on_notify) + + # Virtual overriden methods + + def do_mnemonic_activate(self, group_cycling): + self._entry.grab_focus() + return True + + def do_grab_focus(self): + self._entry.grab_focus() + + # Signal handlers + + def _on_entry_activate(self, entry): + """Signal handler. + + Called when the entry is activated. + + """ + self._entry_changed(entry) + + def _on_entry_focus_out_event(self, widget, event): + """Signal handler. + + Called when the focus leaves the entry. + + """ + self._entry_changed(widget) + + def _on_entry_key_press_event(self, entry, event): + """Signal handler. + + Its purpose is to handle escape button. + + """ + if event.keyval == gtk.keysyms.Escape: + entry.set_text(self._active_text) + + return False + + def _on_changed(self, combobox): + """Signal handler. + + Called when the active row is changed in the combo box. + + """ + iter = self.get_active_iter() + if iter: + model = self.get_model() + new_text = model.get_value(iter, self._text_column) + self._entry.set_text(new_text) + + def _on_notify(self, object, gparamspec): + """Signal handler. + + Called whenever a property of the object is changed. + + """ + if gparamspec.name == 'has-frame': + self._has_frame_changed() + + # Private methods + + def _entry_changed(self, entry): + new_text = entry.get_text() + + if (self._validator is not None) and not self._validator(new_text): + entry.set_text(self._active_text) + return + + self._active_text = new_text + self.handler_block(self.changed_cb_id) + self.set_active(-1) + self.handler_unblock(self.changed_cb_id) + + def _has_frame_changed(self): + has_frame = self.get_property('has-frame') + self._entry.set_has_frame(has_frame) + + # Public methods + + def set_text_column(self, text_column): + if text_column < 0: + return + + if text_column > self.get_model().get_n_columns(): + return + + if self._text_column == -1: + self._text_column = text_column + self.set_attributes(self._text_renderer, text=text_column) + + def get_text_column(self): + return self._text_column + + def set_active_text(self, text): + if self._entry: + self._entry.set_text(text) + #self._entry.activate() + + def get_active_text(self): + if self._entry: + return self._entry.get_text() + + return None + + def set_entry_editable(self, is_editable): + self._entry.set_editable(is_editable) diff --git a/src/widgets/styledtexteditor.py b/src/widgets/styledtexteditor.py index e2d40451c..30ba69dc2 100644 --- a/src/widgets/styledtexteditor.py +++ b/src/widgets/styledtexteditor.py @@ -30,7 +30,7 @@ from gettext import gettext as _ import logging -_LOG = logging.getLogger(".Editors.StyledTextEditor") +_LOG = logging.getLogger(".widgets.styledtexteditor") #------------------------------------------------------------------------- # @@ -49,7 +49,8 @@ from pango import UNDERLINE_SINGLE from gen.lib import StyledTextTagType from widgets import (StyledTextBuffer, ALLOWED_STYLES, MATCH_START, MATCH_END, - MATCH_FLAVOR, MATCH_STRING) + MATCH_FLAVOR, MATCH_STRING, + ComboToolAction, SpringSeparatorAction) from Spell import Spell from GrampsDisplay import url as display_url @@ -350,19 +351,36 @@ class StyledTextEditor(gtk.TextView): ] # ...last the custom actions, which have custom proxies + items = [f.get_name() for f in self.get_pango_context().list_families()] + default = StyledTextTagType.STYLE_DEFAULT[StyledTextTagType.FONTFACE] fontface_action = ComboToolAction(str(StyledTextTagType.FONTFACE), _("Font family"), - _("Font family"), None) + _("Font family"), + None, + items, + str(default), + editable=False) + fontface_action.connect('activate', self._on_comboaction_activate) + + items = [str(size) for size in FONT_SIZES] + default = StyledTextTagType.STYLE_DEFAULT[StyledTextTagType.FONTSIZE] fontsize_action = ComboToolAction(str(StyledTextTagType.FONTSIZE), _("Font size"), - _("Font size"), None) + _("Font size"), + None, + items, + str(default), + sortable=False, + validator=is_valid_fontsize) + fontsize_action.connect('activate', self._on_comboaction_activate) + spring = SpringSeparatorAction("spring", "", "", None) # action accelerators self.action_accels = { - 'i': 'italic', - 'b': 'bold', - 'u': 'underline', + 'i': str(StyledTextTagType.ITALIC), + 'b': str(StyledTextTagType.BOLD), + 'u': str(StyledTextTagType.UNDERLINE), } # create the action group and insert all the actions @@ -379,15 +397,6 @@ class StyledTextEditor(gtk.TextView): uimanager.add_ui_from_string(FORMAT_TOOLBAR) uimanager.ensure_update() - # now that widget is created for the custom actions set them up - fontface = uimanager.get_widget('/ToolBar/%d' % - StyledTextTagType.FONTFACE) - set_fontface_toolitem(fontface, self._on_combotoolitem_changed) - - fontsize = uimanager.get_widget('/ToolBar/%d' % - StyledTextTagType.FONTSIZE) - set_fontsize_toolitem(fontsize, self._on_combotoolitem_changed) - # get the toolbar and set it's style toolbar = uimanager.get_widget('/ToolBar') toolbar.set_style(gtk.TOOLBAR_ICONS) @@ -477,13 +486,21 @@ class StyledTextEditor(gtk.TextView): (style, str(value))) self.textbuffer.apply_style(style, value) - def _on_combotoolitem_changed(self, combobox, style): + def _on_comboaction_activate(self, action): if self._internal_style_change: return - value = StyledTextTagType.STYLE_TYPE[style](combobox.get_active_text()) - _LOG.debug("applying style '%d' with value '%s'" % (style, str(value))) - self.textbuffer.apply_style(style, value) + style = int(action.get_name()) + + text = action.get_active_value() + try: + value = StyledTextTagType.STYLE_TYPE[style](text) + _LOG.debug("applying style '%d' with value '%s'" % + (style, str(value))) + self.textbuffer.apply_style(style, value) + except ValueError: + _LOG.debug("unable to convert '%s' to '%s'" % + (text, StyledTextTagType.STYLE_TYPE[style])) def _format_clear_cb(self, action): """Remove all formats from the selection. @@ -496,7 +513,7 @@ class StyledTextEditor(gtk.TextView): self.textbuffer.remove_style(style) def _on_buffer_style_changed(self, buffer, changed_styles): - # set state of toggle action + # update action values for style in changed_styles.keys(): if str(style) in self.toggle_actions: action = self.action_group.get_action(str(style)) @@ -506,25 +523,9 @@ class StyledTextEditor(gtk.TextView): if ((style == StyledTextTagType.FONTFACE) or (style == StyledTextTagType.FONTSIZE)): - action = self.action_group.get_action(str(style)) - combo = action.get_proxies()[0].child - model = combo.get_model() - iter = model.get_iter_first() - while iter: - if (StyledTextTagType.STYLE_TYPE[style]( - model.get_value(iter, 0)) == changed_styles[style]): - break - iter = model.iter_next(iter) - self._internal_style_change = True - if iter is None: - combo.child.set_text(str(changed_styles[style])) - if style == StyledTextTagType.FONTFACE: - _LOG.debug('font family "%s" is not installed' % - changed_styles[style]) - else: - combo.set_active_iter(iter) + action.set_active_value(str(changed_styles[style])) self._internal_style_change = False def _spell_change_cb(self, menuitem, language): @@ -586,130 +587,11 @@ class StyledTextEditor(gtk.TextView): """ return self.toolbar -#------------------------------------------------------------------------- -# -# ComboToolItem class -# -#------------------------------------------------------------------------- -class ComboToolItem(gtk.ToolItem): - - __gtype_name__ = "ComboToolItem" - - def __init__(self): - gtk.ToolItem.__init__(self) - - self.set_border_width(2) - self.set_homogeneous(False) - self.set_expand(False) - - self.combobox = gtk.combo_box_entry_new_text() - self.combobox.show() - self.add(self.combobox) - - def set_entry_editable(self, editable): - self.combobox.child.set_editable(editable) - -#------------------------------------------------------------------------- -# -# ComboToolAction class -# -#------------------------------------------------------------------------- -class ComboToolAction(gtk.Action): - - __gtype_name__ = "ComboToolAction" - - def __init__(self, name, label, tooltip, stock_id): - gtk.Action.__init__(self, name, label, tooltip, stock_id) - ##self.set_tool_item_type(ComboToolItem) - - ##def create_tool_item(self): - ##combobox = ComboToolButton() - ###self.connect_proxy(combobox) - ##return combobox - - ##def connect_proxy(self, proxy): - ##gtk.Action.connect_proxy(self, proxy) - - ##if isinstance(proxy, ComboToolButton): - ##proxy.combobox.connect('changed', self.changed) - - ##def changed(self, combobox): - ##self.activate() -ComboToolAction.set_tool_item_type(ComboToolItem) - -#------------------------------------------------------------------------- -# -# SpringSeparatorToolItem class -# -#------------------------------------------------------------------------- -class SpringSeparatorToolItem(gtk.SeparatorToolItem): - - __gtype_name__ = "SpringSeparatorToolItem" - - def __init__(self): - gtk.SeparatorToolItem.__init__(self) - - self.set_draw(False) - self.set_expand(True) - -#------------------------------------------------------------------------- -# -# SpringSeparatorAction class -# -#------------------------------------------------------------------------- -class SpringSeparatorAction(gtk.Action): - - __gtype_name__ = "SpringSeparatorAction" - - def __init__(self, name, label, tooltip, stock_id): - gtk.Action.__init__(self, name, label, tooltip, stock_id) - -SpringSeparatorAction.set_tool_item_type(SpringSeparatorToolItem) - #------------------------------------------------------------------------- # # Module functions # #------------------------------------------------------------------------- -def set_fontface_toolitem(combotoolitem, callback): - """Setup font family comboboxentry.""" - combotoolitem.set_entry_editable(False) - - fontface = combotoolitem.child - - families = [family.get_name() - for family in fontface.get_pango_context().list_families()] - families.sort() - for family in families: - fontface.append_text(family) - - try: - def_fam = StyledTextTagType.STYLE_DEFAULT[StyledTextTagType.FONTFACE] - default = families.index(def_fam) - except ValueError: - default = 0 - fontface.set_active(default) - - fontface.connect('changed', callback, StyledTextTagType.FONTFACE) - -def set_fontsize_toolitem(combotoolitem, callback): - """Setup font size comboboxentry.""" - combotoolitem.set_size_request(60, -1) - - fontsize = combotoolitem.child - - for size in FONT_SIZES: - fontsize.append_text(str(size)) - - try: - def_size = StyledTextTagType.STYLE_DEFAULT[StyledTextTagType.FONTSIZE] - default = FONT_SIZES.index(def_size) - except ValueError: - default = 0 - fontsize.set_active(default) - - fontsize.connect('changed', callback, StyledTextTagType.FONTSIZE) - def color_to_hex(color): """Convert gtk.gdk.Color to hex string.""" hexstring = "" @@ -724,3 +606,10 @@ def hex_to_color(hex): """Convert hex string to gtk.gdk.Color.""" color = gtk.gdk.color_parse(hex) return color + +def is_valid_fontsize(text): + try: + size = int(text) + return (size > 0) and (size < 73) + except ValueError: + return False \ No newline at end of file diff --git a/src/widgets/toolbarwidgets.py b/src/widgets/toolbarwidgets.py new file mode 100644 index 000000000..883cc1f90 --- /dev/null +++ b/src/widgets/toolbarwidgets.py @@ -0,0 +1,236 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2008 Zsolt Foldvari +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU 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 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 +# + +# $Id$ + +"Widget classes used for Toolbar." + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".widgets.toolbarwidgets") + +#------------------------------------------------------------------------- +# +# GTK modules +# +#------------------------------------------------------------------------- +import gobject +import gtk + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from widgets import MultiTypeComboEntry + +#------------------------------------------------------------------------- +# +# Constants +# +#------------------------------------------------------------------------- +(COLUMN_ITEM, + COLUMN_IS_SEP,) = range(2) + +#------------------------------------------------------------------------- +# +# ComboToolItem class +# +#------------------------------------------------------------------------- +class ComboToolItem(gtk.ToolItem): + + __gtype_name__ = "ComboToolItem" + + __gsignals__ = { + 'changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, #return value + ()), # arguments + } + + def __init__(self, model, editable, validator=None): + gtk.ToolItem.__init__(self) + + self.set_border_width(2) + self.set_homogeneous(False) + self.set_expand(False) + + combo_entry = MultiTypeComboEntry(model, COLUMN_ITEM, validator) + combo_entry.set_focus_on_click(False) + combo_entry.set_entry_editable(editable) + combo_entry.show() + self.add(combo_entry) + + combo_entry.connect('changed', self._on_combo_changed) + + def _on_combo_changed(self, combo_entry): + self.emit('changed') + + def set_active_iter(self, iter): + self.child.set_active_iter(iter) + + def get_active_iter(self): + return self.child.get_active_iter() + + def set_active_text(self, text): + self.child.set_active_text(text) + + def get_active_text(self): + return self.child.get_active_text() + +#------------------------------------------------------------------------- +# +# ComboToolAction class +# +#------------------------------------------------------------------------- +class ComboToolAction(gtk.Action): + + __gtype_name__ = "ComboToolAction" + + def __init__(self, name, label, tooltip, stock_id, items, + default=None, sortable=True, editable=True, + validator=None): + + gtk.Action.__init__(self, name, label, tooltip, stock_id) + + # create the model and insert the items + self.model = gtk.ListStore(gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN) + for item in items: + self.model.append((item, False)) + + # sort the rows if allowed + if sortable: + self.model.set_sort_column_id(COLUMN_ITEM, gtk.SORT_ASCENDING) + self.model.sort_column_changed() + + # set the first row (after sorting) as default if default was not set + if (default is None) or (default not in items): + self.default = self.model.get_value(self.model.get_iter_first(), + COLUMN_ITEM) + else: + self.default = default + + self.set_active_value(self.default) + + # set the first row as separator + self.model.set_value(self.model.get_iter_first(), COLUMN_IS_SEP, True) + + # remember if the proxy combo is editable + self.editable = editable + self.validator = validator + + def do_create_tool_item(self): + """Create a toolbar item widget that proxies for the given action. + + Override the default method, to be able to pass the required + parameters to the proxy's constructor. + + @returns: a toolbar item connected to the action. + @returntype: ComboToolItem + + """ + combo = ComboToolItem(self.model, self.editable, self.validator) + self.connect_proxy(combo) + return combo + + def connect_proxy(self, proxy): + """Connect a widget to an action object as a proxy. + + @param proxy: widget to be connected + @type proxy: gtk.Widget + + """ + # do this before hand, so that we don't call the "activate" handler + if isinstance(proxy, ComboToolItem): + proxy.set_active_iter(self.active_iter) + proxy.connect('changed', self._on_proxy_changed) + + #gtk.Action.connect_proxy(self, proxy) + + def _on_proxy_changed(self, proxy): + if isinstance(proxy, ComboToolItem): + self.active_iter = proxy.get_active_iter() + + if self.active_iter: + value = self.model.get_value(self.active_iter, COLUMN_ITEM) + else: + value = proxy.get_active_text() + + self.set_active_value(value) + + def set_active_value(self, value): + self.active_value = value + + iter = self.model.get_iter_first() + while iter: + if self.model.get_value(iter, COLUMN_ITEM) == value: + self.active_iter = iter + break + iter = self.model.iter_next(iter) + + for proxy in self.get_proxies(): + if isinstance(proxy, ComboToolItem): + if iter: + proxy.set_active_iter(self.active_iter) + else: + proxy.set_active_text(self.active_value) + else: + _LOG.warning("Don't know how to activate %s widget" % + proxy.__class__) + self.activate() + + def get_active_value(self): + return self.active_value + +ComboToolAction.set_tool_item_type(ComboToolItem) + +#------------------------------------------------------------------------- +# +# SpringSeparatorToolItem class +# +#------------------------------------------------------------------------- +class SpringSeparatorToolItem(gtk.SeparatorToolItem): + + __gtype_name__ = "SpringSeparatorToolItem" + + def __init__(self): + gtk.SeparatorToolItem.__init__(self) + + self.set_draw(False) + self.set_expand(True) + +#------------------------------------------------------------------------- +# +# SpringSeparatorAction class +# +#------------------------------------------------------------------------- +class SpringSeparatorAction(gtk.Action): + + __gtype_name__ = "SpringSeparatorAction" + + def __init__(self, name, label, tooltip, stock_id): + gtk.Action.__init__(self, name, label, tooltip, stock_id) + +SpringSeparatorAction.set_tool_item_type(SpringSeparatorToolItem) +