Introducing StyledText in Notes.
svn: r10410
This commit is contained in:
@@ -28,7 +28,8 @@
|
||||
from gettext import gettext as _
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(".")
|
||||
_LOG = logging.getLogger(".Editors.EditNote")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK libraries
|
||||
@@ -41,19 +42,20 @@ import pango
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS classes
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import const
|
||||
import Spell
|
||||
import Config
|
||||
import GrampsDisplay
|
||||
import MarkupText
|
||||
from const import GLADE_FILE
|
||||
from Spell import Spell
|
||||
from GrampsDisplay import url
|
||||
from Editors._StyledTextBuffer import (StyledTextBuffer, MATCH_START,
|
||||
MATCH_END, MATCH_FLAVOR, MATCH_STRING)
|
||||
from Editors._EditPrimary import EditPrimary
|
||||
from DisplayTabs import GrampsTab, NoteBackRefList
|
||||
from GrampsWidgets import (MonitoredDataType, MonitoredCheckbox,
|
||||
MonitoredEntry, PrivacyButton)
|
||||
import gen.lib
|
||||
from gen.lib import Note
|
||||
from QuestionDialog import ErrorDialog
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
@@ -61,17 +63,16 @@ from QuestionDialog import ErrorDialog
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
#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,\\\"]"
|
||||
#
|
||||
#(GENERAL, HTTP, MAIL) = range(3)
|
||||
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,\\\"]"
|
||||
|
||||
(GENERAL, HTTP, MAIL) = range(3)
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
@@ -91,17 +92,17 @@ class NoteTab(GrampsTab):
|
||||
the database, along with other state information. The GrampsTab
|
||||
uses this to access the database and to pass to and created
|
||||
child windows (such as edit dialogs).
|
||||
@type dbstate: DbState
|
||||
@type dbstate: L{DbState.DbState}
|
||||
@param uistate: The UI state. Used primarily to pass to any created
|
||||
subwindows.
|
||||
@type uistate: DisplayState
|
||||
@type uistate: L{DisplayState.DisplayState}
|
||||
@param track: The window tracking mechanism used to manage windows.
|
||||
This is only used to pass to generted child windows.
|
||||
@type track: list
|
||||
@param name: Notebook label name
|
||||
@type name: str/unicode
|
||||
@param widget: widget to be shown in the tab
|
||||
@type widge: gtk widget
|
||||
@type widget: gtk widget
|
||||
"""
|
||||
GrampsTab.__init__(self, dbstate, uistate, track, name)
|
||||
eventbox = gtk.EventBox()
|
||||
@@ -131,11 +132,12 @@ class EditNote(EditPrimary):
|
||||
callertitle = None, extratype = None):
|
||||
"""Create an EditNote window. Associate a note with the window.
|
||||
|
||||
@param callertitle: a text passed by calling object to add to title
|
||||
@param callertitle: Text passed by calling object to add to title
|
||||
@type callertitle: str
|
||||
@param extratype: extra NoteType values to add to the default types
|
||||
They are removed from the ignorelist of NoteType.
|
||||
@param extratype: Extra L{NoteType} values to add to the default types.
|
||||
They are removed from the ignorelist of L{NoteType}.
|
||||
@type extratype: list of int
|
||||
|
||||
"""
|
||||
self.callertitle = callertitle
|
||||
self.extratype = extratype
|
||||
@@ -146,9 +148,10 @@ class EditNote(EditPrimary):
|
||||
def empty_object(self):
|
||||
"""Return an empty Note object for comparison for changes.
|
||||
|
||||
It is used by the base class (EditPrimary).
|
||||
It is used by the base class L{EditPrimary}.
|
||||
|
||||
"""
|
||||
empty_note = gen.lib.Note();
|
||||
empty_note = Note();
|
||||
if self.extratype:
|
||||
empty_note.set_type(self.extratype[0])
|
||||
return empty_note
|
||||
@@ -157,16 +160,16 @@ class EditNote(EditPrimary):
|
||||
if self.obj.get_handle():
|
||||
if self.callertitle :
|
||||
title = _('Note: %(id)s - %(context)s') % {
|
||||
'id' : self.obj.get_gramps_id(),
|
||||
'context' : self.callertitle
|
||||
}
|
||||
'id' : self.obj.get_gramps_id(),
|
||||
'context' : self.callertitle
|
||||
}
|
||||
else :
|
||||
title = _('Note: %s') % self.obj.get_gramps_id()
|
||||
else:
|
||||
if self.callertitle :
|
||||
title = _('New Note - %(context)s') % {
|
||||
'context' : self.callertitle
|
||||
}
|
||||
'context' : self.callertitle
|
||||
}
|
||||
else :
|
||||
title = _('New Note')
|
||||
|
||||
@@ -179,11 +182,11 @@ class EditNote(EditPrimary):
|
||||
"""Local initialization function.
|
||||
|
||||
Perform basic initialization, including setting up widgets
|
||||
and the glade interface. It is called by the base class (EditPrimary),
|
||||
and the glade interface. It is called by the base class L{EditPrimary},
|
||||
and overridden here.
|
||||
|
||||
"""
|
||||
self.top = glade.XML(const.GLADE_FILE, "edit_note", "gramps")
|
||||
self.top = glade.XML(GLADE_FILE, "edit_note", "gramps")
|
||||
win = self.top.get_widget("edit_note")
|
||||
self.set_window(win, None, self.get_menu_title())
|
||||
|
||||
@@ -242,7 +245,7 @@ class EditNote(EditPrimary):
|
||||
def _connect_signals(self):
|
||||
"""Connects any signals that need to be connected.
|
||||
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
Called by the init routine of the base class L{EditPrimary}.
|
||||
|
||||
"""
|
||||
self.define_ok_button(self.top.get_widget('ok'), self.save)
|
||||
@@ -250,122 +253,68 @@ class EditNote(EditPrimary):
|
||||
self.define_help_button(self.top.get_widget('help'), '')
|
||||
|
||||
def _create_tabbed_pages(self):
|
||||
"""
|
||||
Create the notebook tabs and inserts them into the main
|
||||
window.
|
||||
"""
|
||||
"""Create the notebook tabs and inserts them into the main window."""
|
||||
notebook = self.top.get_widget("note_notebook")
|
||||
|
||||
self._add_tab(notebook, self.ntab)
|
||||
|
||||
self.backref_tab = self._add_tab(
|
||||
notebook,
|
||||
NoteBackRefList(self.dbstate, self.uistate, self.track,
|
||||
self.dbstate.db.find_backlink_handles(
|
||||
self.obj.handle))
|
||||
)
|
||||
handles = self.dbstate.db.find_backlink_handles(self.obj.handle)
|
||||
rlist = NoteBackRefList(self.dbstate, self.uistate, self.track, handles)
|
||||
self.backref_tab = self._add_tab(notebook, rlist)
|
||||
|
||||
self._setup_notebook_tabs( notebook)
|
||||
self._setup_notebook_tabs(notebook)
|
||||
|
||||
# THIS IS THE MARKUP VERSION - enable for markup
|
||||
# def build_interface(self):
|
||||
# FORMAT_TOOLBAR = '''
|
||||
# <ui>
|
||||
# <toolbar name="ToolBar">
|
||||
# <toolitem action="italic"/>
|
||||
# <toolitem action="bold"/>
|
||||
# <toolitem action="underline"/>
|
||||
# <separator/>
|
||||
# <toolitem action="font"/>
|
||||
# <toolitem action="foreground"/>
|
||||
# <toolitem action="background"/>
|
||||
# <separator/>
|
||||
# <toolitem action="clear"/>
|
||||
# </toolbar>
|
||||
# </ui>
|
||||
# '''
|
||||
#
|
||||
# buffer_ = MarkupText.MarkupBuffer()
|
||||
# buffer_.create_tag('hyperlink',
|
||||
# underline=pango.UNDERLINE_SINGLE,
|
||||
# foreground='blue')
|
||||
# buffer_.match_add("(www|ftp)[" + HOSTCHARS + "]*\\.[" + HOSTCHARS +
|
||||
# ".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", HTTP)
|
||||
# buffer_.match_add("(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9][a-z0-9-]*"
|
||||
# "(\\.[a-z0-9][a-z0-9-]*)+", MAIL)
|
||||
# buffer_.match_add(SCHEME + "//(" + USER + "@)?[" + HOSTCHARS + ".]+" +
|
||||
# "(:[0-9]+)?(" + URLPATH + ")?/?", GENERAL)
|
||||
# self.match = None
|
||||
# self.last_match = None
|
||||
#
|
||||
# self.text = self.top.get_widget('text')
|
||||
# self.text.set_editable(not self.dbstate.db.readonly)
|
||||
# self.text.set_buffer(buffer_)
|
||||
# self.text.connect('key-press-event',
|
||||
# self.on_textview_key_press_event)
|
||||
# self.text.connect('insert-at-cursor',
|
||||
# self.on_textview_insert_at_cursor)
|
||||
# self.text.connect('delete-from-cursor',
|
||||
# self.on_textview_delete_from_cursor)
|
||||
# self.text.connect('paste-clipboard',
|
||||
# self.on_textview_paste_clipboard)
|
||||
# self.text.connect('motion-notify-event',
|
||||
# self.on_textview_motion_notify_event)
|
||||
# self.text.connect('button-press-event',
|
||||
# self.on_textview_button_press_event)
|
||||
# self.text.connect('populate-popup',
|
||||
# self.on_textview_populate_popup)
|
||||
#
|
||||
# # setup spell checking interface
|
||||
# spellcheck = Spell.Spell(self.text)
|
||||
# liststore = gtk.ListStore(gobject.TYPE_STRING)
|
||||
# cell = gtk.CellRendererText()
|
||||
# lang_selector = self.top.get_widget('spell')
|
||||
# lang_selector.set_model(liststore)
|
||||
# lang_selector.pack_start(cell, True)
|
||||
# lang_selector.add_attribute(cell, 'text', 0)
|
||||
# act_lang = spellcheck.get_active_language()
|
||||
# idx = 0
|
||||
# for lang in spellcheck.get_all_languages():
|
||||
# lang_selector.append_text(lang)
|
||||
# if lang == act_lang:
|
||||
# act_idx = idx
|
||||
# idx = idx + 1
|
||||
# lang_selector.set_active(act_idx)
|
||||
# lang_selector.connect('changed', self.on_spell_change, spellcheck)
|
||||
# #lang_selector.set_sensitive(Config.get(Config.SPELLCHECK))
|
||||
#
|
||||
# # create a formatting toolbar
|
||||
# if not self.dbstate.db.readonly:
|
||||
# uimanager = gtk.UIManager()
|
||||
# uimanager.insert_action_group(buffer_.format_action_group, 0)
|
||||
# uimanager.add_ui_from_string(FORMAT_TOOLBAR)
|
||||
# uimanager.ensure_update()
|
||||
#
|
||||
# toolbar = uimanager.get_widget('/ToolBar')
|
||||
# toolbar.set_style(gtk.TOOLBAR_ICONS)
|
||||
# vbox = self.top.get_widget('container')
|
||||
# vbox.pack_start(toolbar)
|
||||
#
|
||||
# # setup initial values for textview and buffer_
|
||||
# if self.obj:
|
||||
# self.empty = False
|
||||
# self.flow_changed(self.obj.get_format())
|
||||
# buffer_.set_text(self.obj.get(markup=True))
|
||||
# log.debug("Initial Note: %s" % buffer_.get_text())
|
||||
# else:
|
||||
# self.empty = True
|
||||
|
||||
# NON-MARKUP VERSION - Disable for markup
|
||||
def build_interface(self):
|
||||
buffer_ = gtk.TextBuffer()
|
||||
FORMAT_TOOLBAR = '''
|
||||
<ui>
|
||||
<toolbar name="ToolBar">
|
||||
<toolitem action="italic"/>
|
||||
<toolitem action="bold"/>
|
||||
<toolitem action="underline"/>
|
||||
<separator/>
|
||||
<toolitem action="font"/>
|
||||
<toolitem action="foreground"/>
|
||||
<toolitem action="background"/>
|
||||
<separator/>
|
||||
<toolitem action="clear"/>
|
||||
</toolbar>
|
||||
</ui>
|
||||
'''
|
||||
|
||||
textbuffer = StyledTextBuffer()
|
||||
textbuffer.create_tag('hyperlink',
|
||||
underline=pango.UNDERLINE_SINGLE,
|
||||
foreground='blue')
|
||||
textbuffer.match_add("(www|ftp)[" + HOSTCHARS + "]*\\.[" + HOSTCHARS +
|
||||
".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", HTTP)
|
||||
textbuffer.match_add("(mailto:)?[a-z0-9][a-z0-9.-]*@[a-z0-9][a-z0-9-]*"
|
||||
"(\\.[a-z0-9][a-z0-9-]*)+", MAIL)
|
||||
textbuffer.match_add(SCHEME + "//(" + USER + "@)?[" + HOSTCHARS +
|
||||
".]+" + "(:[0-9]+)?(" + URLPATH + ")?/?", GENERAL)
|
||||
self.match = None
|
||||
self.last_match = None
|
||||
|
||||
self.text = self.top.get_widget('text')
|
||||
self.text.set_editable(not self.dbstate.db.readonly)
|
||||
self.text.set_buffer(buffer_)
|
||||
self.text.set_buffer(textbuffer)
|
||||
self.text.connect('key-press-event',
|
||||
self.on_textview_key_press_event)
|
||||
self.text.connect('insert-at-cursor',
|
||||
self.on_textview_insert_at_cursor)
|
||||
self.text.connect('delete-from-cursor',
|
||||
self.on_textview_delete_from_cursor)
|
||||
self.text.connect('paste-clipboard',
|
||||
self.on_textview_paste_clipboard)
|
||||
self.text.connect('motion-notify-event',
|
||||
self.on_textview_motion_notify_event)
|
||||
self.text.connect('button-press-event',
|
||||
self.on_textview_button_press_event)
|
||||
self.text.connect('populate-popup',
|
||||
self.on_textview_populate_popup)
|
||||
|
||||
# setup spell checking interface
|
||||
spellcheck = Spell.Spell(self.text)
|
||||
spellcheck = Spell(self.text)
|
||||
liststore = gtk.ListStore(gobject.TYPE_STRING)
|
||||
cell = gtk.CellRendererText()
|
||||
lang_selector = self.top.get_widget('spell')
|
||||
@@ -382,14 +331,62 @@ class EditNote(EditPrimary):
|
||||
lang_selector.set_active(act_idx)
|
||||
lang_selector.connect('changed', self.on_spell_change, spellcheck)
|
||||
#lang_selector.set_sensitive(Config.get(Config.SPELLCHECK))
|
||||
|
||||
# create a formatting toolbar
|
||||
if not self.dbstate.db.readonly:
|
||||
uimanager = gtk.UIManager()
|
||||
uimanager.insert_action_group(textbuffer.format_action_group, 0)
|
||||
uimanager.add_ui_from_string(FORMAT_TOOLBAR)
|
||||
uimanager.ensure_update()
|
||||
|
||||
toolbar = uimanager.get_widget('/ToolBar')
|
||||
toolbar.set_style(gtk.TOOLBAR_ICONS)
|
||||
vbox = self.top.get_widget('container')
|
||||
vbox.pack_start(toolbar)
|
||||
|
||||
# setup initial values for textview and buffer_
|
||||
# setup initial values for textview and textbuffer
|
||||
if self.obj:
|
||||
self.empty = False
|
||||
self.flow_changed(self.obj.get_format())
|
||||
buffer_.set_text(self.obj.get())
|
||||
textbuffer.set_text(self.obj.get_styledtext())
|
||||
_LOG.debug("Initial Note: %s" % str(textbuffer.get_text()))
|
||||
else:
|
||||
self.empty = True
|
||||
|
||||
# NON-MARKUP VERSION - Disable for markup
|
||||
#def build_interface(self):
|
||||
#textbuffer = gtk.TextBuffer()
|
||||
|
||||
#self.text = self.top.get_widget('text')
|
||||
#self.text.set_editable(not self.dbstate.db.readonly)
|
||||
#self.text.set_buffer(textbuffer)
|
||||
|
||||
## setup spell checking interface
|
||||
#spellcheck = Spell(self.text)
|
||||
#liststore = gtk.ListStore(gobject.TYPE_STRING)
|
||||
#cell = gtk.CellRendererText()
|
||||
#lang_selector = self.top.get_widget('spell')
|
||||
#lang_selector.set_model(liststore)
|
||||
#lang_selector.pack_start(cell, True)
|
||||
#lang_selector.add_attribute(cell, 'text', 0)
|
||||
#act_lang = spellcheck.get_active_language()
|
||||
#idx = 0
|
||||
#for lang in spellcheck.get_all_languages():
|
||||
#lang_selector.append_text(lang)
|
||||
#if lang == act_lang:
|
||||
#act_idx = idx
|
||||
#idx = idx + 1
|
||||
#lang_selector.set_active(act_idx)
|
||||
#lang_selector.connect('changed', self.on_spell_change, spellcheck)
|
||||
##lang_selector.set_sensitive(Config.get(Config.SPELLCHECK))
|
||||
|
||||
## setup initial values for textview and textbuffer
|
||||
#if self.obj:
|
||||
#self.empty = False
|
||||
#self.flow_changed(self.obj.get_format())
|
||||
#textbuffer.set_text(self.obj.get())
|
||||
#else:
|
||||
#self.empty = True
|
||||
|
||||
def build_menu_names(self, person):
|
||||
"""
|
||||
@@ -401,119 +398,116 @@ class EditNote(EditPrimary):
|
||||
def _post_init(self):
|
||||
self.text.grab_focus()
|
||||
|
||||
# enable for markup
|
||||
# def on_textview_key_press_event(self, textview, event):
|
||||
# """Handle shortcuts in the TextView."""
|
||||
# return textview.get_buffer().on_key_press_event(textview, event)
|
||||
#
|
||||
# def on_textview_insert_at_cursor(self, textview, string):
|
||||
# log.debug("Textview insert '%s'" % string)
|
||||
#
|
||||
# def on_textview_delete_from_cursor(self, textview, type, count):
|
||||
# log.debug("Textview delete type %d count %d" % (type, count))
|
||||
#
|
||||
# def on_textview_paste_clipboard(self, textview):
|
||||
# log.debug("Textview paste clipboard")
|
||||
#
|
||||
# def on_textview_motion_notify_event(self, textview, event):
|
||||
# window = textview.get_window(gtk.TEXT_WINDOW_TEXT)
|
||||
# x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
|
||||
# int(event.x), int(event.y))
|
||||
# iter = textview.get_iter_at_location(x, y)
|
||||
# buffer_ = textview.get_buffer()
|
||||
# self.match = buffer_.match_check(iter.get_offset())
|
||||
#
|
||||
# if self.match != self.last_match:
|
||||
# start, end = buffer_.get_bounds()
|
||||
# buffer_.remove_tag_by_name('hyperlink', start, end)
|
||||
# if self.match:
|
||||
# start_offset = self.match[MarkupText.MATCH_START]
|
||||
# end_offset = self.match[MarkupText.MATCH_END]
|
||||
#
|
||||
# start = buffer_.get_iter_at_offset(start_offset)
|
||||
# end = buffer_.get_iter_at_offset(end_offset)
|
||||
#
|
||||
# buffer_.apply_tag_by_name('hyperlink', start, end)
|
||||
# window.set_cursor(self.hand_cursor)
|
||||
# else:
|
||||
# window.set_cursor(self.regular_cursor)
|
||||
#
|
||||
# self.last_match = self.match
|
||||
#
|
||||
# textview.window.get_pointer()
|
||||
# return False
|
||||
#
|
||||
# def on_textview_button_press_event(self, textview, event):
|
||||
# if ((event.type == gtk.gdk.BUTTON_PRESS) and
|
||||
# (event.button == 1) and
|
||||
# (event.state and gtk.gdk.CONTROL_MASK) and
|
||||
# (self.match)):
|
||||
#
|
||||
# flavor = self.match[MarkupText.MATCH_FLAVOR]
|
||||
# url = self.match[MarkupText.MATCH_STRING]
|
||||
# self.open_url_cb(None, url, flavor)
|
||||
#
|
||||
# return False
|
||||
#
|
||||
# def on_textview_populate_popup(self, textview, menu):
|
||||
# """Insert extra menuitems according to matched pattern."""
|
||||
# if self.match:
|
||||
# flavor = self.match[MarkupText.MATCH_FLAVOR]
|
||||
# url = self.match[MarkupText.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)
|
||||
def on_textview_key_press_event(self, textview, event):
|
||||
"""Handle shortcuts in the TextView."""
|
||||
return textview.get_buffer().on_key_press_event(textview, event)
|
||||
|
||||
def on_textview_insert_at_cursor(self, textview, string):
|
||||
_LOG.debug("Textview insert '%s'" % string)
|
||||
|
||||
def on_textview_delete_from_cursor(self, textview, type, count):
|
||||
_LOG.debug("Textview delete type %d count %d" % (type, count))
|
||||
|
||||
def on_textview_paste_clipboard(self, textview):
|
||||
_LOG.debug("Textview paste clipboard")
|
||||
|
||||
def on_textview_motion_notify_event(self, textview, event):
|
||||
window = textview.get_window(gtk.TEXT_WINDOW_TEXT)
|
||||
x, y = textview.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
|
||||
int(event.x), int(event.y))
|
||||
iter = textview.get_iter_at_location(x, y)
|
||||
textbuffer = textview.get_buffer()
|
||||
self.match = textbuffer.match_check(iter.get_offset())
|
||||
|
||||
if self.match != self.last_match:
|
||||
start, end = textbuffer.get_bounds()
|
||||
textbuffer.remove_tag_by_name('hyperlink', start, end)
|
||||
if self.match:
|
||||
start_offset = self.match[MATCH_START]
|
||||
end_offset = self.match[MATCH_END]
|
||||
|
||||
start = textbuffer.get_iter_at_offset(start_offset)
|
||||
end = textbuffer.get_iter_at_offset(end_offset)
|
||||
|
||||
textbuffer.apply_tag_by_name('hyperlink', start, end)
|
||||
window.set_cursor(self.hand_cursor)
|
||||
else:
|
||||
window.set_cursor(self.regular_cursor)
|
||||
|
||||
self.last_match = self.match
|
||||
|
||||
textview.window.get_pointer()
|
||||
return False
|
||||
|
||||
def on_textview_button_press_event(self, textview, event):
|
||||
if ((event.type == gtk.gdk.BUTTON_PRESS) and
|
||||
(event.button == 1) and
|
||||
(event.state and gtk.gdk.CONTROL_MASK) and
|
||||
(self.match)):
|
||||
|
||||
flavor = self.match[MATCH_FLAVOR]
|
||||
url = self.match[MATCH_STRING]
|
||||
self.open_url_cb(None, url, flavor)
|
||||
|
||||
return False
|
||||
|
||||
def on_textview_populate_popup(self, textview, menu):
|
||||
"""Insert extra menuitems according to matched pattern."""
|
||||
if self.match:
|
||||
flavor = self.match[MATCH_FLAVOR]
|
||||
url = self.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)
|
||||
|
||||
def on_spell_change(self, combobox, spell):
|
||||
"""Set spell checker language according to user selection."""
|
||||
lang = combobox.get_active_text()
|
||||
spell.set_active_language(lang)
|
||||
|
||||
# enable for markup
|
||||
# def open_url_cb(self, menuitem, url, flavor):
|
||||
# if not url:
|
||||
# return
|
||||
#
|
||||
# if flavor == HTTP:
|
||||
# url = 'http:' + url
|
||||
# elif flavor == MAIL:
|
||||
# if not url.startswith('mailto:'):
|
||||
# url = 'mailto:' + url
|
||||
# elif flavor == GENERAL:
|
||||
# pass
|
||||
# else:
|
||||
# return
|
||||
#
|
||||
# GrampsDisplay.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)
|
||||
def open_url_cb(self, menuitem, url, flavor):
|
||||
if not url:
|
||||
return
|
||||
|
||||
if flavor == HTTP:
|
||||
url = 'http:' + url
|
||||
elif flavor == MAIL:
|
||||
if not url.startswith('mailto:'):
|
||||
url = 'mailto:' + url
|
||||
elif flavor == GENERAL:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
def update_note(self):
|
||||
"""Update the Note object with current value."""
|
||||
if self.obj:
|
||||
buffer_ = self.text.get_buffer()
|
||||
(start, stop) = buffer_.get_bounds()
|
||||
text = buffer_.get_text(start, stop)
|
||||
self.obj.set(text)
|
||||
log.debug(text)
|
||||
textbuffer = self.text.get_buffer()
|
||||
text = textbuffer.get_text()
|
||||
self.obj.set_styledtext(text)
|
||||
_LOG.debug(str(text))
|
||||
|
||||
def flow_changed(self, active):
|
||||
if active:
|
||||
|
||||
653
src/Editors/_StyledTextBuffer.py
Normal file
653
src/Editors/_StyledTextBuffer.py
Normal file
@@ -0,0 +1,653 @@
|
||||
#
|
||||
# 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(".StyledTextBuffer")
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gtk
|
||||
from pango import WEIGHT_BOLD, STYLE_ITALIC, UNDERLINE_SINGLE
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gen.lib import (StyledText, StyledTextTag, StyledTextTagType)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
(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 get/set the text use the L{get_text} and
|
||||
L{set_text} methods.
|
||||
|
||||
It provides an action group (L{format_action_group}) for GUIs.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
__gtype_name__ = 'StyledTextBuffer'
|
||||
|
||||
formats = ('italic', 'bold', 'underline',
|
||||
'font', 'foreground', 'background',)
|
||||
|
||||
def __init__(self):
|
||||
gtk.TextBuffer.__init__(self)
|
||||
|
||||
# Create fix tags.
|
||||
# Other tags (e.g. color) have to be created on the fly
|
||||
self.create_tag('bold', weight=WEIGHT_BOLD)
|
||||
self.create_tag('italic', style=STYLE_ITALIC)
|
||||
self.create_tag('underline', underline=UNDERLINE_SINGLE)
|
||||
|
||||
# Setup action group used from user interface
|
||||
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'), 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
|
||||
## 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 format actions are activated (self.*_action_activate)
|
||||
self.italic = False
|
||||
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
|
||||
self._internal_toggle = False
|
||||
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 format in self.__class__.formats:
|
||||
value = getattr(self, format)
|
||||
if value:
|
||||
if format in self.toggle_actions:
|
||||
value = None
|
||||
|
||||
self.apply_tag(self._find_tag_by_name(format, 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 format attributes 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()]
|
||||
for format in self.__class__.formats:
|
||||
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)
|
||||
|
||||
# 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):
|
||||
##"""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 _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):
|
||||
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 remove_format_from_selection(self, format):
|
||||
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(format):
|
||||
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.
|
||||
|
||||
@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, name, value):
|
||||
"""Fetch TextTag from buffer's tag table by it's name.
|
||||
|
||||
If TextTag does not exist yet, it is created.
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
tag_name = name
|
||||
else:
|
||||
tag_name = "%s %s" % (name, value)
|
||||
tag = self.get_tag_table().lookup(tag_name)
|
||||
if not tag:
|
||||
if value is not None:
|
||||
tag = self.create_tag(tag_name)
|
||||
tag.set_property(name, value)
|
||||
else:
|
||||
return None
|
||||
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 = gtk.FontSelectionDialog(_("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
|
||||
|
||||
def set_text(self, r_text):
|
||||
"""Set the content of the buffer with markup tags."""
|
||||
gtk.TextBuffer.set_text(self, str(r_text))
|
||||
|
||||
r_tags = r_text.get_tags()
|
||||
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)
|
||||
if g_tag is not None:
|
||||
for (start, end) in r_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."""
|
||||
if not start:
|
||||
start = self.get_start_iter()
|
||||
if not end:
|
||||
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()
|
||||
r_tags = []
|
||||
|
||||
for g_tagname, g_ranges in g_tags.items():
|
||||
name_value = g_tagname.split(' ', 1)
|
||||
|
||||
if len(name_value) == 1:
|
||||
name = name_value[0]
|
||||
r_value = None
|
||||
else:
|
||||
(name, r_value) = name_value
|
||||
|
||||
if name in self.formats:
|
||||
r_tagtype = self._tagname_to_tagtype(name)
|
||||
r_ranges = [(start, end+1) for (start, end) in g_ranges]
|
||||
r_tag = StyledTextTag(r_tagtype, r_value, r_ranges)
|
||||
|
||||
r_tags.append(r_tag)
|
||||
|
||||
return StyledText(txt, r_tags)
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user