diff --git a/src/Editors/_EditNote.py b/src/Editors/_EditNote.py index ee932d7b0..6e7edc146 100644 --- a/src/Editors/_EditNote.py +++ b/src/Editors/_EditNote.py @@ -47,10 +47,7 @@ import pango #------------------------------------------------------------------------- import Config 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._StyledTextEditor import StyledTextEditor from Editors._EditPrimary import EditPrimary from DisplayTabs import GrampsTab, NoteBackRefList from GrampsWidgets import (MonitoredDataType, MonitoredCheckbox, @@ -58,23 +55,6 @@ from GrampsWidgets import (MonitoredDataType, MonitoredCheckbox, from gen.lib import Note 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) - - #------------------------------------------------------------------------- # # NoteTab @@ -125,9 +105,6 @@ class NoteTab(GrampsTab): #------------------------------------------------------------------------- class EditNote(EditPrimary): - hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) - regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM) - def __init__(self, dbstate, uistate, track, note, callback=None, callertitle = None, extratype = None): """Create an EditNote window. Associate a note with the window. @@ -194,10 +171,6 @@ class EditNote(EditPrimary): height = Config.get(Config.NOTE_HEIGHT) self.window.set_default_size(width, height) - settings = gtk.settings_get_default() - self.show_unicode = settings.get_property('gtk-show-unicode-menu') - settings.set_property('gtk-show-unicode-menu', False) - vboxnote = self.top.get_widget('vbox131') notebook = self.top.get_widget('note_notebook') #recreate start page as GrampsTab @@ -266,90 +239,21 @@ class EditNote(EditPrimary): # THIS IS THE MARKUP VERSION - enable for markup def build_interface(self): - FORMAT_TOOLBAR = ''' - - - - - - - - - - - - - - ''' - - 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(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(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)) + self.texteditor = self.top.get_widget('texteditor') + self.texteditor.set_editable(not self.dbstate.db.readonly) # 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) + vbox.pack_start(self.texteditor.get_toolbar(), + expand=False, fill=False) # 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_styledtext()) - _LOG.debug("Initial Note: %s" % str(textbuffer.get_text())) + self.texteditor.set_text(self.obj.get_styledtext()) + _LOG.debug("Initial Note: %s" % str(self.texteditor.get_text())) else: self.empty = True @@ -396,116 +300,12 @@ class EditNote(EditPrimary): return (_('Edit Note'), self.get_menu_title()) def _post_init(self): - self.text.grab_focus() + self.texteditor.grab_focus() - 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) - - 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: - textbuffer = self.text.get_buffer() - text = textbuffer.get_text() + text = self.texteditor.get_text() self.obj.set_styledtext(text) _LOG.debug(str(text)) @@ -513,11 +313,11 @@ class EditNote(EditPrimary): if active: # Set the text style to monospace self.text.set_wrap_mode(gtk.WRAP_NONE) - self.text.modify_font(pango.FontDescription("monospace")) + self.texteditor.modify_font(pango.FontDescription("monospace")) else: # Set the text style to normal - self.text.set_wrap_mode(gtk.WRAP_WORD) - self.text.modify_font(pango.FontDescription("normal")) + self.texteditor.set_wrap_mode(gtk.WRAP_WORD) + self.texteditor.modify_font(pango.FontDescription("normal")) def save(self, *obj): """Save the data.""" @@ -567,10 +367,6 @@ class EditNote(EditPrimary): Config.set(Config.NOTE_HEIGHT, height) Config.sync() - settings = gtk.settings_get_default() - settings.set_property('gtk-show-unicode-menu', self.show_unicode) - - class DeleteNoteQuery: def __init__(self, dbstate, uistate, note, the_lists): self.note = note diff --git a/src/Editors/_StyledTextEditor.py b/src/Editors/_StyledTextEditor.py new file mode 100644 index 000000000..8dc749a65 --- /dev/null +++ b/src/Editors/_StyledTextEditor.py @@ -0,0 +1,323 @@ +# +# 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 +# +#------------------------------------------------------------------------- +import logging +_LOG = logging.getLogger(".Editors.StyledTextEditor") + +#------------------------------------------------------------------------- +# +# GTK libraries +# +#------------------------------------------------------------------------- +import gobject +import gtk +from pango import UNDERLINE_SINGLE + +#------------------------------------------------------------------------- +# +# GRAMPS modules +# +#------------------------------------------------------------------------- +from Editors._StyledTextBuffer import (StyledTextBuffer, 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 = ''' + + + + + + + + + + + + + +''' + +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): + """ + """ + __gtype_name__ = 'StyledTextEditor' + + __gsignals__ = { + 'match-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, #return value + (gobject.TYPE_PYOBJECT,)), # arguments + } + + def __init__(self): + """ + """ + self.textbuffer = StyledTextBuffer() + gtk.TextView.__init__(self, self.textbuffer) + + self.match = None + self.last_match = None + + self._connect_signals() + self._create_toolbar() + self._init_url_match() + + self.spellcheck = Spell(self) + + # 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. + + @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 callback. + + 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): + """Handle shortcuts in the TextView.""" + return self.get_buffer().on_key_press_event(self, event) + + def on_insert_at_cursor(self, widget, string): + _LOG.debug("Textview insert '%s'" % string) + + def on_delete_from_cursor(self, widget, type, count): + _LOG.debug("Textview delete type %d count %d" % (type, count)) + + def on_paste_clipboard(self, widget): + _LOG.debug("Textview paste clipboard") + + def on_motion_notify_event(self, widget, event): + x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET, + int(event.x), int(event.y)) + iter = self.get_iter_at_location(x, y) + self.match = self.textbuffer.match_check(iter.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): + 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): + """Insert extra menuitems. + + 1. Insert language selector submenu for spell checking. + + 2. Insert extra menus depending on ULR match result. + + """ + spell_menu = gtk.MenuItem(_('Spell')) + spell_menu.set_submenu(self._create_spell_menu()) + spell_menu.show_all() + menu.prepend(spell_menu) + + 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) + + def on_spell_change(self, menuitem, language): + """Set spell checker language according to user selection.""" + self.spellcheck.set_active_language(language) + + # private methods + + def _connect_signals(self): + 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): + uimanager = gtk.UIManager() + uimanager.insert_action_group(self.textbuffer.format_action_group, 0) + uimanager.add_ui_from_string(FORMAT_TOOLBAR) + uimanager.ensure_update() + + self.toolbar = uimanager.get_widget('/ToolBar') + self.toolbar.set_style(gtk.TOOLBAR_ICONS) + + def _init_url_match(self): + """ + """ + 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) + + self.url_match = None + + def _create_spell_menu(self): + """ + """ + 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.on_spell_change, lang) + menu.append(menuitem) + + if group is None: + group = menuitem + + return menu + + 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 == 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): + self.textbuffer.set_text(text) + + def get_text(self): + return self.textbuffer.get_text() + + def get_toolbar(self): + return self.toolbar \ No newline at end of file diff --git a/src/GrampsWidgets.py b/src/GrampsWidgets.py index f67f459f3..ac8b65af0 100644 --- a/src/GrampsWidgets.py +++ b/src/GrampsWidgets.py @@ -77,8 +77,11 @@ except: # Enabling custom widgets to be included in Glade def get_custom_handler(glade, function_name, widget_name, str1, str2, int1, int2): + from Editors._StyledTextEditor import StyledTextEditor if function_name == 'ValidatableMaskedEntry': return ValidatableMaskedEntry() + if function_name == 'StyledTextEditor': + return StyledTextEditor() gtk.glade.set_custom_handler(get_custom_handler) diff --git a/src/glade/gramps.glade b/src/glade/gramps.glade index d092553db..f8fbaf1ae 100644 --- a/src/glade/gramps.glade +++ b/src/glade/gramps.glade @@ -15407,40 +15407,19 @@ Very High 0 - + 6 True False 6 - + True - True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN - GTK_CORNER_TOP_LEFT - - - - True - True - True - False - True - GTK_JUSTIFY_LEFT - GTK_WRAP_NONE - True - 0 - 0 - 0 - 0 - 0 - 0 - - - + StyledTextEditor + 0 + 0 + Fri, 04 Apr 2008 11:46:59 GMT 0 @@ -15449,76 +15428,6 @@ Very High GTK_PACK_END - - - - True - False - 6 - - - - - - - - True - False - 6 - - - - True - _Spelling: - True - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - spell - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - False - False - - - - - - True - False - True - - - 0 - True - True - - - - - 0 - False - True - GTK_PACK_END - - - - - 0 - False - True - - 0