Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sourceview_editor.py
diff options
context:
space:
mode:
authorolpc user <olpc@localhost.localdomain>2010-01-30 13:13:29 (GMT)
committer olpc user <olpc@localhost.localdomain>2010-01-30 13:13:29 (GMT)
commitf8a54f5c2f4487f264fe4647cec62599db93d063 (patch)
tree8758b1412c524129dad7e90a4da6b463a85ed96f /sourceview_editor.py
initial save 1-30-2010 bangladesh
Diffstat (limited to 'sourceview_editor.py')
-rw-r--r--sourceview_editor.py636
1 files changed, 636 insertions, 0 deletions
diff --git a/sourceview_editor.py b/sourceview_editor.py
new file mode 100644
index 0000000..0ecd004
--- /dev/null
+++ b/sourceview_editor.py
@@ -0,0 +1,636 @@
+# Copyright 2008 Paul Swartz
+#
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk, gobject
+import pango
+import notebook
+import gtksourceview2
+import os.path
+import sys
+import re
+import mimetypes
+from exceptions import *
+import hashlib
+from gettext import gettext as _
+
+# Initialize logging.
+import logging
+from sugar import logger
+#Get the standard logging directory.
+std_log_dir = logger.get_logs_dir()
+_logger = logging.getLogger('PyDebug')
+
+_logger.setLevel(logging.DEBUG)
+
+class S_WHERE:
+ selection, file, multifile = range(3) #an enum
+
+class GtkSourceview2Editor(notebook.Notebook):
+ __gsignals__ = {
+ 'changed': (gobject.SIGNAL_RUN_FIRST, None, [])
+ }
+
+ def __init__(self, activity):
+ notebook.Notebook.__init__(self, can_close_tabs=True)
+ self._can_close_tabs = True #redundant, but above call broken for some reason
+ self.activity = activity
+ self.set_size_request(900, 350)
+ self.connect('page-removed', self._page_removed_cb)
+ self.connect('switch-page', self._switch_page_cb)
+
+ def _page_removed_cb(self, notebook, page, n):
+ _logger.debug('removing page %d'%n)
+ if self.interactive_close:
+ self.interactive_close = False
+ page.save(interactive_close=True)
+ page.save()
+
+ def _switch_page_cb(self, notebook, page_gptr, page_num):
+ pass
+ #self.activity.update_sidebar_to_page(self.get_nth_page(page_num))
+
+ def set_to_page_like(self,eq_to_page):
+ for n in range(self.get_n_pages()):
+ page = self.get_nth_page(n)
+ if page == eq_to_page:
+ self.set_current_page(n)
+ return True
+ return False
+
+ def load_object(self, fullPath, filename):
+ if self.set_to_page_like(fullPath):
+ return
+ page = GtkSourceview2Page(fullPath, self.activity)
+ label = filename
+ page.text_buffer.connect('changed', self._changed_cb)
+ self.add_page(label, page)
+ #label object is passed back in Notebook object -- remember it
+ page.label = self.tab_label
+ _logger.debug('new label text: %s'%page.label.get_text())
+ self.set_current_page(-1)
+ self._changed_cb(page.text_buffer)
+
+ def position_to(self, fullPath, line = 0, col = 0):
+ self.load_object(fullPath, os.path.basename(fullPath))
+ page = self._get_page()
+ page._scroll_to_line(line)
+
+
+ def _changed_cb(self, buffer):
+ if not buffer.can_undo():
+ buffer.set_modified(False)
+ self.clear_changed_star()
+ elif not self.activity.dirty:
+ self.activity.set_dirty(True)
+ self.emit('changed')
+ if buffer.can_undo():
+ self.set_changed_star()
+
+ def _get_page(self):
+ n = self.get_current_page()
+ return self.get_nth_page(n)
+
+ def get_full_path(self):
+ page = self._get_page()
+ return page.fullPath
+
+ def set_changed_star(self, button = None):
+ page = self._get_page()
+ if page:
+ current = page.label.get_text()
+ if current.startswith('*'):return
+ page.label.set_text('*' + current)
+
+ def clear_changed_star(self, button = None):
+ page = self._get_page()
+ if page:
+ current = os.path.basename(page.fullPath)
+ page.label.set_text(current)
+
+ def set_focus(self):
+ page = self._get_page()
+ if page:
+ page.text_view.grab_focus()
+
+
+ def can_undo_redo(self):
+ page = self._get_page()
+ if page is None:
+ return (False, False)
+ else:
+ return page.can_undo_redo()
+
+ def undo(self, button = None):
+ page = self._get_page()
+ if page:
+ page.undo()
+
+ def redo(self, button = None):
+ page = self._get_page()
+ if page:
+ page.redo()
+
+ def copy(self, button = None):
+ page = self._get_page()
+ if page:
+ page.copy()
+
+ def cut(self, button = None):
+ page = self._get_page()
+ if page:
+ page.cut()
+
+ def paste(self, button = None):
+ page = self._get_page()
+ if page:
+ page.paste()
+
+ def replace(self, ftext, rtext, s_opts):
+ replaced = False
+ if s_opts.use_regex and issubclass(type(ftext),basestring):
+ ftext = re.compile(ftext)
+ multifile = (s_opts.where == S_WHERE.multifile)
+ if multifile and s_opts.replace_all:
+ for n in range(self.get_n_pages()):
+ page = self.get_nth_page(n)
+ replaced = page.replace(ftext, rtext,
+ s_opts) or replaced
+ return (replaced, False) #not found-again
+
+ page = self._get_page()
+ if page:
+ selection = s_opts.where == S_WHERE.selection
+ replaced = page.replace(ftext, rtext, s_opts)
+ if s_opts.replace_all:
+ return (replaced, False)
+ elif not selection:
+ found = self.find_next(ftext,s_opts,page)
+ return (replaced, found)
+ else:
+ #for replace-in-selection, leave selection unmodified
+ return (replaced, replaced)
+
+ def find_next(self, ftext, s_opts, page=None):
+ if not page:
+ page = self._get_page()
+ if page:
+ if s_opts.use_regex and issubclass(type(ftext),basestring):
+ ftext = re.compile(ftext)
+ if page.find_next(ftext,s_opts,
+ wrap=(s_opts.where != S_WHERE.multifile)):
+ return True
+ else:
+ if (s_opts.where == S_WHERE.multifile):
+ current_page = self.get_current_page()
+ n_pages = self.get_n_pages()
+ for i in range(1,n_pages):
+ page = self.get_nth_page((current_page + i) % n_pages)
+ if isinstance(page,SearchablePage):
+ if page.find_next(ftext,s_opts,
+ wrap = True):
+ self.set_current_page((current_page + i) %
+ n_pages)
+ return True
+ return False
+ else:
+ return False #first file failed, not multifile
+ else:
+ return False #no open pages
+
+ def get_all_filenames(self):
+ for i in range(self.get_n_pages()):
+ page = self.get_nth_page(i)
+ if isinstance(page,GtkSourceview2Page):
+ yield page.fullPath
+
+ def save_all(self):
+ _logger.info('save all %i Editor pages' % self.get_n_pages())
+ #if self.activity.is_foreign_dir():
+ #_logger.info('save all aborting, still viewing in place')
+ #return
+ for i in range(self.get_n_pages()):
+ page = self.get_nth_page(i)
+ if isinstance(page,GtkSourceview2Page):
+ _logger.debug('%s' % page.fullPath)
+ page.save()
+
+ def remove_all(self):
+ for i in range(self.get_n_pages(),0,-1):
+ self.remove_page(i-1)
+ """
+ page = self.get_nth_page(i)
+ if isinstance(page,GtkSourceview2Page):
+ self._close_page(None,page
+ """
+ def reroot(self,olddir, newdir):
+ _logger.info('reroot from %s to %s' % (olddir,newdir))
+ for i in range(self.get_n_pages()):
+ page = self.get_nth_page(i)
+ if isinstance(page,GtkSourceview2Page):
+ if page.reroot(olddir, newdir):
+ _logger.info('rerooting page %s failed' %
+ page.fullPath)
+ else:
+ _logger.info('rerooting page %s succeeded' %
+ page.fullPath)
+
+ def get_selected(self):
+ return self._get_page().get_selected()
+
+ def change_font_size(self,size):
+ page = self._get_page()
+ page.set_font_size(size)
+
+class SearchablePage(gtk.ScrolledWindow):
+ def get_selected(self):
+ try:
+ start,end = self.text_buffer.get_selection_bounds()
+ return self.text_buffer.get_slice(start,end)
+ except ValueError:
+ return 0
+
+ def get_text(self):
+ """
+ Return the text that's currently being edited.
+ """
+ start, end = self.text_buffer.get_bounds()
+ return self.text_buffer.get_text(start, end)
+
+ def get_offset(self):
+ """
+ Return the current character position in the currnet file.
+ """
+ insert = self.text_buffer.get_insert()
+ _iter = self.text_buffer.get_iter_at_mark(insert)
+ return _iter.get_offset()
+
+ def copy(self):
+ """
+ Copy the currently selected text to the clipboard.
+ """
+ self.text_buffer.copy_clipboard(gtk.Clipboard())
+
+ def paste(self):
+ """
+ Cut the currently selected text the clipboard into the current file.
+ """
+ self.text_buffer.paste_clipboard(gtk.Clipboard(), None, True)
+
+ def cut(self):
+ """
+ Paste from the clipboard.
+ """
+ self.text_buffer.cut_clipboard(gtk.Clipboard(), True)
+
+ def _getMatches(self,buffertext,fpat,s_opts,offset):
+ if s_opts.use_regex:
+ while True:
+ match = fpat.search(buffertext,re.I if s_opts.ignore_caps else 0)
+ if match:
+ start,end = match.span()
+ yield (start+offset,end+offset,match)
+ else:
+ return
+ buffertext, offset = buffertext[end:],offset+end
+ else:
+ while True:
+ if s_opts.ignore_caps:
+ #possible optimization: turn fpat into a regex by escaping,
+ #then use re.i
+ buffertext = buffertext.lower()
+ fpat = fpat.lower()
+ match = buffertext.find(fpat)
+ if match >= 0:
+ end = match+len(fpat)
+ yield (offset + match, offset + end, None)
+ else:
+ return
+ buffertext, offset = buffertext[end:], offset + end
+
+ def _match(self, pattern, text, s_opts):
+ if s_opts.use_regex:
+ return pattern.match(text,re.I if s_opts.ignore_caps else 0)
+ else:
+ if s_opts.ignore_caps:
+ pattern = pattern.lower()
+ text = text.lower()
+ return pattern == text
+
+ def _find_in(self, text, fpat, offset, s_opts, offset_add = 0):
+ if s_opts.forward:
+ matches = self._getMatches(text[offset:],fpat,s_opts,
+ offset+offset_add)
+ try:
+ return matches.next()
+ except StopIteration:
+ return ()
+ else:
+ if offset != 0:
+ text = text[:offset]
+ matches = list(self._getMatches(text,fpat,s_opts,
+ offset_add))
+ if matches:
+ return matches[-1]
+ else:
+ return ()
+
+ def find_next(self, ftext, s_opts, wrap=True):
+ """
+ Scroll to the next place where the string text appears.
+ If stay is True and text is found at the current position, stay where we are.
+ """
+ if s_opts.where == S_WHERE.selection:
+ try:
+ selstart, selend = self.text_buffer.get_selection_bounds()
+ except (ValueError,TypeError):
+ return False
+ offsetadd = selstart.get_offset()
+ buffertext = self.text_buffer.get_slice(selstart,selend)
+ print buffertext
+ try:
+ start, end, match = self._find_in(buffertext, ftext, 0,
+ s_opts, offsetadd)
+ except (ValueError,TypeError):
+ return False
+ else:
+ offset = self.get_offset() + (not s_opts.stay) #add 1 if not stay.
+ text = self.get_text()
+ try:
+ start,end,match = self._find_in(text, ftext, offset,
+ s_opts, 0)
+ except (ValueError,TypeError):
+ #find failed.
+ if wrap:
+ try:
+ start,end,match = self._find_in(text, ftext, 0,
+ s_opts, 0)
+ except (ValueError,TypeError):
+ return False
+ else:
+ return False
+ self._scroll_to_offset(start,end)
+ self.text_view.grab_focus()
+ return True
+
+ def _scroll_to_offset(self, offset, bound):
+ _iter = self.text_buffer.get_iter_at_offset(offset)
+ _iter2 = self.text_buffer.get_iter_at_offset(bound)
+ self.text_buffer.select_range(_iter,_iter2)
+ self.text_view.scroll_mark_onscreen(self.text_buffer.get_insert())
+
+ def _scroll_to_line(self,line):
+ _iter = self.text_buffer.get_iter_at_line(line)
+ self.text_buffer.select_range(_iter,_iter)
+ self.text_view.scroll_mark_onscreen(self.text_buffer.get_insert())
+
+
+ def __eq__(self,other):
+ if isinstance(other,GtkSourceview2Page):
+ return self.fullPath == other.fullPath
+ #elif isinstance(other,type(self.fullPath)):
+ # other = other.metadata['source']
+ if isinstance(other,basestring):
+ return other == self.fullPath
+ else:
+ return False
+
+class GtkSourceview2Page(SearchablePage):
+
+ def __init__(self, fullPath, activity):
+ """
+ Do any initialization here.
+ """
+ gtk.ScrolledWindow.__init__(self)
+
+ self.fullPath = fullPath
+ self.activity = activity
+
+ self.text_buffer = gtksourceview2.Buffer()
+ self.text_view = gtksourceview2.View(self.text_buffer)
+
+ self.text_view.set_size_request(900, 350)
+ self.text_view.set_editable(True)
+ self.text_view.set_cursor_visible(True)
+ self.text_view.set_highlight_current_line(True)
+ self.text_view.set_show_line_numbers(True)
+ self.text_view.set_insert_spaces_instead_of_tabs(True)
+ if hasattr(self.text_view, 'set_tabs_width'):
+ self.text_view.set_tabs_width(4)
+ else:
+ self.text_view.set_tab_width(4)
+ self.text_view.set_auto_indent(True)
+
+ self.text_view.set_wrap_mode(gtk.WRAP_CHAR)
+ #self.text_view.modify_font(pango.FontDescription("Monospace 6.5"))
+ self.set_font_size(self.activity.font_size)
+
+ # We could change the color theme here, if we want to.
+ mgr = gtksourceview2.StyleSchemeManager()
+ mgr.prepend_search_path(self.activity.pydebug_path)
+ _logger.debug('search path for gtksourceview is %r'%mgr.get_search_path())
+ style_scheme = mgr.get_scheme('vibrant')
+ self.text_buffer.set_style_scheme(style_scheme)
+
+ self.set_policy(gtk.POLICY_AUTOMATIC,
+ gtk.POLICY_AUTOMATIC)
+ self.add(self.text_view)
+ self.text_view.show()
+ self.load_text()
+ self.show()
+
+ def set_font_size(self,font_size=None):
+ if font_size == None: font_size = self.activity.font_size
+ self.text_view.modify_font(pango.FontDescription("Monospace %d"%font_size))
+ _logger.debug('setting font size to %d'%font_size)
+
+ def load_text(self, offset=None):
+ """
+ Load the text, and optionally scroll to the given offset in the file.
+ """
+ self.text_buffer.begin_not_undoable_action()
+ _file = file(self.fullPath)
+ self.text_buffer.set_text(_file.read())
+ _file.close()
+ self.save_hash()
+ if offset is not None:
+ self._scroll_to_offset(offset)
+
+ if hasattr(self.text_buffer, 'set_highlight'):
+ self.text_buffer.set_highlight(False)
+ else:
+ self.text_buffer.set_highlight_syntax(False)
+ mime_type = mimetypes.guess_type(self.fullPath)[0]
+ if mime_type:
+ lang_manager = gtksourceview2.language_manager_get_default()
+ if hasattr(lang_manager, 'list_languages'):
+ langs = lang_manager.list_languages()
+ else:
+ lang_ids = lang_manager.get_language_ids()
+ langs = [lang_manager.get_language(i) for i in lang_ids]
+ for lang in langs:
+ for m in lang.get_mime_types():
+ if m == mime_type:
+ self.text_buffer.set_language(lang)
+ if hasattr(self.text_buffer, 'set_highlight'):
+ self.text_buffer.set_highlight(True)
+ else:
+ self.text_buffer.set_highlight_syntax(True)
+ self.text_buffer.end_not_undoable_action()
+ self.text_buffer.set_modified(False)
+ self.text_view.grab_focus()
+
+ def save_hash(self):
+ self.md5sum = self.activity.md5sum(self.fullPath)
+
+ def remove(self):
+ self.save()
+
+ def save(self,skip_md5 = False, interactive_close=False):
+ if not self.text_buffer.can_undo() or self.activity.abandon_changes:
+ if not self.text_buffer.can_undo():
+ _logger.debug('no changes for %s'%os.path.basename(self.fullPath))
+ return #only save if there's something to save
+ if not skip_md5:
+ hash = self.activity.md5sum(self.fullPath)
+ if self.md5sum != hash: #underlying file has changed
+ _logger.warning('md5sum stored:%s. Calculated:%s'%(self.md5sum,hash))
+ _logger.warning('md5sum changed outside editor for %s. Save file questioned'
+ %os.path.basename(self.fullPath))
+ self.activity.confirmation_alert(_('Would you like to overwrite the changes in %s?'%os.path.basename(self.fullPath)),
+ _('The Underlying File Has Been Changed By Another Application'),
+ self.continue_save)
+ return
+ if self.interactive_close and self.text_buffer.can_undo():
+ self.activity.confirmation_alert(_('Would you to Save the file, or cancel the Save?',
+ _('This File Has Been Changed'),self.continue_save))
+
+ self.continue_save(None)
+
+ def continue_save(self, alert, response = None):
+ if not self.fullPath.startswith(self.activity.debugger_home):
+ self.activity.alert(_('Can only save in locations under %s'%self.activity.debugger_home))
+ _logger.debug('failed to save self.fullPath: %s, Checked for starting with %s'%(self.fullPath, self.activity.debugger_home))
+ return
+ _logger.debug('saving %s'%os.path.basename(self.fullPath))
+ text = self.get_text()
+ _file = file(self.fullPath, 'w')
+ try:
+ _file.write(text)
+ _file.close()
+ self.save_hash()
+ self.label.set_text(os.path.basename(self.fullPath))
+ self.text_buffer.set_modified(False)
+ msg = _("File saved: %s md5sumn:%s"%(os.path.basename(self.fullPath),self.md5sum))
+ _logger.debug(msg)
+ except IOError as (errno, strerror):
+ msg = _("I/O error({0}): {1}".format(errno, strerror))
+ self.activity.alert(msg)
+ except:
+ msg = "Unexpected error:", sys.exc_info()[0]
+ self.activity.alert(msg)
+ if _file:
+ _file.close()
+
+ def underlying_change_cb(self,response):
+ #remove the alert from the screen, since either a response button
+ #was clicked or there was a timeout
+ self.remove_alert(alert)
+
+ #Do any work that is specific to the type of button clicked.
+ if response_id is gtk.RESPONSE_OK:
+ self.continue_save()
+ elif response_id is gtk.RESPONSE_CANCEL:
+ return
+
+ def can_undo_redo(self):
+ """
+ Returns a two-tuple (can_undo, can_redo) with Booleans of those abilities.
+ """
+ return (self.text_buffer.can_undo(), self.text_buffer.can_redo())
+
+ def undo(self):
+ """
+ Undo the last change in the file. If we can't do anything, ignore.
+ """
+ self.text_buffer.undo()
+
+ def redo(self):
+ """
+ Redo the last change in the file. If we can't do anything, ignore.
+ """
+ self.text_buffer.redo()
+
+ def replace(self, ftext, rtext, s_opts):
+ """returns true if replaced (succeeded)"""
+ selection = s_opts.where == S_WHERE.selection
+ if s_opts.replace_all or selection:
+ result = False
+ if selection:
+ try:
+ selstart, selend = self.text_buffer.get_selection_bounds()
+ except (ValueError,TypeError):
+ return False
+ offsetadd = selstart.get_offset()
+ buffertext = self.text_buffer.get_slice(selstart,selend)
+ else:
+ offsetadd = 0
+ buffertext = self.get_text()
+ results = list(self._getMatches(buffertext,ftext,
+ s_opts,offsetadd))
+ if not s_opts.replace_all:
+ results = [results[0]]
+ else:
+ results.reverse() #replace right-to-left so that
+ #unreplaced indexes remain valid.
+ self.text_buffer.begin_user_action()
+ for start, end, match in results:
+ start = self.text_buffer.get_iter_at_offset(start)
+ end = self.text_buffer.get_iter_at_offset(end)
+ self.text_buffer.delete(start,end)
+ self.text_buffer.insert(start, self.makereplace(rtext,match,s_opts.use_regex))
+ result = True
+ self.text_buffer.end_user_action()
+ return result
+ else: #replace, the &find part handled by caller
+ try:
+ start,end = self.text_buffer.get_selection_bounds()
+ except TypeError:
+ return False
+ match = self._match(ftext,
+ self.text_buffer.get_slice(start,end),
+ s_opts)
+ if match:
+ self.text_buffer.delete(start, end)
+ rtext = self.makereplace(rtext,match,s_opts.use_regex)
+ self.text_buffer.insert(start, rtext)
+ return True
+ else:
+ return False
+
+ def makereplace(self, rpat, match, use_regex):
+ if use_regex:
+ return match.expand(rpat)
+ else:
+ return rpat
+
+ def reroot(self,olddir,newdir):
+ """Returns False if it works"""
+ oldpath = self.fullPath
+ if oldpath.startswith(olddir):
+ self.fullPath = os.path.join(newdir, oldpath[len(olddir):])
+ return False
+ else:
+ return True