Refactoring:

-  better functionality devision between buffer and editor;
 -  better integration with StyledTextTagType;
 -  font family and font size tags are separated;
 -  new font family and font size toolbar widgets;


svn: r10626
This commit is contained in:
Zsolt Foldvari 2008-04-23 09:07:02 +00:00
parent c4024f91db
commit d003d5e61b
3 changed files with 489 additions and 273 deletions

View File

@ -38,6 +38,7 @@ _LOG = logging.getLogger(".Editors.StyledTextBuffer")
# GTK modules # GTK modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
import gobject
import gtk import gtk
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
@ -53,6 +54,43 @@ from gen.lib import (StyledText, StyledTextTag, StyledTextTagType)
# Constants # Constants
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# FIXME
ALLOWED_STYLES = [i for (i, s, e) in StyledTextTagType._DATAMAP]
STYLE_TYPE = {
StyledTextTagType.BOLD: bool,
StyledTextTagType.ITALIC: bool,
StyledTextTagType.UNDERLINE: bool,
StyledTextTagType.FONTCOLOR: str,
StyledTextTagType.HIGHLIGHT: str,
StyledTextTagType.FONTFACE: str,
StyledTextTagType.FONTSIZE: int,
StyledTextTagType.SUPERSCRIPT: bool,
}
STYLE_DEFAULT = {
StyledTextTagType.BOLD: False,
StyledTextTagType.ITALIC: False,
StyledTextTagType.UNDERLINE: False,
StyledTextTagType.FONTCOLOR: '#000000',
StyledTextTagType.HIGHLIGHT: '#FFFFFF',
StyledTextTagType.FONTFACE: 'Sans',
StyledTextTagType.FONTSIZE: 10,
StyledTextTagType.SUPERSCRIPT: False,
}
STYLE_TO_PROPERTY = {
StyledTextTagType.BOLD: 'weight', # permanent tag is used instead
StyledTextTagType.ITALIC: 'style', # permanent tag is used instead
StyledTextTagType.UNDERLINE: 'underline', # permanent tag is used instead
StyledTextTagType.FONTCOLOR: 'foreground',
StyledTextTagType.HIGHLIGHT: 'background',
StyledTextTagType.FONTFACE: 'family',
StyledTextTagType.FONTSIZE: 'size-points',
StyledTextTagType.SUPERSCRIPT: 'rise',
}
(MATCH_START, (MATCH_START,
MATCH_END, MATCH_END,
MATCH_FLAVOR, MATCH_FLAVOR,
@ -189,8 +227,14 @@ class StyledTextBuffer(gtk.TextBuffer):
and gtk.TextBuffer. To set and get the text use the L{set_text} and and gtk.TextBuffer. To set and get the text use the L{set_text} and
L{get_text} methods. L{get_text} methods.
StyledTextBuffer provides an action group (L{format_action_group}) To set a style to (a portion of) the text (e.g. from GUI) use the
for GUIs. L{apply_style} and L{remove_style} methods.
To receive information about the style of the text at the cursor position
StyledTextBuffer provides two mechanism: message driven and polling.
To receive notification of style change as cursor moves connect to the
C{style-changed} signal. To get the value of a certain style at the cursor
use the L{get_style_at_cursor) method.
StyledTextBuffer has a regexp pattern matching mechanism too. To add a StyledTextBuffer has a regexp pattern matching mechanism too. To add a
regexp pattern to match in the text use the L{match_add} method. To check regexp pattern to match in the text use the L{match_add} method. To check
@ -200,66 +244,30 @@ class StyledTextBuffer(gtk.TextBuffer):
""" """
__gtype_name__ = 'StyledTextBuffer' __gtype_name__ = 'StyledTextBuffer'
formats = ('italic', 'bold', 'underline', __gsignals__ = {
'font', 'foreground', 'background',) 'style-changed': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE, #return value
(gobject.TYPE_PYOBJECT,)), # arguments
}
def __init__(self): def __init__(self):
gtk.TextBuffer.__init__(self) gtk.TextBuffer.__init__(self)
# Create fix tags. # Create fix tags.
# Other tags (e.g. color) have to be created on the fly # Other tags (e.g. color) have to be created on the fly
self.create_tag('bold', weight=WEIGHT_BOLD) # see self._find_tag_by_name
self.create_tag('italic', style=STYLE_ITALIC) self.create_tag(str(StyledTextTagType.BOLD), weight=WEIGHT_BOLD)
self.create_tag('underline', underline=UNDERLINE_SINGLE) self.create_tag(str(StyledTextTagType.ITALIC), style=STYLE_ITALIC)
self.create_tag(str(StyledTextTagType.UNDERLINE),
# Setup action group used from user interface underline=UNDERLINE_SINGLE)
format_toggle_actions = [
('italic', gtk.STOCK_ITALIC, None, None,
_('Italic'), self._on_toggle_action_activate),
('bold', gtk.STOCK_BOLD, None, None,
_('Bold'), self._on_toggle_action_activate),
('underline', gtk.STOCK_UNDERLINE, None, None,
_('Underline'), self._on_toggle_action_activate),
]
self.toggle_actions = [action[0] for action in format_toggle_actions]
format_actions = [
('font', 'gramps-font', None, None,
_('Font'), self._on_action_activate),
('foreground', 'gramps-font-color', None, None,
_('Font Color'), self._on_action_activate),
('background', 'gramps-font-bgcolor', None, None,
_('Background Color'), self._on_action_activate),
('clear', gtk.STOCK_CLEAR, None, None,
_('Clear Markup'), self._format_clear_cb),
]
self.action_accels = {
'<Control>i': 'italic',
'<Control>b': 'bold',
'<Control>u': 'underline',
}
self.format_action_group = gtk.ActionGroup('Format')
self.format_action_group.add_toggle_actions(format_toggle_actions)
self.format_action_group.add_actions(format_actions)
# internal format state attributes # internal format state attributes
## 1. are used to format inserted characters (self.after_insert_text) ## 1. are used to format inserted characters (self.after_insert_text)
## 2. are set each time the Insert marker is set (self.do_mark_set) ## 2. are set each time the Insert marker is set (self.do_mark_set)
## 3. are set when format actions are activated (self.*_action_activate) ## 3. are set when a style is set (self._apply_style_to_selection)
self.italic = False self.style_state = STYLE_DEFAULT.copy()
self.bold = False
self.underline = False
self.font = None
# TODO could we separate font name and size?
##self.size = None
self.foreground = None
self.background = None
# internally used attribute # internally used attribute
self._internal_toggle = False
self._insert = self.get_insert() self._insert = self.get_insert()
# create a mark used for text formatting # create a mark used for text formatting
@ -298,13 +306,10 @@ class StyledTextBuffer(gtk.TextBuffer):
insert_start = self.get_iter_at_mark(self.mark_insert) insert_start = self.get_iter_at_mark(self.mark_insert)
# apply active formats for the inserted text # apply active formats for the inserted text
for format in self.__class__.formats: for style in ALLOWED_STYLES:
value = getattr(self, format) value = self.style_state[style]
if value: if value and (value != STYLE_DEFAULT[style]):
if format in self.toggle_actions: self.apply_tag(self._find_tag_by_name(style, value),
value = None
self.apply_tag(self._find_tag_by_name(format, value),
insert_start, iter) insert_start, iter)
def after_delete_range(self, textbuffer, start, end): def after_delete_range(self, textbuffer, start, end):
@ -334,7 +339,7 @@ class StyledTextBuffer(gtk.TextBuffer):
break break
def do_mark_set(self, iter, mark): def do_mark_set(self, iter, mark):
"""Update format attributes each time the cursor moves.""" """Update style state each time the cursor moves."""
_LOG.debug("Setting mark %s at %d" % _LOG.debug("Setting mark %s at %d" %
(mark.get_name(), iter.get_offset())) (mark.get_name(), iter.get_offset()))
@ -345,50 +350,28 @@ class StyledTextBuffer(gtk.TextBuffer):
iter.backward_char() iter.backward_char()
tag_names = [tag.get_property('name') for tag in iter.get_tags()] tag_names = [tag.get_property('name') for tag in iter.get_tags()]
for format in self.__class__.formats: changed_styles = {}
if format in self.toggle_actions:
value = format in tag_names
# set state of toggle action
action = self.format_action_group.get_action(format)
self._internal_toggle = True
action.set_active(value)
self._internal_toggle = False
else:
value = None
for tname in tag_names:
if tname.startswith(format):
value = tname.split(' ', 1)[1]
setattr(self, format, value) for style in ALLOWED_STYLES:
if STYLE_TYPE[style] == bool:
value = str(style) in tag_names
else:
value = STYLE_DEFAULT[style]
for tname in tag_names:
if tname.startswith(str(style)):
value = tname.split(' ', 1)[1]
value = STYLE_TYPE[style](value)
if self.style_state[style] != value:
changed_styles[style] = value
self.style_state[style] = value
if changed_styles:
self.emit('style-changed', changed_styles)
# Private # Private
def _tagname_to_tagtype(self, name):
"""Convert gtk.TextTag names to StyledTextTagType values."""
tag2type = {
'bold': StyledTextTagType.BOLD,
'italic': StyledTextTagType.ITALIC,
'underline': StyledTextTagType.UNDERLINE,
'foreground': StyledTextTagType.FONTCOLOR,
'background': StyledTextTagType.HIGHLIGHT,
'font': StyledTextTagType.FONTFACE,
}
return StyledTextTagType(tag2type[name])
def _tagtype_to_tagname(self, tagtype):
"""Convert StyledTextTagType values to gtk.TextTag names."""
type2tag = {
StyledTextTagType.BOLD: 'bold',
StyledTextTagType.ITALIC: 'italic',
StyledTextTagType.UNDERLINE: 'underline',
StyledTextTagType.FONTCOLOR: 'foreground',
StyledTextTagType.HIGHLIGHT: 'background',
StyledTextTagType.FONTFACE: 'font',
}
return type2tag[tagtype]
##def get_tag_value_at_insert(self, name): ##def get_tag_value_at_insert(self, name):
##"""Get the value of the given tag at the insertion point.""" ##"""Get the value of the given tag at the insertion point."""
##tags = self.get_iter_at_mark(self._insert).get_tags() ##tags = self.get_iter_at_mark(self._insert).get_tags()
@ -404,21 +387,6 @@ class StyledTextBuffer(gtk.TextBuffer):
##return tag.get_name().split()[1] ##return tag.get_name().split()[1]
##return None ##return None
def _color_to_hex(self, color):
"""Convert gtk.gdk.Color to hex string."""
hexstring = ""
for col in 'red', 'green', 'blue':
hexfrag = hex(getattr(color, col) / (16 * 16)).split("x")[1]
if len(hexfrag) < 2:
hexfrag = "0" + hexfrag
hexstring += hexfrag
return '#' + hexstring
def _hex_to_color(self, hex):
"""Convert hex string to gtk.gdk.Color."""
color = gtk.gdk.color_parse(hex)
return color
def _get_selection(self): def _get_selection(self):
bounds = self.get_selection_bounds() bounds = self.get_selection_bounds()
if not bounds: if not bounds:
@ -446,11 +414,34 @@ class StyledTextBuffer(gtk.TextBuffer):
if selection: if selection:
self.remove_tag(tag, *selection) self.remove_tag(tag, *selection)
def _remove_format_from_selection(self, format): def _apply_style_to_selection(self, style, value):
# FIXME can this be unified?
if STYLE_TYPE[style] == bool:
start, end = self._get_selection()
if value:
self.apply_tag_by_name(str(style), start, end)
else:
self.remove_tag_by_name(str(style), start, end)
elif STYLE_TYPE[style] == str:
tag = self._find_tag_by_name(style, value)
self._remove_style_from_selection(style)
self._apply_tag_to_selection(tag)
elif STYLE_TYPE[style] == int:
tag = self._find_tag_by_name(style, value)
self._remove_style_from_selection(style)
self._apply_tag_to_selection(tag)
else:
# we should never get until here
return
self.style_state[style] = value
def _remove_style_from_selection(self, style):
start, end = self._get_selection() start, end = self._get_selection()
tags = self._get_tag_from_range(start.get_offset(), end.get_offset()) tags = self._get_tag_from_range(start.get_offset(), end.get_offset())
for tag_name in tags.keys(): for tag_name in tags.keys():
if tag_name.startswith(format): if tag_name.startswith(str(style)):
for start, end in tags[tag_name]: for start, end in tags[tag_name]:
self.remove_tag_by_name(tag_name, self.remove_tag_by_name(tag_name,
self.get_iter_at_offset(start), self.get_iter_at_offset(start),
@ -462,6 +453,8 @@ class StyledTextBuffer(gtk.TextBuffer):
Return only the name of the TextTag from the specified range. Return only the name of the TextTag from the specified range.
If range is not given, tags extracted from the whole buffer. If range is not given, tags extracted from the whole buffer.
@note: TextTag names are always composed like: (%s %s) % (style, value)
@param start: an offset pointing to the start of the range of text @param start: an offset pointing to the start of the range of text
@param type: int @param type: int
@param end: an offset pointing to the end of the range of text @param end: an offset pointing to the end of the range of text
@ -489,130 +482,61 @@ class StyledTextBuffer(gtk.TextBuffer):
tagdict[name]=[(pos, pos)] tagdict[name]=[(pos, pos)]
return tagdict return tagdict
def _find_tag_by_name(self, name, value): def _find_tag_by_name(self, style, value):
"""Fetch TextTag from buffer's tag table by it's name. """Fetch TextTag from buffer's tag table by it's name.
If TextTag does not exist yet, it is created. If TextTag does not exist yet, it is created.
""" """
if value is None: if STYLE_TYPE[style] == bool:
tag_name = name tag_name = str(style)
elif STYLE_TYPE[style] == str:
tag_name = "%d %s" % (style, value)
elif STYLE_TYPE[style] == int:
tag_name = "%d %d" % (style, value)
else: else:
tag_name = "%s %s" % (name, value) raise ValueError("Unknown style (%s) value type: %s" %
(style, value.__class__))
tag = self.get_tag_table().lookup(tag_name) tag = self.get_tag_table().lookup(tag_name)
if not tag: if not tag:
if value is not None: if STYLE_TYPE[style] != bool:
# bool style tags are not created here, but in constuctor
tag = self.create_tag(tag_name) tag = self.create_tag(tag_name)
tag.set_property(name, value) tag.set_property(STYLE_TO_PROPERTY[style], value)
else: else:
return None return None
return tag return tag
# Callbacks
def _on_toggle_action_activate(self, action):
"""Toggle a format.
Toggle formats are e.g. 'bold', 'italic', 'underline'.
"""
if self._internal_toggle:
return
start, end = self._get_selection()
if action.get_active():
self.apply_tag_by_name(action.get_name(), start, end)
else:
self.remove_tag_by_name(action.get_name(), start, end)
setattr(self, action.get_name(), action.get_active())
def _on_action_activate(self, action):
"""Apply a format."""
format = action.get_name()
if format == 'foreground':
color_selection = gtk.ColorSelectionDialog(_("Select font color"))
if self.foreground:
color_selection.colorsel.set_current_color(
self._hex_to_color(self.foreground))
response = color_selection.run()
color = color_selection.colorsel.get_current_color()
value = self._color_to_hex(color)
color_selection.destroy()
elif format == 'background':
color_selection = gtk.ColorSelectionDialog(_("Select "
"background color"))
if self.background:
color_selection.colorsel.set_current_color(
self._hex_to_color(self.background))
response = color_selection.run()
color = color_selection.colorsel.get_current_color()
value = self._color_to_hex(color)
color_selection.destroy()
elif format == 'font':
font_selection = CustomFontSelectionDialog(_("Select font"))
if self.font:
font_selection.fontsel.set_font_name(self.font)
response = font_selection.run()
value = font_selection.fontsel.get_font_name()
font_selection.destroy()
else:
_LOG.debug("unknown format: '%s'" % format)
return
if response == gtk.RESPONSE_OK:
_LOG.debug("applying format '%s' with value '%s'" % (format, value))
tag = self._find_tag_by_name(format, value)
self._remove_format_from_selection(format)
self._apply_tag_to_selection(tag)
setattr(self, format, value)
def _format_clear_cb(self, action):
"""Remove all formats from the selection.
Remove only our own tags without touching other ones (e.g. gtk.Spell),
thus remove_all_tags() can not be used.
"""
for format in self.formats:
self._remove_format_from_selection(format)
def on_key_press_event(self, widget, event):
"""Handle formatting shortcuts."""
for accel in self.action_accels.keys():
key, mod = gtk.accelerator_parse(accel)
if (event.keyval, event.state) == (key, mod):
action_name = self.action_accels[accel]
action = self.format_action_group.get_action(action_name)
action.activate()
return True
return False
# Public API # Public API
def set_text(self, r_text): def set_text(self, s_text):
"""Set the content of the buffer with markup tags.""" """Set the content of the buffer with markup tags.
gtk.TextBuffer.set_text(self, str(r_text))
r_tags = r_text.get_tags() @note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
for r_tag in r_tags:
tagname = self._tagtype_to_tagname(int(r_tag.name)) """
g_tag = self._find_tag_by_name(tagname, r_tag.value) gtk.TextBuffer.set_text(self, str(s_text))
s_tags = s_text.get_tags()
for s_tag in s_tags:
g_tag = self._find_tag_by_name(int(s_tag.name), s_tag.value)
if g_tag is not None: if g_tag is not None:
for (start, end) in r_tag.ranges: for (start, end) in s_tag.ranges:
start_iter = self.get_iter_at_offset(start) start_iter = self.get_iter_at_offset(start)
end_iter = self.get_iter_at_offset(end) end_iter = self.get_iter_at_offset(end)
self.apply_tag(g_tag, start_iter, end_iter) self.apply_tag(g_tag, start_iter, end_iter)
def get_text(self, start=None, end=None, include_hidden_chars=True): def get_text(self, start=None, end=None, include_hidden_chars=True):
"""Return the buffer text.""" """Return the buffer text.
if not start:
@note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
"""
if start is None:
start = self.get_start_iter() start = self.get_start_iter()
if not end: if end is None:
end = self.get_end_iter() end = self.get_end_iter()
txt = gtk.TextBuffer.get_text(self, start, end, include_hidden_chars) txt = gtk.TextBuffer.get_text(self, start, end, include_hidden_chars)
@ -620,25 +544,59 @@ class StyledTextBuffer(gtk.TextBuffer):
# extract tags out of the buffer # extract tags out of the buffer
g_tags = self._get_tag_from_range() g_tags = self._get_tag_from_range()
r_tags = [] s_tags = []
for g_tagname, g_ranges in g_tags.items(): for g_tagname, g_ranges in g_tags.items():
name_value = g_tagname.split(' ', 1) style_and_value = g_tagname.split(' ', 1)
if len(name_value) == 1: style = int(style_and_value[0])
name = name_value[0] if len(style_and_value) == 1:
r_value = None s_value = None
else: else:
(name, r_value) = name_value s_value = STYLE_TYPE[style](style_and_value[1])
if name in self.formats: if style in ALLOWED_STYLES:
r_tagtype = self._tagname_to_tagtype(name) s_ranges = [(start, end+1) for (start, end) in g_ranges]
r_ranges = [(start, end+1) for (start, end) in g_ranges] s_tag = StyledTextTag(style, s_value, s_ranges)
r_tag = StyledTextTag(r_tagtype, r_value, r_ranges)
r_tags.append(r_tag) s_tags.append(s_tag)
return StyledText(txt, r_tags) return StyledText(txt, s_tags)
def apply_style(self, style, value):
"""Apply a style with the given value to the selection.
@param style: style type to apply
@type style: L{StyledTextTagStyle} int value
@param value: value of the style type
@type value: depends on the I{style} type
"""
if not isinstance(value, STYLE_TYPE[style]):
raise TypeError("Style (%d) value must be %s and not %s" %
(style, STYLE_TYPE[style], value.__class__))
self._apply_style_to_selection(style, value)
def remove_style(self, style):
"""Delete all occurences with any value of the given style.
@param style: style type to apply
@type style: L{StyledTextTagStyle} int value
"""
self._remove_style_from_selection(style)
def get_style_at_cursor(self, style):
"""Get the actual value of the given style at the cursor position.
@param style: style type to apply
@type style: L{StyledTextTagStyle} int value
@returns: value of the style type
@returntype: depends on the C{style} type
"""
return self.style_state[style]
def match_add(self, pattern, flavor): def match_add(self, pattern, flavor):
"""Add a pattern to look for in the text.""" """Add a pattern to look for in the text."""
@ -652,27 +610,3 @@ class StyledTextBuffer(gtk.TextBuffer):
return match return match
return None return None
#-------------------------------------------------------------------------
#
# CustomFontSelectionDialog class
#
#-------------------------------------------------------------------------
class CustomFontSelectionDialog(gtk.FontSelectionDialog):
"""A FontSelectionDialog without the Style treeview.
This should be only a workaround until a real custom font selector
is created, because this solution is gtk implementation dependent.
"""
def __init__(self, title):
gtk.FontSelectionDialog.__init__(self, title)
# hide the Style label and treeview
for widget in self.fontsel.get_children():
if isinstance(widget, gtk.Table):
table = widget
for child in table.get_children():
if table.child_get_property(child, 'left-attach') == 1:
child.hide()

View File

@ -46,8 +46,11 @@ from pango import UNDERLINE_SINGLE
# GRAMPS modules # GRAMPS modules
# #
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
from Editors._StyledTextBuffer import (StyledTextBuffer, MATCH_START, from gen.lib import StyledTextTagType
MATCH_END, MATCH_FLAVOR, MATCH_STRING) from Editors._StyledTextBuffer import (StyledTextBuffer, ALLOWED_STYLES,
STYLE_TYPE, STYLE_DEFAULT,
MATCH_START, MATCH_END,
MATCH_FLAVOR, MATCH_STRING)
from Spell import Spell from Spell import Spell
from GrampsDisplay import url as display_url from GrampsDisplay import url as display_url
@ -62,18 +65,28 @@ REGULAR_CURSOR = gtk.gdk.Cursor(gtk.gdk.XTERM)
FORMAT_TOOLBAR = ''' FORMAT_TOOLBAR = '''
<ui> <ui>
<toolbar name="ToolBar"> <toolbar name="ToolBar">
<toolitem action="italic"/> <toolitem action="%d"/>
<toolitem action="bold"/> <toolitem action="%d"/>
<toolitem action="underline"/> <toolitem action="%d"/>
<separator/> <toolitem action="%d"/>
<toolitem action="font"/> <toolitem action="%d"/>
<toolitem action="foreground"/> <toolitem action="%d"/>
<toolitem action="background"/> <toolitem action="%d"/>
<separator/> <separator/>
<toolitem action="clear"/> <toolitem action="clear"/>
</toolbar> </toolbar>
</ui> </ui>
''' ''' % (StyledTextTagType.ITALIC,
StyledTextTagType.BOLD,
StyledTextTagType.UNDERLINE,
StyledTextTagType.FONTFACE,
StyledTextTagType.FONTSIZE,
StyledTextTagType.FONTCOLOR,
StyledTextTagType.HIGHLIGHT,
)
FONT_SIZES = [8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22,
24, 26, 28, 32, 36, 40, 48, 56, 64, 72]
USERCHARS = "-A-Za-z0-9" USERCHARS = "-A-Za-z0-9"
PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'" PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
@ -137,6 +150,7 @@ class StyledTextEditor(gtk.TextView):
def __init__(self): def __init__(self):
"""Setup initial instance variable values.""" """Setup initial instance variable values."""
self.textbuffer = StyledTextBuffer() self.textbuffer = StyledTextBuffer()
self.textbuffer.connect('style-changed', self._on_buffer_style_changed)
gtk.TextView.__init__(self, self.textbuffer) gtk.TextView.__init__(self, self.textbuffer)
self.match = None self.match = None
@ -146,6 +160,7 @@ class StyledTextEditor(gtk.TextView):
self.toolbar = self._create_toolbar() self.toolbar = self._create_toolbar()
self.spellcheck = Spell(self) self.spellcheck = Spell(self)
self._internal_style_change = False
self._connect_signals() self._connect_signals()
@ -196,10 +211,17 @@ class StyledTextEditor(gtk.TextView):
def on_key_press_event(self, widget, event): def on_key_press_event(self, widget, event):
"""Signal handler. """Signal handler.
Handle shortcuts in the TextView. Handle formatting shortcuts.
""" """
return self.get_buffer().on_key_press_event(self, event) for accel in self.action_accels.keys():
key, mod = gtk.accelerator_parse(accel)
if (event.keyval, event.state) == (key, mod):
action_name = self.action_accels[accel]
action = self.action_group.get_action(action_name)
action.activate()
return True
return False
def on_insert_at_cursor(self, widget, string): def on_insert_at_cursor(self, widget, string):
"""Signal handler. for debugging only.""" """Signal handler. for debugging only."""
@ -301,16 +323,73 @@ class StyledTextEditor(gtk.TextView):
def _create_toolbar(self): def _create_toolbar(self):
"""Create a formatting toolbar. """Create a formatting toolbar.
@returns: toolbar according to L{StyedTextBuffer} formatting @returns: toolbar containing text formatting toolitems.
capabilities.
@returntype: gtk.Toolbar @returntype: gtk.Toolbar
""" """
# define the actions...
# ...first the toggle actions, which have a ToggleToolButton as proxy
format_toggle_actions = [
(str(StyledTextTagType.ITALIC), gtk.STOCK_ITALIC, None, None,
_('Italic'), self._on_toggle_action_activate),
(str(StyledTextTagType.BOLD), gtk.STOCK_BOLD, None, None,
_('Bold'), self._on_toggle_action_activate),
(str(StyledTextTagType.UNDERLINE), gtk.STOCK_UNDERLINE, None, None,
_('Underline'), self._on_toggle_action_activate),
]
self.toggle_actions = [action[0] for action in format_toggle_actions]
# ...then the normal actions, which have a ToolButton as proxy
format_actions = [
(str(StyledTextTagType.FONTCOLOR), 'gramps-font-color', None, None,
_('Font Color'), self._on_action_activate),
(str(StyledTextTagType.HIGHLIGHT), 'gramps-font-bgcolor', None, None,
_('Background Color'), self._on_action_activate),
('clear', gtk.STOCK_CLEAR, None, None,
_('Clear Markup'), self._format_clear_cb),
]
# ...last the custom actions, which have custom proxies
fontface_action = ComboToolAction(str(StyledTextTagType.FONTFACE),
_("Font family"),
_("Font family"), None)
fontsize_action = ComboToolAction(str(StyledTextTagType.FONTSIZE),
_("Font size"),
_("Font size"), None)
self.action_accels = {
'<Control>i': 'italic',
'<Control>b': 'bold',
'<Control>u': 'underline',
}
# create the action group and insert all the actions
self.action_group = gtk.ActionGroup('Format')
self.action_group.add_toggle_actions(format_toggle_actions)
self.action_group.add_actions(format_actions)
self.action_group.add_action(fontface_action)
self.action_group.add_action(fontsize_action)
# define the toolbar and create the proxies via ensure_update()
uimanager = gtk.UIManager() uimanager = gtk.UIManager()
uimanager.insert_action_group(self.textbuffer.format_action_group, 0) uimanager.insert_action_group(self.action_group, 0)
uimanager.add_ui_from_string(FORMAT_TOOLBAR) uimanager.add_ui_from_string(FORMAT_TOOLBAR)
uimanager.ensure_update() 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)
##separator = uimanager.get_widget('/ToolBar/abcdef')
##separator = gtk.SeparatorToolItem()
##separator.set_expand(True)
##separator.set_draw(False)
toolbar = uimanager.get_widget('/ToolBar') toolbar = uimanager.get_widget('/ToolBar')
toolbar.set_style(gtk.TOOLBAR_ICONS) toolbar.set_style(gtk.TOOLBAR_ICONS)
@ -357,6 +436,98 @@ class StyledTextEditor(gtk.TextView):
# Callback functions # Callback functions
def _on_toggle_action_activate(self, action):
"""Toggle a style.
Toggle styles are e.g. 'bold', 'italic', 'underline'.
"""
if self._internal_style_change:
return
style = int(action.get_name())
value = action.get_active()
_LOG.debug("applying style '%d' with value '%s'" % (style, str(value)))
self.textbuffer.apply_style(style, value)
def _on_action_activate(self, action):
"""Apply a format."""
style = int(action.get_name())
current_value = self.textbuffer.get_style_at_cursor(style)
if style == StyledTextTagType.FONTCOLOR:
color_selection = gtk.ColorSelectionDialog(_("Select font color"))
elif style == StyledTextTagType.HIGHLIGHT:
color_selection = gtk.ColorSelectionDialog(_("Select "
"background color"))
else:
_LOG.debug("unknown style: '%d'" % style)
return
if current_value:
color_selection.colorsel.set_current_color(
hex_to_color(current_value))
response = color_selection.run()
color = color_selection.colorsel.get_current_color()
value = color_to_hex(color)
color_selection.destroy()
if response == gtk.RESPONSE_OK:
_LOG.debug("applying style '%d' with value '%s'" %
(style, str(value)))
self.textbuffer.apply_style(style, value)
def _on_combotoolitem_changed(self, combobox, style):
if self._internal_style_change:
return
value = STYLE_TYPE[style](combobox.get_active_text())
_LOG.debug("applying style '%d' with value '%s'" % (style, str(value)))
self.textbuffer.apply_style(style, value)
def _format_clear_cb(self, action):
"""Remove all formats from the selection.
Remove only our own tags without touching other ones (e.g. gtk.Spell),
thus remove_all_tags() can not be used.
"""
for style in ALLOWED_STYLES:
self.textbuffer.remove_style(style)
def _on_buffer_style_changed(self, buffer, changed_styles):
# set state of toggle action
for style in changed_styles.keys():
if str(style) in self.toggle_actions:
action = self.action_group.get_action(str(style))
self._internal_style_change = True
action.set_active(changed_styles[style])
self._internal_style_change = False
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 (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)
self._internal_style_change = False
def _spell_change_cb(self, menuitem, language): def _spell_change_cb(self, menuitem, language):
"""Set spell checker language according to user selection.""" """Set spell checker language according to user selection."""
self.spellcheck.set_active_language(language) self.spellcheck.set_active_language(language)
@ -396,6 +567,7 @@ class StyledTextEditor(gtk.TextView):
""" """
self.textbuffer.set_text(text) self.textbuffer.set_text(text)
self.textbuffer.place_cursor(self.textbuffer.get_start_iter())
def get_text(self): def get_text(self):
"""Get the text of the text buffer of the editor. """Get the text of the text buffer of the editor.
@ -414,3 +586,111 @@ class StyledTextEditor(gtk.TextView):
""" """
return self.toolbar 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)
#-------------------------------------------------------------------------
#
# 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:
default = families.index(STYLE_DEFAULT[StyledTextTagType.FONTFACE])
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:
default = FONT_SIZES.index(STYLE_DEFAULT[StyledTextTagType.FONTSIZE])
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 = ""
for col in 'red', 'green', 'blue':
hexfrag = hex(getattr(color, col) / (16 * 16)).split("x")[1]
if len(hexfrag) < 2:
hexfrag = "0" + hexfrag
hexstring += hexfrag
return '#' + hexstring
def hex_to_color(hex):
"""Convert hex string to gtk.gdk.Color."""
color = gtk.gdk.color_parse(hex)
return color

View File

@ -47,23 +47,25 @@ class StyledTextTagType(GrampsType):
Here we only define new class variables. For details see L{GrampsType}. Here we only define new class variables. For details see L{GrampsType}.
""" """
NONE_ = -1 NONE_TYPE = -1
BOLD = 0 BOLD = 0
ITALIC = 1 ITALIC = 1
UNDERLINE = 2 UNDERLINE = 2
FONTFACE = 3 FONTFACE = 3
FONTCOLOR = 4 FONTSIZE = 4
HIGHLIGHT = 5 FONTCOLOR = 5
SUPERSCRIPT = 6 HIGHLIGHT = 6
SUPERSCRIPT = 7
_CUSTOM = NONE_ _CUSTOM = NONE_TYPE
_DEFAULT = NONE_ _DEFAULT = NONE_TYPE
_DATAMAP = [ _DATAMAP = [
(BOLD, _("Bold"), "bold"), (BOLD, _("Bold"), "bold"),
(ITALIC, _("Italic"), "italic"), (ITALIC, _("Italic"), "italic"),
(UNDERLINE, _("Underline"), "underline"), (UNDERLINE, _("Underline"), "underline"),
(FONTFACE, _("Fontface"), "fontface"), (FONTFACE, _("Fontface"), "fontface"),
(FONTSIZE, _("Fontsize"), "fontsize"),
(FONTCOLOR, _("Fontcolor"), "fontcolor"), (FONTCOLOR, _("Fontcolor"), "fontcolor"),
(HIGHLIGHT, _("Highlight"), "highlight"), (HIGHLIGHT, _("Highlight"), "highlight"),
(SUPERSCRIPT, _("Superscript"), "superscript"), (SUPERSCRIPT, _("Superscript"), "superscript"),