Reorganize custom widgets into a new 'widgets' package.
1. moving existing widget modules under src/widgets/ svn: r10694
This commit is contained in:
@@ -51,7 +51,7 @@ from _EditSecondary import EditSecondary
|
||||
from gen.lib import NoteType
|
||||
|
||||
from DisplayTabs import SourceEmbedList, NoteTab
|
||||
from GrampsWidgets import MonitoredDate, MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredDate, MonitoredEntry, PrivacyButton
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
|
||||
@@ -51,7 +51,7 @@ from _EditSecondary import EditSecondary
|
||||
from gen.lib import NoteType
|
||||
|
||||
from DisplayTabs import SourceEmbedList, NoteTab
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
from widgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
|
||||
@@ -51,7 +51,7 @@ from gen.lib import NoteType
|
||||
import Errors
|
||||
|
||||
from DisplayTabs import SourceEmbedList, NoteTab
|
||||
from GrampsWidgets import MonitoredDataType, PrivacyButton
|
||||
from widgets import MonitoredDataType, PrivacyButton
|
||||
from BasicUtils import name_displayer
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -48,8 +48,8 @@ from Editors import EditPrimary
|
||||
from QuestionDialog import ErrorDialog
|
||||
from DisplayTabs import (SourceEmbedList, NoteTab, GalleryTab,
|
||||
EventBackRefList, AttrEmbedList)
|
||||
from GrampsWidgets import (MonitoredEntry, PlaceEntry, PrivacyButton,
|
||||
MonitoredDataType, MonitoredDate)
|
||||
from widgets import (MonitoredEntry, PlaceEntry, PrivacyButton,
|
||||
MonitoredDataType, MonitoredDate)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
|
||||
@@ -45,8 +45,8 @@ import gen.lib
|
||||
|
||||
from DisplayTabs import (SourceEmbedList, NoteTab, GalleryTab,
|
||||
EventBackRefList, AttrEmbedList)
|
||||
from GrampsWidgets import (PrivacyButton, MonitoredEntry, PlaceEntry,
|
||||
MonitoredDate, MonitoredDataType)
|
||||
from widgets import (PrivacyButton, MonitoredEntry, PlaceEntry,
|
||||
MonitoredDate, MonitoredDataType)
|
||||
from _EditReference import RefTab, EditReference
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -64,8 +64,8 @@ from ReportBase import ReportUtils
|
||||
from DisplayTabs import (EmbeddedList, EventEmbedList, SourceEmbedList,
|
||||
FamilyAttrEmbedList, NoteTab, GalleryTab,
|
||||
FamilyLdsEmbedList, ChildModel)
|
||||
from GrampsWidgets import (PrivacyButton, MonitoredEntry, MonitoredDataType,
|
||||
IconButton, LinkBox, BasicLabel)
|
||||
from widgets import (PrivacyButton, MonitoredEntry, MonitoredDataType,
|
||||
IconButton, LinkBox, BasicLabel)
|
||||
from ReportBase import CATEGORY_QR_FAMILY
|
||||
from QuestionDialog import (ErrorDialog, RunDatabaseRepair, WarningDialog,
|
||||
MessageHideDialog)
|
||||
|
||||
@@ -54,8 +54,8 @@ import LdsUtils
|
||||
from _EditSecondary import EditSecondary
|
||||
|
||||
from DisplayTabs import SourceEmbedList,NoteTab
|
||||
from GrampsWidgets import (PrivacyButton, MonitoredDate, PlaceEntry,
|
||||
MonitoredMenu, MonitoredStrMenu)
|
||||
from widgets import (PrivacyButton, MonitoredDate, PlaceEntry,
|
||||
MonitoredMenu, MonitoredStrMenu)
|
||||
|
||||
_DATA_MAP = {
|
||||
gen.lib.LdsOrd.BAPTISM : [
|
||||
|
||||
@@ -36,7 +36,7 @@ import const
|
||||
import Config
|
||||
from _EditSecondary import EditSecondary
|
||||
|
||||
from GrampsWidgets import MonitoredEntry
|
||||
from widgets import MonitoredEntry
|
||||
from gettext import gettext as _
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -47,7 +47,7 @@ import Mime
|
||||
import ThumbNails
|
||||
import Utils
|
||||
from Editors import EditPrimary
|
||||
from GrampsWidgets import MonitoredDate, MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredDate, MonitoredEntry, PrivacyButton
|
||||
from DisplayTabs import (SourceEmbedList, AttrEmbedList, NoteTab,
|
||||
MediaBackRefList)
|
||||
from Editors.AddMedia import AddMediaObject
|
||||
|
||||
@@ -49,7 +49,7 @@ from gen.lib import NoteType
|
||||
|
||||
from DisplayTabs import (SourceEmbedList, AttrEmbedList, MediaBackRefList,
|
||||
NoteTab)
|
||||
from GrampsWidgets import MonitoredSpinButton, MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredSpinButton, MonitoredEntry, PrivacyButton
|
||||
from _EditReference import RefTab, EditReference
|
||||
from AddMedia import AddMediaObject
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ from BasicUtils import name_displayer
|
||||
from _EditSecondary import EditSecondary
|
||||
from gen.lib import NoteType
|
||||
from DisplayTabs import GrampsTab,SourceEmbedList,NoteTab
|
||||
from GrampsWidgets import (MonitoredEntry, MonitoredMenu, MonitoredDate,
|
||||
MonitoredDataType, PrivacyButton)
|
||||
from widgets import (MonitoredEntry, MonitoredMenu, MonitoredDate,
|
||||
MonitoredDataType, PrivacyButton)
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -47,11 +47,11 @@ import pango
|
||||
#-------------------------------------------------------------------------
|
||||
import Config
|
||||
from const import GLADE_FILE
|
||||
from Editors._StyledTextEditor import StyledTextEditor
|
||||
from widgets import StyledTextEditor
|
||||
from Editors._EditPrimary import EditPrimary
|
||||
from DisplayTabs import GrampsTab, NoteBackRefList
|
||||
from GrampsWidgets import (MonitoredDataType, MonitoredCheckbox,
|
||||
MonitoredEntry, PrivacyButton)
|
||||
from widgets import (MonitoredDataType, MonitoredCheckbox,
|
||||
MonitoredEntry, PrivacyButton)
|
||||
from gen.lib import Note
|
||||
from QuestionDialog import ErrorDialog
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import const
|
||||
import Utils
|
||||
import Mime
|
||||
import gen.lib
|
||||
import GrampsWidgets
|
||||
import widgets
|
||||
from BasicUtils import name_displayer
|
||||
import Errors
|
||||
|
||||
@@ -224,12 +224,12 @@ class EditPerson(EditPrimary):
|
||||
|
||||
"""
|
||||
|
||||
self.private = GrampsWidgets.PrivacyButton(
|
||||
self.private = widgets.PrivacyButton(
|
||||
self.top.get_widget('private'),
|
||||
self.obj,
|
||||
self.db.readonly)
|
||||
|
||||
self.gender = GrampsWidgets.MonitoredMenu(
|
||||
self.gender = widgets.MonitoredMenu(
|
||||
self.top.get_widget('gender'),
|
||||
self.obj.set_gender,
|
||||
self.obj.get_gender,
|
||||
@@ -240,7 +240,7 @@ class EditPerson(EditPrimary):
|
||||
),
|
||||
self.db.readonly)
|
||||
|
||||
self.marker = GrampsWidgets.MonitoredDataType(
|
||||
self.marker = widgets.MonitoredDataType(
|
||||
self.top.get_widget('marker'),
|
||||
self.obj.set_marker,
|
||||
self.obj.get_marker,
|
||||
@@ -248,7 +248,7 @@ class EditPerson(EditPrimary):
|
||||
self.db.get_marker_types(),
|
||||
)
|
||||
|
||||
self.ntype_field = GrampsWidgets.MonitoredDataType(
|
||||
self.ntype_field = widgets.MonitoredDataType(
|
||||
self.top.get_widget("ntype"),
|
||||
self.pname.set_type,
|
||||
self.pname.get_type,
|
||||
@@ -256,7 +256,7 @@ class EditPerson(EditPrimary):
|
||||
self.db.get_name_types())
|
||||
|
||||
if self.use_patronymic:
|
||||
self.prefix = GrampsWidgets.MonitoredEntry(
|
||||
self.prefix = widgets.MonitoredEntry(
|
||||
self.top.get_widget("prefix"),
|
||||
self.pname.set_patronymic,
|
||||
self.pname.get_patronymic,
|
||||
@@ -266,44 +266,44 @@ class EditPerson(EditPrimary):
|
||||
prefix_label.set_text(_('Patronymic:'))
|
||||
prefix_label.set_use_underline(True)
|
||||
else:
|
||||
self.prefix = GrampsWidgets.MonitoredEntry(
|
||||
self.prefix = widgets.MonitoredEntry(
|
||||
self.top.get_widget("prefix"),
|
||||
self.pname.set_surname_prefix,
|
||||
self.pname.get_surname_prefix,
|
||||
self.db.readonly)
|
||||
|
||||
self.suffix = GrampsWidgets.MonitoredEntry(
|
||||
self.suffix = widgets.MonitoredEntry(
|
||||
self.top.get_widget("suffix"),
|
||||
self.pname.set_suffix,
|
||||
self.pname.get_suffix,
|
||||
self.db.readonly)
|
||||
|
||||
self.call = GrampsWidgets.MonitoredEntry(
|
||||
self.call = widgets.MonitoredEntry(
|
||||
self.top.get_widget("call"),
|
||||
self.pname.set_call_name,
|
||||
self.pname.get_call_name,
|
||||
self.db.readonly)
|
||||
|
||||
self.given = GrampsWidgets.MonitoredEntry(
|
||||
self.given = widgets.MonitoredEntry(
|
||||
self.top.get_widget("given_name"),
|
||||
self.pname.set_first_name,
|
||||
self.pname.get_first_name,
|
||||
self.db.readonly)
|
||||
|
||||
self.title = GrampsWidgets.MonitoredEntry(
|
||||
self.title = widgets.MonitoredEntry(
|
||||
self.top.get_widget("title"),
|
||||
self.pname.set_title,
|
||||
self.pname.get_title,
|
||||
self.db.readonly)
|
||||
|
||||
self.surname_field = GrampsWidgets.MonitoredEntry(
|
||||
self.surname_field = widgets.MonitoredEntry(
|
||||
self.top.get_widget("surname"),
|
||||
self.pname.set_surname,
|
||||
self.pname.get_surname,
|
||||
self.db.readonly,
|
||||
autolist=self.db.get_surname_list())
|
||||
|
||||
self.gid = GrampsWidgets.MonitoredEntry(
|
||||
self.gid = widgets.MonitoredEntry(
|
||||
self.top.get_widget("gid"),
|
||||
self.obj.set_gramps_id,
|
||||
self.obj.get_gramps_id,
|
||||
|
||||
@@ -50,7 +50,7 @@ import Config
|
||||
from BasicUtils import name_displayer
|
||||
from _EditSecondary import EditSecondary
|
||||
from gen.lib import NoteType
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredEntry, PrivacyButton
|
||||
from DisplayTabs import SourceEmbedList, NoteTab
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -49,7 +49,7 @@ import gen.lib
|
||||
from Editors._EditPrimary import EditPrimary
|
||||
from DisplayTabs import (GrampsTab, LocationEmbedList, SourceEmbedList,
|
||||
GalleryTab, NoteTab, WebEmbedList, PlaceBackRefList)
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredEntry, PrivacyButton
|
||||
from Errors import ValidationError
|
||||
from PlaceUtils import conv_lat_lon
|
||||
from QuestionDialog import ErrorDialog
|
||||
|
||||
@@ -45,7 +45,7 @@ import Config
|
||||
from gen.lib import NoteType
|
||||
|
||||
from DisplayTabs import NoteTab,AddrEmbedList,WebEmbedList,SourceBackRefList
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
from widgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
from _EditReference import RefTab, EditReference
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -44,7 +44,7 @@ import const
|
||||
import Config
|
||||
import gen.lib
|
||||
|
||||
from GrampsWidgets import MonitoredEntry, MonitoredDataType, PrivacyButton
|
||||
from widgets import MonitoredEntry, MonitoredDataType, PrivacyButton
|
||||
from DisplayTabs import AddrEmbedList, WebEmbedList, NoteTab, SourceBackRefList
|
||||
from Editors._EditPrimary import EditPrimary
|
||||
from QuestionDialog import ErrorDialog
|
||||
|
||||
@@ -50,7 +50,7 @@ from Editors._EditPrimary import EditPrimary
|
||||
|
||||
from DisplayTabs import (NoteTab, GalleryTab, DataEmbedList,
|
||||
SourceBackRefList, RepoEmbedList)
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton
|
||||
from widgets import MonitoredEntry, PrivacyButton
|
||||
from QuestionDialog import ErrorDialog
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -45,8 +45,8 @@ import gen.lib
|
||||
|
||||
from DisplayTabs import (NoteTab, GalleryTab, SourceBackRefList,
|
||||
DataEmbedList, RepoEmbedList)
|
||||
from GrampsWidgets import (PrivacyButton, MonitoredEntry, MonitoredMenu,
|
||||
MonitoredDate)
|
||||
from widgets import (PrivacyButton, MonitoredEntry, MonitoredMenu,
|
||||
MonitoredDate)
|
||||
from _EditReference import RefTab, EditReference
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@@ -42,7 +42,7 @@ from gtk import glade
|
||||
import const
|
||||
import Config
|
||||
from _EditSecondary import EditSecondary
|
||||
from GrampsWidgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
from widgets import MonitoredEntry, PrivacyButton, MonitoredDataType
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
|
||||
@@ -1,600 +0,0 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2007-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$
|
||||
|
||||
"Text buffer subclassed from gtk.TextBuffer handling L{StyledText}."
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gettext import gettext as _
|
||||
import re
|
||||
|
||||
import logging
|
||||
_LOG = logging.getLogger(".Editors.StyledTextBuffer")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gobject
|
||||
import gtk
|
||||
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gen.lib import (StyledText, StyledTextTag, StyledTextTagType)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
ALLOWED_STYLES = (
|
||||
StyledTextTagType.BOLD,
|
||||
StyledTextTagType.ITALIC,
|
||||
StyledTextTagType.UNDERLINE,
|
||||
StyledTextTagType.FONTCOLOR,
|
||||
StyledTextTagType.HIGHLIGHT,
|
||||
StyledTextTagType.FONTFACE,
|
||||
StyledTextTagType.FONTSIZE,
|
||||
)
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
(MATCH_START,
|
||||
MATCH_END,
|
||||
MATCH_FLAVOR,
|
||||
MATCH_STRING,) = range(4)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GtkSpellState class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class GtkSpellState:
|
||||
"""A simple state machine kinda thingy.
|
||||
|
||||
Trying to track gtk.Spell activities on a buffer and re-apply formatting
|
||||
after gtk.Spell replaces a misspelled word.
|
||||
|
||||
"""
|
||||
(STATE_NONE,
|
||||
STATE_CLICKED,
|
||||
STATE_DELETED,
|
||||
STATE_INSERTING) = range(4)
|
||||
|
||||
def __init__(self, textbuffer):
|
||||
if not isinstance(textbuffer, gtk.TextBuffer):
|
||||
raise TypeError("Init parameter must be instance of gtk.TextBuffer")
|
||||
|
||||
textbuffer.connect('mark-set', self.on_buffer_mark_set)
|
||||
textbuffer.connect('delete-range', self.on_buffer_delete_range)
|
||||
textbuffer.connect('insert-text', self.on_buffer_insert_text)
|
||||
textbuffer.connect_after('insert-text', self.after_buffer_insert_text)
|
||||
|
||||
self.reset_state()
|
||||
|
||||
def reset_state(self):
|
||||
self.state = self.STATE_NONE
|
||||
self.start = 0
|
||||
self.end = 0
|
||||
self.tags = None
|
||||
|
||||
def on_buffer_mark_set(self, textbuffer, iter, mark):
|
||||
mark_name = mark.get_name()
|
||||
if mark_name == 'gtkspell-click':
|
||||
self.state = self.STATE_CLICKED
|
||||
self.start, self.end = self.get_word_extents_from_mark(textbuffer,
|
||||
mark)
|
||||
_LOG.debug("SpellState got start %d end %d" % (self.start, self.end))
|
||||
elif mark_name == 'insert':
|
||||
self.reset_state()
|
||||
|
||||
def on_buffer_delete_range(self, textbuffer, start, end):
|
||||
if ((self.state == self.STATE_CLICKED) and
|
||||
(start.get_offset() == self.start) and
|
||||
(end.get_offset() == self.end)):
|
||||
self.state = self.STATE_DELETED
|
||||
self.tags = start.get_tags()
|
||||
|
||||
def on_buffer_insert_text(self, textbuffer, iter, text, length):
|
||||
if self.state == self.STATE_DELETED and iter.get_offset() == self.start:
|
||||
self.state = self.STATE_INSERTING
|
||||
|
||||
def after_buffer_insert_text(self, textbuffer, iter, text, length):
|
||||
if self.state == self.STATE_INSERTING:
|
||||
mark = textbuffer.get_mark('gtkspell-insert-start')
|
||||
insert_start = textbuffer.get_iter_at_mark(mark)
|
||||
for tag in self.tags:
|
||||
textbuffer.apply_tag(tag, insert_start, iter)
|
||||
|
||||
self.reset_state()
|
||||
|
||||
def get_word_extents_from_mark(self, textbuffer, mark):
|
||||
"""Get the word extents as gtk.Spell does.
|
||||
|
||||
Used to get the beginning of the word, in which user right clicked.
|
||||
Formatting found at that position used after gtk.Spell replaces
|
||||
misspelled words.
|
||||
|
||||
"""
|
||||
start = textbuffer.get_iter_at_mark(mark)
|
||||
if not start.starts_word():
|
||||
#start.backward_word_start()
|
||||
self.backward_word_start(start)
|
||||
end = start.copy()
|
||||
if end.inside_word():
|
||||
#end.forward_word_end()
|
||||
self.forward_word_end(end)
|
||||
return start.get_offset(), end.get_offset()
|
||||
|
||||
def forward_word_end(self, iter):
|
||||
"""gtk.Spell style gtk.TextIter.forward_word_end.
|
||||
|
||||
The parameter 'iter' is changing as side effect.
|
||||
|
||||
"""
|
||||
if not iter.forward_word_end():
|
||||
return False
|
||||
|
||||
if iter.get_char() != "'":
|
||||
return True
|
||||
|
||||
i = iter.copy()
|
||||
if i.forward_char():
|
||||
if i.get_char().isalpha():
|
||||
return iter.forward_word_end()
|
||||
|
||||
return True
|
||||
|
||||
def backward_word_start(self, iter):
|
||||
"""gtk.Spell style gtk.TextIter.backward_word_start.
|
||||
|
||||
The parameter 'iter' is changing as side effect.
|
||||
|
||||
"""
|
||||
if not iter.backward_word_start():
|
||||
return False
|
||||
|
||||
i = iter.copy()
|
||||
if i.backward_char():
|
||||
if i.get_char() == "'":
|
||||
if i.backward_char():
|
||||
if i.get_char().isalpha():
|
||||
return iter.backward_word_start()
|
||||
|
||||
return True
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# StyledTextBuffer class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class StyledTextBuffer(gtk.TextBuffer):
|
||||
"""An extended TextBuffer for handling StyledText strings.
|
||||
|
||||
StyledTextBuffer is an interface between GRAMPS' L{StyledText} format
|
||||
and gtk.TextBuffer. To set and get the text use the L{set_text} and
|
||||
L{get_text} methods.
|
||||
|
||||
To set a style to (a portion of) the text (e.g. from GUI) use the
|
||||
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
|
||||
regexp pattern to match in the text use the L{match_add} method. To check
|
||||
if there's a match at a certain position in the text use the L{match_check}
|
||||
method. For an example how to use the matching see L{EditNote}.
|
||||
|
||||
"""
|
||||
__gtype_name__ = 'StyledTextBuffer'
|
||||
|
||||
__gsignals__ = {
|
||||
'style-changed': (gobject.SIGNAL_RUN_FIRST,
|
||||
gobject.TYPE_NONE, #return value
|
||||
(gobject.TYPE_PYOBJECT,)), # arguments
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
gtk.TextBuffer.__init__(self)
|
||||
|
||||
# Create fix tags.
|
||||
# Other tags (e.g. color) have to be created on the fly
|
||||
# see self._find_tag_by_name
|
||||
self.create_tag(str(StyledTextTagType.BOLD), weight=WEIGHT_BOLD)
|
||||
self.create_tag(str(StyledTextTagType.ITALIC), style=STYLE_ITALIC)
|
||||
self.create_tag(str(StyledTextTagType.UNDERLINE),
|
||||
underline=UNDERLINE_SINGLE)
|
||||
|
||||
# internal format state attributes
|
||||
## 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)
|
||||
## 3. are set when a style is set (self._apply_style_to_selection)
|
||||
self.style_state = StyledTextTagType.STYLE_DEFAULT.copy()
|
||||
|
||||
# internally used attribute
|
||||
self._insert = self.get_insert()
|
||||
|
||||
# create a mark used for text formatting
|
||||
start, end = self.get_bounds()
|
||||
self.mark_insert = self.create_mark('insert-start', start, True)
|
||||
|
||||
# pattern matching attributes
|
||||
self.patterns = []
|
||||
self.matches = []
|
||||
|
||||
# hook up on some signals whose default handler cannot be overriden
|
||||
self.connect('insert-text', self.on_insert_text)
|
||||
self.connect_after('insert-text', self.after_insert_text)
|
||||
self.connect_after('delete-range', self.after_delete_range)
|
||||
|
||||
# init gtkspell "state machine"
|
||||
self.gtkspell_state = GtkSpellState(self)
|
||||
|
||||
# Virtual methods
|
||||
|
||||
def on_insert_text(self, textbuffer, iter, text, length):
|
||||
_LOG.debug("Will insert at %d length %d" % (iter.get_offset(), length))
|
||||
|
||||
# let's remember where we started inserting
|
||||
self.move_mark(self.mark_insert, iter)
|
||||
|
||||
def after_insert_text(self, textbuffer, iter, text, length):
|
||||
"""Format inserted text."""
|
||||
_LOG.debug("Have inserted at %d length %d (%s)" %
|
||||
(iter.get_offset(), length, text))
|
||||
|
||||
if not length:
|
||||
return
|
||||
|
||||
# where did we start inserting
|
||||
insert_start = self.get_iter_at_mark(self.mark_insert)
|
||||
|
||||
# apply active formats for the inserted text
|
||||
for style in ALLOWED_STYLES:
|
||||
value = self.style_state[style]
|
||||
if value and (value != StyledTextTagType.STYLE_DEFAULT[style]):
|
||||
self.apply_tag(self._find_tag_by_name(style, value),
|
||||
insert_start, iter)
|
||||
|
||||
def after_delete_range(self, textbuffer, start, end):
|
||||
_LOG.debug("Deleted from %d till %d" %
|
||||
(start.get_offset(), end.get_offset()))
|
||||
|
||||
# move 'insert' marker to have the format attributes updated
|
||||
self.move_mark(self._insert, start)
|
||||
|
||||
def do_changed(self):
|
||||
"""Parse for patterns in the text."""
|
||||
self.matches = []
|
||||
text = unicode(gtk.TextBuffer.get_text(self,
|
||||
self.get_start_iter(),
|
||||
self.get_end_iter()))
|
||||
for regex, flavor in self.patterns:
|
||||
iter = regex.finditer(text)
|
||||
while True:
|
||||
try:
|
||||
match = iter.next()
|
||||
self.matches.append((match.start(), match.end(),
|
||||
flavor, match.group()))
|
||||
_LOG.debug("Matches: %d, %d: %s [%d]" %
|
||||
(match.start(), match.end(),
|
||||
match.group(), flavor))
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
def do_mark_set(self, iter, mark):
|
||||
"""Update style state each time the cursor moves."""
|
||||
_LOG.debug("Setting mark %s at %d" %
|
||||
(mark.get_name(), iter.get_offset()))
|
||||
|
||||
if mark.get_name() != 'insert':
|
||||
return
|
||||
|
||||
if not iter.starts_line():
|
||||
iter.backward_char()
|
||||
|
||||
tag_names = [tag.get_property('name') for tag in iter.get_tags()]
|
||||
changed_styles = {}
|
||||
|
||||
for style in ALLOWED_STYLES:
|
||||
if StyledTextTagType.STYLE_TYPE[style] == bool:
|
||||
value = str(style) in tag_names
|
||||
else:
|
||||
value = StyledTextTagType.STYLE_DEFAULT[style]
|
||||
for tname in tag_names:
|
||||
if tname.startswith(str(style)):
|
||||
value = tname.split(' ', 1)[1]
|
||||
value = StyledTextTagType.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
|
||||
|
||||
##def get_tag_value_at_insert(self, name):
|
||||
##"""Get the value of the given tag at the insertion point."""
|
||||
##tags = self.get_iter_at_mark(self._insert).get_tags()
|
||||
|
||||
##if name in self.toggle_actions:
|
||||
##for tag in tags:
|
||||
##if tag.get_name() == name:
|
||||
##return True
|
||||
##return False
|
||||
##else:
|
||||
##for tag in tags:
|
||||
##if tag.get_name().startswith(name):
|
||||
##return tag.get_name().split()[1]
|
||||
##return None
|
||||
|
||||
def _get_selection(self):
|
||||
bounds = self.get_selection_bounds()
|
||||
if not bounds:
|
||||
iter = self.get_iter_at_mark(self._insert)
|
||||
if iter.inside_word():
|
||||
start_pos = iter.get_offset()
|
||||
iter.forward_word_end()
|
||||
word_end = iter.get_offset()
|
||||
iter.backward_word_start()
|
||||
word_start = iter.get_offset()
|
||||
iter.set_offset(start_pos)
|
||||
bounds = (self.get_iter_at_offset(word_start),
|
||||
self.get_iter_at_offset(word_end))
|
||||
else:
|
||||
bounds = (iter, self.get_iter_at_offset(iter.get_offset() + 1))
|
||||
return bounds
|
||||
|
||||
def _apply_tag_to_selection(self, tag):
|
||||
selection = self._get_selection()
|
||||
if selection:
|
||||
self.apply_tag(tag, *selection)
|
||||
|
||||
def _remove_tag_from_selection(self, tag):
|
||||
selection = self._get_selection()
|
||||
if selection:
|
||||
self.remove_tag(tag, *selection)
|
||||
|
||||
def _apply_style_to_selection(self, style, value):
|
||||
# FIXME can this be unified?
|
||||
if StyledTextTagType.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 StyledTextTagType.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 StyledTextTagType.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()
|
||||
tags = self._get_tag_from_range(start.get_offset(), end.get_offset())
|
||||
for tag_name in tags.keys():
|
||||
if tag_name.startswith(str(style)):
|
||||
for start, end in tags[tag_name]:
|
||||
self.remove_tag_by_name(tag_name,
|
||||
self.get_iter_at_offset(start),
|
||||
self.get_iter_at_offset(end+1))
|
||||
|
||||
def _get_tag_from_range(self, start=None, end=None):
|
||||
"""Extract gtk.TextTags from buffer.
|
||||
|
||||
Return only the name of the TextTag from the specified range.
|
||||
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 type: int
|
||||
@param end: an offset pointing to the end of the range of text
|
||||
@param type: int
|
||||
@return: tagdict
|
||||
@rtype: {TextTag_Name: [(start, end),]}
|
||||
|
||||
"""
|
||||
if start is None:
|
||||
start = 0
|
||||
if end is None:
|
||||
end = self.get_char_count()
|
||||
|
||||
tagdict = {}
|
||||
for pos in range(start, end):
|
||||
iter = self.get_iter_at_offset(pos)
|
||||
for tag in iter.get_tags():
|
||||
name = tag.get_property('name')
|
||||
if tagdict.has_key(name):
|
||||
if tagdict[name][-1][1] == pos - 1:
|
||||
tagdict[name][-1] = (tagdict[name][-1][0], pos)
|
||||
else:
|
||||
tagdict[name].append((pos, pos))
|
||||
else:
|
||||
tagdict[name]=[(pos, pos)]
|
||||
return tagdict
|
||||
|
||||
def _find_tag_by_name(self, style, value):
|
||||
"""Fetch TextTag from buffer's tag table by it's name.
|
||||
|
||||
If TextTag does not exist yet, it is created.
|
||||
|
||||
"""
|
||||
if StyledTextTagType.STYLE_TYPE[style] == bool:
|
||||
tag_name = str(style)
|
||||
elif StyledTextTagType.STYLE_TYPE[style] == str:
|
||||
tag_name = "%d %s" % (style, value)
|
||||
elif StyledTextTagType.STYLE_TYPE[style] == int:
|
||||
tag_name = "%d %d" % (style, value)
|
||||
else:
|
||||
raise ValueError("Unknown style (%s) value type: %s" %
|
||||
(style, value.__class__))
|
||||
|
||||
tag = self.get_tag_table().lookup(tag_name)
|
||||
|
||||
if not tag:
|
||||
if StyledTextTagType.STYLE_TYPE[style] != bool:
|
||||
# bool style tags are not created here, but in constuctor
|
||||
tag = self.create_tag(tag_name)
|
||||
tag.set_property(STYLE_TO_PROPERTY[style], value)
|
||||
else:
|
||||
return None
|
||||
return tag
|
||||
|
||||
# Public API
|
||||
|
||||
def set_text(self, s_text):
|
||||
"""Set the content of the buffer with markup tags.
|
||||
|
||||
@note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
|
||||
|
||||
"""
|
||||
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:
|
||||
for (start, end) in s_tag.ranges:
|
||||
start_iter = self.get_iter_at_offset(start)
|
||||
end_iter = self.get_iter_at_offset(end)
|
||||
self.apply_tag(g_tag, start_iter, end_iter)
|
||||
|
||||
def get_text(self, start=None, end=None, include_hidden_chars=True):
|
||||
"""Return the buffer text.
|
||||
|
||||
@note: 's_' prefix means StyledText*, while 'g_' prefix means gtk.*.
|
||||
|
||||
"""
|
||||
if start is None:
|
||||
start = self.get_start_iter()
|
||||
if end is None:
|
||||
end = self.get_end_iter()
|
||||
|
||||
txt = gtk.TextBuffer.get_text(self, start, end, include_hidden_chars)
|
||||
txt = unicode(txt)
|
||||
|
||||
# extract tags out of the buffer
|
||||
g_tags = self._get_tag_from_range()
|
||||
s_tags = []
|
||||
|
||||
for g_tagname, g_ranges in g_tags.items():
|
||||
style_and_value = g_tagname.split(' ', 1)
|
||||
|
||||
try:
|
||||
style = int(style_and_value[0])
|
||||
if len(style_and_value) == 1:
|
||||
s_value = None
|
||||
else:
|
||||
s_value = StyledTextTagType.STYLE_TYPE[style]\
|
||||
(style_and_value[1])
|
||||
|
||||
if style in ALLOWED_STYLES:
|
||||
s_ranges = [(start, end+1) for (start, end) in g_ranges]
|
||||
s_tag = StyledTextTag(style, s_value, s_ranges)
|
||||
|
||||
s_tags.append(s_tag)
|
||||
except ValueError:
|
||||
_LOG.debug("silently skipping gtk.TextTag '%s'" % g_tagname)
|
||||
|
||||
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, StyledTextTagType.STYLE_TYPE[style]):
|
||||
raise TypeError("Style (%d) value must be %s and not %s" %
|
||||
(style, StyledTextTagType.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):
|
||||
"""Add a pattern to look for in the text."""
|
||||
regex = re.compile(pattern)
|
||||
self.patterns.append((regex, flavor))
|
||||
|
||||
def match_check(self, pos):
|
||||
"""Check if pos falls into any of the matched patterns."""
|
||||
for match in self.matches:
|
||||
if pos >= match[MATCH_START] and pos <= match[MATCH_END]:
|
||||
return match
|
||||
|
||||
return None
|
||||
@@ -1,726 +0,0 @@
|
||||
#
|
||||
# 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$
|
||||
|
||||
"Text editor subclassed from gtk.TextView handling L{StyledText}."
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gettext import gettext as _
|
||||
|
||||
import logging
|
||||
_LOG = logging.getLogger(".Editors.StyledTextEditor")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK libraries
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gobject
|
||||
import gtk
|
||||
from pango import UNDERLINE_SINGLE
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gen.lib import StyledTextTagType
|
||||
from Editors._StyledTextBuffer import (StyledTextBuffer, ALLOWED_STYLES,
|
||||
MATCH_START, MATCH_END,
|
||||
MATCH_FLAVOR, MATCH_STRING)
|
||||
from Spell import Spell
|
||||
from GrampsDisplay import url as display_url
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
HAND_CURSOR = gtk.gdk.Cursor(gtk.gdk.HAND2)
|
||||
REGULAR_CURSOR = gtk.gdk.Cursor(gtk.gdk.XTERM)
|
||||
|
||||
FORMAT_TOOLBAR = '''
|
||||
<ui>
|
||||
<toolbar name="ToolBar">
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="%d"/>
|
||||
<toolitem action="spring"/>
|
||||
<toolitem action="clear"/>
|
||||
</toolbar>
|
||||
</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"
|
||||
PASSCHARS = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
|
||||
HOSTCHARS = "-A-Za-z0-9"
|
||||
PATHCHARS = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%"
|
||||
#SCHEME = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
|
||||
SCHEME = "(file:/|https?:|ftps?:|webcal:)"
|
||||
USER = "[" + USERCHARS + "]+(:[" + PASSCHARS + "]+)?"
|
||||
URLPATH = "/[" + PATHCHARS + "]*[^]'.}>) \t\r\n,\\\"]"
|
||||
|
||||
(GENURL, HTTP, MAIL) = range(3)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# StyledTextEditor
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class StyledTextEditor(gtk.TextView):
|
||||
"""StyledTextEditor is an enhanced gtk.TextView to edit L{StyledText}.
|
||||
|
||||
StyledTextEditor is a gui object for L{StyledTextBuffer}. It offers
|
||||
L{set_text} and L{get_text} convenience methods to set and get the
|
||||
buffer's text.
|
||||
|
||||
StyledTextEditor provides a formatting toolbar, which can be retrieved
|
||||
by the L{get_toolbar} method.
|
||||
|
||||
StyledTextEdit defines a new signal: 'match-changed', which is raised
|
||||
whenever the mouse cursor reaches or leaves a matched string in the text.
|
||||
The feature uses the regexp pattern mathing mechanism L{StyledTextBuffer}.
|
||||
The signal's default handler highlights the URL strings. URL's can be
|
||||
followed from the editor's popup menu.
|
||||
|
||||
@ivar last_match: previously matched string, used for generating the
|
||||
'match-changed' signal.
|
||||
@type last_match: tuple or None
|
||||
@ivar match: currently matched string, used for generating the
|
||||
'match-changed' signal.
|
||||
@type match: tuple or None
|
||||
@ivar show_unicode: stores the user's gtk.settings['gtk-show-unicode-menu']
|
||||
value.
|
||||
@type show_unicode: bool
|
||||
@ivar spellcheck: spell checker object created for the editor instance.
|
||||
@type spellcheck: L{Spell}
|
||||
@ivar textbuffer: text buffer assigned to the edit instance.
|
||||
@type textbuffer: L{StyledTextBuffer}
|
||||
@ivar toolbar: toolbar to be used for text formatting.
|
||||
@type toolbar: gtk.Toolbar
|
||||
@ivar url_match: stores the matched URL and other mathing parameters.
|
||||
@type url_match: tuple or None
|
||||
|
||||
"""
|
||||
__gtype_name__ = 'StyledTextEditor'
|
||||
|
||||
__gsignals__ = {
|
||||
'match-changed': (gobject.SIGNAL_RUN_FIRST,
|
||||
gobject.TYPE_NONE, #return value
|
||||
(gobject.TYPE_PYOBJECT,)), # arguments
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Setup initial instance variable values."""
|
||||
self.textbuffer = StyledTextBuffer()
|
||||
self.textbuffer.connect('style-changed', self._on_buffer_style_changed)
|
||||
gtk.TextView.__init__(self, self.textbuffer)
|
||||
|
||||
self.match = None
|
||||
self.last_match = None
|
||||
self._init_url_match()
|
||||
self.url_match = None
|
||||
|
||||
self.toolbar = self._create_toolbar()
|
||||
self.spellcheck = Spell(self)
|
||||
self._internal_style_change = False
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
# we want to disable the unicode menu in the popup
|
||||
settings = gtk.settings_get_default()
|
||||
self.show_unicode = settings.get_property('gtk-show-unicode-menu')
|
||||
settings.set_property('gtk-show-unicode-menu', False)
|
||||
|
||||
# virtual methods
|
||||
|
||||
def do_match_changed(self, match):
|
||||
"""Default signal handler.
|
||||
|
||||
URL highlighting.
|
||||
|
||||
@param match: the new match parameters
|
||||
@type match: tuple or None
|
||||
|
||||
@attention: Do not override the handler, but connect to the signal.
|
||||
|
||||
"""
|
||||
window = self.get_window(gtk.TEXT_WINDOW_TEXT)
|
||||
start, end = self.textbuffer.get_bounds()
|
||||
self.textbuffer.remove_tag_by_name('hyperlink', start, end)
|
||||
if match and (match[MATCH_FLAVOR] in (GENURL, HTTP, MAIL)):
|
||||
start_offset = match[MATCH_START]
|
||||
end_offset = match[MATCH_END]
|
||||
|
||||
start = self.textbuffer.get_iter_at_offset(start_offset)
|
||||
end = self.textbuffer.get_iter_at_offset(end_offset)
|
||||
|
||||
self.textbuffer.apply_tag_by_name('hyperlink', start, end)
|
||||
window.set_cursor(HAND_CURSOR)
|
||||
self.url_match = match
|
||||
else:
|
||||
window.set_cursor(REGULAR_CURSOR)
|
||||
self.url_match = None
|
||||
|
||||
def on_unrealize(self, widget):
|
||||
"""Signal handler.
|
||||
|
||||
Set the default Gtk settings back before leaving.
|
||||
|
||||
"""
|
||||
settings = gtk.settings_get_default()
|
||||
settings.set_property('gtk-show-unicode-menu', self.show_unicode)
|
||||
|
||||
def on_key_press_event(self, widget, event):
|
||||
"""Signal handler.
|
||||
|
||||
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.action_group.get_action(action_name)
|
||||
action.activate()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_insert_at_cursor(self, widget, string):
|
||||
"""Signal handler. for debugging only."""
|
||||
_LOG.debug("Textview insert '%s'" % string)
|
||||
|
||||
def on_delete_from_cursor(self, widget, mode, count):
|
||||
"""Signal handler. for debugging only."""
|
||||
_LOG.debug("Textview delete type %d count %d" % (mode, count))
|
||||
|
||||
def on_paste_clipboard(self, widget):
|
||||
"""Signal handler. for debugging only."""
|
||||
_LOG.debug("Textview paste clipboard")
|
||||
|
||||
def on_motion_notify_event(self, widget, event):
|
||||
"""Signal handler.
|
||||
|
||||
As the mouse cursor moves the handler checks if there's a new
|
||||
regexp match at the new location. If match changes the
|
||||
'match-changed' signal is raised.
|
||||
|
||||
"""
|
||||
x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
|
||||
int(event.x), int(event.y))
|
||||
iter_at_location = self.get_iter_at_location(x, y)
|
||||
self.match = self.textbuffer.match_check(iter_at_location.get_offset())
|
||||
|
||||
if self.match != self.last_match:
|
||||
self.emit('match-changed', self.match)
|
||||
|
||||
self.last_match = self.match
|
||||
|
||||
self.window.get_pointer()
|
||||
return False
|
||||
|
||||
def on_button_press_event(self, widget, event):
|
||||
"""Signal handler.
|
||||
|
||||
Handles the <CTRL> + Left click over a URL match.
|
||||
|
||||
"""
|
||||
if ((event.type == gtk.gdk.BUTTON_PRESS) and
|
||||
(event.button == 1) and
|
||||
(event.state and gtk.gdk.CONTROL_MASK) and
|
||||
(self.url_match)):
|
||||
|
||||
flavor = self.url_match[MATCH_FLAVOR]
|
||||
url = self.url_match[MATCH_STRING]
|
||||
self._open_url_cb(None, url, flavor)
|
||||
|
||||
return False
|
||||
|
||||
def on_populate_popup(self, widget, menu):
|
||||
"""Signal handler.
|
||||
|
||||
Insert extra menuitems:
|
||||
1. Insert language selector submenu for spell checking.
|
||||
2. Insert extra menus depending on ULR match result.
|
||||
|
||||
"""
|
||||
# spell checker submenu
|
||||
spell_menu = gtk.MenuItem(_('Spell'))
|
||||
spell_menu.set_submenu(self._create_spell_menu())
|
||||
spell_menu.show_all()
|
||||
menu.prepend(spell_menu)
|
||||
|
||||
# url menu items
|
||||
if self.url_match:
|
||||
flavor = self.url_match[MATCH_FLAVOR]
|
||||
url = self.url_match[MATCH_STRING]
|
||||
|
||||
if flavor == MAIL:
|
||||
open_menu = gtk.MenuItem(_('_Send Mail To...'))
|
||||
copy_menu = gtk.MenuItem(_('Copy _E-mail Address'))
|
||||
else:
|
||||
open_menu = gtk.MenuItem(_('_Open Link'))
|
||||
copy_menu = gtk.MenuItem(_('Copy _Link Address'))
|
||||
|
||||
copy_menu.connect('activate', self._copy_url_cb, url, flavor)
|
||||
copy_menu.show()
|
||||
menu.prepend(copy_menu)
|
||||
|
||||
open_menu.connect('activate', self._open_url_cb, url, flavor)
|
||||
open_menu.show()
|
||||
menu.prepend(open_menu)
|
||||
|
||||
# private methods
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect to several signals of the super class gtk.TextView."""
|
||||
self.connect('key-press-event', self.on_key_press_event)
|
||||
self.connect('insert-at-cursor', self.on_insert_at_cursor)
|
||||
self.connect('delete-from-cursor', self.on_delete_from_cursor)
|
||||
self.connect('paste-clipboard', self.on_paste_clipboard)
|
||||
self.connect('motion-notify-event', self.on_motion_notify_event)
|
||||
self.connect('button-press-event', self.on_button_press_event)
|
||||
self.connect('populate-popup', self.on_populate_popup)
|
||||
self.connect('unrealize', self.on_unrealize)
|
||||
|
||||
def _create_toolbar(self):
|
||||
"""Create a formatting toolbar.
|
||||
|
||||
@returns: toolbar containing text formatting toolitems.
|
||||
@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)
|
||||
spring = SpringSeparatorAction("spring", "", "", None)
|
||||
|
||||
# action accelerators
|
||||
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)
|
||||
self.action_group.add_action(spring)
|
||||
|
||||
# define the toolbar and create the proxies via ensure_update()
|
||||
uimanager = gtk.UIManager()
|
||||
uimanager.insert_action_group(self.action_group, 0)
|
||||
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)
|
||||
|
||||
return toolbar
|
||||
|
||||
def _init_url_match(self):
|
||||
"""Setup regexp matching for URL match."""
|
||||
self.textbuffer.create_tag('hyperlink',
|
||||
underline=UNDERLINE_SINGLE,
|
||||
foreground='blue')
|
||||
self.textbuffer.match_add("(www|ftp)[" + HOSTCHARS + "]*\\.[" +
|
||||
HOSTCHARS + ".]+" + "(:[0-9]+)?(" +
|
||||
URLPATH + ")?/?", HTTP)
|
||||
self.textbuffer.match_add("(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9]"
|
||||
"[a-z0-9-]*(\\.[a-z0-9][a-z0-9-]*)+", MAIL)
|
||||
self.textbuffer.match_add(SCHEME + "//(" + USER + "@)?[" +
|
||||
HOSTCHARS + ".]+" + "(:[0-9]+)?(" +
|
||||
URLPATH + ")?/?", GENURL)
|
||||
|
||||
def _create_spell_menu(self):
|
||||
"""Create a menu with all the installed languages.
|
||||
|
||||
It is called each time the popup menu is opened. Each language
|
||||
forms a radio menu item, and the selected language is set as active.
|
||||
|
||||
@returns: menu containing all the installed languages.
|
||||
@returntype: gtk.Menu
|
||||
|
||||
"""
|
||||
active_language = self.spellcheck.get_active_language()
|
||||
|
||||
menu = gtk.Menu()
|
||||
group = None
|
||||
for lang in self.spellcheck.get_all_languages():
|
||||
menuitem = gtk.RadioMenuItem(group, lang)
|
||||
menuitem.set_active(lang == active_language)
|
||||
menuitem.connect('activate', self._spell_change_cb, lang)
|
||||
menu.append(menuitem)
|
||||
|
||||
if group is None:
|
||||
group = menuitem
|
||||
|
||||
return menu
|
||||
|
||||
# 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 = 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)
|
||||
|
||||
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 (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)
|
||||
self._internal_style_change = False
|
||||
|
||||
def _spell_change_cb(self, menuitem, language):
|
||||
"""Set spell checker language according to user selection."""
|
||||
self.spellcheck.set_active_language(language)
|
||||
|
||||
def _open_url_cb(self, menuitem, url, flavor):
|
||||
"""Open the URL in a browser."""
|
||||
if not url:
|
||||
return
|
||||
|
||||
if flavor == HTTP:
|
||||
url = 'http:' + url
|
||||
elif flavor == MAIL:
|
||||
if not url.startswith('mailto:'):
|
||||
url = 'mailto:' + url
|
||||
elif flavor == GENURL:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
display_url(url)
|
||||
|
||||
def _copy_url_cb(self, menuitem, url, flavor):
|
||||
"""Copy url to both useful selections."""
|
||||
clipboard = gtk.Clipboard(selection="CLIPBOARD")
|
||||
clipboard.set_text(url)
|
||||
|
||||
clipboard = gtk.Clipboard(selection="PRIMARY")
|
||||
clipboard.set_text(url)
|
||||
|
||||
# public methods
|
||||
|
||||
def set_text(self, text):
|
||||
"""Set the text of the text buffer of the editor.
|
||||
|
||||
@param text: formatted text to edit in the view.
|
||||
@type text: L{StyledText}
|
||||
|
||||
"""
|
||||
self.textbuffer.set_text(text)
|
||||
self.textbuffer.place_cursor(self.textbuffer.get_start_iter())
|
||||
|
||||
def get_text(self):
|
||||
"""Get the text of the text buffer of the editor.
|
||||
|
||||
@returns: the formatted text from the editor.
|
||||
@returntype: L{StyledText}
|
||||
|
||||
"""
|
||||
return self.textbuffer.get_text()
|
||||
|
||||
def get_toolbar(self):
|
||||
"""Get the formatting toolbar of the editor.
|
||||
|
||||
@returns: toolbar widget to use as formatting GUI.
|
||||
@returntype: gtk.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)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# 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 = ""
|
||||
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
|
||||
Reference in New Issue
Block a user