root/branches/mbutscher/work/lib/pwiki/WikiTxtCtrl.py @ 296

Revision 296, 233.3 kB (checked in by mbutscher, 22 months ago)

branches/mbutscher/work:
* vi improvements (4) (Ross' repSVN.: fb42caf36c727c3d226c27e7171148fe86d4a2d1:5c1d3d71349722711e7b6348fa84182e68c28fd6)

Line 
1from __future__ import with_statement
2## import hotshot
3## _prof = hotshot.Profile("hotshot.prf")
4
5import traceback, codecs
6from cStringIO import StringIO
7import string, itertools, contextlib
8import re # import pwiki.srePersistent as re
9import threading
10
11import subprocess
12import string
13
14from os.path import exists, dirname, isfile, join, basename
15from os import rename, unlink
16
17from time import time, sleep
18
19import wx, wx.stc
20
21from Consts import FormatTypes
22
23#from Utilities import *  # TODO Remove this
24from .Utilities import DUMBTHREADSTOP, callInMainThread, ThreadHolder, \
25        calcResizeArIntoBoundingBox
26
27from .wxHelper import GUI_ID, getTextFromClipboard, copyTextToClipboard, \
28        wxKeyFunctionSink, getAccelPairFromKeyDown, appendToMenuByMenuDesc
29from . import wxHelper
30
31from . import OsAbstract
32
33from .WikiExceptions import *
34
35from .SystemInfo import isUnicode, isOSX, isLinux, isWindows
36
37from .ParseUtilities import getFootnoteAnchorDict
38
39from .EnhancedScintillaControl import StyleCollector
40
41from .SearchableScintillaControl import SearchableScintillaControl
42
43
44
45from . import Configuration
46from . import AdditionalDialogs
47from . import WikiTxtDialogs
48
49# image stuff
50import imghdr
51
52
53# import WikiFormatting
54from . import DocPages
55from . import UserActionCoord, WindowLayout
56
57from .SearchAndReplace import SearchReplaceOperation
58from . import StringOps
59from . import SpellChecker
60
61# from StringOps import *  # TODO Remove this
62# mbcsDec, uniToGui, guiToUni, \
63#        wikiWordToLabel, revStr, lineendToInternal, lineendToOs
64
65
66from ViHelper import ViHintDialog, ViHelper
67from collections import defaultdict
68
69try:
70    import WindowsHacks
71except:
72    if isWindows():
73        traceback.print_exc()
74    WindowsHacks = None
75
76
77# Python compiler flag for float division
78CO_FUTURE_DIVISION = 0x2000
79
80
81
82def bytelenSct_utf8(us):
83    """
84    us -- unicode string
85    returns: Number of bytes us requires in Scintilla (with UTF-8 encoding=Unicode)
86    """
87    return len(StringOps.utf8Enc(us)[0])
88
89
90def bytelenSct_mbcs(us):
91    """
92    us -- unicode string
93    returns: Number of bytes us requires in Scintilla (with mbcs encoding=Ansi)
94    """
95    return len(StringOps.mbcsEnc(us)[0])
96
97
98
99# etEVT_STYLE_DONE_COMMAND = wx.NewEventType()
100# EVT_STYLE_DONE_COMMAND = wx.PyEventBinder(etEVT_STYLE_DONE_COMMAND, 0)
101#
102# class StyleDoneEvent(wx.PyCommandEvent):
103#     """
104#     This wx Event is fired when style and folding calculations are finished.
105#     It is needed to savely transfer data from the style thread to the main thread.
106#     """
107#     def __init__(self, stylebytes, foldingseq):
108#         wx.PyCommandEvent.__init__(self, etEVT_STYLE_DONE_COMMAND, -1)
109#         self.stylebytes = stylebytes
110# #         self.pageAst = pageAst
111#         self.foldingseq = foldingseq
112
113
114
115class WikiTxtCtrl(SearchableScintillaControl):
116    NUMBER_MARGIN = 0
117    FOLD_MARGIN = 2
118    SELECT_MARGIN = 1
119
120    # Not the best of all possible solutions
121    SUGGESTION_CMD_IDS = [GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_0,
122            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_1,
123            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_2,
124            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_3,
125            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_4,
126            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_5,
127            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_6,
128            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_7,
129            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_8,
130            GUI_ID.CMD_REPLACE_THIS_SPELLING_WITH_SUGGESTION_9]
131
132    def __init__(self, presenter, parent, ID):
133        SearchableScintillaControl.__init__(self, presenter,
134                presenter.getMainControl(), parent, ID)
135        self.evalScope = None
136        self.stylingThreadHolder = ThreadHolder()
137        self.calltipThreadHolder = ThreadHolder()
138        self.clearStylingCache()
139        self.pageType = "normal"   # The pagetype controls some special editor behaviour
140#         self.idleCounter = 0       # Used to reduce idle load
141#         self.loadedDocPage = None
142        self.lastFont = None
143        self.ignoreOnChange = False
144        self.dwellLockCounter = 0  # Don't process dwell start messages if >0
145        self.wikiLanguageHelper = None
146        self.templateIdRecycler = wxHelper.IdRecycler()
147        self.vi = None  # Contains ViHandler instance if vi key handling enabled
148
149        # If autocompletion word was choosen, how many bytes to delete backward
150        # before inserting word
151        self.autoCompBackBytesMap = {} # Maps selected word to number of backbytes
152
153        # Inline image
154        self.tooltip_image = None
155
156        # configurable editor settings
157        config = self.presenter.getConfig()
158        self.setWrapMode(config.getboolean("main", "wrap_mode"))
159        self.SetIndentationGuides(config.getboolean("main", "indentation_guides"))
160        self.autoIndent = config.getboolean("main", "auto_indent")
161        self.autoBullets = config.getboolean("main", "auto_bullets")
162        self.setShowLineNumbers(config.getboolean("main", "show_lineNumbers"))
163        self.foldingActive = config.getboolean("main", "editor_useFolding")
164        self.tabsToSpaces = config.getboolean("main", "editor_tabsToSpaces")
165
166        # editor settings
167        self.applyBasicSciSettings()
168
169        self.defaultFont = config.get("main", "font",
170                self.presenter.getDefaultFontFaces()["mono"])
171
172        self.CallTipSetForeground(wx.Colour(0, 0, 0))
173
174        shorthintDelay = self.presenter.getConfig().getint("main",
175                "editor_shortHint_delay", 500)
176        self.SetMouseDwellTime(shorthintDelay)
177
178        # Popup menu must be created by Python code to replace clipboard functions
179        # for unicode build on Win 98/ME
180        self.UsePopUp(0)
181
182        self.SetMarginMask(self.FOLD_MARGIN, wx.stc.STC_MASK_FOLDERS)
183        self.SetMarginMask(self.NUMBER_MARGIN, 0)
184        self.SetMarginMask(self.SELECT_MARGIN, 0)
185
186        if self.foldingActive:
187            self.SetMarginWidth(self.FOLD_MARGIN, 16)
188        else:
189            self.SetMarginWidth(self.FOLD_MARGIN, 0)
190        self.SetMarginWidth(self.SELECT_MARGIN, 16)
191        self.SetMarginWidth(self.NUMBER_MARGIN, 0)
192
193        self.SetMarginType(self.FOLD_MARGIN, wx.stc.STC_MARGIN_SYMBOL)
194        self.SetMarginType(self.SELECT_MARGIN, wx.stc.STC_MARGIN_SYMBOL)
195        self.SetMarginType(self.NUMBER_MARGIN, wx.stc.STC_MARGIN_NUMBER)
196
197        # Optical details
198        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDER, wx.stc.STC_MARK_PLUS)
199        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEROPEN, wx.stc.STC_MARK_MINUS)
200        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEREND, wx.stc.STC_MARK_EMPTY)
201        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERMIDTAIL, wx.stc.STC_MARK_EMPTY)
202        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDEROPENMID, wx.stc.STC_MARK_EMPTY)
203        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERSUB, wx.stc.STC_MARK_EMPTY)
204        self.MarkerDefine(wx.stc.STC_MARKNUM_FOLDERTAIL, wx.stc.STC_MARK_EMPTY)
205        self.SetFoldFlags(16)
206
207        self.SetMarginSensitive(self.FOLD_MARGIN, True)
208        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" %
209                self.presenter.getDefaultFontFaces())
210
211#         self.setFoldingActive(self.foldingActive)
212
213        for i in xrange(32):
214            self.StyleSetEOLFilled(i, True)
215
216        # i plan on lexing myself
217        self.SetLexer(wx.stc.STC_LEX_CONTAINER)
218
219
220        # make the text control a drop target for files and text
221        self.SetDropTarget(WikiTxtCtrlDropTarget(self))
222
223#         self.CmdKeyClearAll()
224#
225#         # register some keyboard commands
226#         self.CmdKeyAssign(ord('+'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMIN)
227#         self.CmdKeyAssign(ord('-'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMOUT)
228#         self.CmdKeyAssign(wx.stc.STC_KEY_HOME, 0, wx.stc.STC_CMD_HOMEWRAP)
229#         self.CmdKeyAssign(wx.stc.STC_KEY_END, 0, wx.stc.STC_CMD_LINEENDWRAP)
230#         self.CmdKeyAssign(wx.stc.STC_KEY_HOME, wx.stc.STC_SCMOD_SHIFT,
231#                 wx.stc.STC_CMD_HOMEWRAPEXTEND)
232#         self.CmdKeyAssign(wx.stc.STC_KEY_END, wx.stc.STC_SCMOD_SHIFT,
233#                 wx.stc.STC_CMD_LINEENDWRAPEXTEND)
234#
235#
236#         # Clear all key mappings for clipboard operations
237#         # PersonalWikiFrame handles them and calls the special clipboard functions
238#         # instead of the normal ones
239#         self.CmdKeyClear(wx.stc.STC_KEY_INSERT, wx.stc.STC_SCMOD_CTRL)
240#         self.CmdKeyClear(wx.stc.STC_KEY_INSERT, wx.stc.STC_SCMOD_SHIFT)
241#         self.CmdKeyClear(wx.stc.STC_KEY_DELETE, wx.stc.STC_SCMOD_SHIFT)
242#
243#         self.CmdKeyClear(ord('X'), wx.stc.STC_SCMOD_CTRL)
244#         self.CmdKeyClear(ord('C'), wx.stc.STC_SCMOD_CTRL)
245#         self.CmdKeyClear(ord('V'), wx.stc.STC_SCMOD_CTRL)
246
247        self.SetModEventMask(
248                wx.stc.STC_MOD_INSERTTEXT | wx.stc.STC_MOD_DELETETEXT)
249
250        # set the autocomplete separator
251        self.AutoCompSetSeparator(1)   # ord('~')
252        self.AutoCompSetTypeSeparator(2)   # ord('?')
253
254        # register some event handlers
255        self.presenterListener = wxKeyFunctionSink((
256                ("saving all pages", self.onSavingAllPages),
257                ("closing current wiki", self.onClosingCurrentWiki),
258                ("dropping current wiki", self.onDroppingCurrentWiki),
259                ("reloaded current doc page", self.onReloadedCurrentPage)
260        ), self.presenter.getMiscEvent())
261
262        self.__sinkApp = wxKeyFunctionSink((
263                ("options changed", self.onOptionsChanged),
264        ), wx.GetApp().getMiscEvent(), self)
265
266        self.__sinkGlobalConfig = wxKeyFunctionSink((
267                ("changed configuration", self.onChangedConfiguration),
268        ), wx.GetApp().getGlobalConfig().getMiscEvent(), self)
269
270#         if not self.presenter.getMainControl().isMainWindowConstructed():
271#             # Install event handler to wait for construction
272#             self.__sinkMainFrame = wxKeyFunctionSink((
273#                     ("constructed main window", self.onConstructedMainWindow),
274#             ), self.presenter.getMainControl().getMiscEvent(), self)
275#         else:
276#             self.onConstructedMainWindow(None)
277
278        self.__sinkMainFrame = wxKeyFunctionSink((
279                ("idle visible", self.onIdleVisible),
280        ), self.presenter.getMainControl().getMiscEvent(), self)
281
282
283#         self.presenter.getMiscEvent().addListener(self.presenterListener)
284
285
286        self.wikiPageSink = wxKeyFunctionSink((
287                ("updated wiki page", self.onWikiPageUpdated),   # fired by a WikiPage
288                ("modified spell checker session", self.OnStyleNeeded),  # ???
289                ("changed read only flag", self.onPageChangedReadOnlyFlag)
290        ))
291
292
293        wx.stc.EVT_STC_STYLENEEDED(self, ID, self.OnStyleNeeded)
294        wx.stc.EVT_STC_CHARADDED(self, ID, self.OnCharAdded)
295        wx.stc.EVT_STC_MODIFIED(self, ID, self.OnModified)
296        wx.stc.EVT_STC_USERLISTSELECTION(self, ID, self.OnUserListSelection)
297        wx.stc.EVT_STC_MARGINCLICK(self, ID, self.OnMarginClick)
298        wx.stc.EVT_STC_DWELLSTART(self, ID, self.OnDwellStart)
299        wx.stc.EVT_STC_DWELLEND(self, ID, self.OnDwellEnd)
300
301        wx.EVT_LEFT_DOWN(self, self.OnClick)
302        wx.EVT_MIDDLE_DOWN(self, self.OnMiddleDown)
303        wx.EVT_LEFT_DCLICK(self, self.OnDoubleClick)
304
305        if config.getboolean("main", "editor_useImeWorkaround", False):
306            wx.EVT_CHAR(self, self.OnChar_ImeWorkaround)
307
308        wx.EVT_SET_FOCUS(self, self.OnSetFocus)
309
310        wx.EVT_CONTEXT_MENU(self, self.OnContextMenu)
311
312#         self.incSearchCharStartPos = 0
313        self.incSearchPreviousHiddenLines = None
314        self.incSearchPreviousHiddenStartLine = -1
315
316        self.onlineSpellCheckerActive = SpellChecker.isSpellCheckSupported() and \
317                self.presenter.getConfig().getboolean(
318                "main", "editor_onlineSpellChecker_active", False)
319
320        self.optionColorizeSearchFragments = self.presenter.getConfig()\
321                .getboolean("main", "editor_colorizeSearchFragments", False)
322
323        self.onOptionsChanged(None)
324
325        # when was a key pressed last. used to check idle time.
326        self.lastKeyPressed = time()
327        self.eolMode = self.GetEOLMode()
328
329        self.contextMenuTokens = None
330        self.contextMenuSpellCheckSuggestions = None
331
332        # Connect context menu events to functions
333        wx.EVT_MENU(self, GUI_ID.CMD_UNDO, lambda evt: self.Undo())
334        wx.EVT_MENU(self, GUI_ID.CMD_REDO, lambda evt: self.Redo())
335
336        wx.EVT_MENU(self, GUI_ID.CMD_CLIPBOARD_CUT, lambda evt: self.Cut())
337        wx.EVT_MENU(self, GUI_ID.CMD_CLIPBOARD_COPY, lambda evt: self.Copy())
338        wx.EVT_MENU(self, GUI_ID.CMD_CLIPBOARD_PASTE, lambda evt: self.Paste())
339        wx.EVT_MENU(self, GUI_ID.CMD_SELECT_ALL, lambda evt: self.SelectAll())
340
341        wx.EVT_MENU(self, GUI_ID.CMD_TEXT_DELETE, lambda evt: self.ReplaceSelection(""))
342        wx.EVT_MENU(self, GUI_ID.CMD_ZOOM_IN,
343                lambda evt: self.CmdKeyExecute(wx.stc.STC_CMD_ZOOMIN))
344        wx.EVT_MENU(self, GUI_ID.CMD_ZOOM_OUT,
345                lambda evt: self.CmdKeyExecute(wx.stc.STC_CMD_ZOOMOUT))
346
347
348        for sps in self.SUGGESTION_CMD_IDS:
349            wx.EVT_MENU(self, sps, self.OnReplaceThisSpellingWithSuggestion)
350
351        wx.EVT_MENU(self, GUI_ID.CMD_ADD_THIS_SPELLING_SESSION,
352                self.OnAddThisSpellingToIgnoreSession)
353        wx.EVT_MENU(self, GUI_ID.CMD_ADD_THIS_SPELLING_GLOBAL,
354                self.OnAddThisSpellingToIgnoreGlobal)
355        wx.EVT_MENU(self, GUI_ID.CMD_ADD_THIS_SPELLING_LOCAL,
356                self.OnAddThisSpellingToIgnoreLocal)
357
358        wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_THIS, self.OnActivateThis)
359        wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_NEW_TAB_THIS,
360                self.OnActivateNewTabThis)
361        wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_NEW_TAB_BACKGROUND_THIS,
362                self.OnActivateNewTabBackgroundThis)
363        wx.EVT_MENU(self, GUI_ID.CMD_ACTIVATE_NEW_WINDOW_THIS,
364                self.OnActivateNewWindowThis)
365
366        wx.EVT_MENU(self, GUI_ID.CMD_CONVERT_URL_ABSOLUTE_RELATIVE_THIS,
367                self.OnConvertUrlAbsoluteRelativeThis)
368
369        wx.EVT_MENU(self, GUI_ID.CMD_OPEN_CONTAINING_FOLDER_THIS,
370                self.OnOpenContainingFolderThis)
371
372        wx.EVT_MENU(self, GUI_ID.CMD_DELETE_FILE,
373                self.OnDeleteFile)
374
375        wx.EVT_MENU(self, GUI_ID.CMD_RENAME_FILE,
376                self.OnRenameFile)
377
378        wx.EVT_MENU(self, GUI_ID.CMD_CLIPBOARD_COPY_URL_TO_THIS_ANCHOR,
379                self.OnClipboardCopyUrlToThisAnchor)
380
381        wx.EVT_MENU(self, GUI_ID.CMD_SELECT_TEMPLATE, self.OnSelectTemplate)
382
383
384
385#     def __getattr__(self, attr):
386#         return getattr(self.cnt, attr)
387
388    def getLoadedDocPage(self):
389        return self.presenter.getDocPage()
390
391    def close(self):
392        """
393        Close the editor (=prepare for destruction)
394        """
395        self.stylingThreadHolder.setThread(None)
396        self.calltipThreadHolder.setThread(None)
397
398        self.unloadCurrentDocPage({})   # ?
399        self.presenterListener.disconnect()
400#         self.presenter.getMiscEvent().removeListener(self.presenterListener)
401
402
403#     def onConstructedMainWindow(self, evt):
404#         """
405#         Now we can register idle handler.
406#         """
407#         wx.EVT_IDLE(self, self.OnIdle)
408
409
410    def Copy(self, text=None):
411        if text is None:
412            text = self.GetSelectedText()
413
414        if len(text) == 0:
415            return
416
417        cbIcept = self.presenter.getMainControl().getClipboardInterceptor()
418        if cbIcept is not None:
419            cbIcept.informCopyInWikidPadStart(text=text)
420            try:
421                copyTextToClipboard(text)
422            finally:
423                cbIcept.informCopyInWikidPadStop()
424        else:
425            copyTextToClipboard(text)
426
427    def Paste(self):
428        # Text pasted?
429        text = getTextFromClipboard()
430        if text:
431            self.ReplaceSelection(text)
432            return True
433
434        # File(name)s pasted?
435        filenames = wxHelper.getFilesFromClipboard()
436        if filenames is not None:
437            mc = self.presenter.getMainControl()
438
439            paramDict = {"editor": self, "filenames": filenames,
440                    "x": -1, "y": -1, "main control": mc,
441                    "processDirectly": True}
442
443            mc.getUserActionCoord().runAction(
444                    u"action/editor/this/paste/files/insert/url/ask", paramDict)
445
446            return True
447
448        fs = self.presenter.getWikiDocument().getFileStorage()
449        imgsav = WikiTxtDialogs.ImagePasteSaver()
450        imgsav.readOptionsFromConfig(self.presenter.getConfig())
451
452        # Bitmap pasted?
453        bmp = wxHelper.getBitmapFromClipboard()
454        if bmp is not None:
455            img = bmp.ConvertToImage()
456            del bmp
457
458            if self.presenter.getConfig().getboolean("main",
459                    "editor_imagePaste_askOnEachPaste", True):
460                # Options say to present dialog on an image paste operation
461                # Send image so it can be used for preview
462                dlg = WikiTxtDialogs.ImagePasteDialog(
463                        self.presenter.getMainControl(), -1, imgsav, img)
464                try:
465                    dlg.ShowModal()
466                    imgsav = dlg.getImagePasteSaver()
467                finally:
468                    dlg.Destroy()
469
470            destPath = imgsav.saveFile(fs, img)
471            if destPath is None:
472                # Couldn't find unused filename or saving denied
473                return True
474
475#                 destPath = fs.findDestPathNoSource(u".png", u"")
476#
477#                 print "Paste6", repr(destPath)
478#                 if destPath is None:
479#                     # Couldn't find unused filename
480#                     return
481#
482#                 img.SaveFile(destPath, wx.BITMAP_TYPE_PNG)
483
484            url = self.presenter.getWikiDocument().makeAbsPathRelUrl(destPath)
485
486            if url is None:
487                url = u"file:" + StringOps.urlFromPathname(destPath)
488
489            self.ReplaceSelection(url)
490
491#             locPath = self.presenter.getMainControl().getWikiConfigPath()
492#
493#             if locPath is not None:
494#                 locPath = dirname(locPath)
495#                 relPath = relativeFilePath(locPath, destPath)
496#                 url = None
497#                 if relPath is None:
498#                     # Absolute path needed
499#                     url = "file:%s" % urlFromPathname(destPath)
500#                 else:
501#                     url = "rel://%s" % urlFromPathname(relPath)
502#
503#             if url:
504#                 self.ReplaceSelection(url)
505
506            return True
507
508        if not WindowsHacks:
509            return False
510
511        # Windows Meta File pasted?
512        destPath = imgsav.saveWmfFromClipboardToFileStorage(fs)
513        if destPath is not None:
514            url = self.presenter.getWikiDocument().makeAbsPathRelUrl(destPath)
515
516            if url is None:
517                url = u"file:" + StringOps.urlFromPathname(destPath)
518
519            self.ReplaceSelection(url)
520            return True
521
522
523#         if destPath is not None:
524#             locPath = self.presenter.getMainControl().getWikiConfigPath()
525#
526#             if locPath is not None:
527#                 locPath = dirname(locPath)
528#                 relPath = relativeFilePath(locPath, destPath)
529#                 url = None
530#                 if relPath is None:
531#                     # Absolute path needed
532#                     url = "file:%s>i" % urlFromPathname(destPath)
533#                 else:
534#                     url = "rel://%s>i" % urlFromPathname(relPath)
535#
536#                 if url:
537#                     self.ReplaceSelection(url)
538
539        return False
540
541
542    def onCmdCopy(self, miscevt):
543        if wx.Window.FindFocus() != self:
544            return
545        self.Copy()
546
547
548
549    def setLayerVisible(self, vis, scName=""):
550        """
551        Informs the widget if it is really visible on the screen or not
552        """
553#         if vis:
554#             self.Enable(True)
555        self.Enable(vis)
556
557    def setWrapMode(self, onOrOff):
558        if onOrOff:
559            self.SetWrapMode(wx.stc.STC_WRAP_WORD)
560        else:
561            self.SetWrapMode(wx.stc.STC_WRAP_NONE)
562
563    def getWrapMode(self):
564        return self.GetWrapMode() == wx.stc.STC_WRAP_WORD
565
566    def setAutoIndent(self, onOff):
567        self.autoIndent = onOff
568
569    def getAutoIndent(self):
570        return self.autoIndent
571
572    def setAutoBullets(self, onOff):
573        self.autoBullets = onOff
574
575    def getAutoBullets(self):
576        return self.autoBullets
577
578    def setTabsToSpaces(self, onOff):
579        self.tabsToSpaces = onOff
580        self.SetUseTabs(not onOff)
581
582    def getTabsToSpaces(self):
583        return self.tabsToSpaces
584
585    def setShowLineNumbers(self, onOrOff):
586        if onOrOff:
587            self.SetMarginWidth(self.NUMBER_MARGIN,
588                    self.TextWidth(wx.stc.STC_STYLE_LINENUMBER, "_99999"))
589            self.SetMarginWidth(self.SELECT_MARGIN, 0)
590        else:
591            self.SetMarginWidth(self.NUMBER_MARGIN, 0)
592            self.SetMarginWidth(self.SELECT_MARGIN, 16)
593
594    def getShowLineNumbers(self):
595        return self.GetMarginWidth(self.NUMBER_MARGIN) != 0
596
597
598    def setFoldingActive(self, onOrOff, forceSync=False):
599        """
600        forceSync -- when setting folding on, the folding is completed
601            before function returns iff forceSync is True
602        """
603        if onOrOff:
604            self.SetMarginWidth(self.FOLD_MARGIN, 16)
605            self.foldingActive = True
606            if forceSync:
607                try:
608                    self.applyFolding(self.processFolding(
609                            self.getPageAst(), DUMBTHREADSTOP))
610                except NoPageAstException:
611                    return
612            else:
613                self.OnStyleNeeded(None)
614        else:
615            self.SetMarginWidth(self.FOLD_MARGIN, 0)
616            self.unfoldAll()
617            self.foldingActive = False
618
619    def getFoldingActive(self):
620        return self.foldingActive
621
622
623    def SetStyles(self, styleFaces = None):
624        self.SetStyleBits(5)
625
626        # create the styles
627        if styleFaces is None:
628            styleFaces = self.presenter.getDefaultFontFaces()
629
630        config = self.presenter.getConfig()
631        styles = self.presenter.getMainControl().getPresentationExt()\
632                .getStyles(styleFaces, config)
633
634        for type, style in styles:
635            self.StyleSetSpec(type, style)
636
637            if type == wx.stc.STC_STYLE_CALLTIP:
638                self.CallTipUseStyle(10)
639
640        self.IndicatorSetStyle(2, wx.stc.STC_INDIC_SQUIGGLE)
641        self.IndicatorSetForeground(2, wx.Colour(255, 0, 0))
642
643
644    def SetText(self, text, emptyUndo=True):
645        """
646        Overrides the wxStyledTextCtrl method.
647        text -- Unicode text content to set
648        """
649        self.incSearchCharStartPos = 0
650        self.clearStylingCache()
651        self.pageType = "normal"
652
653        self.SetSelection(-1, -1)
654        self.ignoreOnChange = True
655        if isUnicode():
656            wx.stc.StyledTextCtrl.SetText(self, text)
657        else:
658            wx.stc.StyledTextCtrl.SetText(self,
659                    StringOps.mbcsEnc(text, "replace")[0])
660        self.ignoreOnChange = False
661
662        if emptyUndo:
663            self.EmptyUndoBuffer()
664        # self.applyBasicSciSettings()
665
666
667    def replaceText(self, text):
668        if isUnicode():
669            wx.stc.StyledTextCtrl.SetText(self, text)
670        else:
671            wx.stc.StyledTextCtrl.SetText(self,
672                    StringOps.mbcsEnc(text, "replace")[0])
673
674
675    def replaceTextAreaByCharPos(self, newText, start, end):
676        text = self.GetText()
677        bs = self.bytelenSct(text[:start])
678        be = bs + self.bytelenSct(text[start:end])
679        self.SetTargetStart(bs)
680        self.SetTargetEnd(be)
681
682        if isUnicode():
683            self.ReplaceTarget(newText)
684        else:
685            self.ReplaceTarget(StringOps.mbcsEnc(newText, "replace")[0])
686
687#         text = self.GetText()
688#         text = text[:pos] + newText + text[(pos + len):]
689#
690#         self.replaceText(text)
691
692
693    def showSelectionByCharPos(self, start, end):
694        """
695        Same as SetSelectionByCharPos(), but scrolls to position correctly
696        """
697        text = self.GetText()
698        bs = self.bytelenSct(text[:start])
699        be = bs + self.bytelenSct(text[start:end])
700
701        self.ensureTextRangeByBytePosExpanded(bs, be)
702        super(WikiTxtCtrl, self).showSelectionByCharPos(start, end)
703
704
705    def applyBasicSciSettings(self):
706        """
707        Apply the basic Scintilla settings which are resetted to wrong
708        default values by some operations
709        """
710        if isUnicode():
711            self.SetCodePage(wx.stc.STC_CP_UTF8)
712        self.SetTabIndents(True)
713        self.SetBackSpaceUnIndents(True)
714        self.SetUseTabs(not self.tabsToSpaces)
715        self.SetEOLMode(wx.stc.STC_EOL_LF)
716
717        tabWidth = self.presenter.getConfig().getint("main",
718                "editor_tabWidth", 4)
719
720        self.SetIndent(tabWidth)
721        self.SetTabWidth(tabWidth)
722
723        self.AutoCompSetFillUps(u":="# TODO Add '.'?
724#         self.SetYCaretPolicy(wxSTC_CARET_SLOP, 2)
725#         self.SetYCaretPolicy(wxSTC_CARET_JUMPS | wxSTC_CARET_EVEN, 4)
726        self.SetYCaretPolicy(wx.stc.STC_CARET_SLOP | wx.stc.STC_CARET_EVEN, 4)
727
728
729
730    def saveLoadedDocPage(self):
731        """
732        Save loaded wiki page into database. Does not check if dirty
733        """
734        if self.getLoadedDocPage() is None:
735            return
736
737        page = self.getLoadedDocPage()
738
739#         if not self.loadedDocPage.getDirty()[0]:
740#             return
741
742#         text = self.GetText()
743#         page.replaceLiveText(text)
744        if self.presenter.getMainControl().saveDocPage(page):
745            self.SetSavePoint()
746
747
748    def unloadCurrentDocPage(self, evtprops=None):
749        ## _prof.start()
750        # Stop threads
751        self.stylingThreadHolder.setThread(None)
752        self.calltipThreadHolder.setThread(None)
753
754        docPage = self.getLoadedDocPage()
755        if docPage is not None:
756            wikiWord = docPage.getWikiWord()
757            if wikiWord is not None:
758                docPage.setPresentation((self.GetCurrentPos(),
759                        self.GetScrollPos(wx.HORIZONTAL),
760                        self.GetScrollPos(wx.VERTICAL)), 0)
761                docPage.setPresentation((self.getFoldInfo(),), 5)
762
763            if docPage.getDirty()[0]:
764                self.saveLoadedDocPage()
765
766            docPage.removeTxtEditor(self)
767
768            self.SetDocPointer(None)
769            self.applyBasicSciSettings()
770
771            self.wikiPageSink.disconnect()
772
773            self.presenter.setDocPage(None)
774
775            self.clearStylingCache()
776#             self.stylebytes = None
777#             self.foldingseq = None
778#             self.pageAst = None
779            self.pageType = "normal"
780
781        ## _prof.stop()
782
783
784    def loadFuncPage(self, funcPage, evtprops=None):
785        self.unloadCurrentDocPage(evtprops)
786        # set the editor text
787        content = None
788        wikiDataManager = self.presenter.getWikiDocument()
789
790        self.presenter.setDocPage(funcPage)
791
792        if self.getLoadedDocPage() is None:
793            return  # TODO How to handle?
794
795        globalAttrs = wikiDataManager.getWikiData().getGlobalAttributes()
796        # get the font that should be used in the editor
797        font = globalAttrs.get("global.font", self.defaultFont)
798
799        # set the styles in the editor to the font
800        if self.lastFont != font:
801            faces = self.presenter.getDefaultFontFaces().copy()
802            faces["mono"] = font
803            self.SetStyles(faces)
804            self.lastEditorFont = font
805
806#         p2 = evtprops.copy()
807#         p2.update({"loading current page": True})
808#         self.pWiki.fireMiscEventProps(p2)  # TODO Remove this hack
809
810        self.wikiPageSink.setEventSource(self.getLoadedDocPage().getMiscEvent())
811
812        otherEditor = self.getLoadedDocPage().getTxtEditor()
813        if otherEditor is not None:
814            # Another editor contains already this page, so share its
815            # Scintilla document object for synchronized editing
816            self.SetDocPointer(otherEditor.GetDocPointer())
817            self.applyBasicSciSettings()
818        else:
819            # Load content
820            try:
821                content = self.getLoadedDocPage().getLiveText()
822            except WikiFileNotFoundException, e:
823                assert 0   # TODO
824
825            # now fill the text into the editor
826            self.SetReadOnly(False)
827            self.SetText(content)
828
829        self.getLoadedDocPage().addTxtEditor(self)
830        self._checkForReadOnly()
831        self.presenter.setTitle(self.getLoadedDocPage().getTitle())
832
833
834    def loadWikiPage(self, wikiPage, evtprops=None):
835        """
836        Save loaded page, if necessary, then load wikiPage into editor
837        """
838        self.unloadCurrentDocPage(evtprops)
839        # set the editor text
840        wikiDataManager = self.presenter.getWikiDocument()
841
842        self.presenter.setDocPage(wikiPage)
843
844        docPage = self.getLoadedDocPage()
845
846        if docPage is None:
847            return  # TODO How to handle?
848
849        self.wikiPageSink.setEventSource(docPage.getMiscEvent())
850
851        otherEditor = docPage.getTxtEditor()
852        if otherEditor is not None:
853            # Another editor contains already this page, so share its
854            # Scintilla document object for synchronized editing
855            self.SetDocPointer(otherEditor.GetDocPointer())
856            self.applyBasicSciSettings()
857        else:
858            # Load content
859            try:
860                content = docPage.getLiveText()
861            except WikiFileNotFoundException, e:
862                assert 0   # TODO
863
864            # now fill the text into the editor
865            self.SetReadOnly(False)
866            self.setTextAgaUpdated(content)
867
868        if self.wikiLanguageHelper is None or \
869                self.wikiLanguageHelper.getWikiLanguageName() != \
870                docPage.getWikiLanguageName():
871
872            wx.GetApp().freeWikiLanguageHelper(self.wikiLanguageHelper)
873            self.wikiLanguageHelper = docPage.createWikiLanguageHelper()
874
875        docPage.addTxtEditor(self)
876        self._checkForReadOnly()
877
878        if evtprops is None:
879            evtprops = {}
880        p2 = evtprops.copy()
881        p2.update({"loading wiki page": True, "wikiPage": docPage})
882        self.presenter.fireMiscEventProps(p2)  # TODO Remove this hack
883
884        # get the font that should be used in the editor
885        font = docPage.getAttributeOrGlobal("font", self.defaultFont)
886
887        # set the styles in the editor to the font
888        if self.lastFont != font:
889            faces = self.presenter.getDefaultFontFaces().copy()
890            faces["mono"] = font
891            self.SetStyles(faces)
892            self.lastEditorFont = font
893
894        self.pageType = docPage.getAttributes().get(u"pagetype",
895                [u"normal"])[-1]
896
897        if self.pageType == u"normal":
898            if not docPage.isDefined():
899                # This is a new, not yet defined page, so go to the end of page
900                self.GotoPos(self.GetLength())
901            else:
902                anchor = evtprops.get("anchor")
903                if anchor:
904                    # Scroll page according to the anchor
905                    pageAst = self.getPageAst()
906
907                    anchorNodes = pageAst.iterDeepByName("anchorDef")
908                    for node in anchorNodes:
909                        if node.anchorLink == anchor:
910                            self.gotoCharPos(node.pos + node.strLength)
911                            break
912                    else:
913                        anchor = None # Not found
914
915                if not anchor:
916                    # Is there a position given in the eventprops?
917                    firstcharpos = evtprops.get("firstcharpos", -1)
918                    if firstcharpos != -1:
919                        charlength = max(0, evtprops.get("charlength", 0))
920                        self.showSelectionByCharPos(firstcharpos,
921                                firstcharpos + charlength)
922                        anchor = True
923
924                if not anchor:
925                    # see if there is a saved position for this page
926                    prst = docPage.getPresentation()
927                    lastPos, scrollPosX, scrollPosY = prst[0:3]
928                    foldInfo = prst[5]
929                    self.setFoldInfo(foldInfo)
930                    self.GotoPos(lastPos)
931
932                    self.scrollXY(scrollPosX, scrollPosY)
933        else:
934            self.handleSpecialPageType()
935
936        self.presenter.setTitle(docPage.getTitle())
937
938
939    def handleSpecialPageType(self):
940#         self.allowRectExtend(self.pageType != u"texttree")
941
942        if self.pageType == u"form":
943            self.GotoPos(0)
944            self._goToNextFormField()
945            return True
946
947        return False
948
949
950    def onReloadedCurrentPage(self, miscevt):
951        """
952        Called when already loaded page should be loaded again, mainly
953        interesting if a link with anchor is activated
954        """
955        if not self.presenter.isCurrent():
956            return
957
958        anchor = miscevt.get("anchor")
959        if not anchor:
960            if self.pageType == u"normal":
961                # Is there a position given in the eventprops?
962                firstcharpos = miscevt.get("firstcharpos", -1)
963                if firstcharpos != -1:
964                    charlength = max(0, miscevt.get("charlength", 0))
965                    self.showSelectionByCharPos(firstcharpos,
966                            firstcharpos + charlength)
967
968            return
969
970
971#         if not anchor:
972#             return
973
974        docPage = self.getLoadedDocPage()
975
976        if not docPage.isDefined():
977            return
978
979        if self.wikiLanguageHelper is None or \
980                self.wikiLanguageHelper.getWikiLanguageName() != \
981                docPage.getWikiLanguageName():
982
983            wx.GetApp().freeWikiLanguageHelper(self.wikiLanguageHelper)
984            self.wikiLanguageHelper = docPage.createWikiLanguageHelper()
985
986        if self.pageType == u"normal":
987            # Scroll page according to the anchor
988            try:
989                anchorNodes = self.getPageAst().iterDeepByName("anchorDef")
990                anchorNodes = self.getPageAst().iterDeepByName("anchorDef")
991                for node in anchorNodes:
992                    if node.anchorLink == anchor:
993                        self.gotoCharPos(node.pos + node.strLength)
994                        break
995#                 else:
996#                     anchor = None # Not found
997
998            except NoPageAstException:
999                return
1000
1001
1002    def _checkForReadOnly(self):
1003        """
1004        Set/unset read-only mode of editor according to read-only state of page.
1005        """
1006        docPage = self.getLoadedDocPage()
1007        if docPage is None:
1008            self.SetReadOnly(True)
1009        else:
1010            self.SetReadOnly(docPage.isReadOnlyEffect())
1011
1012
1013    def _getColorFromOption(self, option, defColTuple):
1014        """
1015        Helper for onOptionsChanged() to read a color from an option
1016        and create a wx.Colour object from it.
1017        """
1018        coltuple = StringOps.colorDescToRgbTuple(self.presenter.getConfig().get(
1019                "main", option))
1020
1021        if coltuple is None:
1022            coltuple = defColTuple
1023
1024        return wx.Colour(*coltuple)
1025
1026
1027    def onPageChangedReadOnlyFlag(self, miscevt):
1028        self._checkForReadOnly()
1029
1030
1031    def onOptionsChanged(self, miscevt):
1032        faces = self.presenter.getDefaultFontFaces().copy()
1033
1034        if isinstance(self.getLoadedDocPage(),
1035                (DocPages.WikiPage, DocPages.AliasWikiPage)):
1036
1037            font = self.getLoadedDocPage().getAttributeOrGlobal("font",
1038                    self.defaultFont)
1039            faces["mono"] = font
1040            self.lastEditorFont = font    # ???
1041
1042        self._checkForReadOnly()
1043       
1044        self.SetStyles(faces)
1045
1046        color = self._getColorFromOption("editor_bg_color", (255, 255, 255))
1047
1048        for i in xrange(32):
1049            self.StyleSetBackground(i, color)
1050        self.StyleSetBackground(wx.stc.STC_STYLE_DEFAULT, color)
1051
1052        self.SetSelForeground(True, self._getColorFromOption(
1053                "editor_selection_fg_color", (0, 0, 0)))
1054        self.SetSelBackground(True, self._getColorFromOption(
1055                "editor_selection_bg_color", (192, 192, 192)))
1056        self.SetCaretForeground(self._getColorFromOption(
1057                "editor_caret_color", (0, 0, 0)))
1058        # Set default color (especially for folding lines)
1059        self.StyleSetForeground(wx.stc.STC_STYLE_DEFAULT, self._getColorFromOption(
1060                "editor_plaintext_color", (0, 0, 0)))
1061        self.StyleSetBackground(wx.stc.STC_STYLE_LINENUMBER, self._getColorFromOption(
1062                "editor_margin_bg_color", (212, 208, 200)))
1063
1064        shorthintDelay = self.presenter.getConfig().getint("main",
1065                "editor_shortHint_delay", 500)
1066        self.SetMouseDwellTime(shorthintDelay)
1067
1068        tabWidth = self.presenter.getConfig().getint("main",
1069                "editor_tabWidth", 4)
1070
1071        self.SetIndent(tabWidth)
1072        self.SetTabWidth(tabWidth)
1073
1074
1075        # To allow switching vi keys on and off without restart
1076        use_vi_navigation = self.presenter.getConfig().getboolean("main",
1077                "editor_compatibility_ViKeys", False)
1078
1079        self.Unbind(wx.EVT_KEY_DOWN)
1080        self.Unbind(wx.EVT_LEFT_UP)
1081
1082        if use_vi_navigation:
1083            if self.vi is None:
1084                self.vi = ViHandler(self)
1085
1086           
1087            #self.Bind(wx.EVT_CHAR, self.vi.OnViKeyDown)
1088            self.Bind(wx.EVT_KEY_DOWN, self.vi.OnViKeyDown)
1089            self.Bind(wx.EVT_LEFT_UP, self.vi.OnLeftMouseUp)
1090            self.Bind(wx.EVT_SCROLLWIN, self.vi.OnScroll)
1091            self.Bind(wx.EVT_MOUSEWHEEL, self.vi.OnMouseScroll)
1092            # Should probably store shortcut state in a global
1093            # variable otherwise this will be run each time
1094            # a new tab is opened
1095            wx.CallAfter(self.vi._enableMenuShortcuts, False)
1096        else:
1097            if self.vi is not None:
1098                self.vi.TurnOff()
1099                self.vi = None
1100            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
1101
1102
1103    def onChangedConfiguration(self, miscevt):
1104        """
1105        Called when global configuration was changed. Most things are processed
1106        by onOptionsChanged so only the online spell checker switch must be
1107        handled here.
1108        """
1109        restyle = False
1110
1111        newSetting = SpellChecker.isSpellCheckSupported() and \
1112                self.presenter.getConfig().getboolean(
1113                "main", "editor_onlineSpellChecker_active", False)
1114
1115        if newSetting != self.onlineSpellCheckerActive:
1116            self.onlineSpellCheckerActive = newSetting
1117            restyle = True
1118
1119        newSetting = self.presenter.getConfig()\
1120                .getboolean("main", "editor_colorizeSearchFragments", False)
1121
1122        if newSetting != self.optionColorizeSearchFragments:
1123            self.optionColorizeSearchFragments = newSetting
1124            restyle = True
1125
1126        if restyle:
1127            self.OnStyleNeeded(None)
1128
1129
1130
1131    def onWikiPageUpdated(self, miscevt):
1132        if self.getLoadedDocPage() is None or \
1133                not isinstance(self.getLoadedDocPage(),
1134                (DocPages.WikiPage, DocPages.AliasWikiPage)):
1135            return
1136
1137        # get the font that should be used in the editor
1138        font = self.getLoadedDocPage().getAttributeOrGlobal("font",
1139                self.defaultFont)
1140
1141        # set the styles in the editor to the font
1142        if self.lastFont != font:
1143            faces = self.presenter.getDefaultFontFaces().copy()
1144            faces["mono"] = font
1145            self.SetStyles(faces)
1146            self.lastEditorFont = font
1147
1148        self.pageType = self.getLoadedDocPage().getAttributes().get(u"pagetype",
1149                [u"normal"])[-1]
1150
1151
1152    def handleInvalidFileSignature(self, docPage):
1153        """
1154        Called directly from a doc page to repair the editor state if an
1155        invalid file signature was detected.
1156
1157        docPage -- calling docpage
1158        """
1159        if docPage is not self.getLoadedDocPage() or \
1160                not isinstance(docPage,
1161                        (DocPages.DataCarryingPage, DocPages.AliasWikiPage)):
1162            return
1163
1164        sd, ud = docPage.getDirty()
1165        if sd:
1166            return   # TODO What to do on conflict?
1167
1168        content = docPage.getContent()
1169        docPage.setEditorText(content, dirty=False)
1170        self.ignoreOnChange = True
1171        # TODO: Store/restore selection & scroll pos.
1172        self.setTextAgaUpdated(content)
1173        self.ignoreOnChange = False
1174
1175
1176    def onSavingAllPages(self, miscevt):
1177        if self.getLoadedDocPage() is not None and (
1178                self.getLoadedDocPage().getDirty()[0] or miscevt.get("force",
1179                False)):
1180            self.saveLoadedDocPage()
1181
1182    def onClosingCurrentWiki(self, miscevt):
1183        self.unloadCurrentDocPage()
1184
1185    def onDroppingCurrentWiki(self, miscevt):
1186        """
1187        An access error occurred. Get rid of any data without trying to save
1188        it.
1189        """
1190        if self.getLoadedDocPage() is not None:
1191            self.wikiPageSink.disconnect()
1192
1193            self.SetDocPointer(None)
1194            self.applyBasicSciSettings()
1195
1196            self.getLoadedDocPage().removeTxtEditor(self)
1197            self.presenter.setDocPage(None)
1198#             self.loadedDocPage = None
1199            self.pageType = "normal"
1200
1201
1202    def OnStyleNeeded(self, evt):
1203        "Styles the text of the editor"
1204        docPage = self.getLoadedDocPage()
1205        if docPage is None:
1206            # This avoids further request from STC:
1207            self.stopStcStyler()
1208            return
1209
1210        # get the text to regex against (use doc pages getLiveText because
1211        # it's cached
1212        text = docPage.getLiveText()  # self.GetText()
1213        textlen = len(text)
1214
1215        t = self.stylingThreadHolder.getThread()
1216        if t is not None:
1217            self.stylingThreadHolder.setThread(None)
1218            self.clearStylingCache()
1219
1220
1221        if textlen < self.presenter.getConfig().getint(
1222                "main", "sync_highlight_byte_limit"):
1223#         if True:
1224            # Synchronous styling
1225            self.stylingThreadHolder.setThread(None)
1226            self.buildStyling(text, 0, threadstop=DUMBTHREADSTOP)
1227
1228            self.applyStyling(self.stylebytes)   # TODO Necessary?
1229            # We can't call applyFolding directly because this in turn
1230            # calls repairFoldingVisibility which can't work while in
1231            # EVT_STC_STYLENEEDED event (at least for wxPython 2.6.2)
1232            # storeStylingAndAst() sends a StyleDoneEvent instead
1233            if self.getFoldingActive():
1234                self.storeStylingAndAst(None, self.foldingseq)
1235        else:
1236            # Asynchronous styling
1237            # This avoids further request from STC:
1238            self.stopStcStyler()
1239
1240            sth = self.stylingThreadHolder
1241
1242            delay = self.presenter.getConfig().getfloat(
1243                    "main", "async_highlight_delay")
1244            t = threading.Thread(None, self.buildStyling, args = (text, delay, sth))
1245            sth.setThread(t)
1246            t.setDaemon(True)
1247            t.start()
1248
1249
1250    def _fillTemplateMenu(self, menu):
1251        idRecycler = self.templateIdRecycler
1252        idRecycler.clearAssoc()
1253
1254        config = self.presenter.getConfig()
1255
1256        templateRePat = config.get(u"main", u"template_pageNamesRE",
1257                u"^template/")
1258
1259        try:
1260            templateRe = re.compile(templateRePat, re.DOTALL | re.UNICODE)
1261        except re.error:
1262            templateRe = re.compile(u"^template/", re.DOTALL | re.UNICODE)
1263
1264        wikiDocument = self.presenter.getWikiDocument()
1265        templateNames = [n for n in wikiDocument.getAllDefinedWikiPageNames()
1266                if templateRe.search(n)]
1267
1268        wikiDocument.getCollator().sort(templateNames)
1269
1270        for tn in templateNames:
1271            menuID, reused = idRecycler.assocGetIdAndReused(tn)
1272
1273            if not reused:
1274                # For a new id, an event must be set
1275                wx.EVT_MENU(self, menuID, self.OnTemplateUsed)
1276
1277            menu.Append(menuID, StringOps.uniToGui(tn))
1278
1279
1280    def OnTemplateUsed(self, evt):
1281        docPage = self.getLoadedDocPage()
1282        if docPage is None:
1283            return
1284        templateName = self.templateIdRecycler.get(evt.GetId())
1285
1286        if templateName is None:
1287            return
1288
1289        wikiDocument = self.presenter.getWikiDocument()
1290        templatePage = wikiDocument.getWikiPage(templateName)
1291
1292        content = self.getLoadedDocPage().getContentOfTemplate(templatePage,
1293                templatePage)
1294        docPage.setMetaDataFromTemplate(templatePage)
1295
1296        self.SetText(content, emptyUndo=False)
1297        self.pageType = docPage.getAttributes().get(u"pagetype",
1298                [u"normal"])[-1]
1299        self.handleSpecialPageType()
1300        # TODO Handle form mode
1301        self.presenter.informEditorTextChanged(self)
1302
1303
1304    def OnSelectTemplate(self, evt):
1305        docPage = self.getLoadedDocPage()
1306        if docPage is None:
1307            return
1308
1309        if not isinstance(docPage, DocPages.WikiPage):
1310            return
1311
1312        if not docPage.isDefined() and not docPage.getDirty()[0]:
1313            title = _(u"Select Template")
1314        else:
1315            title = _(u"Select Template (deletes current content!)")
1316
1317        templateName = AdditionalDialogs.SelectWikiWordDialog.runModal(
1318                self.presenter.getMainControl(), self, -1,
1319                title=title)
1320        if templateName is None:
1321            return
1322
1323        wikiDocument = self.presenter.getWikiDocument()
1324        templatePage = wikiDocument.getWikiPage(templateName)
1325
1326        content = self.getLoadedDocPage().getContentOfTemplate(templatePage,
1327                templatePage)
1328        docPage.setMetaDataFromTemplate(templatePage)
1329
1330        self.SetText(content, emptyUndo=False)
1331        self.pageType = docPage.getAttributes().get(u"pagetype",
1332                [u"normal"])[-1]
1333        self.handleSpecialPageType()
1334        self.presenter.informEditorTextChanged(self)
1335
1336
1337    # TODO Wrong reaction on press of context menu button on keyboard
1338    def OnContextMenu(self, evt):
1339        mousePos = self.ScreenToClient(wx.GetMousePosition())
1340
1341        leftFold = 0
1342        for i in range(self.FOLD_MARGIN):
1343            leftFold += self.GetMarginWidth(i)
1344
1345        rightFold = leftFold + self.GetMarginWidth(self.FOLD_MARGIN)
1346
1347        menu = wx.Menu()
1348
1349        if mousePos.x >= leftFold and mousePos.x < rightFold:
1350            # Right click in fold margin
1351
1352            appendToMenuByMenuDesc(menu, FOLD_MENU)
1353        else:
1354
1355            nodes = self.getTokensForMousePos(mousePos)
1356
1357            self.contextMenuTokens = nodes
1358            addActivateItem = False
1359            addFileUrlItem = False
1360            addWikiUrlItem = False
1361            addUrlToClipboardItem = False
1362            unknownWord = None
1363            for node in nodes:
1364                if node.name == "wikiWord":
1365                    addActivateItem = True
1366                elif node.name == "urlLink":
1367                    addActivateItem = True
1368                    if node.url.startswith(u"file:") or \
1369                            node.url.startswith(u"rel://"):
1370                        addFileUrlItem = True
1371                    elif node.url.startswith(u"wiki:") or \
1372                            node.url.startswith(u"wikirel://"):
1373                        addWikiUrlItem = True
1374                elif node.name == "insertion" and node.key == u"page":
1375                    addActivateItem = True
1376                elif node.name == "anchorDef":
1377                    addUrlToClipboardItem = True
1378                elif node.name == "unknownSpelling":
1379                    unknownWord = node.getText()
1380
1381            if unknownWord:
1382                # Right click on a word not in spelling dictionary
1383                spellCheckerSession = self.presenter.getWikiDocument()\
1384                        .createOnlineSpellCheckerSessionClone()
1385                spellCheckerSession.setCurrentDocPage(self.getLoadedDocPage())
1386                if spellCheckerSession:
1387                    # Show suggestions if available (up to first 5)
1388                    suggestions = spellCheckerSession.suggest(unknownWord)[:5]
1389                    spellCheckerSession.close()
1390
1391                    if len(suggestions) > 0:
1392                        for s, mid in zip(suggestions, self.SUGGESTION_CMD_IDS):
1393                            menuitem = wx.MenuItem(menu, mid, s)
1394                            font = menuitem.GetFont()
1395                            font.SetWeight(wx.FONTWEIGHT_BOLD)
1396                            menuitem.SetFont(font)
1397
1398                            menu.AppendItem(menuitem)
1399
1400                        self.contextMenuSpellCheckSuggestions = suggestions
1401                    # Show other spelling menu items
1402                    appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_SPELLING)
1403
1404
1405            appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_BASE)
1406
1407            if addActivateItem:
1408                appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_ACTIVATE)
1409
1410                if addFileUrlItem:
1411                    appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_FILE_URL)
1412                elif addWikiUrlItem:
1413                    appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_WIKI_URL)
1414
1415            if addUrlToClipboardItem:
1416                appendToMenuByMenuDesc(menu,
1417                        _CONTEXT_MENU_INTEXT_URL_TO_CLIPBOARD)
1418
1419            docPage = self.getLoadedDocPage()
1420            if isinstance(docPage, DocPages.WikiPage):
1421                if not docPage.isDefined() and not docPage.getDirty()[0]:
1422                    templateSubmenu = wx.Menu()
1423                    self._fillTemplateMenu(templateSubmenu)
1424                    appendToMenuByMenuDesc(templateSubmenu,
1425                            _CONTEXT_MENU_SELECT_TEMPLATE_IN_TEMPLATE_MENU)
1426
1427                    menu.AppendSeparator()
1428                    menu.AppendMenu(wx.NewId(), _(u'Use Template'),
1429                            templateSubmenu)
1430                else:
1431                    appendToMenuByMenuDesc(menu,
1432                            _CONTEXT_MENU_SELECT_TEMPLATE)
1433
1434            appendToMenuByMenuDesc(menu, _CONTEXT_MENU_INTEXT_BOTTOM)
1435
1436            # Enable/Disable appropriate menu items
1437            item = menu.FindItemById(GUI_ID.CMD_UNDO)
1438            if item: item.Enable(self.CanUndo())
1439            item = menu.FindItemById(GUI_ID.CMD_REDO)
1440            if item: item.Enable(self.CanRedo())
1441
1442            cancopy = self.GetSelectionStart() != self.GetSelectionEnd()
1443
1444            item = menu.FindItemById(GUI_ID.CMD_TEXT_DELETE)
1445            if item: item.Enable(cancopy and not self.GetReadOnly())
1446            item = menu.FindItemById(GUI_ID.CMD_CLIPBOARD_CUT)
1447            if item: item.Enable(cancopy and not self.GetReadOnly())
1448            item = menu.FindItemById(GUI_ID.CMD_CLIPBOARD_COPY)
1449            if item: item.Enable(cancopy)
1450            item = menu.FindItemById(GUI_ID.CMD_CLIPBOARD_PASTE)
1451            if item: item.Enable(self.CanPaste())
1452
1453        # Dwell lock to avoid image popup while context menu is shown
1454        with self.dwellLock():
1455            # Show menu
1456            self.PopupMenu(menu)
1457
1458        self.contextMenuTokens = None
1459        self.contextMenuSpellCheckSuggestions = None
1460        menu.Destroy()
1461
1462
1463    def _goToNextFormField(self):
1464        """
1465        If pagetype is "form" this is called when user presses TAB in
1466        text editor and after loading a form page
1467        """
1468        searchOp = SearchReplaceOperation()
1469        searchOp.wikiWide = False
1470        searchOp.wildCard = 'regex'
1471        searchOp.caseSensitive = True
1472        searchOp.searchStr = "&&[a-zA-Z]"
1473
1474        text = self.GetText()
1475        charStartPos = len(self.GetTextRange(0, self.GetSelectionEnd()))
1476        while True:
1477            start, end = searchOp.searchText(text, charStartPos)[:2]
1478            if start is None:
1479                return False
1480
1481            fieldcode = text[start + 2]
1482            if fieldcode == "i":
1483                self.showSelectionByCharPos(start, end)
1484                return True
1485
1486            charStartPos = end
1487
1488
1489    def handleDropText(self, x, y, text):
1490        if x != -1:
1491            # Real drop
1492            self.DoDropText(x, y, text)
1493            self.gotoCharPos(self.GetSelectionCharPos()[1], scroll=False)
1494        else:
1495            self.ReplaceSelection(text)
1496
1497        self.SetFocus()
1498
1499
1500    def clearStylingCache(self):
1501        self.stylebytes = None
1502        self.foldingseq = None
1503#         self.pageAst = None
1504
1505
1506    def stopStcStyler(self):
1507        """
1508        Stops further styling requests from Scintilla until text is modified
1509        """
1510        self.StartStyling(self.GetLength(), 0xff)
1511        self.SetStyling(0, 0)
1512
1513
1514
1515    def storeStylingAndAst(self, stylebytes, foldingseq, styleMask=0xff):
1516        self.stylebytes = stylebytes
1517#         self.pageAst = pageAst
1518        self.foldingseq = foldingseq
1519
1520        def putStyle():
1521            if stylebytes:
1522                self.applyStyling(stylebytes, styleMask)
1523
1524            if foldingseq:
1525                self.applyFolding(foldingseq)
1526
1527        wx.CallAfter(putStyle)
1528
1529#         self.AddPendingEvent(StyleDoneEvent(stylebytes, foldingseq))
1530
1531
1532
1533    def buildStyling(self, text, delay, threadstop=DUMBTHREADSTOP):
1534        try:
1535            if delay != 0 and not threadstop is DUMBTHREADSTOP:
1536                sleep(delay)
1537                threadstop.testRunning()
1538
1539            docPage = self.getLoadedDocPage()
1540            if docPage is None:
1541                return
1542
1543            for i in range(20):   # "while True" is too dangerous
1544                formatDetails = docPage.getFormatDetails()
1545                pageAst = docPage.getLivePageAst(threadstop=threadstop)
1546                threadstop.testRunning()
1547                if not formatDetails.isEquivTo(docPage.getFormatDetails()):
1548                    continue
1549                else:
1550                    break
1551
1552            stylebytes = self.processTokens(text, pageAst, threadstop)
1553
1554            threadstop.testRunning()
1555
1556            if self.getFoldingActive():
1557                foldingseq = self.processFolding(pageAst, threadstop)
1558            else:
1559                foldingseq = None
1560
1561            threadstop.testRunning()
1562
1563            if self.onlineSpellCheckerActive and \
1564                    isinstance(docPage, DocPages.AbstractWikiPage):
1565
1566                # Show intermediate syntax highlighting results before spell check
1567                # if we are in asynchronous mode
1568                if not threadstop is DUMBTHREADSTOP:
1569                    self.storeStylingAndAst(stylebytes, foldingseq, styleMask=0x1f)
1570
1571                scTokens = docPage.getSpellCheckerUnknownWords(threadstop=threadstop)
1572
1573                threadstop.testRunning()
1574
1575                if scTokens.getChildrenCount() > 0:
1576                    spellStyleBytes = self.processSpellCheckTokens(text, scTokens,
1577                            threadstop)
1578
1579                    threadstop.testRunning()
1580
1581                    # TODO: Faster? How?
1582                    stylebytes = "".join([chr(ord(a) | ord(b))
1583                            for a, b in itertools.izip(stylebytes, spellStyleBytes)])
1584
1585                    self.storeStylingAndAst(stylebytes, None, styleMask=0xff)
1586                else:
1587                    self.storeStylingAndAst(stylebytes, None, styleMask=0xff)
1588            else:
1589                self.storeStylingAndAst(stylebytes, foldingseq, styleMask=0xff)
1590
1591        except NotCurrentThreadException:
1592            return
1593
1594
1595
1596    _TOKEN_TO_STYLENO = {
1597        "bold": FormatTypes.Bold,
1598        "italics": FormatTypes.Italic,
1599        "urlLink": FormatTypes.Url,
1600        "script": FormatTypes.Script,
1601        "property": FormatTypes.Attribute,             # TODO remove "property"-compatibility
1602        "attribute": FormatTypes.Attribute,
1603        "insertion": FormatTypes.Script,
1604        "anchorDef": FormatTypes.Bold,
1605        "plainText": FormatTypes.Default
1606        }
1607
1608
1609    def _findFragmentSearch(self, linkNode):
1610        """
1611        linkNode -- AST node of type "wikiWord"
1612        returns
1613            (<first char pos>, <after last char pos>) of targeted search
1614                fragment if present
1615            (None, None) if not present
1616            (-1, -1) if search is not applicable
1617        """
1618        unaliasedTarget = self.presenter.getWikiDocument()\
1619                .getWikiPageNameForLinkTermOrAsIs(linkNode.wikiWord)
1620
1621        docPage = self.getLoadedDocPage()
1622        if docPage is None:
1623            return (-1, -1)
1624
1625        wikiWord = docPage.getWikiWord()
1626        if wikiWord is None:
1627            return (-1, -1)
1628
1629        if wikiWord == unaliasedTarget:
1630            forbiddenSearchfragHit = (linkNode.pos,
1631                    linkNode.pos + linkNode.strLength)
1632        else:
1633            forbiddenSearchfragHit = (0, 0)
1634       
1635        searchfrag = linkNode.searchFragment
1636        if searchfrag is None:
1637            return (-1, -1)
1638
1639        searchOp = SearchReplaceOperation()
1640        searchOp.wildCard = "no"
1641        searchOp.searchStr = searchfrag
1642       
1643        targetPage = self.presenter.getWikiDocument().getWikiPage(
1644                linkNode.wikiWord)
1645
1646        found = searchOp.searchDocPageAndText(targetPage,
1647                targetPage.getLiveText(), 0)
1648
1649        if found[0] >= forbiddenSearchfragHit[0] and \
1650                found[0] < forbiddenSearchfragHit[1]:
1651            # Searchfrag found its own link -> search after link
1652            found = searchOp.searchDocPageAndText(targetPage,
1653                    targetPage.getLiveText(), forbiddenSearchfragHit[1])
1654
1655        return found
1656
1657
1658
1659    def processTokens(self, text, pageAst, threadstop):
1660        wikiDoc = self.presenter.getWikiDocument()
1661        stylebytes = StyleCollector(FormatTypes.Default,
1662                text, self.bytelenSct)
1663
1664        def process(pageAst, stack):
1665            for node in pageAst.iterFlatNamed():
1666                threadstop.testRunning()
1667
1668                styleNo = WikiTxtCtrl._TOKEN_TO_STYLENO.get(node.name)
1669
1670                if styleNo is not None:
1671                    stylebytes.bindStyle(node.pos, node.strLength, styleNo)
1672                elif node.name == "wikiWord":
1673                    if wikiDoc.isCreatableWikiWord(node.wikiWord):
1674                        styleNo = FormatTypes.WikiWord
1675                    else:
1676                        styleNo = FormatTypes.AvailWikiWord
1677                        if self.optionColorizeSearchFragments and \
1678                                node.searchFragment:
1679                            if self._findFragmentSearch(node)[0] == None:
1680#                             if targetTxt.find(node.searchFragment) == -1:
1681                                searchFragNode = node.fragmentNode
1682
1683                                stylebytes.bindStyle(node.pos,
1684                                        searchFragNode.pos - node.pos,
1685                                        FormatTypes.AvailWikiWord)
1686
1687                                stylebytes.bindStyle(searchFragNode.pos,
1688                                        searchFragNode.strLength,
1689                                        FormatTypes.WikiWord)
1690
1691                                stylebytes.bindStyle(searchFragNode.pos +
1692                                        searchFragNode.strLength,
1693                                        node.strLength -
1694                                        (searchFragNode.pos - node.pos) -
1695                                        searchFragNode.strLength,
1696                                        FormatTypes.AvailWikiWord)
1697                                continue
1698
1699                    stylebytes.bindStyle(node.pos, node.strLength, styleNo)
1700
1701                elif node.name == "todoEntry":
1702                    process(node, stack + ["todoEntry"])
1703                elif node.name == "key" and "todoEntry" in stack:
1704                    stylebytes.bindStyle(node.pos, node.strLength,
1705                            FormatTypes.ToDo)
1706                elif node.name == "value" and "todoEntry" in stack:
1707                    process(node, stack[:])
1708
1709                elif node.name == "heading":
1710                    if node.level < 5:
1711                        styleNo = FormatTypes.Heading1 + \
1712                                (node.level - 1)
1713                    else:
1714                        styleNo = FormatTypes.Bold
1715
1716                    stylebytes.bindStyle(node.pos, node.strLength, styleNo)
1717
1718                elif node.name in ("table", "tableRow", "tableCell",
1719                        "orderedList", "unorderedList", "indentedText",
1720                        "noExport"):
1721                    process(node, stack[:])
1722
1723        process(pageAst, [])
1724        return stylebytes.value()
1725
1726
1727    def processSpellCheckTokens(self, text, scTokens, threadstop):
1728        stylebytes = StyleCollector(0, text, self.bytelenSct)
1729        for node in scTokens:
1730            threadstop.testRunning()
1731            stylebytes.bindStyle(node.pos, node.strLength,
1732                    wx.stc.STC_INDIC2_MASK)
1733
1734        return stylebytes.value()
1735
1736
1737    def processFolding(self, pageAst, threadstop):
1738        foldingseq = []
1739        currLine = 0
1740        prevLevel = 0
1741        levelStack = []
1742        foldHeader = False
1743
1744        for node in pageAst:
1745            threadstop.testRunning()
1746
1747            if node.name == "heading":
1748                while levelStack and (levelStack[-1][0] != "heading" or
1749                        levelStack[-1][1] > node.level):
1750                    del levelStack[-1]
1751                if not levelStack or levelStack[-1] != ("heading", node.level):
1752                    levelStack.append(("heading", node.level))
1753                foldHeader = True
1754
1755            lfc = node.getString().count(u"\n")
1756            if len(levelStack) > prevLevel:
1757                foldHeader = True
1758
1759            if foldHeader and lfc > 0:
1760                foldingseq.append(len(levelStack) | wx.stc.STC_FOLDLEVELHEADERFLAG)
1761                foldHeader = False
1762                lfc -= 1
1763
1764            if lfc > 0:
1765                foldingseq += [len(levelStack) + 1] * lfc
1766
1767            prevLevel = len(levelStack) + 1
1768
1769        # final line
1770        foldingseq.append(len(levelStack) + 1)
1771
1772        return foldingseq
1773
1774
1775    def applyStyling(self, stylebytes, styleMask=0xff):
1776        if len(stylebytes) == self.GetLength():
1777            self.StartStyling(0, styleMask)
1778            self.SetStyleBytes(len(stylebytes), stylebytes)
1779
1780    def applyFolding(self, foldingseq):
1781        if foldingseq and self.getFoldingActive() and \
1782                len(foldingseq) == self.GetLineCount():
1783            for ln in xrange(len(foldingseq)):
1784                self.SetFoldLevel(ln, foldingseq[ln])
1785            self.repairFoldingVisibility()
1786
1787
1788    def unfoldAll(self):
1789        """
1790        Unfold all folded lines
1791        """
1792        for i in xrange(self.GetLineCount()):
1793            self.SetFoldExpanded(i, True)
1794
1795        self.ShowLines(0, self.GetLineCount()-1)
1796
1797
1798    def foldAll(self):
1799        """
1800        Fold all foldable lines
1801        """
1802        if not self.getFoldingActive():
1803            self.setFoldingActive(True, forceSync=True)
1804
1805        for ln in xrange(self.GetLineCount()):
1806            if self.GetFoldLevel(ln) & wx.stc.STC_FOLDLEVELHEADERFLAG and \
1807                    self.GetFoldExpanded(ln):
1808                self.ToggleFold(ln)
1809#                 self.SetFoldExpanded(ln, False)
1810#             else:
1811#                 self.HideLines(ln, ln)
1812
1813        self.Refresh()
1814
1815
1816    def toggleCurrentFolding(self):
1817        if not self.getFoldingActive():
1818            return
1819
1820        self.ToggleFold(self.LineFromPosition(self.GetCurrentPos()))
1821
1822
1823    def getFoldInfo(self):
1824        if not self.getFoldingActive():
1825            return None
1826
1827        result = [0] * self.GetLineCount()
1828        for ln in xrange(self.GetLineCount()):
1829            levComb = self.GetFoldLevel(ln)
1830            levOut = levComb & 4095
1831            if levComb & wx.stc.STC_FOLDLEVELHEADERFLAG:
1832                levOut |= 4096
1833            if self.GetFoldExpanded(ln):
1834                levOut |= 8192
1835            if self.GetLineVisible(ln):
1836                levOut |= 16384
1837            result[ln] = levOut
1838
1839        return result
1840
1841
1842    def setFoldInfo(self, fldInfo):
1843        if fldInfo is None or \
1844                not self.getFoldingActive() or \
1845                len(fldInfo) != self.GetLineCount():
1846            return
1847
1848        for ln, levIn in enumerate(fldInfo):
1849            levComb = levIn & 4095
1850            if levIn & 4096:
1851                levComb |= wx.stc.STC_FOLDLEVELHEADERFLAG
1852
1853            self.SetFoldLevel(ln, levComb)
1854            self.SetFoldExpanded(ln, bool(levIn & 8192))
1855            if levIn & 16384:
1856                self.ShowLines(ln, ln)
1857            else:
1858                self.HideLines(ln, ln)
1859
1860        self.repairFoldingVisibility()
1861
1862
1863
1864    def repairFoldingVisibility(self):
1865        if not self.getFoldingActive():
1866            return
1867
1868        lc = self.GetLineCount()
1869
1870        if lc == 0:
1871            return
1872
1873        self.ShowLines(0, 0)
1874        if lc == 1:
1875            return
1876
1877        combLevel = self.GetFoldLevel(0)
1878        prevLevel = combLevel & 4095
1879        prevIsHeader = combLevel & wx.stc.STC_FOLDLEVELHEADERFLAG
1880        prevIsExpanded = self.GetFoldExpanded(0)
1881        prevVisible = True  # First line must always be visible
1882        prevLn = 0
1883
1884#         print "0", prevLevel, bool(prevIsHeader), bool(prevIsExpanded), bool(prevVisible)
1885
1886        for ln in xrange(1, lc):
1887            combLevel = self.GetFoldLevel(ln)
1888            level = combLevel & 4095
1889            isHeader = combLevel & wx.stc.STC_FOLDLEVELHEADERFLAG
1890            isExpanded = self.GetFoldExpanded(ln)
1891            visible = self.GetLineVisible(ln)
1892#             print ln, level, bool(isHeader), bool(isExpanded), bool(visible)
1893
1894            if prevVisible and not visible:
1895                # Previous line visible, current not -> check if we must show it
1896                if ((level <= prevLevel) and \
1897                            not (prevIsHeader and not prevIsExpanded)) or \
1898                        (prevIsHeader and prevIsExpanded):
1899                    # if current level is not larger than previous this indicates
1900                    # an error except that the previous line is a header line and
1901                    # folded (not expanded).
1902                    # Other possibility of an error is if previous line is a
1903                    # header and IS expanded.
1904
1905                    # Show line in these cases
1906                    self.SetFoldExpanded(prevLn, True) # Needed?
1907                    self.ShowLines(ln, ln)
1908                    # self.EnsureVisible(ln)
1909                    visible = True
1910
1911            prevLevel = level
1912            prevIsHeader = isHeader
1913            prevIsExpanded = isExpanded
1914            prevVisible = visible
1915            prevLn = ln
1916
1917
1918
1919    def snip(self):
1920        # get the selected text
1921        text = self.GetSelectedText()
1922
1923        # copy it to the clipboard also
1924        self.Copy()
1925
1926        wikiPage = self.presenter.getWikiDocument().getWikiPageNoError("ScratchPad")
1927
1928#         wikiPage.appendLiveText("\n%s\n---------------------------\n\n%s\n" %
1929#                 (mbcsDec(strftime("%x %I:%M %p"), "replace")[0], text))
1930        wikiPage.appendLiveText("\n%s\n---------------------------\n\n%s\n" %
1931                (StringOps.strftimeUB("%x %I:%M %p"), text))
1932
1933    def styleSelection(self, startChars, endChars=None):
1934        """
1935        """
1936        if endChars is None:
1937            endChars = startChars
1938
1939        (startBytePos, endBytePos) = self.GetSelection()
1940        if startBytePos == endBytePos:
1941            (startBytePos, endBytePos) = self.getNearestWordPositions()
1942
1943        emptySelection = startBytePos == endBytePos  # is selection empty
1944
1945        startCharPos = len(self.GetTextRange(0, startBytePos))
1946        endCharPos = startCharPos + len(self.GetTextRange(startBytePos, endBytePos))
1947
1948        self.BeginUndoAction()
1949        try:
1950            endCharPos += len(startChars)
1951
1952            if emptySelection:
1953                # If selection is empty, cursor will in the end
1954                # stand between the style characters
1955                cursorCharPos = endCharPos
1956            else:
1957                # If not, it will stand after styled word
1958                cursorCharPos = endCharPos + len(endChars)
1959
1960            self.gotoCharPos(startCharPos, scroll=False)
1961            self.AddText(startChars)
1962
1963            self.gotoCharPos(endCharPos, scroll=False)
1964            self.AddText(endChars)
1965
1966            self.gotoCharPos(cursorCharPos, scroll=False)
1967        finally:
1968            self.EndUndoAction()
1969
1970
1971    def getPageAst(self):
1972        docPage = self.getLoadedDocPage()
1973        if docPage is None:
1974            raise NoPageAstException(u"Internal error: No docPage => no page AST")
1975
1976        return docPage.getLivePageAst()
1977
1978
1979    def activateTokens(self, nodeList, tabMode=0):
1980        """
1981        Helper for activateLink()
1982        tabMode -- 0:Same tab; 2: new tab in foreground; 3: new tab in background
1983        """
1984        if len(nodeList) == 0:
1985            return False
1986
1987        for node in nodeList:
1988            if node.name == "wikiWord":
1989                searchStr = None
1990
1991                # open the wiki page
1992                if tabMode & 2:
1993                    if tabMode == 6:
1994                        # New Window
1995                        presenter = self.presenter.getMainControl().\
1996                                createNewDocPagePresenterTabInNewFrame()
1997                    else:
1998                        # New tab
1999                        presenter = self.presenter.getMainControl().\
2000                                createNewDocPagePresenterTab()
2001                else:
2002                    # Same tab
2003                    presenter = self.presenter
2004
2005                titleFromLink = self.presenter.getConfig().getboolean("main",
2006                        "wikiPageTitle_fromLinkTitle", False)
2007
2008                if not titleFromLink or node.titleNode is None:
2009                    suggNewPageTitle = None
2010                else:
2011                    suggNewPageTitle = node.titleNode.getString()
2012
2013                unaliasedTarget = self.presenter.getWikiDocument()\
2014                        .getWikiPageNameForLinkTermOrAsIs(node.wikiWord)
2015
2016                docPage = self.getLoadedDocPage()
2017
2018                # Contains start and end character position where a search fragment
2019                # search should never match
2020                # If the target wikiword is the current one, the search fragment
2021                # search should not find the link itself
2022
2023                forbiddenSearchfragHit = (0, 0)
2024                if docPage is not None:
2025                    wikiWord = docPage.getWikiWord()
2026                    if wikiWord is not None:
2027                        if wikiWord == unaliasedTarget:
2028                            forbiddenSearchfragHit = (node.pos, node.pos + node.strLength)
2029
2030                presenter.openWikiPage(unaliasedTarget,
2031                        motionType="child", anchor=node.anchorLink,
2032                        suggNewPageTitle=suggNewPageTitle)
2033
2034                searchfrag = node.searchFragment
2035                if searchfrag is not None:
2036                    searchOp = SearchReplaceOperation()
2037                    searchOp.wildCard = "no"
2038                    searchOp.searchStr = searchfrag
2039
2040                    found = presenter.getSubControl("textedit").executeSearch(
2041                            searchOp, 0)
2042
2043                    if found[0] >= forbiddenSearchfragHit[0] and \
2044                            found[0] < forbiddenSearchfragHit[1]:
2045                        # Searchfrag found its own link -> search after link
2046                        presenter.getSubControl("textedit").executeSearch(
2047                            searchOp, forbiddenSearchfragHit[1])
2048
2049                if not tabMode & 1:
2050                    # Show in foreground
2051                    presenter.getMainControl().getMainAreaPanel().\
2052                            showPresenter(presenter)
2053
2054                return True
2055
2056            elif node.name == "urlLink":
2057                self.presenter.getMainControl().launchUrl(node.url)
2058                return True
2059
2060            elif node.name == "insertion":
2061                if node.key == u"page":
2062
2063                    # open the wiki page
2064                    if tabMode & 2:
2065                        if tabMode == 6:
2066                            # New Window
2067                            presenter = self.presenter.getMainControl().\
2068                                    createNewDocPagePresenterTabInNewFrame()
2069                        else:
2070                            # New tab
2071                            presenter = self.presenter.getMainControl().\
2072                                    createNewDocPagePresenterTab()
2073                    else:
2074                        # Same tab
2075                        presenter = self.presenter
2076
2077                    presenter.openWikiPage(node.value,
2078                            motionType="child"# , anchor=node.value)
2079
2080                    if not tabMode & 1:
2081                        # Show in foreground (if presenter is in other window,
2082                        # this does nothing)
2083                        presenter.getMainControl().getMainAreaPanel().\
2084                                showPresenter(presenter)
2085
2086                    return True
2087
2088                    # TODO: Make this work correctly
2089#                 elif tok.node.key == u"rel":
2090#                     if tok.node.value == u"back":
2091#                         # Go back in history
2092#                         self.presenter.getMainControl().goBrowserBack()
2093
2094            elif node.name == "footnote":
2095                try:
2096                    pageAst = self.getPageAst()
2097                    footnoteId = node.footnoteId
2098
2099                    anchorNode = getFootnoteAnchorDict(pageAst).get(footnoteId)
2100                    if anchorNode is not None:
2101                        if anchorNode.pos != node.pos:
2102                            # Activated footnote was not last -> go to last
2103                            self.gotoCharPos(anchorNode.pos)
2104                        else:
2105                            # Activated footnote was last -> go to first
2106                            for fnNode in pageAst.iterDeepByName("footnote"):
2107                                if fnNode.footnoteId == footnoteId:
2108                                    self.gotoCharPos(fnNode.pos)
2109                                    break
2110
2111                    return True
2112                except NoPageAstException:
2113                    return False
2114            else:
2115                continue
2116
2117        return False
2118
2119
2120    def getTokensForMousePos(self, mousePosition=None):
2121        # mouse position overrides current pos
2122        if mousePosition and mousePosition != wx.DefaultPosition:
2123            linkBytePos = self.PositionFromPoint(mousePosition)
2124        else:
2125            linkBytePos = self.GetCurrentPos()
2126
2127        try:
2128            pageAst = self.getPageAst()
2129        except NoPageAstException:
2130            return []
2131
2132        linkCharPos = len(self.GetTextRange(0, linkBytePos))
2133
2134        result = pageAst.findNodesForCharPos(linkCharPos)
2135
2136
2137        if linkCharPos > 0:
2138            # Maybe a token left to the cursor was meant, so check
2139            # one char to the left
2140            result += pageAst.findNodesForCharPos(linkCharPos - 1)
2141
2142        if self.onlineSpellCheckerActive:
2143            docPage = self.getLoadedDocPage()
2144            if isinstance(docPage, DocPages.AbstractWikiPage):
2145                allUnknownWords = docPage.getSpellCheckerUnknownWords()
2146                wantedUnknownWords = allUnknownWords.findNodesForCharPos(
2147                        linkCharPos)
2148
2149                if linkCharPos > 0 and len(wantedUnknownWords) == 0:
2150                    # No unknown word found -> try left to cursor
2151                    wantedUnknownWords = allUnknownWords.findNodesForCharPos(
2152                            linkCharPos - 1)
2153
2154                result += wantedUnknownWords
2155
2156        return result
2157
2158
2159
2160    def activateLink(self, mousePosition=None, tabMode=0):
2161        """
2162        Activates link (wiki word or URL)
2163        tabMode -- 0:Same tab; 2: new tab in foreground; 3: new tab in background
2164        """
2165        tokens = self.getTokensForMousePos(mousePosition)
2166        return self.activateTokens(tokens, tabMode)
2167
2168
2169
2170    def OnReplaceThisSpellingWithSuggestion(self, evt):
2171        if self.contextMenuTokens and self.contextMenuSpellCheckSuggestions:
2172            for node in self.contextMenuTokens:
2173                if node.name == "unknownSpelling":
2174                    self.replaceTextAreaByCharPos(
2175                            self.contextMenuSpellCheckSuggestions[
2176                            self.SUGGESTION_CMD_IDS.index(evt.GetId())],
2177                            node.pos, node.pos + node.strLength)
2178                    break
2179
2180
2181
2182    def OnAddThisSpellingToIgnoreSession(self, evt):
2183        if self.contextMenuTokens:
2184            for node in self.contextMenuTokens:
2185                if node.name == "unknownSpelling":
2186                    self.presenter.getWikiDocument()\
2187                            .getOnlineSpellCheckerSession().addIgnoreWordSession(
2188                            node.getText())
2189                    break
2190
2191
2192    def OnAddThisSpellingToIgnoreGlobal(self, evt):
2193        if self.contextMenuTokens:
2194            for node in self.contextMenuTokens:
2195                if node.name == "unknownSpelling":
2196                    self.presenter.getWikiDocument()\
2197                            .getOnlineSpellCheckerSession().addIgnoreWordGlobal(
2198                            node.getText())
2199                    break
2200
2201    def OnAddThisSpellingToIgnoreLocal(self, evt):
2202        if self.contextMenuTokens:
2203            for node in self.contextMenuTokens:
2204                if node.name == "unknownSpelling":
2205                    self.presenter.getWikiDocument()\
2206                            .getOnlineSpellCheckerSession().addIgnoreWordLocal(
2207                            node.getText())
2208                    break
2209
2210
2211
2212    def OnActivateThis(self, evt):
2213        if self.contextMenuTokens:
2214            self.activateTokens(self.contextMenuTokens, 0)
2215
2216    def OnActivateNewTabThis(self, evt):
2217        if self.contextMenuTokens:
2218            self.activateTokens(self.contextMenuTokens, 2)
2219
2220    def OnActivateNewTabBackgroundThis(self, evt):
2221        if self.contextMenuTokens:
2222            self.activateTokens(self.contextMenuTokens, 3)
2223
2224    def OnActivateNewWindowThis(self, evt):
2225        if self.contextMenuTokens:
2226            self.activateTokens(self.contextMenuTokens, 6)
2227
2228
2229    def OnOpenContainingFolderThis(self, evt):
2230        if self.contextMenuTokens:
2231            for node in self.contextMenuTokens:
2232                if node.name == "urlLink":
2233                    link = node.url
2234
2235                    if link.startswith(u"rel://") or link.startswith(u"wikirel://"):
2236                        link = self.presenter.getWikiDocument()\
2237                                .makeRelUrlAbsolute(link)
2238
2239                    if link.startswith(u"file:") or link.startswith(u"wiki:"):
2240                        try:
2241                            path = dirname(StringOps.pathnameFromUrl(link))
2242                            if not exists(StringOps.longPathEnc(path)):
2243                                self.presenter.displayErrorMessage(
2244                                        _(u"Folder does not exist"))
2245                                return
2246
2247                            OsAbstract.startFile(self.presenter.getMainControl(),
2248                                    path)
2249                        except IOError:
2250                            pass   # Error message?
2251
2252                    break
2253
2254    def OnDeleteFile(self, evt):
2255         if self.contextMenuTokens:
2256            for node in self.contextMenuTokens:
2257                if node.name == "urlLink":
2258                    link = self.presenter.getWikiDocument().makeFileUrlAbsPath(
2259                            node.url)
2260                    if link is None:
2261                        continue
2262                   
2263#                     link = node.url
2264#
2265#                     if link.startswith(u"rel://"):
2266#                         link = StringOps.pathnameFromUrl(self.presenter.getMainControl().makeRelUrlAbsolute(link))
2267#                     else:
2268#                         break
2269
2270#                     path = dirname(link)
2271
2272                    if not isfile(link):
2273                        self.presenter.displayErrorMessage(
2274                                _(u"File does not exist"))
2275                        return
2276
2277                    filename = basename(link)
2278
2279                    choice = wx.MessageBox(
2280                            _("Are you sure you want to delete the file: %s") %
2281                            filename, _("Delete File"),
2282                            wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION, self)
2283
2284                    if choice == wx.YES:
2285                        OsAbstract.deleteFile(link)
2286                        self.replaceTextAreaByCharPos(u"", node.pos,
2287                                node.pos + node.strLength)
2288                    return
2289
2290       
2291    def OnRenameFile(self, evt):
2292        if not self.contextMenuTokens:
2293            return
2294
2295        for node in self.contextMenuTokens:
2296            if node.name == "urlLink":
2297                link = self.presenter.getWikiDocument().makeFileUrlAbsPath(
2298                        node.url)
2299                if link is not None:
2300                    break
2301        else:
2302            return
2303
2304
2305#                 link = node.url
2306#
2307#                 if link.startswith(u"rel://"):
2308#                     link = StringOps.pathnameFromUrl(self.presenter.getMainControl().makeRelUrlAbsolute(link))
2309#                 else:
2310#                     break
2311
2312        if not isfile(link):
2313            self.presenter.displayErrorMessage(_(u"File does not exist"))
2314            return
2315
2316        path = dirname(link)
2317        filename = basename(link)
2318
2319        newName = filename
2320        while True:
2321            newName = wx.GetTextFromUser(_(u"Enter new name"),
2322                    _(u"Rename File"), newName, self)
2323            if not newName:
2324                # User cancelled
2325                return
2326
2327            newfile = join(path, newName)
2328           
2329            if exists(newfile):
2330                if not isfile(newfile):
2331                    self.presenter.displayErrorMessage(
2332                            _(u"Target is not a file"))
2333                    continue
2334
2335                choice = wx.MessageBox(
2336                        _("Target file exists already. Overwrite?"),
2337                        _("Overwrite File"),
2338                        wx.YES_NO | wx.CANCEL  | wx.NO_DEFAULT | wx.ICON_QUESTION,
2339                        self)
2340                if choice == wx.CANCEL:
2341                    return
2342                elif choice == wx.NO:
2343                    continue
2344
2345            # Either file doesn't exist or user allowed overwrite
2346           
2347            OsAbstract.moveFile(link, newfile)
2348           
2349            if node.url.startswith(u"rel://"):
2350                # Relative URL/path
2351                newUrl = self.presenter.getWikiDocument().makeAbsPathRelUrl(
2352                        newfile)
2353            else:
2354                # Absolute URL/path
2355                newUrl = u"file:" + StringOps.urlFromPathname(newfile)
2356
2357            self.replaceTextAreaByCharPos(newUrl, node.coreNode.pos,
2358                    node.coreNode.pos + node.coreNode.strLength)
2359
2360            return
2361
2362
2363
2364    def convertUrlAbsoluteRelative(self, tokenList):
2365        for node in tokenList:
2366            if node.name == "urlLink":
2367                link = node.url
2368
2369                if ' ' in node.coreNode.getString():
2370                    addSafe = ' '
2371                else:
2372                    addSafe = ''
2373
2374                if link.startswith(u"rel://") or link.startswith(u"wikirel://"):
2375                    link = self.presenter.getWikiDocument()\
2376                            .makeRelUrlAbsolute(link, addSafe=addSafe)
2377
2378                elif link.startswith(u"file:"):
2379                    link = self.presenter.getWikiDocument()\
2380                            .makeAbsPathRelUrl(StringOps.pathnameFromUrl(
2381                            link), addSafe=addSafe)
2382                    if link is None:
2383                        continue # TODO Message?
2384                elif link.startswith(u"wiki:"):
2385                    link = self.presenter.getWikiDocument()\
2386                            .makeAbsPathRelUrl(StringOps.pathnameFromUrl(
2387                            link), addSafe=addSafe)
2388                    if link is None:
2389                        continue # TODO Message?
2390                    else:
2391                        link = u"wiki" + link  # Combines to "wikirel://"
2392
2393                else:
2394                    continue
2395
2396                self.replaceTextAreaByCharPos(link, node.coreNode.pos,
2397                        node.coreNode.pos + node.coreNode.strLength)
2398
2399                break
2400
2401
2402    def convertSelectedUrlAbsoluteRelative(self):
2403        tokenList = self.getTokensForMousePos(None)
2404        self.convertUrlAbsoluteRelative(tokenList)
2405
2406
2407    def OnConvertUrlAbsoluteRelativeThis(self, evt):
2408        if self.contextMenuTokens:
2409            self.convertUrlAbsoluteRelative(self.contextMenuTokens)
2410
2411
2412    def OnClipboardCopyUrlToThisAnchor(self, evt):
2413        wikiWord = self.presenter.getWikiWord()
2414        if wikiWord is None:
2415            wx.MessageBox(
2416                    _(u"This can only be done for the page of a wiki word"),
2417                    _(u'Not a wiki page'), wx.OK, self)
2418            return
2419
2420        path = self.presenter.getWikiDocument().getWikiConfigPath()
2421        for node in self.contextMenuTokens:
2422            if node.name == "anchorDef":
2423                copyTextToClipboard(StringOps.pathWordAndAnchorToWikiUrl(path,
2424                        wikiWord, node.anchorLink))
2425                return
2426
2427
2428    # TODO More efficient
2429    def evalScriptBlocks(self, index=-1):
2430        """
2431        Evaluates scripts. Respects "script_security_level" option
2432        """
2433        securityLevel = self.presenter.getConfig().getint(
2434                "main", "script_security_level")
2435        if securityLevel == 0:
2436            # No scripts allowed
2437            # Print warning message
2438            wx.MessageBox(_(u"Set in menu \"Wiki\", item \"Options...\", "
2439                    "options page \"Security\", \n"
2440                    "item \"Script security\" an appropriate value "
2441                    "to execute a script."), _(u"Script execution disabled"),
2442                    wx.OK, self.presenter.getMainControl())
2443            return
2444
2445        SCRIPTFORMAT = "script"
2446        # it is important to python to have consistent eol's
2447        self.ConvertEOLs(self.eolMode)
2448        (startPos, endPos) = self.GetSelection()
2449
2450        # if no selection eval all scripts
2451        if startPos == endPos or index > -1:
2452            # Execute all or selected script blocks on the page (or other
2453            #   related pages)
2454            try:
2455                pageAst = self.getPageAst()
2456            except NoPageAstException:
2457                return
2458
2459            scriptNodeGroups = [list(pageAst.iterDeepByName(SCRIPTFORMAT))]
2460
2461            # process script imports
2462            if securityLevel > 1: # Local import_scripts attributes allowed
2463                if self.getLoadedDocPage().getAttributes().has_key(
2464                        u"import_scripts"):
2465                    scriptNames = self.getLoadedDocPage().getAttributes()[
2466                            u"import_scripts"]
2467                    for sn in scriptNames:
2468                        try:
2469                            importPage = self.presenter.getWikiDocument().\
2470                                    getWikiPage(sn)
2471                            pageAst = importPage.getLivePageAst()
2472                            scriptNodeGroups.append(list(
2473                                    pageAst.iterDeepByName(SCRIPTFORMAT)))
2474                        except:
2475                            pass
2476
2477            if securityLevel > 2: # global.import_scripts attribute also allowed
2478                globScriptName = self.presenter.getWikiDocument().getWikiData().\
2479                        getGlobalAttributes().get(u"global.import_scripts")
2480
2481                if globScriptName is not None:
2482                    try:
2483                        importPage = self.presenter.getWikiDocument().\
2484                                getWikiPage(globScriptName)
2485                        pageAst = importPage.getLivePageAst()
2486                        scriptNodeGroups.append(list(
2487                                    pageAst.iterDeepByName(SCRIPTFORMAT)))
2488                    except:
2489                        pass
2490
2491            if self.presenter.getConfig().getboolean("main",
2492                    "script_search_reverse", False):
2493                scriptNodeGroups.reverse()
2494
2495            scriptNodes = reduce(lambda a, b: a + b, scriptNodeGroups)
2496
2497            for node in scriptNodes:
2498                script = node.findFlatByName("code").getString()
2499                script = re.sub(u"^[\r\n\s]+", u"", script)
2500                script = re.sub(u"[\r\n\s]+$", u"", script)
2501                try:
2502                    if index == -1:
2503                        script = re.sub(u"^\d:?\s?", u"", script)
2504                        exec(script) in self.evalScope
2505                    elif index > -1 and script.startswith(str(index)):
2506                        script = re.sub(u"^\d:?\s?", u"", script)
2507                        exec(script) in self.evalScope
2508                        break # Execute only the first found script
2509
2510                except Exception, e:
2511                    s = StringIO()
2512                    traceback.print_exc(file=s)
2513                    self.AddText(_(u"\nException: %s") % s.getvalue())
2514        else:
2515            # Evaluate selected text
2516            text = self.GetSelectedText()
2517            try:
2518                compThunk = compile(re.sub(u"[\n\r]", u"", text), "<string>",
2519                        "eval", CO_FUTURE_DIVISION)
2520                result = eval(compThunk, self.evalScope)
2521            except Exception, e:
2522                s = StringIO()
2523                traceback.print_exc(file=s)
2524                result = s.getvalue()
2525
2526            pos = self.GetCurrentPos()
2527            self.GotoPos(endPos)
2528            self.AddText(u" = %s" % unicode(result))
2529            self.GotoPos(pos)
2530
2531
2532    def cleanAutoGenAreas(self, text):
2533        """
2534        Remove any content from the autogenerated areas and return
2535        cleaned text. Call this before storing page in the database.
2536        The original text is returned if option
2537        "process_autogenerated_areas" is False.
2538        """
2539        return text
2540        # TODO: Reactivate function
2541#         if not self.presenter.getConfig().getboolean("main",
2542#                 "process_autogenerated_areas"):
2543#             return text
2544#
2545#         return WikiFormatting.AutoGenAreaRE.sub(ur"\1\2\4", text)
2546
2547
2548    def _agaReplace(self, match):
2549        try:
2550            result = unicode(eval(match.group(2), self.evalScope))
2551        except Exception, e:
2552            s = StringIO()
2553            traceback.print_exc(file=s)
2554            result = unicode(s.getvalue())
2555
2556        if len(result) == 0 or result[-1] != u"\n":
2557            result += u"\n"
2558
2559        return match.group(1) + match.group(2) + result + match.group(4)
2560
2561
2562    def updateAutoGenAreas(self, text):
2563        """
2564        Update content of the autogenerated areas and return
2565        updated text. Call this before loading the text in the editor
2566        and on user request. The original text is returned if
2567        option "process_autogenerated_areas" is False.
2568        """
2569        return text
2570        # TODO: Reactivate function
2571
2572#         if not self.presenter.getConfig().getboolean("main",
2573#                 "process_autogenerated_areas"):
2574#             return text
2575#
2576#         # So the text can be referenced from an AGA function
2577#         self.agatext = text
2578#
2579#         return WikiFormatting.AutoGenAreaRE.sub(self._agaReplace, text)
2580
2581
2582    def getAgaCleanedText(self):
2583        """
2584        Get editor text after cleaning of autogenerated area content
2585        if configuration option is set appropriately, otherwise, the
2586        text is not modified
2587        """
2588        return self.cleanAutoGenAreas(self.GetText())
2589
2590
2591    def setTextAgaUpdated(self, text):
2592        """
2593        Set editor text after updating of autogenerated area content
2594        if configuration option is set appropriately, otherwise, the
2595        text is not modified
2596        """
2597        self.SetText(self.updateAutoGenAreas(text))
2598
2599
2600    # TODO  Reflect possible changes in WikidPadParser.py
2601    AGACONTENTTABLERE = re.compile(ur"^(\+{1,4})([^\n\+][^\n]*)", re.DOTALL | re.LOCALE | re.MULTILINE)
2602
2603    def agaContentTable(self, omitfirst = False):
2604        """
2605        Can be called by an aga to present the content table of the current page.
2606        The text is assumed to be in self.agatext variable(see updateAutoGenAreas()).
2607        If omitfirst is true, the first entry (normally the title) is not shown.
2608        """
2609        allmatches = map(lambda m: m.group(0), self.AGACONTENTTABLERE.finditer(self.agatext))
2610        if omitfirst and len(allmatches) > 0:
2611            allmatches = allmatches[1:]
2612
2613        return u"\n".join(allmatches)
2614
2615
2616        # TODO Multi column support
2617    def agaFormatList(self, l):
2618        """
2619        Format a list l of strings in a nice way for an aga content
2620        """
2621        return u"\n".join(l)
2622
2623
2624    def agaParentsTable(self):
2625        """
2626        Can be called by an aga to present all parents of the current page.
2627        """
2628        relations = self.getLoadedDocPage().getParentRelationships()[:]
2629
2630        # Apply sort order
2631        relations.sort(key=string.lower) # sort alphabetically
2632
2633        return self.agaFormatList(relations)
2634
2635
2636    def ensureTextRangeByBytePosExpanded(self, byteStart, byteEnd):
2637        self.repairFoldingVisibility()
2638
2639        startLine = self.LineFromPosition(byteStart)
2640        endLine = self.LineFromPosition(byteEnd)
2641
2642        # Just to be sure, shouldn't happen normally
2643        if endLine < startLine:
2644            startLine, endLine = endLine, startLine
2645
2646        for checkLine in xrange(endLine, startLine - 1, -1):
2647            if not self.GetLineVisible(checkLine):
2648                line = checkLine
2649
2650                while True:
2651                    line = self.GetFoldParent(line)
2652                    if line == -1:
2653                        break
2654                    if not self.GetFoldExpanded(line):
2655                        self.ToggleFold(line)
2656
2657
2658
2659    def ensureSelectionExpanded(self):
2660        """
2661        Ensure that the selection is visible and not in a folded area
2662        """
2663        byteStart = self.GetSelectionStart()
2664        byteEnd = self.GetSelectionEnd()
2665
2666        self.ensureTextRangeByBytePosExpanded(byteStart, byteEnd)
2667
2668        self.SetSelection(byteStart, byteEnd)
2669
2670
2671    def setSelectionForIncSearchByCharPos(self, start, end):
2672        """
2673        Overwrites SearchableScintillaControl.setSelectionForIncSearchByCharPos
2674        Called during incremental search to select text. Will be called with
2675        start=-1 if nothing is found to select.
2676        This variant handles showing/hiding of folded lines
2677        """
2678
2679        # Hide lines which were previously shown
2680        if self.incSearchPreviousHiddenLines is not None:
2681            line = self.incSearchPreviousHiddenStartLine
2682            for state in self.incSearchPreviousHiddenLines:
2683                if state:
2684                    self.ShowLines(line, line)
2685                else:
2686                    self.HideLines(line, line)
2687
2688                line += 1
2689
2690        self.incSearchPreviousHiddenLines = None
2691        self.incSearchPreviousHiddenStartLine = -1
2692
2693        if start == -1:
2694#             self.SetSelection(-1, -1)
2695            self.SetSelection(self.GetSelectionStart(), self.GetSelectionStart())
2696            return
2697        text = self.GetText()
2698
2699        byteStart = self.bytelenSct(text[:start])
2700        byteEnd = byteStart + self.bytelenSct(text[start:end])
2701        startLine = self.LineFromPosition(byteStart)
2702        endLine = self.LineFromPosition(byteEnd)
2703
2704        # Store current show/hide state of lines to show
2705        shownList = []
2706        for i in xrange(startLine, endLine + 1):
2707            shownList.append(self.GetLineVisible(i))
2708
2709        self.incSearchPreviousHiddenLines = shownList
2710        self.incSearchPreviousHiddenStartLine = startLine
2711
2712        # Show lines
2713        self.ShowLines(startLine, endLine)
2714        self.SetSelection(byteStart, byteEnd)
2715
2716
2717
2718    def startIncrementalSearch(self, initSearch=None):
2719        self.incSearchPreviousHiddenLines = None
2720        self.incSearchPreviousHiddenStartLine = -1
2721
2722        super(WikiTxtCtrl, self).startIncrementalSearch(initSearch)
2723
2724
2725    def endIncrementalSearch(self):
2726        super(WikiTxtCtrl, self).endIncrementalSearch()
2727
2728        self.ensureSelectionExpanded()
2729
2730
2731    def rewrapText(self):
2732        return self.wikiLanguageHelper.handleRewrapText(self, {})
2733
2734
2735    def getNearestWordPositions(self, bytepos=None):
2736        if not bytepos:
2737            bytepos = self.GetCurrentPos()
2738        return (self.WordStartPosition(bytepos, 1), self.WordEndPosition(bytepos, 1))
2739
2740
2741    def autoComplete(self):
2742        """
2743        Called when user wants autocompletion.
2744        """
2745        text = self.GetText()
2746        wikiDocument = self.presenter.getWikiDocument()
2747        closingBracket = self.presenter.getConfig().getboolean("main",
2748                "editor_autoComplete_closingBracket", False)
2749
2750        bytePos = self.GetCurrentPos()
2751        lineStartBytePos = self.PositionFromLine(self.LineFromPosition(bytePos))
2752
2753        lineStartCharPos = len(self.GetTextRange(0, lineStartBytePos))
2754        charPos = lineStartCharPos + len(self.GetTextRange(lineStartBytePos,
2755                bytePos))
2756
2757        acResultTuples = self.wikiLanguageHelper.prepareAutoComplete(self, text,
2758                charPos, lineStartCharPos, wikiDocument, self.getLoadedDocPage(),
2759                {"closingBracket": closingBracket, "builtinAttribs": True})
2760
2761        if len(acResultTuples) > 0:
2762            self.presenter.getWikiDocument().getCollator().sortByFirst(
2763                    acResultTuples)
2764
2765            self.autoCompBackBytesMap = dict( (
2766                    (art[1], self.bytelenSct(text[charPos - art[2]:charPos]))
2767                    for art in acResultTuples) )
2768
2769            self.UserListShow(1, u"\x01".join(
2770                    [art[1] for art in acResultTuples]))
2771
2772
2773    def OnModified(self, evt):
2774        if not self.ignoreOnChange:
2775
2776            if evt.GetModificationType() & \
2777                    (wx.stc.STC_MOD_INSERTTEXT | wx.stc.STC_MOD_DELETETEXT):
2778
2779                self.presenter.informEditorTextChanged(self)
2780
2781#                 docPage = self.getLoadedDocPage()
2782
2783
2784
2785
2786    def OnCharAdded(self, evt):
2787        "When the user presses enter reindent to the previous level"
2788
2789#         currPos = self.GetScrollPos(wxVERTICAL)
2790
2791        evt.Skip()
2792        key = evt.GetKey()
2793
2794        if key == 10:
2795            text = self.GetText()
2796            wikiDocument = self.presenter.getWikiDocument()
2797            bytePos = self.GetCurrentPos()
2798            lineStartBytePos = self.PositionFromLine(self.LineFromPosition(bytePos))
2799
2800            lineStartCharPos = len(self.GetTextRange(0, lineStartBytePos))
2801            charPos = lineStartCharPos + len(self.GetTextRange(lineStartBytePos,
2802                    bytePos))
2803
2804            autoUnbullet = self.presenter.getConfig().getboolean("main",
2805                    "editor_autoUnbullets", False)
2806
2807            settings = {
2808                    "autoUnbullet": autoUnbullet,
2809                    "autoBullets": self.autoBullets,
2810                    "autoIndent": self.autoIndent
2811                    }
2812
2813            self.wikiLanguageHelper.handleNewLineAfterEditor(self, text,
2814                    charPos, lineStartCharPos, wikiDocument, settings)
2815
2816
2817    def _getExpandedByteSelectionToLine(self, extendOverChildren):
2818        """
2819        Move the start of current selection to start of the line it's in and
2820        move end of selection to end of its line.
2821        """
2822        selByteStart = self.GetSelectionStart();
2823        selByteEnd = self.GetSelectionEnd();
2824        lastLine = self.LineFromPosition(selByteEnd)
2825        selByteStart = self.PositionFromLine(self.LineFromPosition(selByteStart))
2826        selByteEnd = self.PositionFromLine(lastLine + 1)
2827
2828        if extendOverChildren:
2829            # Extend over all lines which are more indented than the last line
2830
2831            lastLineDeep = StringOps.splitIndentDeepness(self.GetLine(lastLine))[0]
2832
2833            testLine = lastLine + 1
2834            while True:
2835                testLineContent = self.GetLine(testLine)
2836                if len(testLineContent) == 0:
2837                    # End of text reached
2838                    break
2839
2840                if StringOps.splitIndentDeepness(testLineContent)[0] <= lastLineDeep:
2841                    break
2842
2843                testLine += 1
2844
2845            selByteEnd = self.PositionFromLine(testLine)
2846
2847        self.SetSelectionMode(0)
2848        self.SetSelectionStart(selByteStart)
2849        self.SetSelectionMode(0)
2850        self.SetSelectionEnd(selByteEnd)
2851
2852        return selByteStart, selByteEnd
2853
2854
2855
2856    def moveSelectedLinesOneUp(self, extendOverChildren):
2857        """
2858        Extend current selection to full logical lines and move selected lines
2859        upward one line.
2860        extendOverChildren -- iff true, extend selection over lines more indented
2861            below the initial selection
2862        """
2863        self.BeginUndoAction()
2864        try:
2865            selByteStart, selByteEnd = self._getExpandedByteSelectionToLine(
2866                    extendOverChildren)
2867
2868            firstLine = self.LineFromPosition(selByteStart)
2869            if firstLine > 0:
2870                content = self.GetSelectedText()
2871                if len(content) > 0:
2872                    if content[-1] == u"\n":
2873                        selByteEnd -= 1
2874                    else:
2875                        content += u"\n"
2876                    # Now content ends with \n and selection end points
2877                    # before this newline
2878                    self.ReplaceSelection("")
2879                    target = self.PositionFromLine(firstLine - 1)
2880                    self.InsertText(target, content)
2881                    self.SetSelectionMode(0)
2882                    self.SetSelectionStart(target)
2883                    self.SetSelectionMode(0)
2884                    self.SetSelectionEnd(target + (selByteEnd - selByteStart))
2885        finally:
2886            self.EndUndoAction()
2887
2888
2889
2890    def moveSelectedLinesOneDown(self, extendOverChildren):
2891        """
2892        Extend current selection to full logical lines and move selected lines
2893        upward one line.
2894        extendOverChildren -- iff true, extend selection over lines more indented
2895            below the initial selection
2896        """
2897        self.BeginUndoAction()
2898        try:
2899            selByteStart, selByteEnd = self._getExpandedByteSelectionToLine(
2900                    extendOverChildren)
2901
2902            lastLine = self.LineFromPosition(selByteEnd)
2903            lineCount = self.GetLineCount() - 1
2904            if lastLine <= lineCount:
2905                content = self.GetSelectedText()
2906                if len(content) > 0:
2907                    # Now content ends with \n and selection end points
2908                    # before this newline
2909                    target = self.PositionFromLine(lastLine + 1)
2910                    target -= selByteEnd - selByteStart
2911
2912                    if content[-1] == u"\n"# Necessary for downward move?
2913                        selByteEnd -= 1
2914                    else:
2915                        content += u"\n"
2916
2917                    self.ReplaceSelection("")
2918                    if self.GetTextRange(target - 1,
2919                            target) != u"\n":
2920                        self.InsertText(target, u"\n")
2921                        target += 1
2922
2923                    self.InsertText(target, content)
2924                    self.SetSelectionMode(0)
2925                    self.SetSelectionStart(target)
2926                    self.SetSelectionMode(0)
2927                    self.SetSelectionEnd(target + (selByteEnd - selByteStart))
2928        finally:
2929            self.EndUndoAction()
2930
2931
2932
2933    def OnKeyDown(self, evt):
2934        key = evt.GetKeyCode()
2935
2936        self.lastKeyPressed = time()
2937        accP = getAccelPairFromKeyDown(evt)
2938        matchesAccelPair = self.presenter.getMainControl().keyBindings.\
2939                matchesAccelPair
2940
2941#         if self.pageType == u"texttree":
2942#             if accP in ( (wx.ACCEL_ALT, wx.WXK_NUMPAD_UP),
2943#                     (wx.ACCEL_ALT, wx.WXK_UP),
2944#                     (wx.ACCEL_SHIFT | wx.ACCEL_ALT, wx.WXK_NUMPAD_UP),
2945#                     (wx.ACCEL_SHIFT | wx.ACCEL_ALT, wx.WXK_UP) ):
2946#
2947#                 self.moveSelectedLinesOneUp(accP[0] & wx.ACCEL_SHIFT)
2948#                 return
2949#             elif accP in ( (wx.ACCEL_ALT, wx.WXK_NUMPAD_DOWN),
2950#                     (wx.ACCEL_ALT, wx.WXK_DOWN),
2951#                     (wx.ACCEL_SHIFT | wx.ACCEL_ALT, wx.WXK_NUMPAD_DOWN),
2952#                     (wx.ACCEL_SHIFT | wx.ACCEL_ALT, wx.WXK_DOWN) ):
2953#
2954#                 self.moveSelectedLinesOneDown(accP[0] & wx.ACCEL_SHIFT)
2955#                 return
2956#
2957#             evt.Skip()
2958
2959
2960        if matchesAccelPair("AutoComplete", accP):
2961            # AutoComplete is normally Ctrl-Space
2962            # Handle autocompletion
2963            self.autoComplete()
2964
2965        elif matchesAccelPair("ActivateLink2", accP):
2966            # ActivateLink2 is normally Ctrl-Return
2967            self.activateLink()
2968
2969        elif matchesAccelPair("ActivateLinkBackground", accP):
2970            # ActivateLink2 is normally Ctrl-Return
2971            self.activateLink(tabMode=3)
2972
2973        elif matchesAccelPair("ActivateLink", accP):
2974            # ActivateLink is normally Ctrl-L
2975            # There is also a shortcut for it. This can only happen
2976            # if OnKeyDown is called indirectly
2977            # from IncrementalSearchDialog.OnKeyDownInput
2978            self.activateLink()
2979
2980        elif matchesAccelPair("ActivateLinkNewTab", accP):
2981            # ActivateLinkNewTab is normally Ctrl-Alt-L
2982            # There is also a shortcut for it. This can only happen
2983            # if OnKeyDown is called indirectly
2984            # from IncrementalSearchDialog.OnKeyDownInput
2985            self.activateLink(tabMode=2)
2986
2987        elif matchesAccelPair("ActivateLinkNewWindow", accP):
2988            self.activateLink(tabMode=6)
2989
2990        elif matchesAccelPair("LogLineUp", accP):
2991            # LogLineUp is by default undefined
2992            self.moveSelectedLinesOneUp(False)
2993        elif matchesAccelPair("LogLineUpWithIndented", accP):
2994            # LogLineUp is by default undefined
2995            self.moveSelectedLinesOneUp(True)
2996        elif matchesAccelPair("LogLineDown", accP):
2997            # LogLineUp is by default undefined
2998            self.moveSelectedLinesOneDown(False)
2999        elif matchesAccelPair("LogLineDownWithIndented", accP):
3000            # LogLineUp is by default undefined
3001            self.moveSelectedLinesOneDown(True)
3002
3003        elif not evt.ControlDown() and not evt.ShiftDown():  # TODO Check all modifiers
3004            if key == wx.WXK_TAB:
3005                if self.pageType == u"form":
3006                    if not self._goToNextFormField():
3007                        self.presenter.getMainControl().showStatusMessage(
3008                                _(u"No more fields in this 'form' page"), -1)
3009                    return
3010                evt.Skip()
3011            elif key == wx.WXK_RETURN and not self.AutoCompActive():
3012                text = self.GetText()
3013                wikiDocument = self.presenter.getWikiDocument()
3014                bytePos = self.GetCurrentPos()
3015                lineStartBytePos = self.PositionFromLine(self.LineFromPosition(bytePos))
3016
3017                lineStartCharPos = len(self.GetTextRange(0, lineStartBytePos))
3018                charPos = lineStartCharPos + len(self.GetTextRange(lineStartBytePos,
3019                        bytePos))
3020
3021                autoUnbullet = self.presenter.getConfig().getboolean("main",
3022                        "editor_autoUnbullets", False)
3023
3024                settings = {
3025                        "autoUnbullet": autoUnbullet,
3026                        "autoBullets": self.autoBullets,
3027                        "autoIndent": self.autoIndent
3028                        }
3029
3030                if self.wikiLanguageHelper.handleNewLineBeforeEditor(self, text,
3031                        charPos, lineStartCharPos, wikiDocument, settings):
3032                    evt.Skip()
3033                    return
3034
3035            else:
3036                evt.Skip()
3037
3038        else:
3039            super(WikiTxtCtrl, self).OnKeyDown(evt)
3040
3041
3042    def OnChar_ImeWorkaround(self, evt):
3043        """
3044        Workaround for problem of Scintilla with some input method editors,
3045        e.g. UniKey vietnamese IME.
3046        """
3047        key = evt.GetKeyCode()
3048
3049        # Return if this doesn't seem to be a real character input
3050        if evt.ControlDown() or (0 < key < 32):
3051            evt.Skip()
3052            return
3053
3054        if key >= wx.WXK_START and (not isUnicode() or evt.GetUnicodeKey() != key):
3055            evt.Skip()
3056            return
3057
3058        if isUnicode():
3059            unichar = unichr(evt.GetUnicodeKey())
3060        else:
3061            unichar = StringOps.mbcsDec(chr(key))[0]
3062
3063        self.ReplaceSelection(unichar)
3064
3065
3066    if isLinux():
3067        def OnSetFocus(self, evt):
3068#             self.presenter.makeCurrent()
3069            evt.Skip()
3070
3071            wikiPage = self.getLoadedDocPage()
3072            if wikiPage is None:
3073                return
3074            if not isinstance(wikiPage,
3075                    (DocPages.DataCarryingPage, DocPages.AliasWikiPage)):
3076                return
3077
3078            try:
3079                wikiPage.checkFileSignatureAndMarkDirty()
3080            except (IOError, OSError, DbAccessError), e:
3081                self.presenter.getMainControl().lostAccess(e)
3082
3083
3084    else:
3085        def OnSetFocus(self, evt):
3086            self.presenter.makeCurrent()
3087            evt.Skip()
3088
3089            wikiPage = self.getLoadedDocPage()
3090            if wikiPage is None:
3091                return
3092            if not isinstance(wikiPage,
3093                    (DocPages.DataCarryingPage, DocPages.AliasWikiPage)):
3094                return
3095
3096            try:
3097                wikiPage.checkFileSignatureAndMarkDirty()
3098            except (IOError, OSError, DbAccessError), e:
3099                self.presenter.getMainControl().lostAccess(e)
3100
3101
3102
3103
3104    def OnUserListSelection(self, evt):
3105        text = evt.GetText()
3106        toerase = self.autoCompBackBytesMap[text]
3107
3108        self.SetSelection(self.GetCurrentPos() - toerase, self.GetCurrentPos())
3109
3110        self.ReplaceSelection(text)
3111
3112
3113    def OnClick(self, evt):
3114        if evt.ControlDown():
3115            x = evt.GetX()
3116            y = evt.GetY()
3117            if not self.activateLink(wx.Point(x, y)):
3118                evt.Skip()
3119        else:
3120            evt.Skip()
3121
3122    def OnMiddleDown(self, evt):
3123        if not evt.ControlDown():
3124            middleConfig = self.presenter.getConfig().getint("main",
3125                    "mouse_middleButton_withoutCtrl", 2)
3126        else:
3127            middleConfig = self.presenter.getConfig().getint("main",
3128                    "mouse_middleButton_withCtrl", 3)
3129
3130        tabMode = Configuration.MIDDLE_MOUSE_CONFIG_TO_TABMODE[middleConfig]
3131
3132        if not self.activateLink(evt.GetPosition(), tabMode=tabMode):
3133            evt.Skip()
3134
3135
3136    def OnDoubleClick(self, evt):
3137        x = evt.GetX()
3138        y = evt.GetY()
3139        if not self.activateLink(wx.Point(x, y)):
3140            evt.Skip()
3141
3142#     def OnMouseMove(self, evt):
3143#         if (not evt.ControlDown()) or evt.Dragging():
3144#             self.SetCursor(WikiTxtCtrl.CURSOR_IBEAM)
3145#             evt.Skip()
3146#             return
3147#         else:
3148#             textPos = self.PositionFromPoint(evt.GetPosition())
3149#
3150#             if (self.isPositionInWikiWord(textPos) or
3151#                         self.isPositionInLink(textPos)):
3152#                 self.SetCursor(WikiTxtCtrl.CURSOR_HAND)
3153#                 return
3154#             else:
3155#                 # self.SetCursor(WikiTxtCtrl.CURSOR_IBEAM)
3156#                 evt.Skip()
3157#                 return
3158
3159
3160
3161#     def OnStyleDone(self, evt):
3162#         if evt.stylebytes:
3163#             self.applyStyling(evt.stylebytes)
3164#
3165#         if evt.foldingseq:
3166#             self.applyFolding(evt.foldingseq)
3167#
3168
3169
3170    def onIdleVisible(self, miscevt):
3171        if (self.IsEnabled()):
3172            if self.presenter.isCurrent():
3173                # fix the line, pos and col numbers
3174                currentLine = self.GetCurrentLine()+1
3175                currentPos = self.GetCurrentPos()
3176                currentCol = self.GetColumn(currentPos)
3177                self.presenter.SetStatusText(_(u"Line: %d Col: %d Pos: %d") %
3178                        (currentLine, currentCol, currentPos), 2)
3179
3180
3181    def OnDestroy(self, evt):
3182        # This is how the clipboard contents can be preserved after
3183        # the app has exited.
3184        wx.TheClipboard.Flush()
3185        evt.Skip()
3186
3187
3188    def OnMarginClick(self, evt):
3189        if evt.GetMargin() == self.FOLD_MARGIN:
3190            pos = evt.GetPosition()
3191            line = self.LineFromPosition(pos)
3192            modifiers = evt.GetModifiers() #?
3193
3194            self.ToggleFold(line)
3195            self.repairFoldingVisibility()
3196
3197        evt.Skip()
3198
3199
3200
3201    def _threadShowCalltip(self, wikiDocument, charPos, bytePos,
3202            threadstop=DUMBTHREADSTOP):
3203        try:
3204            docPage = self.getLoadedDocPage()
3205            if docPage is None:
3206                return
3207
3208            pageAst = docPage.getLivePageAst(threadstop=threadstop)
3209
3210            astNodes = pageAst.findNodesForCharPos(charPos)
3211
3212            if charPos > 0:
3213                # Maybe a token left to the cursor was meant, so check
3214                # one char to the left
3215                astNodes += pageAst.findNodesForCharPos(charPos - 1)
3216
3217            callTip = None
3218            for astNode in astNodes:
3219                if astNode.name == "wikiWord":
3220                    threadstop.testRunning()
3221                    wikiWord = wikiDocument.getWikiPageNameForLinkTerm(
3222                            astNode.wikiWord)
3223
3224                    # Set status to wikipage
3225                    callInMainThread(
3226                            self.presenter.getMainControl().showStatusMessage,
3227                            _(u"Link to page: %s") % wikiWord, 0)
3228
3229                    if wikiWord is not None:
3230                        propList = wikiDocument.getAttributeTriples(
3231                                wikiWord, u"short_hint", None)
3232                        if len(propList) > 0:
3233                            callTip = propList[-1][2]
3234                            break
3235                elif astNode.name == "urlLink":
3236                    # Should we show image preview tooltips for local URLs?
3237                    if not self.presenter.getConfig().getboolean("main",
3238                            "editor_imageTooltips_localUrls", True):
3239                        continue
3240
3241                    # Decision code taken from HtmlExporter.HtmlExporter._processUrlLink
3242                    if astNode.appendixNode is None:
3243                        appendixDict = {}
3244                    else:
3245                        appendixDict = dict(astNode.appendixNode.entries)
3246           
3247                    # Decide if this is an image link
3248                    if appendixDict.has_key("l"):
3249                        urlAsImage = False
3250                    elif appendixDict.has_key("i"):
3251                        urlAsImage = True
3252#                     elif self.asHtmlPreview and \
3253#                             self.mainControl.getConfig().getboolean(
3254#                             "main", "html_preview_pics_as_links"):
3255#                         urlAsImage = False
3256#                     elif not self.asHtmlPreview and self.addOpt[0]:
3257#                         urlAsImage = False
3258                    elif astNode.url.lower().split(".")[-1] in \
3259                            ("jpg", "jpeg", "gif", "png", "tif", "bmp"):
3260                        urlAsImage = True
3261                    else:
3262                        urlAsImage = False
3263
3264                    # If link is a picture display it as a tooltip
3265                    if urlAsImage:
3266                        path = self.presenter.getWikiDocument()\
3267                                .makeFileUrlAbsPath(astNode.url)
3268
3269                        if path is not None and isfile(path):
3270                            if imghdr.what(path):
3271                                config = self.presenter.getConfig()
3272                                maxWidth = config.getint("main",
3273                                        "editor_imageTooltips_maxWidth", 200)
3274                                maxHeight = config.getint("main",
3275                                        "editor_imageTooltips_maxHeight", 200)
3276
3277                                def SetImageTooltip(path):
3278                                    self.tooltip_image = ImageTooltipPanel(self,
3279                                            path, maxWidth, maxHeight)
3280                                threadstop.testRunning()
3281                                callInMainThread(SetImageTooltip, path)
3282                            else:
3283                                callTip = _(u"Not a valid image")
3284                            break
3285
3286            if callTip:
3287                threadstop.testRunning()
3288                callInMainThread(self.CallTipShow, bytePos, callTip)
3289
3290        except NotCurrentThreadException:
3291            pass
3292
3293    @contextlib.contextmanager
3294    def dwellLock(self):
3295        if self.dwellLockCounter == 0:
3296            self.OnDwellEnd(None)
3297       
3298        self.dwellLockCounter += 1
3299        yield
3300        self.dwellLockCounter -= 1
3301
3302
3303    def OnDwellStart(self, evt):
3304        if self.dwellLockCounter > 0:
3305            return
3306
3307        wikiDocument = self.presenter.getWikiDocument()
3308        if wikiDocument is None:
3309            return
3310        bytePos = evt.GetPosition()
3311        charPos = len(self.GetTextRange(0, bytePos))
3312
3313        thread = threading.Thread(target=self._threadShowCalltip,
3314                args=(wikiDocument, charPos, bytePos),
3315                kwargs={"threadstop": self.calltipThreadHolder})
3316
3317        self.calltipThreadHolder.setThread(thread)
3318        thread.setDaemon(True)
3319        thread.start()
3320
3321
3322    def OnDwellEnd(self, evt):
3323        if self.dwellLockCounter > 0:
3324            return
3325
3326        self.calltipThreadHolder.setThread(None)
3327        self.CallTipCancel()
3328
3329        # Set status back to nothing
3330        callInMainThread(self.presenter.getMainControl().showStatusMessage, "",
3331                0)
3332        # And close any shown pic
3333        if self.tooltip_image:
3334            self.tooltip_image.Close()
3335            self.tooltip_image = None
3336
3337    @staticmethod
3338    def userActionPasteFiles(unifActionName, paramDict):
3339        """
3340        User action to handle pasting or dropping of files into editor.
3341        """
3342        editor = paramDict.get("editor")
3343        if editor is None:
3344            return
3345
3346        filenames = paramDict.get("filenames")
3347        x = paramDict.get("x")
3348        y = paramDict.get("y")
3349
3350        dlgParams = WikiTxtDialogs.FilePasteParams()
3351#             config = editor.presenter.getMainControl().getConfig()
3352        dlgParams.readOptionsFromConfig(
3353                editor.presenter.getMainControl().getConfig())
3354
3355        if unifActionName == u"action/editor/this/paste/files/insert/url/ask":
3356            # Ask user
3357            if not paramDict.get("processDirectly", False):
3358                # If files are drag&dropped, at least on Windows the dragging
3359                # source (e.g. Windows Explorer) is halted until the drop
3360                # event returns.
3361                # So do an idle call to open dialog later
3362                paramDict["processDirectly"] = True
3363                wx.CallAfter(WikiTxtCtrl.userActionPasteFiles, unifActionName,
3364                        paramDict)
3365                return
3366
3367            dlgParams = WikiTxtDialogs.FilePasteDialog.runModal(
3368                    editor.presenter.getMainControl(), -1, dlgParams)
3369            if dlgParams is None:
3370                # User abort
3371                return
3372
3373            unifActionName = dlgParams.unifActionName
3374
3375        moveToStorage = False
3376
3377        if unifActionName == u"action/editor/this/paste/files/insert/url/absolute":
3378            modeToStorage = False
3379            modeRelativeUrl = False
3380        elif unifActionName == u"action/editor/this/paste/files/insert/url/relative":
3381            modeToStorage = False
3382            modeRelativeUrl = True
3383        elif unifActionName == u"action/editor/this/paste/files/insert/url/tostorage":
3384            modeToStorage = True
3385            modeRelativeUrl = False
3386        elif unifActionName == u"action/editor/this/paste/files/insert/url/movetostorage":
3387            modeToStorage = True
3388            modeRelativeUrl = False
3389            moveToStorage = True
3390        else:
3391            return
3392
3393        try:
3394            prefix = StringOps.strftimeUB(dlgParams.rawPrefix)
3395        except:
3396            traceback.print_exc()
3397            prefix = u""   # TODO Error message?
3398
3399        try:
3400            middle = StringOps.strftimeUB(dlgParams.rawMiddle)
3401        except:
3402            traceback.print_exc()
3403            middle = u" "   # TODO Error message?
3404
3405        try:
3406            suffix = StringOps.strftimeUB(dlgParams.rawSuffix)
3407        except:
3408            traceback.print_exc()
3409            suffix = u""   # TODO Error message?
3410
3411
3412        urls = []
3413
3414        for fn in filenames:
3415            protocol = None
3416            if fn.endswith(u".wiki"):
3417                protocol = "wiki"
3418
3419            toStorage = False
3420            if modeToStorage and protocol is None:
3421                # Copy file into file storage
3422                fs = editor.presenter.getWikiDocument().getFileStorage()
3423                try:
3424                    fn = fs.createDestPath(fn, move=moveToStorage)
3425                    toStorage = True
3426                except Exception, e:
3427                    traceback.print_exc()
3428                    editor.presenter.getMainControl().displayErrorMessage(
3429                            _(u"Couldn't copy file"), e)
3430                    return
3431
3432            urls.append(editor.wikiLanguageHelper.createUrlLinkFromPath(
3433                    editor.presenter.getWikiDocument(), fn,
3434                    relative=modeRelativeUrl or toStorage,
3435                    bracketed=dlgParams.bracketedUrl, protocol=protocol))
3436
3437        editor.handleDropText(x, y, prefix + middle.join(urls) + suffix)
3438
3439
3440    def GetEOLChar(self):
3441        """
3442        Gets the end of line char currently being used
3443        """
3444        m_id = self.GetEOLMode()
3445        if m_id == wx.stc.STC_EOL_CR:
3446            return u'\r'
3447        elif m_id == wx.stc.STC_EOL_CRLF:
3448            return u'\r\n'
3449        else:
3450            return u'\n'
3451
3452    def GetLastVisibleLine(self):
3453        """
3454        Returns line number of the first visible line in viewport
3455        """
3456        return self.GetFirstVisibleLine() + self.LinesOnScreen() - 1
3457
3458    def GetMiddleVisibleLine(self):
3459        """
3460        Returns line number of the middle visible line in viewport
3461        """
3462        # TODO: Fix this for long lines
3463        fl = self.GetFirstVisibleLine()
3464        ll = self.GetLastVisibleLine()
3465
3466        lines = ll - fl
3467
3468        mid = fl + lines // 2
3469
3470        #if self.LinesOnScreen() < self.GetLineCount():
3471        #    # This may return a float with .5  Really wanted? (MB)
3472        #    mid = (fl + (self.LinesOnScreen() / 2))
3473        #else:
3474        #    mid = (fl + (self.GetLineCount() / 2))
3475        return mid
3476
3477    # TODO
3478#     def setMouseCursor(self):
3479#         """
3480#         Set the right mouse cursor depending on some circumstances.
3481#         Returns True iff a special cursor was choosen.
3482#         """
3483#         mousePos = wxGetMousePosition()
3484#         mouseBtnPressed = wxGetKeyState(WXK_LBUTTON) or \
3485#                 wxGetKeyState(WXK_MBUTTON) or \
3486#                 wxGetKeyState(WXK_RBUTTON)
3487#
3488#         ctrlPressed = wxGetKeyState(WXK_CONTROL)
3489#
3490#         if (not ctrlPressed) or mouseBtnPressed:
3491#             self.SetCursor(WikiTxtCtrl.CURSOR_IBEAM)
3492#             return False
3493#         else:
3494#             linkPos = self.PositionFromPoint(wxPoint(*self.ScreenToClientXY(*mousePos)))
3495#
3496#             if (self.isPositionInWikiWord(linkPos) or
3497#                         self.isPositionInLink(linkPos)):
3498#                 self.SetCursor(WikiTxtCtrl.CURSOR_HAND)
3499#                 return True
3500#             else:
3501#                 self.SetCursor(WikiTxtCtrl.CURSOR_IBEAM)
3502#                 return False
3503
3504
3505class WikiTxtCtrlDropTarget(wx.PyDropTarget):
3506    def __init__(self, editor):
3507        wx.PyDropTarget.__init__(self)
3508
3509        self.editor = editor
3510        self.resetDObject()
3511
3512    def resetDObject(self):
3513        """
3514        (Re)sets the dataobject at init and after each drop
3515        """
3516        dataob = wx.DataObjectComposite()
3517        self.tobj = wx.TextDataObject()  # Char. size depends on wxPython build!
3518
3519        dataob.Add(self.tobj)
3520
3521        self.fobj = wx.FileDataObject()
3522        dataob.Add(self.fobj)
3523
3524        self.dataob = dataob
3525        self.SetDataObject(dataob)
3526
3527
3528    def OnDragOver(self, x, y, defresult):
3529        return self.editor.DoDragOver(x, y, defresult)
3530
3531
3532    def OnData(self, x, y, defresult):
3533        try:
3534            if self.GetData():
3535                fnames = self.fobj.GetFilenames()
3536                text = self.tobj.GetText()
3537
3538                if fnames:
3539                    self.OnDropFiles(x, y, fnames)
3540                elif text:
3541                    text = StringOps.lineendToInternal(text)
3542                    self.OnDropText(x, y, text)
3543
3544            return defresult
3545
3546        finally:
3547            self.resetDObject()
3548
3549
3550    def OnDropText(self, x, y, text):
3551        text = StringOps.lineendToInternal(text)
3552        self.editor.handleDropText(x, y, text)
3553
3554
3555    def OnDropFiles(self, x, y, filenames):
3556        urls = []
3557
3558        # Necessary because key state may change during the loop
3559        controlPressed = wx.GetKeyState(wx.WXK_CONTROL)
3560        shiftPressed = wx.GetKeyState(wx.WXK_SHIFT)
3561
3562        if isLinux():
3563            # On Linux, at least Ubuntu, fn may be a UTF-8 encoded unicode(!?)
3564            # string
3565            try:
3566                filenames = [StringOps.utf8Dec(fn.encode("latin-1"))[0]
3567                        for fn in filenames]
3568            except (UnicodeEncodeError, UnicodeDecodeError):
3569                pass
3570
3571
3572        mc = self.editor.presenter.getMainControl()
3573
3574        paramDict = {"editor": self.editor, "filenames": filenames,
3575                "x": x, "y": y, "main control": mc}
3576
3577        if controlPressed:
3578            suffix = u"/modkeys/ctrl"
3579        elif shiftPressed:
3580            suffix = u"/modkeys/shift"
3581        else:
3582            suffix = u""
3583           
3584        mc.getUserActionCoord().reactOnUserEvent(
3585                u"mouse/leftdrop/editor/files" + suffix, paramDict)
3586
3587
3588
3589
3590
3591# User actions to register
3592# _ACTION_EDITOR_PASTE_FILES_ABSOLUTE = UserActionCoord.SimpleAction("",
3593#         u"action/editor/this/paste/files/insert/url/absolute",
3594#         WikiTxtCtrl.userActionPasteFiles)
3595#
3596# _ACTION_EDITOR_PASTE_FILES_RELATIVE = UserActionCoord.SimpleAction("",
3597#         u"action/editor/this/paste/files/insert/url/relative",
3598#         WikiTxtCtrl.userActionPasteFiles)
3599#
3600# _ACTION_EDITOR_PASTE_FILES_TOSTORAGE = UserActionCoord.SimpleAction("",
3601#         u"action/editor/this/paste/files/insert/url/tostorage",
3602#         WikiTxtCtrl.userActionPasteFiles)
3603#
3604# _ACTION_EDITOR_PASTE_FILES_ASK = UserActionCoord.SimpleAction("",
3605#         u"action/editor/this/paste/files/insert/url/ask",
3606#         WikiTxtCtrl.userActionPasteFiles)
3607#
3608#
3609# _ACTIONS = (
3610#         _ACTION_EDITOR_PASTE_FILES_ABSOLUTE, _ACTION_EDITOR_PASTE_FILES_RELATIVE,
3611#         _ACTION_EDITOR_PASTE_FILES_TOSTORAGE, _ACTION_EDITOR_PASTE_FILES_ASK)
3612
3613
3614# Register paste actions
3615_ACTIONS = tuple( UserActionCoord.SimpleAction("", unifName,
3616        WikiTxtCtrl.userActionPasteFiles) for unifName in (
3617            u"action/editor/this/paste/files/insert/url/absolute",
3618            u"action/editor/this/paste/files/insert/url/relative",
3619            u"action/editor/this/paste/files/insert/url/tostorage",
3620            u"action/editor/this/paste/files/insert/url/movetostorage",
3621            u"action/editor/this/paste/files/insert/url/ask") )
3622
3623
3624UserActionCoord.registerActions(_ACTIONS)
3625
3626
3627
3628_CONTEXT_MENU_INTEXT_SPELLING = \
3629u"""
3630-
3631Ignore;CMD_ADD_THIS_SPELLING_SESSION
3632Add Globally;CMD_ADD_THIS_SPELLING_GLOBAL
3633Add Locally;CMD_ADD_THIS_SPELLING_LOCAL
3634"""
3635
3636
3637_CONTEXT_MENU_INTEXT_BASE = \
3638u"""
3639-
3640Undo;CMD_UNDO
3641Redo;CMD_REDO
3642-
3643Cut;CMD_CLIPBOARD_CUT
3644Copy;CMD_CLIPBOARD_COPY
3645Paste;CMD_CLIPBOARD_PASTE
3646Delete;CMD_TEXT_DELETE
3647-
3648Select All;CMD_SELECT_ALL
3649"""
3650
3651
3652_CONTEXT_MENU_INTEXT_ACTIVATE = \
3653u"""
3654-
3655Follow Link;CMD_ACTIVATE_THIS
3656Follow Link New Tab;CMD_ACTIVATE_NEW_TAB_THIS
3657Follow Link New Tab Backgrd.;CMD_ACTIVATE_NEW_TAB_BACKGROUND_THIS
3658Follow Link New Window;CMD_ACTIVATE_NEW_WINDOW_THIS
3659"""
3660
3661_CONTEXT_MENU_INTEXT_WIKI_URL = \
3662u"""
3663-
3664Convert Absolute/Relative File URL;CMD_CONVERT_URL_ABSOLUTE_RELATIVE_THIS
3665Open Containing Folder;CMD_OPEN_CONTAINING_FOLDER_THIS
3666"""
3667
3668_CONTEXT_MENU_INTEXT_FILE_URL = \
3669u"""
3670-
3671Convert Absolute/Relative File URL;CMD_CONVERT_URL_ABSOLUTE_RELATIVE_THIS
3672Open Containing Folder;CMD_OPEN_CONTAINING_FOLDER_THIS
3673Rename file;CMD_RENAME_FILE
3674Delete file;CMD_DELETE_FILE
3675"""
3676
3677
3678_CONTEXT_MENU_INTEXT_URL_TO_CLIPBOARD = \
3679u"""
3680-
3681Copy Anchor URL to Clipboard;CMD_CLIPBOARD_COPY_URL_TO_THIS_ANCHOR
3682"""
3683
3684_CONTEXT_MENU_SELECT_TEMPLATE_IN_TEMPLATE_MENU = \
3685u"""
3686-
3687Other...;CMD_SELECT_TEMPLATE
3688"""
3689
3690_CONTEXT_MENU_SELECT_TEMPLATE = \
3691u"""
3692-
3693Use Template...;CMD_SELECT_TEMPLATE
3694"""
3695
3696
3697_CONTEXT_MENU_INTEXT_BOTTOM = \
3698u"""
3699-
3700Close Tab;CMD_CLOSE_CURRENT_TAB
3701"""
3702
3703
3704
3705FOLD_MENU = \
3706u"""
3707+Show folding;CMD_CHECKBOX_SHOW_FOLDING;Show folding marks and allow folding;*ShowFolding
3708&Toggle current folding;CMD_TOGGLE_CURRENT_FOLDING;Toggle folding of the current line;*ToggleCurrentFolding
3709&Unfold All;CMD_UNFOLD_ALL_IN_CURRENT;Unfold everything in current editor;*UnfoldAll
3710&Fold All;CMD_FOLD_ALL_IN_CURRENT;Fold everything in current editor;*FoldAll
3711"""
3712
3713
3714# Entries to support i18n of context menus
3715if False:
3716    N_(u"Ignore")
3717    N_(u"Add Globally")
3718    N_(u"Add Locally")
3719
3720    N_(u"Undo")
3721    N_(u"Redo")
3722    N_(u"Cut")
3723    N_(u"Copy")
3724    N_(u"Paste")
3725    N_(u"Delete")
3726    N_(u"Select All")
3727
3728    N_(u"Follow Link")
3729    N_(u"Follow Link New Tab")
3730    N_(u"Follow Link New Tab Backgrd.")
3731    N_(u"Follow Link New Window")
3732
3733    N_(u"Convert Absolute/Relative File URL")
3734    N_(u"Open Containing Folder")
3735    N_(u"Rename file")
3736    N_(u"Delete file")
3737
3738    N_(u"Copy anchor URL to clipboard")
3739
3740    N_(u"Other...")
3741    N_(u"Use Template...")
3742
3743    N_(u"Close Tab")
3744
3745    N_(u"Show folding")
3746    N_(u"Show folding marks and allow folding")
3747    N_(u"&Toggle current folding")
3748    N_(u"Toggle folding of the current line")
3749    N_(u"&Unfold All")
3750    N_(u"Unfold everything in current editor")
3751    N_(u"&Fold All")
3752    N_(u"Fold everything in current editor")
3753
3754
3755# I will move this to wxHelper later (MB)
3756try:
3757    class wxPopupOrFrame(wx.PopupWindow):
3758        def __init__(self, parent, id=-1, style=None):
3759            wx.PopupWindow.__init__(self, parent)
3760
3761except AttributeError:
3762    class wxPopupOrFrame(wx.Frame):
3763        def __init__(self, parent, id=-1,
3764                style=wx.NO_BORDER|wx.FRAME_NO_TASKBAR|wx.FRAME_FLOAT_ON_PARENT):
3765            wx.Frame.__init__(self, parent, id, style=style)
3766
3767
3768class ImageTooltipPanel(wxPopupOrFrame):
3769    """Quick panel for image tooltips"""
3770    def __init__(self, pWiki, filePath, maxWidth=200, maxHeight=200):
3771        wxPopupOrFrame.__init__(self, pWiki, -1)
3772
3773        self.url = filePath
3774        self.pWiki = pWiki
3775        self.firstMove = True
3776       
3777        img = wx.Image(filePath, wx.BITMAP_TYPE_ANY)
3778
3779        origWidth = img.GetWidth()
3780        origHeight = img.GetHeight()
3781
3782        # Set defaults for invalid values
3783        if maxWidth <= 0:
3784            maxWidth = 200
3785        if maxHeight <= 0:
3786            maxHeight = 200
3787
3788        if origWidth > 0 and origHeight > 0:
3789            self.width, self.height = calcResizeArIntoBoundingBox(origWidth,
3790                    origHeight, maxWidth, maxHeight)
3791           
3792            img.Rescale(self.width, self.height, quality = wx.IMAGE_QUALITY_HIGH)
3793        else:
3794            self.width = origWidth
3795            self.height = origHeight
3796
3797        img = img.ConvertToBitmap()
3798
3799        self.SetSize((self.width, self.height))
3800
3801        self.bmp = wx.StaticBitmap(self, -1, img, (0, 0), (img.GetWidth(), img.GetHeight()))
3802        self.bmp.Bind(wx.EVT_LEFT_DOWN, self.OnLeftClick)
3803        self.bmp.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClick)
3804        self.bmp.Bind(wx.EVT_MOTION, self.OnMouseMotion)
3805        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
3806
3807        mousePos = wx.GetMousePosition()
3808        # If possible the frame shouldn't be exactly under mouse pointer
3809        # so doubleclicking on link works
3810        mousePos.x += 1
3811        mousePos.y += 1
3812        WindowLayout.setWindowPos(self, mousePos, fullVisible=True)
3813
3814        self.Show()
3815        # Works for Windows (but not for GTK), maybe also for Mac (MB)
3816        self.GetParent().SetFocus() 
3817
3818    def Close(self, event=None):
3819        self.Destroy()
3820
3821    def OnLeftClick(self, event=None):
3822#         scrPos = self.ClientToScreen(evt.GetPosition())
3823        self.Close()
3824#         wnd = wx.FindWindowAtPoint(scrPos)
3825#         print "--OnLeftClick1", repr((self, wnd))
3826#         if wnd is not None:
3827#             cliPos = wnd.ScreenToClient(scrPos)
3828#             evt.m_x = cliPos.x
3829#             evt.m_y = cliPos.y
3830#             wnd.ProcessEvent(evt)
3831
3832    def OnRightClick(self, event=None):
3833        self.Close()
3834
3835    def OnMouseMotion(self, evt):
3836        if self.firstMove:
3837            self.firstMove = False
3838            evt.Skip()
3839            return
3840
3841        self.Close()
3842
3843
3844    def OnKeyDown(self, event):
3845        kc = event.GetKeyCode()
3846
3847        if kc == wx.WXK_ESCAPE:
3848            self.Close()
3849
3850
3851class ViHandler(ViHelper):
3852    # TODO: Add search commands
3853    #       Fix long line inconsistency?
3854    #       Brace matching - finish % cmd
3855    #       Find a way to monitor selection (to enter visual mode from mouse selecttion)
3856    #       autocompletion ctrl-n, ctrl-f
3857   
3858    def __init__(self, stc):
3859        ViHelper.__init__(self, stc)
3860
3861        self._anchor = None
3862
3863        wx.CallAfter(self.SetDefaultCaretColour)
3864        # Set default mode
3865        wx.CallAfter(self.SetMode, ViHelper.NORMAL)
3866        wx.CallAfter(self.Setup)
3867
3868        self.text_object_map = {
3869                    "w" : (False, self.SelectInWord),
3870                    "W" : (False, self.SelectInWORD),
3871                    "s" : (False, self.SelectInSentence),
3872                    "p" : (False, self.SelectInParagraph),
3873
3874                    "[" : (True, self.SelectInSquareBracket),
3875                    "]" : (True, self.SelectInSquareBracket),
3876
3877                    "(" : (True, self.SelectInRoundBracket),
3878                    ")" : (True, self.SelectInRoundBracket),
3879                    "b" : (True, self.SelectInRoundBracket),
3880
3881                    "<" : (True, self.SelectInInequalitySigns),
3882                    ">" : (True, self.SelectInInequalitySigns),
3883
3884                    "t" : (True, self.SelectInTagBlock),
3885
3886                    "{" : (True, self.SelectInBlock),
3887                    "}" : (True, self.SelectInBlock),
3888                    "B" : (True, self.SelectInBlock),
3889
3890                    '"' : (True, self.SelectInDoubleQuote),
3891                    "'" : (True, self.SelectInSingleQuote),
3892                    "`" : (True, self.SelectInTilde),
3893
3894                    # The commands below are not present in vim but may
3895                    # be useful for quickly editing parser syntax
3896                    u"\xc2" : (True, self.SelectInPoundSigns),
3897                    u"$" : (True, self.SelectInDollarSigns),
3898                    u"^" : (True, self.SelectInHats),
3899                    u"%" : (True, self.SelectInPercentSigns),
3900                    u"&" : (True, self.SelectInAmpersands),
3901                    u"*" : (True, self.SelectInStars),
3902                    u"-" : (True, self.SelectInHyphens),
3903                    u"_" : (True, self.SelectInUnderscores),
3904                    u"=" : (True, self.SelectInEqualSigns),
3905                    u"+" : (True, self.SelectInPlusSigns),
3906                    u"!" : (True, self.SelectInExclamationMarks),
3907                    u"?" : (True, self.SelectInQuestionMarks),
3908                    u"@" : (True, self.SelectInAtSigns),
3909                    u"#" : (True, self.SelectInHashs),
3910                    u"~" : (True, self.SelectInApproxSigns),
3911                    u"|" : (True, self.SelectInVerticalBars),
3912                    u";" : (True, self.SelectInSemicolons),
3913                    u":" : (True, self.SelectInColons),
3914                    u"\\" : (True, self.SelectInBackslashes),
3915                    u"/" : (True, self.SelectInForwardslashes),
3916                }
3917
3918    # Format
3919    # key code : (command type, (function, arguments), repeatable)
3920
3921    # command type -
3922    #               0 : Normal
3923    #               1 : Motion
3924    #               2 : Mode change
3925
3926    # function : function to call on keypress
3927
3928    # arguments : arguments can be None, a single argument or a dictionary
3929
3930    # repeatable - repeat types
3931    #               0 : Not repeatable
3932    #               1 : Normal repeat
3933    #               2 : Repeat with insertion (i.e. i/a/etc)
3934    #               3 : Replace
3935    # Note:
3936    # The repeats provided by ; and , are managed within the FindChar function
3937        self.keys = {
3938            0 : {
3939            # Normal mode
3940        (60, "motion")  : (0, (self.DedentText, None), 1), # <motion
3941        (62, "motion")  : (0, (self.IndentText, None), 1), # >motion
3942        (99, "motion")  : (2, (self.EndDeleteInsert, None), 2), # cmotion
3943        (100, "motion") : (0, (self.EndDelete, None), 1), # dmotion
3944        (100, 115, "*") : (0, (self.DeleteSurrounding, None), 1), # ds
3945        (121, "motion") : (0, (self.Yank, None), 1), # ymotion
3946        (99, 115, "*", "*") : (0, (self.ChangeSurrounding, None), 1), # cs**
3947        # TODO: yS and ySS (indentation on new line)
3948        (121, 115, "motion", "*") : (0, (self.PreSurround, None), 1), # ysmotion*
3949        (121, 115, 115, "*") : (0, (self.PreSurroundLine, None), 1), # yss*
3950        (103, 117, "motion") : (0, (self.PreLowercase, None), 1), # gu
3951        (103, 85, "motion") : (0, (self.PreUppercase, None), 1), # gU
3952        (39, "*")  : (1, (self.GotoMark, None), 0), # '
3953        (96, "*")  : (1, (self.GotoMarkIndent, None), 0), # `
3954        (109, "*") : (0, (self.Mark, None), 0), # m
3955        (102, "*") : (1, (self.FindNextChar, None), 4), # f
3956        (70, "*")  : (1, (self.FindNextCharBackwards, None), 4), # F
3957        (116, "*") : (1, (self.FindUpToNextChar, None), 5), # t
3958        (84, "*")  : (1, (self.FindUpToNextCharBackwards, None), 5), # T
3959        (114, "*") : (0, (self.ReplaceChar, None), 0), # r
3960
3961    (105,) : (0, (self.Insert, None), 2), # i
3962    (97,) : (0, (self.Append, None), 2), # a
3963    (73,) : (0, (self.InsertAtLineStart, None), 2), # I
3964    (65,) : (0, (self.AppendAtLineEnd, None), 2), # A
3965    (111,) : (0, (self.OpenNewLine, False), 2), # o
3966    (79,) : (0, (self.OpenNewLine, True), 2), # O
3967    (67,) : (0, (self.TruncateLineAndInsert, None), 2), # C
3968    (68,) : (0, (self.TruncateLine, None), 2), # D
3969
3970    (120,) : (0, (self.DeleteRight, None), 1), # x
3971    (88,) : (0, (self.DeleteLeft, None), 1), # X
3972
3973    (115,) : (0, (self.DeleteRightAndInsert, None), 2), # s
3974    (83,) : (0, (self.DeleteLinesAndIndentInsert, None), 2), # S
3975
3976    (119,) : (1, (self.MoveCaretNextWord, None), 0), # w
3977    (87,) : (1, (self.MoveCaretNextWORD, None), 0), # W
3978    (103, 101) : (1, (self.MoveCaretPreviousWordEnd, None), 0), # ge
3979    # TODO: gE
3980    (101,) : (1, (self.MoveCaretWordEnd, None), 0), # e
3981    (69,) : (1, (self.MoveCaretWordEND, None), 0), # E
3982    (98,) : (1, (self.MoveCaretBackWord, None), 0), # b
3983    (66,) : (1, (self.MoveCaretBackWORD, None), 0), # B
3984
3985    (123,) : (1, (self.MoveCaretParaUp, None), 0), # {
3986    (125,) : (1, (self.MoveCaretParaDown, None), 0), # }
3987
3988    # TODO: complete search
3989    # Search should use a custom implementation of wikidpads incremental search
3990    (47,)  : (0, (self.StartSearch, None), 0), # /
3991    #47  : (0, (self.StartSearchReverse, None), 0), # /
3992    (110,) : (1, (self.Repeat, self.ContinueLastSearchSameDirection), 0), # n
3993    (78,) : (1, (self.Repeat, self.ContinueLastSearchReverseDirection), 0), # N
3994
3995    (42,) : (1, (self.Repeat, self.SearchCaretWordForwards), 0), # *
3996    (35,) : (1, (self.Repeat, self.SearchCaretWordBackwards), 0), # #
3997
3998    (103, 42)  : (1, (self.Repeat, self.SearchPartialCaretWordForwards), 0), # g*
3999    (103, 35)  : (1, (self.Repeat, self.SearchPartialCaretWordBackwards), 0), # g#
4000
4001    # Basic movement
4002    # TODO: j and k should not act on screenlines
4003    (104,) : (1, (self.MoveCaretLeft, None), 0), # h
4004    (107,) : (1, (self.MoveCaretUp, None), 0), # k
4005    (108,) : (1, (self.MoveCaretRight, None), 0), # l
4006    (106,) : (1, (self.MoveCaretDown, None), 0), # j
4007    (103, 107) : (1, (self.MoveCaretUp, None), 0), # gk
4008    (103, 106) : (1, (self.MoveCaretDown, None), 0), # gj
4009    # Arrow keys
4010    (65361,) : (1, (self.MoveCaretLeft, None), 0), # left
4011    (65362,) : (1, (self.MoveCaretUp, None), 0), # up
4012    (65363,) : (1, (self.MoveCaretRight, None), 0), # right
4013    (65364,) : (1, (self.MoveCaretDown, None), 0), # down
4014
4015    (65293,) : (1, (self.MoveCaretDownAndIndent, None), 0), # enter
4016    (65293,) : (1, (self.MoveCaretDownAndIndent, None), ), # return
4017
4018    # Line movement
4019    (36,)    : (1, (self.GotoLineEnd, False), 0), # $
4020    (65367,) : (1, (self.GotoLineEnd, False), 0), # home
4021    (48,)    : (1, (self.GotoLineStart, None), 0), # 0
4022    (65360,) : (1, (self.GotoLineStart, None), 0), # end
4023    (45,)    : (1, (self.GotoLineIndentPreviousLine, None), 0), # -
4024    (43,)    : (1, (self.GotoLineIndentNextLine, None), 0), # +
4025    (94,)    : (1, (self.GotoLineIndent, None), 0), # ^
4026    (124,)   : (1, (self.GotoColumn, None), 0), # |
4027
4028    (40,)   : (1, (self.GotoSentenceStart, None), 0), # (
4029    (41,)   : (1, (self.GotoNextSentence, None), 0), # )
4030
4031    # Page scroll control
4032    (103, 103)  : (1, (self.DocumentNavigation, (103, 103)), 0), # gg
4033    (71,)        : (1, (self.DocumentNavigation, 71), 0), # G
4034    (37,)        : (1, (self.DocumentNavigation, 37), 0), # %
4035
4036    (72,)        : (1, (self.GotoViewportTop, None), 0), # H
4037    (76,)        : (1, (self.GotoViewportBottom, None), 0), # L
4038    (77,)        : (1, (self.GotoViewportMiddle, None), 0), # M
4039
4040    (122, 122)  : (0, (self.ScrollViewportMiddle, None), 0), # zz
4041    (122, 116)  : (0, (self.ScrollViewportTop, None), 0), # zt
4042    (122, 98)   : (0, (self.ScrollViewportBottom, None), 0), # zb
4043
4044    (("Ctrl", 117),)    : (0, (self.ScrollViewportUpHalfScreen,
4045                                                        None), 0), # <c-u>
4046    (("Ctrl", 100),)    : (0, (self.ScrollViewportDownHalfScreen,
4047                                                        None), 0), # <c-d>
4048    (("Ctrl", 98),)     : (0, (self.ScrollViewportUpFullScreen,
4049                                                        None), 0), # <c-b>
4050    (("Ctrl", 102),)    : (0, (self.ScrollViewportDownFullScreen,
4051                                                        None), 0), # <c-f>
4052
4053    (("Ctrl", 101),)    : (0, (self.ScrollViewportLineDown,
4054                                                        None), 0), # <c-e>
4055    (("Ctrl", 121),)    : (0, (self.ScrollViewportLineUp,
4056                                                        None), 0), # <c-y>
4057
4058    (90, 90)    : (0, (self.ctrl.presenter.getMainControl().\
4059                                        exitWiki, None), 0), # ZZ
4060
4061    (117,)              : (0, (self.Undo, None), 0), # u
4062    (("Ctrl", 114),)    : (0, (self.Redo, None), 0), # <c-r>
4063
4064    (("Ctrl", 105),)    : (1, (self.GotoNextJump, None), 0), # <c-i>
4065    (65289,)            : (1, (self.GotoNextJump, None), 0), # Tab
4066    (("Ctrl", 111),)    : (1, (self.GotoPreviousJump, None), 0), # <c-o>
4067
4068    # These two are motions
4069    (59,)   : (1, (self.RepeatLastFindCharCmd, None), 0), # ;
4070    (44,)   : (1, (self.RepeatLastFindCharCmdReverse, None), 0), # ,
4071
4072    # Replace ?
4073    #(114)   : (1, (self.ReplaceChar, None)), # r
4074    # repeatable?
4075    (82,)   : (0, (self.StartReplaceMode, None), 0), # R
4076
4077    (118,)   : (2, (self.EnterVisualMode, None), 0), # v
4078    (86,)   : (2, (self.EnterLineVisualMode, None), 0), # V
4079
4080    (74,)   : (0, (self.JoinLines, None), 1), # J
4081
4082    (126,)   : (0, (self.SwapCase, None), 0), # ~
4083
4084    (121, 121)  : (0, (self.YankLine, None), 0), # yy
4085    (89,)        : (0, (self.YankLine, None), 0), # Y
4086    (112,)       : (0, (self.Put, False), 0), # p
4087    (80,)        : (0, (self.Put, True), 0), # P
4088
4089    (100, 100)  : (0, (self.DeleteLine, None), 1), # dd
4090
4091    (62, 62)    : (0, (self.Indent, True), 1), # >>
4092    (60, 60)    : (0, (self.Indent, False), 1), # <<
4093
4094    (46,)    : (0, (self.RepeatCmd, None), 0), # .
4095
4096    # Wikipage navigation
4097    # As some command (e.g. HL) are already being used in most cases
4098    # these navigation commands have been prefixed by "g".
4099    # TODO: different repeat command for these?
4100    (103, 102)  : (0, (self.ctrl.activateLink, { "tabMode" : 0 }), 0), # gf
4101    (("Ctrl", 119), 103, 102)  : (0, (self.ctrl.activateLink, { "tabMode" : 2 }), 0), # <c-w>gf
4102    (103, 70)   : (0, (self.ctrl.activateLink, { "tabMode" : 2 }), 0), # gF
4103    (103, 98)   : (0, (self.ctrl.activateLink, { "tabMode" : 3 }), 0), # gb
4104    # This might be going a bit overboard with history nagivaiton!
4105    (103, 72)   : (0, (self.GoBackwardInHistory, None), 0), # gH
4106    (103, 76)   : (0, (self.GoForwardInHistory, None), 0), # gL
4107    (103, 104)  : (0, (self.GoBackwardInHistory, None), 0), # gh
4108    (103, 108)  : (0, (self.GoForwardInHistory, None), 0), # gl
4109    (91,)        : (0, (self.GoBackwardInHistory, None), 0), # [
4110    (93,)        : (0, (self.GoForwardInHistory, None), 0), # ]
4111    (103, 116) : (0, (self.SwitchTabs, None), 0), # gt
4112    (103, 84)  : (0, (self.SwitchTabs, True), 0), # gT
4113    (103, 114) : (0, (self.OpenHomePage, False), 0), # gr
4114    (103, 82) : (0, (self.OpenHomePage, True), 0), # gR
4115    (103, 111) : (0, (self.ctrl.presenter.getMainControl()). \
4116                                    showWikiWordOpenDialog, None, 0), # go
4117    # TODO: rewrite open dialog so it can be opened with new tab as default
4118    (103, 79): (0, (self.ctrl.presenter.getMainControl()). \
4119                                    showWikiWordOpenDialog, None, 0), # gO
4120
4121    (92, 117) : (0, (self.ViewParents, False), 0), # \u
4122    (92, 85) : (0, (self.ViewParents, True), 0), # \U
4123
4124    (103, 115)  : (0, (self.SwitchEditorPreview, None), 0), # gs
4125   
4126    # TODO: think of suitable commands for the following
4127    #(103, 101)  : (0, (self.SwitchEditorPreview, "textedit"), 0), # ge
4128    (103, 112)  : (0, (self.SwitchEditorPreview, "preview"), 0), # gp
4129    (65470,)     : (0, (self.SwitchEditorPreview, "textedit"), 0), # F1
4130    (65471,)     : (0, (self.SwitchEditorPreview, "preview"), 0), # F2
4131            }
4132            }
4133
4134
4135        # Could be changed to use a wildcard
4136        for i in self.text_object_map:
4137            self.keys[0][(105, ord(i))] = (1, (self.SelectInTextObject, i), 0)
4138            self.keys[0][(97, ord(i))] = (1, (self.SelectATextObject, i), 0)
4139
4140        # Shortcuts available in insert mode (need to be repeatable by ".",
4141        # i.e. must work with EmulateKeypresses)
4142        self.keys[1] = {
4143        (("Ctrl", 64),)  : (0, (self.InsertPreviousText, None), 0), # Ctrl-@
4144        (("Ctrl", 97),)  : (0, (self.InsertPreviousTextLeaveInsertMode,
4145                                                            None), 0), # Ctrl-a
4146        (("Ctrl", 110),)  : (0, (self.Autocomplete, True), 0), # Ctrl-n
4147        (("Ctrl", 112),)  : (0, (self.Autocomplete, False), 0), # Ctrl-p
4148        }
4149
4150        # Rather than rewrite all the keys for other modes it is easier just
4151        # to modify those that need to be changed
4152
4153        # VISUAL MODE
4154        self.keys[2] = self.keys[0].copy()
4155        self.keys[2].update({
4156
4157                (39, "*")  : (1, (self.GotoMark, None), 0), # '
4158                (96, "*")  : (1, (self.GotoMarkIndent, None), 0), # `
4159                (109, "*") : (0, (self.Mark, None), 0), # m
4160                (102, "*") : (1, (self.FindNextChar, None), 0), # f
4161                (70, "*")  : (1, (self.FindNextCharBackwards, None), 0), # F
4162                (116, "*") : (1, (self.FindUpToNextChar, None), 0), # t
4163                (84, "*")  : (1, (self.FindUpToNextCharBackwards, None), 0), # T
4164                (114, "*") : (0, (self.ReplaceChar, None), 2), # r
4165                (83, "*")  : (0, (self.SurroundSelection, None), 2), # S
4166
4167
4168                (99,)  : (0, (self.DeleteSelectionAndInsert, None), 2), # c
4169                (100,)  : (0, (self.DeleteSelection, None), 1), # d
4170                (120,)  : (0, (self.DeleteSelection, None), 1), # x
4171                (121,) : (0, (self.Yank, None), 0), # y
4172                (89,) : (0, (self.Yank, True), 0), # Y
4173                (60,) : (0, (self.Indent, {"forward":False, "visual":True}), 0), # <
4174                (62,) : (0, (self.Indent, {"forward":True, "visual":True}), 0), # >
4175                (117,) : (0, (self.LowerCase, None), 0), # u
4176                (85,) : (0, (self.UpperCase, None), 0), # U
4177                (103, 117) : (1, self.LowerCase, 0), # gu
4178                (103, 85) : (1, self.UpperCase, 0), # gU
4179            })
4180        # And delete a few so our key mods are correct
4181        # These are keys that who do not serve the same function in visual mode
4182        # as in normal mode (and it most cases are replaced by other function)
4183        del self.keys[2][(100, 100)] # dd
4184        del self.keys[2][(121, 121)] # yy
4185        del self.keys[2][(105,)] # i
4186        del self.keys[2][(97,)] # a
4187        del self.keys[2][(83,)] # S
4188
4189        self.keys[3] = {}
4190
4191
4192        #self._motion_chains = self.GenerateMotionKeyChains(self.keys)
4193        self.key_mods = self.GenerateKeyModifiers(self.keys)
4194        self.motion_keys = self.GenerateMotionKeys(self.keys)
4195        self.motion_key_mods = self.GenerateKeyModifiers(self.motion_keys)
4196
4197        # Used for rewriting menu shortcuts
4198        self.viKeyAccels = self.GenerateKeyAccelerators(self.keys)
4199
4200
4201        self.SINGLE_LINE_WHITESPACE = [9, 11, 12, 32]
4202        self.WORD_BREAK =   '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
4203        self.WORD_BREAK_INCLUDING_WHITESPACE = \
4204                            '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~ \n\r'
4205        self.SENTENCE_ENDINGS = '.!?'
4206        self.SENTENCE_ENDINGS_SUFFIXS = '\'")]'
4207
4208        self.BRACES = {
4209                        "(" : ")",
4210                        "[" : "]",
4211                        "{" : "}",
4212                        "<" : ">",
4213                      }
4214        self.REVERSE_BRACES = dict((v,k) for k, v in self.BRACES.iteritems())
4215
4216        self._undo_state = 0
4217        self._undo_pos = -1
4218        self._undo_start_position = None
4219        self._undo_positions = []
4220
4221 
4222    def Setup(self):
4223        self.AddJumpPosition(self.ctrl.GetCurrentPos())
4224
4225    def SetMode(self, mode):
4226        """
4227        It would be nice to set caret alpha but i don't think its
4228        possible at the moment
4229        """
4230        # If switching from insert mode vi does a few things
4231        if self.mode == ViHelper.INSERT:
4232            # Move back one pos if not at the start of a line
4233            # and not on the last line
4234            if self.ctrl.GetCurrentPos() != \
4235                    self.GetLineStartPos(self.ctrl.GetCurrentLine()) and \
4236                    self.ctrl.GetLineCount() != self.ctrl.GetCurrentLine() + 1:
4237                self.ctrl.CharLeft()
4238            self.EndUndo(force=True)
4239
4240            if self.mode == ViHelper.INSERT:
4241                # If current line only contains whitespace remove it
4242                if self.ctrl.GetCurLine()[0].strip() == u"":
4243                    self.ctrl.LineDelete()
4244                    self.ctrl.AddText(self.ctrl.GetEOLChar())
4245                    self.ctrl.CharLeft()
4246        elif self.mode == ViHelper.REPLACE:
4247            # End undo action
4248            self.EndUndo()
4249       
4250        self.mode = mode
4251
4252        # Save caret position
4253        self.ctrl.ChooseCaretX()
4254
4255        if mode == ViHelper.NORMAL:
4256            # Set block caret (Not in wxpython < ?)
4257            #self.ctrl.SendMsg(2512, 2)
4258            self.ctrl.SetCaretPeriod(800)
4259            #self.ctrl.SetSelectionMode(0)
4260            self.RemoveSelection()
4261            self.ctrl.SetCaretForeground(wx.Colour(255, 0, 0))
4262            self.ctrl.SetCaretWidth(40)
4263            self.ctrl.SetOvertype(False)
4264            self.SetSelMode("NORMAL")
4265            # Vim never goes right to the end of the line
4266            self.CheckLineEnd()
4267        elif mode == ViHelper.VISUAL:
4268            self.ctrl.SetCaretWidth(40)
4269            self.ctrl.SetCaretForeground(wx.Colour(250, 250, 210))
4270            self.ctrl.SetOvertype(False)
4271        elif mode == ViHelper.INSERT:
4272            self.insert_action = []
4273            self.ctrl.SetCaretWidth(1)
4274            self.ctrl.SetCaretForeground(self.default_caret_colour)
4275            self.ctrl.SetOvertype(False)
4276        elif mode == ViHelper.REPLACE:
4277            self.ctrl.SetCaretWidth(1)
4278            self.ctrl.SetCaretForeground(self.default_caret_colour)
4279            self.ctrl.SetOvertype(True)
4280
4281    # Starting code to allow correct postitioning when undoing and redoing
4282    # actions
4283
4284    # Need to overide Undo and Redo to goto positions
4285    def BeginUndo(self, use_start_pos=False):
4286        if self._undo_state == 0:
4287            self.ctrl.BeginUndoAction()
4288            #self._undo_start_positions = \
4289            #            self._undo_start_positions[:self._undo_pos + 1]
4290            self._undo_positions = \
4291                        self._undo_positions[:self._undo_pos + 1]
4292
4293            if use_start_pos:
4294                if self.HasSelection:
4295                    self._undo_start_position = self.ctrl.GetSelectionStart()
4296                else:
4297                    self._undo_start_position = self.ctrl.GetCurrentPos()
4298        self._undo_state += 1
4299
4300
4301    def EndUndo(self, force=False):
4302        if force: self._undo_state = 1
4303
4304        if self._undo_state == 1:
4305            self.ctrl.EndUndoAction()
4306            if self._undo_start_position is not None:
4307                self._undo_positions.append(self._undo_start_position)
4308                self._undo_start_position = None
4309               
4310            elif self.HasSelection:
4311                self._undo_positions.append(self.ctrl.GetSelectionStart())
4312            else:
4313                self._undo_positions.append(self.ctrl.GetCurrentPos())
4314            self._undo_pos += 1
4315        self._undo_state -= 1
4316
4317    def _Undo(self):
4318        if self._undo_pos < 0:
4319            return False
4320        self.ctrl.Undo()
4321        self.ctrl.GotoPos(self._undo_positions[self._undo_pos])
4322        self._undo_pos -= 1
4323
4324    def _Redo(self):
4325        # NOTE: the position may be off on some redo's
4326        if self._undo_pos > len(self._undo_positions):
4327            return False
4328
4329        self.ctrl.Redo()
4330        self._undo_pos += 1
4331
4332
4333    # TODO: Remember caret position
4334    def GotoFirstVisibleLine(self):
4335        line = self.ctrl.GetFirstVisibleLine()
4336        if line < self.ctrl.GetCurrentLine():
4337            return
4338        self.ctrl.GotoLine(line)
4339
4340    def GotoLastVisibleLine(self):
4341        line = self.ctrl.GetLastVisibleLine()
4342        if line > self.ctrl.GetCurrentLine():
4343            return
4344        self.ctrl.GotoLine(line)
4345
4346    def OnMouseScroll(self, evt):
4347        current_line = self.ctrl.GetCurrentLine()
4348        top_line = self.ctrl.GetFirstVisibleLine() + 1
4349        bottom_line = self.ctrl.GetLastVisibleLine()
4350
4351        if current_line < top_line:
4352            wx.CallAfter(self.GotoFirstVisibleLine)
4353        elif current_line > bottom_line:
4354            wx.CallAfter(self.GotoLastVisibleLine)
4355            #offset = evt.GetWheelRotation() / 40
4356            #print offset
4357            #if offset < 0:
4358            #        func = self.ctrl.LineDown
4359            #else:
4360            #        func = self.ctrl.LineUp
4361            #   
4362            #for i in range(abs(offset)):
4363            #    print "i", i
4364            #    func()
4365
4366        evt.Skip()
4367
4368
4369    def OnScroll(self, evt):
4370        """
4371        Vim never lets the caret out of the viewport so track any
4372        viewport movements
4373        """
4374        # NOTE: may be inefficient?
4375        current_line = self.ctrl.GetCurrentLine()
4376        top_line = self.ctrl.GetFirstVisibleLine()+1
4377        bottom_line = self.ctrl.GetLastVisibleLine()-1
4378
4379        if current_line < top_line:
4380            self.MoveCaretToLine(top_line)
4381        elif current_line > bottom_line:
4382            self.MoveCaretToLine(bottom_line)
4383        evt.Skip()
4384
4385    def OnLeftMouseUp(self, evt):
4386        """Enter visual mode if text is selected by mouse"""
4387        if len(self.ctrl.GetSelectedText()) > 0:
4388            self.EnterVisualMode(True)
4389        else:
4390            self.LeaveVisualMode()
4391        # Prevent the end of line character from being selected as per vim
4392        # This will cause a slight delay, there may be a better solution
4393        # May be possible to override MOUSE_DOWN event.
4394        wx.CallAfter(self.CheckLineEnd)
4395        evt.Skip()
4396
4397    def OnAutocompleteKeyDown(self, evt):
4398        if evt.GetKeyCode() in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_RETURN, wx.WXK_ESCAPE):
4399            evt.Skip()
4400            return
4401
4402        if evt.GetRawKeyCode() in (65505, 65507, 65513):
4403            return
4404
4405        # Messy
4406        if evt.ControlDown():
4407            if evt.GetKeyCode() == 80:
4408                evt = wx.KeyEvent(wx.wxEVT_KEY_DOWN)
4409                evt.m_keyCode = wx.WXK_UP
4410                wx.PostEvent(self.ctrl, evt)
4411                return
4412
4413            elif evt.GetKeyCode() == 78:
4414                evt = wx.KeyEvent(wx.wxEVT_KEY_DOWN)
4415                evt.m_keyCode = wx.WXK_DOWN
4416                wx.PostEvent(self.ctrl, evt)
4417                return
4418
4419            elif evt.GetKeyCode() == 91:
4420                evt = wx.KeyEvent(wx.wxEVT_KEY_DOWN)
4421                evt.m_keyCode = wx.WXK_ESCAPE
4422                wx.PostEvent(self.ctrl, evt)
4423                return
4424
4425
4426        evt.Skip()
4427        self.ctrl.Bind(wx.EVT_KEY_DOWN, None)
4428
4429    def OnViKeyDown(self, evt):
4430        """
4431        Handle keypresses when in Vi mode
4432
4433        """
4434
4435
4436        # The following code is mostly duplicated from OnKeyDown (should be
4437        # rewritten to avoid duplication)
4438        key = evt.GetKeyCode()
4439        # TODO Check all modifiers
4440        if not evt.ControlDown() and not evt.ShiftDown(): 
4441            if key == wx.WXK_TAB:
4442                if self.ctrl.pageType == u"form":
4443                    if not self.ctrl._goToNextFormField():
4444                        self.ctrl.presenter.getMainControl().showStatusMessage(
4445                                _(u"No more fields in this 'form' page"), -1)
4446                    return
4447                evt.Skip()
4448            elif key == wx.WXK_RETURN and not self.ctrl.AutoCompActive():
4449                text = self.ctrl.GetText()
4450                wikiDocument = self.ctrl.presenter.getWikiDocument()
4451                bytePos = self.ctrl.GetCurrentPos()
4452                lineStartBytePos = self.ctrl.PositionFromLine(
4453                                        self.ctrl.LineFromPosition(bytePos))
4454
4455                lineStartCharPos = len(self.ctrl.GetTextRange(0,
4456                                                        lineStartBytePos))
4457                charPos = lineStartCharPos + len(self.ctrl.GetTextRange(
4458                                                lineStartBytePos, bytePos))
4459
4460                autoUnbullet = self.ctrl.presenter.getConfig().getboolean("main",
4461                        "editor_autoUnbullets", False)
4462
4463                settings = {
4464                        "autoUnbullet": autoUnbullet,
4465                        "autoBullets": self.ctrl.autoBullets,
4466                        "autoIndent": self.ctrl.autoIndent
4467                        }
4468
4469                if self.ctrl.wikiLanguageHelper.handleNewLineBeforeEditor(
4470                        self.ctrl, text, charPos, lineStartCharPos,
4471                        wikiDocument, settings):
4472                    evt.Skip()
4473                    return
4474                # Hack to maintain consistency when pressing return
4475                # on an empty bullet
4476                elif bytePos != self.ctrl.GetCurrentPos():
4477                    return
4478
4479        # NOTE: need to check cross platform compat
4480
4481        key = evt.GetRawKeyCode()
4482
4483        # Pass modifier keys on
4484        if key in (65505, 65507, 65513):
4485            return
4486
4487        accP = getAccelPairFromKeyDown(evt)
4488
4489        # TODO: Replace with override keys? break and run function
4490        # Escape, Ctrl-[, Ctrl-C
4491        # In VIM Ctrl-C triggers *InsertLeave*
4492        if key == 65307 or accP == (2, 91) or accP == (2, 99):
4493            # TODO: Move into ViHandler?
4494            self.SetMode(ViHandler.NORMAL)
4495            self.FlushBuffers()
4496            return
4497
4498
4499        # There should be a better way to monitor for selection changed
4500        if self.HasSelection():
4501            self.EnterVisualMode()
4502
4503        #control_mask = False
4504        try:
4505            if 2 in accP[0]: # Ctrl
4506            #    control_mask = True
4507                key = ("Ctrl", key)
4508        except TypeError:
4509            if accP[0] == 2:
4510                key = ("Ctrl", key)
4511
4512        m = self.mode
4513
4514        if m in [1, 3]: # Insert mode, replace mode,
4515            # Store each keyevent
4516            # NOTE: this may be terribly inefficient (i'm not sure)
4517            #       !!may need to seperate insert and replace modes!!
4518            #       what about autocomplete?
4519            # It would be possbile to just store the text that is inserted
4520            # however then actions would be ignored
4521            self.insert_action.append(key)
4522            if key in [65362, 65362]: # Arrow up / arrow down
4523                self.insert_action = []
4524            if not self.RunKeyChain((key,), m):
4525                evt.Skip()
4526            return
4527
4528
4529
4530
4531
4532        if self._acceptable_keys is None or \
4533                                "*" not in self._acceptable_keys:
4534            if 48 <= key <= 57: # Normal
4535                if self.SetNumber(key-48):
4536                    return
4537            elif 65456 <= key <= 65465: # Numpad
4538                if self.SetNumber(key-65456):
4539                    return
4540
4541        self.SetCount()
4542
4543        if self._motion and self._acceptable_keys is None:
4544            #self._acceptable_keys = None
4545            self._motion.append(key)
4546
4547            temp = self._motion[:-1]
4548            temp.append("*")
4549            if tuple(self._motion) in self.motion_keys[m]:
4550                self.RunKeyChain(tuple(self.key_inputs), m)
4551                return
4552                #self._motion = []
4553            elif tuple(temp) in self.motion_keys[m]:
4554                self._motion[-1] = "*"
4555                self._motion_wildcard.append(key)
4556                self.RunKeyChain(tuple(self.key_inputs), m)
4557                #self._motion = []
4558                return
4559               
4560            elif tuple(self._motion) in self.motion_key_mods[m]:
4561                #self._acceptable_keys = self.motion_key_mods[m][tuple(self._motion)]
4562                return
4563
4564            self.FlushBuffers()
4565            return
4566
4567
4568        if self._acceptable_keys is not None:
4569            if key in self._acceptable_keys:
4570                self._acceptable_keys = None
4571                pass
4572            elif "*" in self._acceptable_keys:
4573                self._wildcard.append(key)
4574                self.key_inputs.append("*")
4575                self._acceptable_keys = None
4576                self.RunKeyChain(tuple(self.key_inputs), m)
4577
4578                return
4579            elif "motion" in self._acceptable_keys:
4580                self._acceptable_keys = None
4581                self._motion.append(key)
4582                if (key,) in self.motion_keys[m]:
4583                    self.key_inputs.append("motion")
4584                    self.RunKeyChain(tuple(self.key_inputs), m)
4585                    return
4586                if (key,) in self.motion_key_mods[m]:
4587                    self.key_inputs.append("motion")
4588                    return
4589
4590
4591        self.key_inputs.append(key)
4592        self.updateViStatus()
4593
4594        key_chain = tuple(self.key_inputs)
4595
4596        if self.RunKeyChain(key_chain, m):
4597            return
4598
4599        self.FlushBuffers()
4600           
4601    def TurnOff(self):
4602        self._enableMenuShortcuts(True)
4603        self.ctrl.SetCaretWidth(1)
4604
4605    def GetChar(self, length=1):
4606        """
4607        Retrieves text from under caret
4608        @param length: the number of characters to get
4609        """
4610        pos = self.ctrl.GetCurrentPos()
4611        start, end = self.minmax(pos, pos + length)
4612        start = max(0, start)
4613        end = min(end, self.ctrl.GetLength())
4614        return self.ctrl.GetTextRange(start, end)
4615
4616    def EmulateKeypresses(self, actions):
4617        if len(actions) > 0:
4618
4619            eol = self.ctrl.GetEOLChar()
4620           
4621            for i in actions:
4622                # TODO: handle modifier keys, e.g. ctrl
4623                if i == 65361:
4624                    self.ctrl.CharLeft()
4625                elif i == 65363:
4626                    self.ctrl.CharRight()
4627                elif i == 65288:
4628                    self.ctrl.DeleteBackNotLine()
4629                elif i in [65535, 65439]:
4630                    self.ctrl.CharRight()
4631                    self.ctrl.DeleteBack()
4632                elif i in [65293, 65421]: # enter, return
4633                    self.ctrl.InsertText(self.ctrl.GetCurrentPos(), eol)
4634                    self.ctrl.CharRight()
4635                elif i == 65289: # tab
4636                    self.ctrl.InsertText(self.ctrl.GetCurrentPos(), "\t")
4637                    self.ctrl.CharRight()
4638                else:
4639                    self.ctrl.InsertText(self.ctrl.GetCurrentPos(), unichr(i))
4640                    self.ctrl.CharRight()
4641       
4642   
4643    def RepeatCmd(self):
4644        # TODO: clean this up, move to ViHelper
4645        if self.last_cmd is not None:
4646            self.visualBell("GREEN")
4647            self.BeginUndo()
4648            cmd_type, key, count, motion, motion_wildcards, wildcards = self.last_cmd
4649
4650            self.count = count
4651            actions = self.insert_action
4652            # NOTE: Is "." only going to repeat editable commands as in vim?
4653            if cmd_type == 1:
4654                self.RunFunction(key, motion, motion_wildcards, wildcards)
4655            # If a command ends in insertion mode we also repeat any changes
4656            # made up until the next mode change.
4657            elif cmd_type == 2: # + insertion
4658                self.RunFunction(key, motion, motion_wildcards, wildcards)
4659                # Emulate keypresses
4660                # Return to normal mode
4661                self.EmulateKeypresses(actions)
4662                self.SetMode(ViHandler.NORMAL)
4663            elif cmd_type == 3:
4664                self.ReplaceChar(key)
4665            self.EndUndo()
4666            self.insert_action = actions
4667        else:
4668            self.visualBell("RED")
4669
4670
4671#--------------------------------------------------------------------
4672# Misc stuff
4673#--------------------------------------------------------------------
4674    def Autocomplete(self, forwards):
4675        if not self.ctrl.AutoCompActive():
4676            # NOTE: list order is not correct
4677            if self.GetUnichrAt(self.ctrl.GetCurrentPos()-1) in \
4678                                self.WORD_BREAK_INCLUDING_WHITESPACE:
4679                word = u"[a-zA-Z0-9_]"
4680                word_length = 0
4681
4682            else:
4683                # Vim only selects backwards
4684                self.SelectInWord()
4685                self.ExtendSelectionIfRequired()
4686                word = self.ctrl.GetSelectedText()
4687                word_length = len(word)
4688
4689
4690            # Search for possible autocompletions
4691            # Bad ordering at the moment
4692            text = self.ctrl.GetTextRange(
4693                        self.ctrl.GetCurrentPos(), self.ctrl.GetLength())
4694            word_list = re.findall(ur"\b{0}.*?\b".format(word), text, re.U)
4695            text = self.ctrl.GetTextRange(0, self.ctrl.GetCurrentPos())
4696            word_list.extend(re.findall(ur"\b{0}.*?\b".format(word), text, re.U))
4697
4698            # No completions found
4699            if len(word_list) <= 1:
4700                if self.HasSelection():
4701                    self.ctrl.CharRight()
4702                self.visualBell("RED")
4703                return
4704
4705            # Remove duplicates
4706            words = set()
4707            words.add(word)
4708            word_list_prepped = "\x01".join([i for i in word_list if i not in words and not words.add(i)])
4709
4710            if self.HasSelection():
4711                self.ctrl.CharRight()
4712            self.ctrl.AutoCompShow(word_length, word_list_prepped)
4713            self.ctrl.Bind(wx.EVT_KEY_DOWN, self.OnAutocompleteKeyDown)
4714
4715       
4716    def GotoSelectionStart(self):
4717        self.ctrl.GotoPos(self.ctrl.GetSelectionStart())
4718
4719    def InsertPreviousText(self):
4720        self.EmulateKeypresses(self.insert_action)
4721
4722    def InsertPreviousTextLeaveInsertMode(self):
4723        self.InsertPreviousText()
4724        self.SetMode(ViHelper.NORMAL)
4725
4726    def ChangeSurrounding(self, keycodes):
4727        char_to_change = self.GetCharFromCode(keycodes[0])
4728        if char_to_change in self.text_object_map:
4729            self.SelectATextObject(char_to_change)
4730            if self.HasSelection():
4731                pos = self.ExtendSelectionIfRequired()
4732                self.BeginUndo()
4733                self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText()[1:-1])
4734                self.ctrl.CharLeft()
4735                self.SelectSelection()
4736                self.SurroundSelection(keycodes[1])
4737                self.ctrl.GotoPos(pos)
4738                self.EndUndo()
4739                self.visualBell("GREEN")
4740                return
4741        self.visualBell("RED")
4742
4743    def PreSurround(self, code):
4744        self.SelectSelection(False)
4745        self.SurroundSelection(code)
4746
4747    def PreSurroundLine(self, code):
4748        self.SelectCurrentLine()
4749        self.SurroundSelection(code)
4750
4751    def GetUnichrAt(self, pos):
4752        if -1 < pos < self.ctrl.GetLength():
4753            return self.ctrl.GetTextRaw()[pos]
4754
4755    def SelectInTextObject(self, ob):
4756        self.SelectTextObject(ob, False)
4757
4758    def SelectATextObject(self, ob):
4759        self.SelectTextObject(ob, True)
4760
4761    def SelectTextObject(self, ob=None, extra=False):
4762        """
4763        Selects specified text object
4764
4765        See vim help -> *text-objects*
4766
4767        Two different selection methods are supported. They are given
4768        the names "In" and "A" corresponding to vim's "i(n)" and "a(n)"
4769        commands respectively.
4770
4771        The differences between these is dependent on the type of text
4772        to be selected, it can be either a selection (e.g. words,
4773        sentences, etc...) or a block (e.g. text within [ ] or " " or
4774        ( ) etc...).
4775        """
4776        if ob in self.text_object_map:
4777            self.text_object_map[ob][1](extra)
4778            #if extra:  # extra corresponds to a
4779            #    if self.text_object_map[ob][0]: # text block
4780            #        pass # Select surrouding chars
4781            #    else:
4782            #        self.SelectTrailingWhitespace(sel_start, sel_end)
4783
4784    def SelectTrailingWhitespace(self, sel_start, sel_end):
4785        self.StartSelection(sel_start)
4786        true_end = self.ctrl.GetCurrentPos()
4787        # Vim defaults to selecting trailing whitespace
4788        if self.ctrl.GetCharAt(sel_end+1) in \
4789                self.SINGLE_LINE_WHITESPACE or \
4790                self.ctrl.GetCharAt(sel_start) in \
4791                self.SINGLE_LINE_WHITESPACE:
4792            self.MoveCaretWordEndCountWhitespace(1)
4793        # or if not found it selects preceeding whitespace
4794        elif self.ctrl.GetCharAt(sel_start-1) \
4795                in self.SINGLE_LINE_WHITESPACE:
4796            pos = sel_start-1
4797            while self.ctrl.GetCharAt(pos-1) \
4798                    in  self.SINGLE_LINE_WHITESPACE:
4799                pos -= 1
4800            self.ctrl.GotoPos(pos)
4801            self.StartSelection()
4802            self.ctrl.GotoPos(true_end)
4803        self.SelectSelection()
4804
4805    def SelectInWord(self, extra=False):
4806        """
4807        Selects n words where n is the count. Whitespace between words
4808        is counted.
4809        """
4810        self._SelectInWords(False, extra=extra)
4811
4812    def SelectInWORD(self, extra=False):
4813        self._SelectInWords(True, extra=extra)
4814       
4815    def _SelectInWords(self, WORD=False, extra=False):
4816        # NOTE: Does not select single characters
4817        pos = self.ctrl.GetCurrentPos()
4818
4819        if not WORD:
4820            back_word = self.MoveCaretBackWord
4821            move_caret_word_end_count_whitespace = \
4822                    self.MoveCaretWordEndCountWhitespace
4823        else:
4824            back_word = self.MoveCaretBackWORD
4825            move_caret_word_end_count_whitespace = \
4826                    self.MoveCaretWordENDCountWhitespace
4827
4828        # If the caret is in whitespace the whitespace is selected
4829        if self.ctrl.GetCharAt(pos) in self.SINGLE_LINE_WHITESPACE:
4830            self.MoveCaretToWhitespaceStart()
4831        else:
4832            if pos > 0:
4833                if self.GetUnichrAt(pos-1) in string.whitespace:
4834                    pass
4835                elif ((self.GetUnichrAt(pos) in self.WORD_BREAK) is not (self.GetUnichrAt(pos-1) in self.WORD_BREAK)) and not WORD:
4836                    pass
4837                else:
4838                    back_word(1)
4839        self.StartSelection()
4840        move_caret_word_end_count_whitespace(1)
4841        self.SelectSelection()
4842        if extra:
4843            sel_start, sel_end = self._GetSelectionRange()
4844            self.SelectTrailingWhitespace(sel_start, sel_end)
4845        move_caret_word_end_count_whitespace(self.count-1)
4846        self.SelectSelection()
4847
4848    def SelectInSentence(self, extra=False):
4849        """ Selects current sentence """
4850        # First check if we are at the start of a sentence already
4851        pos = start_pos = self.ctrl.GetCurrentPos()
4852        if pos > 0 and self.GetUnichrAt(pos-1) in string.whitespace:
4853            pos -= 1
4854            char = self.GetUnichrAt(pos-1)
4855            while pos > 0 and char in string.whitespace:
4856                pos -= 1
4857                char = self.GetUnichrAt(pos-1)
4858            if char in self.SENTENCE_ENDINGS_SUFFIXS:
4859                pos -= 1
4860                char = self.GetUnichrAt(pos-1)
4861                while pos > 0 and char in self.SENTENCE_ENDINGS_SUFFIXS:
4862                    pos -= 1
4863                    char = self.GetUnichrAt(pos-1)
4864            if char not in self.SENTENCE_ENDINGS:
4865                self.GotoSentenceStart(1)
4866        else:
4867            self.GotoSentenceStart(1)
4868        self.StartSelection()
4869        self.GotoSentenceEnd(1)
4870        self.SelectSelection()
4871        sel_start, sel_end = self._GetSelectionRange()
4872        self.ctrl.GotoPos(sel_start)
4873        if extra:
4874            self.count += 1
4875        self.GotoSentenceEndCountWhitespace()
4876        self.SelectSelection()
4877
4878    def SelectInParagraph(self, extra=False):
4879        """ Selects current paragraph """
4880        # TODO: fix for multiple counts
4881        self.MoveCaretParaDown(1)
4882        self.MoveCaretParaUp(1)
4883        self.StartSelection()
4884        self.MoveCaretParaDown()
4885        self.ctrl.CharLeft()
4886        self.SelectSelection()
4887
4888    def _SelectInBracket(self, bracket, extra=False, start_pos=None, count=None):
4889        if start_pos is None: start_pos = self.ctrl.GetCurrentPos()
4890        if count is None: count = self.count
4891
4892        if self.SearchBackwardsForChar(bracket, count):
4893            pos = self.ctrl.GetCurrentPos()
4894
4895            pre_text = self.ctrl.GetTextRange(pos, start_pos)
4896
4897            while pre_text.count(bracket) - pre_text.count(self.BRACES[bracket]) \
4898                                                                != self.count:
4899                self.ctrl.CharLeft()
4900                if self.SearchBackwardsForChar(bracket, 1):
4901                    pos = self.ctrl.GetCurrentPos()
4902                    pre_text = self.ctrl.GetTextRange(pos, start_pos)
4903                else:
4904                    break
4905
4906            if self.MatchBraceUnderCaret():
4907                self.StartSelection(pos)
4908                self.SelectSelection()
4909                sel_start, sel_end = self._GetSelectionRange()
4910                if not sel_start <= start_pos <= sel_end:
4911                    self.ctrl.GotoPos(sel_start-1)
4912                    self._SelectInBracket(bracket, extra, start_pos, count)
4913                else:
4914                    # Only select the brackets if required
4915                    if not extra:
4916                        self.StartSelection(pos+1)
4917                        # The sel_start below is ignored due to the
4918                        # StartSelection called above
4919                        self.ctrl.SetSelection(sel_start, sel_end-1)
4920            else:
4921                self.ctrl.GotoPos(start_pos)
4922
4923    def SelectInSquareBracket(self, extra=False):
4924        """ Selects text in [ ] block """
4925        self._SelectInBracket("[", extra)
4926
4927    def SelectInRoundBracket(self, extra=False):
4928        """ Selects text in ( ) block """
4929        self._SelectInBracket("(", extra)
4930
4931    def SelectInInequalitySigns(self, extra=False):
4932        """ Selects text in < > block """
4933        self._SelectInBracket("<", extra)
4934
4935    def SelectInTagBlock(self, extra=False):
4936        """ selects text in <aaa> </aaa> block """
4937        # TODO: requires method of textinput
4938
4939    def SelectInBlock(self, extra=False):
4940        """ selects text in { } block """
4941        self._SelectInBracket("{", extra)
4942
4943    def _SelectInChars(self, char, extra=False):
4944        pos = self.ctrl.GetCurrentPos()
4945        if self.SearchBackwardsForChar(char):
4946            start_pos = self.ctrl.GetCurrentPos()
4947            self.ctrl.GotoPos(pos)
4948            if self.SearchForwardsForChar(char):
4949                self.StartSelection(start_pos)
4950                self.SelectSelection()
4951                if not extra:
4952                    sel_start, sel_end = self._GetSelectionRange()
4953                    self.StartSelection(start_pos+1)
4954                    self.ctrl.SetSelection(sel_start, sel_end-1)
4955
4956    def SelectInDoubleQuote(self, extra=False):
4957        """ selects text in " " block """
4958        self._SelectInChars('"', extra)
4959
4960    def SelectInSingleQuote(self, extra=False):
4961        """ selects text in ' ' block """
4962        self._SelectInChars("'", extra)
4963
4964    def SelectInTilde(self, extra=False):
4965        """ selects text in ` ` block """
4966        self._SelectInChars("`", extra)
4967
4968    # ---------------------------------------
4969
4970    def SelectInPoundSigns(self, extra=False):
4971        self._SelectInChars("\xc2", extra)
4972
4973    def SelectInDollarSigns(self, extra=False):
4974        self._SelectInChars("$", extra)
4975
4976    def SelectInHats(self, extra=False):
4977        self._SelectInChars("^", extra)
4978
4979    def SelectInPercentSigns(self, extra=False):
4980        self._SelectInChars("%", extra)
4981
4982    def SelectInAmpersands(self, extra=False):
4983        self._SelectInChars("&", extra)
4984
4985    def SelectInStars(self, extra=False):
4986        self._SelectInChars("*", extra)
4987
4988    def SelectInHyphens(self, extra=False):
4989        self._SelectInChars("-", extra)
4990
4991    def SelectInUnderscores(self, extra=False):
4992        self._SelectInChars("_", extra)
4993
4994    def SelectInEqualSigns(self, extra=False):
4995        self._SelectInChars("=", extra)
4996
4997    def SelectInPlusSigns(self, extra=False):
4998        self._SelectInChars("+", extra)
4999
5000    def SelectInExclamationMarks(self, extra=False):
5001        self._SelectInChars("!", extra)
5002
5003    def SelectInQuestionMarks(self, extra=False):
5004        self._SelectInChars("?", extra)
5005
5006    def SelectInAtSigns(self, extra=False):
5007        self._SelectInChars("@", extra)
5008
5009    def SelectInHashs(self, extra=False):
5010        self._SelectInChars("#", extra)
5011
5012    def SelectInApproxSigns(self, extra=False):
5013        self._SelectInChars("~", extra)
5014
5015    def SelectInVerticalBars(self, extra=False):
5016        self._SelectInChars("|", extra)
5017
5018    def SelectInSemicolons(self, extra=False):
5019        self._SelectInChars(";", extra)
5020
5021    def SelectInColons(self, extra=False):
5022        self._SelectInChars(":", extra)
5023
5024    def SelectInBackslashes(self, extra=False):
5025        self._SelectInChars("\\", extra)
5026
5027    def SelectInForwardslashes(self, extra=False):
5028        self._SelectInChars("/", extra)
5029
5030
5031    def SurroundSelection(self, keycode):
5032        # TODO: expand to include cs, ds and ys
5033        start = self.ExtendSelectionIfRequired()
5034
5035        text = self.ctrl.GetSelectedText()
5036
5037        if len(text) < 1:
5038            return # Should never happen
5039
5040        self.BeginUndo()
5041
5042        # Fix for EOL
5043        if text[-1] == self.ctrl.GetEOLChar():
5044            sel_start, sel_end = self._GetSelectionRange()
5045            self.ctrl.SetSelection(sel_start, sel_end-1)
5046            text = self.ctrl.GetSelectedText()
5047
5048        replacements = {
5049                        ")" : ("(", ")"),
5050                        "b" : ("(", ")"),
5051                        "}" : ("{", "}"),
5052                        "B" : ("{", "}"),
5053                        "]" : ("[", "]"),
5054                        "r" : ("[", "]"),
5055                        ">" : ("<", ">"),
5056                        "a" : ("<", ">"),
5057
5058                        "(" : ("( ", " )"),
5059                        "{" : ("{ ", " }"),
5060                        "[" : ("[ ", " ]"),
5061
5062                        # TODO
5063                        #"t" : ("<{0}>", "</{0}}"),
5064                        #"<" : ("<{0}>", "</{0}}"),
5065
5066                        "'" : ("'", "'"),
5067                        '"' : ('"', '"'),
5068                        "`" : ("`", "`"),
5069
5070                        }
5071        uni_chr = unichr(keycode)
5072        if uni_chr in replacements:
5073            new_text = "{0}{1}{2}".format(replacements[uni_chr][0], text, replacements[uni_chr][1])
5074        else:
5075            new_text = "{0}{1}{2}".format(uni_chr, text, uni_chr)
5076
5077        self.ctrl.ReplaceSelection(new_text)
5078        self.LeaveVisualMode()
5079        self.ctrl.GotoPos(start)
5080        self.EndUndo()
5081
5082
5083    def CheckLineEnd(self):
5084        # TODO: fix
5085        line, line_pos = self.ctrl.GetCurLine()
5086        if self.mode not in [ViHelper.VISUAL, ViHelper.INSERT]:
5087            unicode_line = unicode(line)
5088            if len(line) > 1 and line_pos >= len(bytes(unicode_line))-1:
5089                # Necessary for unicode chars
5090                pos = self.ctrl.GetCurrentPos()-len(bytes(unicode_line[-1]))
5091                self.ctrl.GotoPos(pos)
5092                #self.ctrl.SetSelection(self.ctrl.GetCurrentPos(),self.ctrl.GetCurrentPos())
5093        #if self.ctrl.GetCurrentPos() == self.ctrl.GetLineEndPosition(self.ctrl.GetCurrentLine()):
5094        #    self.MoveCaretLeft()
5095
5096    def SelectCurrentLine(self, include_eol=True):
5097        line_no = self.ctrl.GetCurrentLine()
5098        max_line = min(line_no+self.count-1, self.ctrl.GetLineCount())
5099        start = self.GetLineStartPos(line_no)
5100        end = self.ctrl.GetLineEndPosition(max_line)
5101
5102        # If we are deleting the last line we also need to delete
5103        # the eol char at the end of the new last line.
5104        if max_line + 1 == self.ctrl.GetLineCount():
5105            start -= 1
5106
5107        if include_eol:
5108            end += 1
5109        self.ctrl.SetSelection(end, start)
5110
5111    def SelectFullLines(self):
5112        """
5113        Could probably be replaced by SetSectionMode,
5114        if it can be made to work.
5115        """
5116        start_line, end_line = self._GetSelectedLines()
5117        if self.ctrl.GetCurrentPos() >= self.visual_line_start_pos:
5118            reverse = False
5119
5120            # Hack needed if selection is started on empty line
5121            if len(self.ctrl.GetLine(start_line)) > 1:
5122                text, pos = self.ctrl.GetCurLine()
5123                if len(text) > 1:
5124                    end_line -= 1
5125
5126        else:
5127            reverse = True
5128            end_line -= 1
5129
5130        cur_line = self.ctrl.GetCurrentLine()
5131        self.SelectLines(start_line, end_line, reverse)
5132
5133
5134    def JoinLines(self):
5135        text = self.ctrl.GetSelectedText()
5136        start_line = self.ctrl.GetCurrentLine()
5137        if len(text) < 1:
5138            # We need at least 2 lines to be able to join
5139            count = self.count if self.count > 1 else 2
5140            self.SelectLines(start_line, min(self.ctrl.GetLineCount(), \
5141                                                    start_line - 1 + count))
5142        else:
5143            start_line, end_line = self._GetSelectedLines()
5144            self.SelectLines(start_line, end_line)
5145
5146        text = self.ctrl.GetSelectedText()
5147
5148        eol_char = self.ctrl.GetEOLChar()
5149
5150        # Probably not the most efficient way to do this
5151        # We need to lstrip every line except the first
5152        lines = text.split(eol_char)
5153        new_text = []
5154        for i in range(len(lines)):
5155            if lines[i] == u"": # Leave out empty lines
5156                continue
5157            if i == 0:
5158                new_text.append(lines[i])
5159            else:   
5160                new_text.append(lines[i].lstrip())
5161           
5162        self.ctrl.ReplaceSelection(" ".join(new_text))
5163
5164    def DeleteSelectionAndInsert(self):
5165        self.DeleteSelection()
5166        self.Insert()
5167
5168    def RemoveSelection(self):
5169        """
5170        Removes the selection.
5171
5172        TODO: don't goto selection start pos
5173        """
5174        pos = self.ctrl.GetAnchor()
5175        self.ctrl.SetSelection(pos,pos)
5176
5177    # TODO: Clean up selection names
5178    def GetSelectionAnchor(self):
5179        return self._anchor
5180
5181    def StartSelection(self, pos=None):
5182        """ Saves the current position to be used for selection start """
5183        if pos is None:
5184            pos = self.ctrl.GetCurrentPos()
5185        self._anchor = pos
5186
5187    def StartSelectionAtAnchor(self):
5188        """
5189        Saves the current position to be used for selection start using
5190        the anchor as the selection start.
5191        """
5192        if len(self.ctrl.GetSelectedText()) > 0:
5193            self._anchor = self.ctrl.GetAnchor()
5194        else:
5195            self._anchor = self.ctrl.GetCurrentPos()
5196
5197
5198    def SelectInLink(self):
5199        pos = self.ctrl.GetCurrentPos()
5200        start_pos = self.FindChar(91, True, 0, 1, False)
5201        self.StartSelection()
5202        end_pos = self.FindChar(93, False, -1, 1, False)
5203
5204        if start_pos and end_pos:
5205            self.SelectSelection()
5206
5207    def SelectSelection(self, offset_motion=True):
5208        # Fix for actions to end of word/WORD (deleting, yanking..)
5209        # These are cases in which vim will perform an action on
5210        # an extra character. (Would it be easier to go the other way?)
5211        if offset_motion and self._motion in (
5212                [101], [69], [36], [102, "*"], [70, "*"], [116, "*"], [84, "*"]):
5213            #self.ctrl.CharRight()
5214            self.ExtendSelectionIfRequired()
5215
5216        self.ctrl.SetSelection(self._anchor, self.ctrl.GetCurrentPos())
5217
5218
5219    def SelectionOnSingleLine(self):
5220        """
5221        Assume that if an EOL char is present we have mutiple lines
5222        """
5223        if self.ctrl.GetEOLChar() in self.ctrl.GetSelectedText():
5224            return False
5225        else:
5226            return True
5227
5228    def ExtendSelectionIfRequired(self):
5229        """
5230        If selection is positive the last character is not actually
5231        selected and so a correction must be applied
5232        """
5233        start, end = self._GetSelectionRange()
5234        if self.ctrl.GetCurrentPos() == end:
5235            self.ctrl.CharRightExtend()
5236        return start
5237
5238    def DeleteSelection(self):
5239        """Yank selection and delete it"""
5240        #if self.mode == ViHelper.VISUAL:
5241        #    start = self.ExtendSelectionIfRequired()
5242        self.BeginUndo()
5243        self.YankSelection()
5244        self.ctrl.Clear()
5245        #self.ctrl.GotoPos(start)
5246        self.EndUndo()
5247        self.LeaveVisualMode()
5248
5249    def _GetSelectionRange(self):
5250        """Get the range of selection such that the start is the visual start
5251        of the selection, not the logical start.
5252
5253        """
5254        start, end = self.minmax(self.ctrl.GetSelectionStart(),
5255                            self.ctrl.GetSelectionEnd())
5256        return start, end
5257
5258    def _GetSelectedLines(self):
5259        """Get the first and last line (exclusive) of selection"""
5260        start, end = self._GetSelectionRange()
5261        start_line, end_line = (self.ctrl.LineFromPosition(start),
5262                                self.ctrl.LineFromPosition(end - 1) + 1)
5263        return start_line, end_line
5264
5265    def HasSelection(self):
5266        """
5267        Detects if there's anything selected
5268        @rtype: bool
5269        """
5270        return len(self.ctrl.GetSelectedText()) > 0
5271
5272    def InsertText(self, text):
5273        self.ctrl.InsertText(self.ctrl.GetCurrentPos(), text)
5274        self.MoveCaretPos(len(text))
5275
5276    def SelectLines(self, start, end, reverse=False):
5277        """
5278        Selects lines
5279
5280        @param start: start line
5281        @param end: end line
5282        @param reverse: if true selection is reversed
5283        """
5284        start_pos = self.GetLineStartPos(start)
5285        end_pos = self.ctrl.GetLineEndPosition(end)
5286
5287        if reverse:
5288            self.ctrl.SetSelection(end_pos, start_pos)
5289        else:
5290            self.ctrl.SetSelection(start_pos, end_pos)
5291
5292    def PreUppercase(self):
5293        start = self.ExtendSelectionIfRequired()
5294        self.SelectSelection()
5295        self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText().upper())
5296        self.ctrl.GotoPos(start)
5297
5298    def PreLowercase(self):
5299        start = self.ExtendSelectionIfRequired()
5300        self.SelectSelection()
5301        self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText().lower())
5302        self.ctrl.GotoPos(start)
5303
5304    def SwapCase(self):
5305        self.BeginUndo()
5306        text = self.ctrl.GetSelectedText()
5307        if len(text) == 0:
5308            self.StartSelection()
5309            self.MoveCaretRight()
5310            self.SelectSelection()
5311            text = self.ctrl.GetSelectedText()
5312        self.ctrl.ReplaceSelection(text.swapcase())
5313        self.EndUndo()
5314
5315    def UpperCase(self):
5316        self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText().upper())
5317
5318    def LowerCase(self):
5319        self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText().lower())
5320
5321    def Indent(self, forward=True, repeat=1, visual=False):
5322        if visual == True:
5323            repeat = self.count
5324
5325        self.BeginUndo()
5326        # If no selected text we work on lines as specified by count
5327        if len(self.ctrl.GetSelectedText()) < 1:
5328            start_line = self.ctrl.GetCurrentLine()
5329            if self.count > 1:
5330                self.SelectLines(start_line, min(self.ctrl.GetLineCount(), \
5331                                                start_line - 1 + self.count))
5332        else:
5333            start_line, end = self._GetSelectedLines()
5334
5335        if self.SelectionOnSingleLine():
5336            self.GotoLineIndent()
5337
5338        for i in range(repeat):
5339            if forward:
5340                self.ctrl.Tab()
5341            else:
5342                self.ctrl.BackTab()
5343
5344        self.ctrl.GotoLine(start_line)
5345        self.GotoLineIndent()
5346        self.EndUndo()
5347
5348    def _PositionViewport(self, n):
5349        """
5350        Helper function for ScrollViewport* functions.
5351
5352        Positions the viewport around caret position
5353
5354        """
5355        lines = self.ctrl.LinesOnScreen() - 1
5356        current = self.ctrl.GetCurrentLine()
5357        diff = int(lines * n)
5358        self.ctrl.ScrollToLine(current - diff)
5359
5360    def GetViewportPosition(self):
5361        lines = self.ctrl.LinesOnScreen() - 1
5362        current = self.ctrl.GetCurrentLine()
5363        first_visible_line = self.ctrl.GetFirstVisibleLine()
5364
5365        n = current - first_visible_line
5366
5367        return n / float(lines)
5368
5369    def _ScrollViewportByLines(self, n):
5370        first_visible_line = self.ctrl.GetFirstVisibleLine()
5371        lines_on_screen = self.ctrl.LinesOnScreen()
5372
5373        line = max(0, first_visible_line + n)
5374        line = min(line, self.ctrl.GetLineCount() - lines_on_screen)
5375        self.ctrl.ScrollToLine(line)
5376        if self.ctrl.GetCurrentLine() < line:
5377            self.ctrl.LineDown()
5378        elif self.ctrl.GetCurrentLine() > first_visible_line + lines_on_screen-2:
5379            self.ctrl.LineUp()
5380
5381    def _ScrollViewport(self, n):
5382        view_pos = self.GetViewportPosition()
5383        lines = self.ctrl.LinesOnScreen()
5384        current_line = self.ctrl.GetCurrentLine()
5385        new_line = max(0, current_line + n * lines)
5386        new_line = min(new_line, self.ctrl.GetLineCount())
5387        self.GotoLineIndent(new_line)
5388        self._PositionViewport(view_pos)
5389
5390    def IndentText(self):
5391        """
5392        Post motion function. Select text and indent it
5393        """
5394        self.SelectSelection()
5395        self.Indent(True)
5396
5397    def DedentText(self):
5398        """
5399        Post motion function. Select text and deindent it
5400        """
5401        self.SelectSelection()
5402        self.Indent(False)
5403
5404    #--------------------------------------------------------------------
5405    # Visual mode
5406    #--------------------------------------------------------------------
5407
5408    def EnterLineVisualMode(self):
5409        """
5410        Enter line visual mode
5411       
5412        Sets a special type of visual mode in which only full lines can
5413        be selected.
5414
5415        NOTE:
5416        Should be possible using StyledTextCtrl.SetSelectionType() but
5417        for some reason I can't get it to work so a SetSelMode() has
5418        been implemented.
5419       
5420        """
5421        self.SetSelMode("LINE")
5422        text, pos = self.ctrl.GetCurLine()
5423        if pos == 0:
5424            self.MoveCaretRight()
5425        self.visual_line_start_pos = self.ctrl.GetCurrentPos()
5426        if self.mode != ViHelper.VISUAL:
5427            self.SetMode(ViHelper.VISUAL)
5428            self.StartSelectionAtAnchor()
5429
5430            #if self.ctrl.GetSelectedText() > 0:
5431            #    self.MoveCaretRight()
5432
5433    def LeaveVisualMode(self):
5434        """Helper function to end visual mode"""
5435        self.ctrl.GotoPos(self.ctrl.GetSelectionStart())
5436        if self.mode == ViHelper.VISUAL:
5437            self.SetSelMode("NORMAL")
5438            self.SetMode(ViHelper.NORMAL)
5439
5440    def EnterVisualMode(self, mouse=False):
5441        """
5442        Change to visual (selection) mode
5443       
5444        Will do nothing if already in visual mode
5445
5446        @param mouse: Visual mode was started by mouse action
5447
5448        """
5449        if self.mode != ViHelper.VISUAL:
5450            self.SetMode(ViHelper.VISUAL)
5451
5452            if not mouse:
5453                self.StartSelectionAtAnchor()
5454
5455                #if self.ctrl.GetSelectedText() > 0:
5456                #    self.MoveCaretRight()
5457            else:
5458                self.StartSelection(self.ctrl.GetSelectionEnd())
5459
5460    #--------------------------------------------------------------------
5461    # Searching
5462    #--------------------------------------------------------------------
5463
5464    def SearchForwardsForChar(self, search_char, count=None,
5465                                    wrap_lines=True, start_offset=-1):
5466        if count is None: count = self.count
5467        pos = start_pos = self.ctrl.GetCurrentPos() + start_offset
5468
5469        text = self.ctrl.GetTextRaw()
5470        #text_to_search = text[pos:]
5471
5472        n = 0
5473        for i in range(count):
5474            pos = text.find(search_char, pos + 1)
5475           
5476
5477           
5478        if pos > -1:
5479            if not wrap_lines:
5480                # TODO: fix eol searching
5481                text_to_check = self.ctrl.GetTextRange(start_pos, pos)
5482                if self.ctrl.GetEOLChar() in text_to_check:
5483                    return False
5484            self.ctrl.GotoPos(pos)
5485            return True
5486
5487        self.visualBell("RED")
5488        return False
5489
5490    def SearchBackwardsForChar(self, search_char, count=None,
5491                                    wrap_lines=True, start_offset=0):
5492        """
5493        Searches backwards in text for character.
5494
5495        @param search_char: Character to search for.
5496        @param count: Number of characeters to find.
5497        @param wrap_lines: Should search occur on multiple lines.
5498        @param start_offset: Start offset for searching. Should be
5499            zero if character under the caret should be included in
5500            the search, -1 if not.
5501
5502        @rtype: bool
5503        @return: True if successful, False if not.
5504
5505        """
5506        if count is None: count = self.count
5507        pos = start_pos = self.ctrl.GetCurrentPos() + start_offset
5508
5509        text = self.ctrl.GetTextRaw()
5510        text_to_search = text[:pos+1]
5511        for i in range(count):
5512            pos = text_to_search.rfind(search_char)
5513            text_to_search = text[:pos]
5514           
5515        if pos > -1:
5516            if not wrap_lines:
5517                text_to_check = self.ctrl.GetTextRange(start_pos, pos)
5518                if self.ctrl.GetEOLChar() in text_to_check:
5519                    return False
5520            self.ctrl.GotoPos(pos)
5521            return True
5522
5523        self.visualBell("RED")
5524        return False
5525
5526    def FindMatchingBrace(self, brace):
5527        if brace in self.BRACES:
5528            forward = True
5529            b = self.BRACES
5530        elif brace in self.REVERSE_BRACES:
5531            forward = False
5532            b = self.REVERSE_BRACES
5533        else:
5534            return
5535
5536        start_pos = self.ctrl.GetCurrentPos()
5537        if forward:
5538            text = self.ctrl.GetTextRaw()[start_pos+1:]
5539        else:
5540            text = self.ctrl.GetTextRaw()[0:start_pos:][::-1]
5541        brace_count = 1
5542        n = 0
5543        pos = -1
5544        for i in text:
5545            n += 1
5546            if i == brace:
5547                brace_count += 1
5548            elif i == b[brace]:
5549                brace_count -= 1
5550
5551            if brace_count < 1:
5552                if forward:
5553                    pos = start_pos + n
5554                else:
5555                    pos = start_pos - n
5556                break
5557
5558        if pos > -1:
5559            self.ctrl.GotoPos(pos)
5560            return True
5561        else:
5562            self.visualBell("RED")
5563            return False
5564
5565    def FindChar(self, code=None, reverse=False, offset=0, count=1, \
5566                                                            repeat=True):
5567        """
5568        Searches current *line* for specified character.
5569       
5570        Will move the caret to this place (+/- any offset supplied).
5571
5572        @param code: keycode of character to search for.
5573        @param reverse: If True will search backwards.
5574        @param offset: Offset to move caret post search.
5575        @param count: Number of characters to find. If not all found,
5576            i.e. count is 3 but only 2 characters on current line, will
5577            not move caret.
5578        @param repeat: Should the search be saved so it will be
5579            repeated (by "," and ";")
5580
5581        @rtype: bool
5582        @return: True if successful, False if not.
5583
5584        """
5585        if code is None:
5586            return False
5587        # Weird stuff happens when searching for a unicode string
5588        char = bytes(self.GetCharFromCode(code))
5589        pos = self.ctrl.GetCurrentPos()
5590       
5591        if repeat:
5592            # First save cmd so it can be repeated later
5593            # Vim doesn't save the count so a new one can be used next time
5594            self.last_find_cmd = {
5595                                            "code": code,
5596                                            "reverse": reverse,
5597                                            "offset": offset,
5598                                            "repeat": False
5599                                            }
5600
5601        if reverse: # Search backwards
5602            search_cmd = self.SearchBackwardsForChar
5603            start_offset = -1
5604            offset = - offset
5605
5606        else: # Search forwards
5607            search_cmd = self.SearchForwardsForChar
5608            start_offset = 0
5609         
5610        if search_cmd(char, count, False, start_offset):
5611            self.MoveCaretPos(offset)
5612            self.ctrl.ChooseCaretX()
5613            return True
5614
5615        return False
5616
5617    def FindNextChar(self, keycode):
5618        self.FindChar(keycode, count=self.count)
5619       
5620    def FindNextCharBackwards(self, keycode):
5621        cmd = self.FindChar(keycode, count=self.count, reverse=True)
5622
5623    def FindUpToNextChar(self, keycode):
5624        cmd = self.FindChar(keycode, count=self.count, offset=-1)
5625       
5626    def FindUpToNextCharBackwards(self, keycode):
5627        cmd = self.FindChar(keycode, count=self.count, reverse=True, offset=-1)
5628
5629    def GetLastFindCharCmd(self):
5630        return self.last_find_cmd
5631
5632    def RepeatLastFindCharCmd(self):
5633        args = self.GetLastFindCharCmd()
5634        if args is not None:
5635            # Set the new count
5636            args["count"] = self.count
5637            self.FindChar(**args)
5638       
5639    def RepeatLastFindCharCmdReverse(self):
5640        args = self.GetLastFindCharCmd()
5641        if args is not None:
5642            args["count"] = self.count
5643            args["reverse"] = not args["reverse"]
5644            self.FindChar(**args)
5645            args["reverse"] = not args["reverse"]
5646
5647    def MatchBraceUnderCaret(self):
5648        return self.FindMatchingBrace(self.GetUnichrAt(
5649                                                self.ctrl.GetCurrentPos()))
5650
5651    # TODO: vim like searching
5652    def _SearchText(self, text, forward=True, match_case=True, wrap=True,
5653                                                            whole_word=True):
5654        """
5655        Searches for next occurance of 'text'
5656
5657        @param text: text to search for
5658        @param forward: if true searches forward in text, else
5659                        search in reverse
5660        @param match_case: should search be case sensitive? 
5661        """
5662        self.AddJumpPosition(self.ctrl.GetCurrentPos() - len(text))
5663
5664        search_cmd = self.ctrl.SearchNext if forward else self.ctrl.SearchPrev
5665       
5666        # There must be a better way to do this
5667        if whole_word and match_case:
5668            flags = wx.stc.STC_FIND_WHOLEWORD|wx.stc.STC_FIND_MATCHCASE
5669        elif whole_word:
5670            flags = wx.stc.STC_FIND_WHOLEWORD
5671        elif match_case:
5672            flags = wx.stc.STC_FIND_MATCHCASE
5673
5674        pos = search_cmd(flags, text)
5675
5676        if pos == -1 and wrap:
5677            if forward:
5678                self.ctrl.GotoLine(0)
5679            else:
5680                self.ctrl.GotoLine(self.ctrl.GetLineCount())
5681            self.ctrl.SearchAnchor()
5682            pos = search_cmd(flags, text)
5683        if pos != -1:
5684            self.ctrl.GotoPos(pos)
5685
5686    def _SearchCaretWord(self, forward=True, match_case=True, whole_word=True):
5687        """
5688        Searches for next occurance of word currently under
5689        the caret
5690
5691        @param forward: if true searches forward in text, else
5692                        search in reverse
5693        @param match_case: should search be case sensitive? 
5694        @param whole_word: must the entire string match as a word
5695        """ 
5696        self.SelectInWord()
5697        self.ExtendSelectionIfRequired()
5698        text = self.ctrl.GetSelectedText()
5699        #offset = 1 if forward else -1
5700        #self.MoveCaretPos(offset)
5701        if forward:
5702            self.ctrl.CharRight()
5703        else:
5704            self.ctrl.CharLeft()
5705        self.ctrl.SearchAnchor()
5706        self._SearchText(text, forward, match_case=match_case, wrap=True, whole_word=whole_word)
5707       
5708        self.last_search_args = {'text' : text, 'forward' : forward,
5709                                 'match_case' : match_case,
5710                                 'whole_word' : whole_word}
5711
5712    def SearchCaretWordForwards(self):
5713        """Helper function to allow repeats"""
5714        self._SearchCaretWord(True, True, True)
5715
5716    def SearchPartialCaretWordForwards(self):
5717        self._SearchCaretWord(True, True, False)
5718
5719    def SearchCaretWordBackwards(self):
5720        """Helper function to allow repeats"""
5721        self._SearchCaretWord(False, True, True)
5722
5723    def SearchPartialCaretWordBackwards(self):
5724        self._SearchCaretWord(False, True, False)
5725
5726    def ContinueLastSearch(self, reverse):
5727        """
5728        Repeats last search command
5729        """
5730        args = self.last_search_args
5731        if args is not None:
5732            # If "N" we need to reverse the search direction
5733            if reverse:
5734                args['forward'] = not args['forward']
5735
5736            offset = 1 if args['forward'] else -1
5737            self.MoveCaretPos(offset)
5738            self.ctrl.SearchAnchor()
5739
5740            self._SearchText(**args)
5741
5742            # Restore search direction (could use copy())
5743            if reverse:
5744                args['forward'] = not args['forward']
5745
5746    def ContinueLastSearchSameDirection(self):
5747        """Helper function to allow repeats"""
5748        self.ContinueLastSearch(False)
5749
5750    def ContinueLastSearchReverseDirection(self):
5751        """Helper function to allow repeats"""
5752        self.ContinueLastSearch(True)
5753
5754    #--------------------------------------------------------------------
5755    # Replace
5756    #--------------------------------------------------------------------
5757   
5758    def ReplaceChar(self, keycode):
5759        """
5760        Replaces character under caret
5761
5762        Contains some custom code to allow repeating
5763        """
5764        # TODO: visual indication
5765        char = unichr(keycode)
5766
5767        # If in visual mode use the seletion we have (not the count)
5768        if self.mode == ViHelper.VISUAL:
5769            sel_start, sel_end = self._GetSelectionRange()
5770            count = sel_end - sel_start
5771            self.ctrl.GotoPos(sel_start)
5772        else:
5773            count = self.count
5774
5775        # Replace does not wrap lines and fails if you try and replace
5776        # non existent chars
5777        line, pos = self.ctrl.GetCurLineRaw()
5778        line_length = len(line)
5779
5780        # If we are on the last line we need to increase the line
5781        # length by 1 (as the last line has no eol char)
5782        if self.ctrl.GetLineCount() == self.ctrl.GetCurrentLine() + 1:
5783            line_length += 1
5784
5785        if pos + count > line_length:
5786            return
5787
5788        self.last_cmd = 3, keycode, count, None
5789
5790        self.BeginUndo()
5791        self.StartSelection()
5792        self.ctrl.GotoPos(self.ctrl.GetCurrentPos()+count)
5793        self.EndDelete()
5794        self.Repeat(self.InsertText, arg=char)
5795        if pos + count != line_length:
5796            self.MoveCaretPos(-1)
5797        self.EndUndo()
5798
5799    def StartReplaceMode(self):
5800        # TODO: visual indication
5801        self.BeginUndo()
5802        self.SetMode(ViHelper.REPLACE)
5803
5804    #--------------------------------------------------------------------
5805    # Marks
5806    #--------------------------------------------------------------------
5807
5808    def _SetMark(self, code):
5809        """
5810        Not called directly (call self.Mark instead)
5811        """
5812        page = self.ctrl.presenter.getWikiWord()
5813        self.marks[page][code] = self.ctrl.GetCurrentPos()
5814
5815    def GotoMark(self, char):
5816        # TODO '' and `` goto previous jump
5817        page = self.ctrl.presenter.getWikiWord()
5818        if char in self.marks[page]:
5819            self.AddJumpPosition()
5820            pos = self.marks[page][char]
5821
5822            # If mark is set past the end of the document just
5823            # go to the end
5824            pos = min(self.ctrl.GetLength(), pos)
5825
5826            self.ctrl.GotoPos(pos)
5827            self.visualBell("GREEN")
5828            return True
5829
5830        self.visualBell("RED")
5831        return False
5832
5833    def GotoMarkIndent(self, char):
5834        if self.GotoMark(char):
5835            self.GotoLineIndent()
5836
5837    #--------------------------------------------------------------------
5838    # Copy and Paste commands
5839    #--------------------------------------------------------------------
5840
5841    def YankLine(self):
5842        """Copy the current line text to the clipboard"""
5843
5844        line_no = self.ctrl.GetCurrentLine()
5845        max_line = min(line_no+self.count-1, self.ctrl.GetLineCount())
5846        start = self.GetLineStartPos(line_no)
5847        end = self.ctrl.GetLineEndPosition(max_line)
5848
5849        text = self.ctrl.GetTextRange(start, end) + self.ctrl.GetEOLChar()
5850
5851        self.ctrl.Copy(text)
5852
5853    def YankSelection(self, lines=False):
5854        """Copy the current selection to the clipboard"""
5855        if self.mode == ViHelper.VISUAL:
5856            self.ExtendSelectionIfRequired()
5857        if lines:
5858            self.SelectFullLines()
5859            self.ctrl.CharRightExtend()
5860        elif self.GetSelMode() == "LINE":
5861            # Selection needs to be the correct way round
5862            start, end = self._GetSelectionRange()
5863            self.ctrl.SetSelection(start, end)
5864            self.ctrl.CharRightExtend()
5865
5866        self.ctrl.Copy()
5867
5868    def Yank(self, lines=False):
5869        self.SelectSelection()
5870        #if self.mode == ViHelper.VISUAL:
5871        #    start = self.ExtendSelectionIfRequired()
5872        self.YankSelection(lines)
5873        self.GotoSelectionStart()
5874
5875    def Put(self, before, count=None):
5876        count = count if count is not None else self.count
5877        text = getTextFromClipboard()
5878
5879        self.BeginUndo(True)
5880
5881        # If its not text paste as normal for now
5882        if not text:
5883            self.ctrl.Paste()
5884
5885        # Test for line as they are handled differently
5886        eol = self.ctrl.GetEOLChar()
5887        eol_len = len(eol)
5888        if len(text) > eol_len:
5889            is_line = text[-len(eol):] == eol
5890        else:
5891            is_line = False
5892
5893
5894        if is_line:
5895            if not before: 
5896                # If pasting a line we have to goto the end before moving caret
5897                # down to handle long lines correctly
5898                self.ctrl.LineEnd()
5899                self.MoveCaretDown(1)
5900            self.GotoLineStart()
5901
5902        if self.HasSelection():
5903            self.ctrl.Clear()
5904
5905        #self.Repeat(self.InsertText, arg=text)
5906        self.InsertText(count * text)
5907
5908        if is_line:
5909            #if before:
5910            #    self.MoveCaretUp(1)
5911            self.GotoLineIndent()
5912
5913        self.EndUndo()
5914               
5915    #--------------------------------------------------------------------
5916    # Deletion commands
5917    #--------------------------------------------------------------------
5918    def DeleteSurrounding(self, code):
5919        char = self.GetCharFromCode(code)
5920        if char in self.text_object_map:
5921            self.SelectATextObject(char)
5922            pos = self.ExtendSelectionIfRequired()
5923            self.ctrl.ReplaceSelection(self.ctrl.GetSelectedText()[1:-1])
5924            self.ctrl.GotoPos(pos)
5925
5926    def EndDelete(self):
5927        self.SelectSelection()
5928        self.DeleteSelection()
5929
5930    def EndDeleteInsert(self):
5931        self.SelectSelection()
5932        self.DeleteSelection()
5933        self.Insert()
5934
5935    def DeleteRight(self):
5936        self.ctrl.BeginUndoAction
5937        self.StartSelection()
5938        self.MoveCaretRight()
5939        self.SelectSelection()
5940       
5941        # If the selection len is less than the count we need to select
5942        # the last character on the line
5943        if len(self.ctrl.GetSelectedText()) < self.count:
5944            self.ctrl.CharRightExtend()
5945        self.DeleteSelection()
5946        self.EndUndo()
5947        self.CheckLineEnd()
5948
5949    def DeleteLeft(self):
5950        self.BeginUndo()
5951        self.StartSelection()
5952        self.MoveCaretLeft()
5953        self.SelectSelection()
5954        self.DeleteSelection()
5955        self.EndUndo()
5956
5957    def DeleteRightAndInsert(self):
5958        self.DeleteRight()
5959        self.Insert()
5960
5961    def DeleteLinesAndIndentInsert(self):
5962        self.BeginUndo()
5963        indent = self.ctrl.GetLineIndentation(self.ctrl.GetCurrentLine())
5964        self.DeleteLine()
5965        self.OpenNewLine(True, indent=indent)
5966        self.EndUndo()
5967
5968    def DeleteLine(self):
5969        self.BeginUndo()
5970        self.SelectCurrentLine()
5971        self.DeleteSelection()
5972        self.EndUndo()
5973       
5974
5975    #--------------------------------------------------------------------
5976    # Movement commands
5977    #--------------------------------------------------------------------
5978
5979    def GetLineStartPos(self, line):
5980        return self.ctrl.GetLineIndentPosition(line) - \
5981                                    self.ctrl.GetLineIndentation(line)
5982
5983    def GotoLineStart(self):
5984        self.ctrl.Home()
5985
5986    def GotoLineEnd(self, true_end=True):
5987        self.ctrl.LineEnd()
5988        if not true_end:
5989            self.ctrl.CharLeft()
5990
5991    def GotoLineIndentPreviousLine(self):
5992        line = max(0, self.ctrl.GetCurrentLine()-1)
5993        self.GotoLineIndent(line)
5994
5995    def GotoLineIndentNextLine(self):
5996        line = min(self.ctrl.GetLineCount(), self.ctrl.GetCurrentLine()+1)
5997        self.GotoLineIndent(line)
5998
5999    def GotoLineIndent(self, line=None):
6000        """
6001        Moves caret to first non-whitespace character on "line".
6002
6003        If "line" is None current line is used.
6004
6005        @param line: Line number
6006
6007        """
6008        if line is None: line = self.ctrl.GetCurrentLine()
6009        self.ctrl.GotoPos(self.ctrl.GetLineIndentPosition(line))
6010        self.ctrl.ChooseCaretX()
6011
6012    def GotoColumn(self, pos=None):
6013        """
6014        Moves caret to "pos" on current line. If no pos specified use "count".
6015
6016        @param pos: Column position to move caret to.
6017        """
6018        if pos is None: pos = self.count
6019        line = self.ctrl.GetCurrentLine()
6020        lstart = self.ctrl.PositionFromLine(line)
6021        lend = self.ctrl.GetLineEndPosition(line)
6022        line_len = lend - lstart
6023        column = min(line_len, pos)
6024        self.ctrl.GotoPos(lstart + column)
6025
6026        self.ctrl.ChooseCaretX()
6027
6028    def GotoSentenceStart(self, count=None):
6029        self.AddJumpPosition()
6030        self.Repeat(self._MoveCaretSentenceStart, count)
6031
6032    def _MoveCaretSentenceStart(self, pos=None, start_pos=None):
6033        """
6034        Internal function to move caret to sentence start.
6035
6036        Call GotoSentenceStart instead.
6037        """
6038        if pos is None:
6039            pos = self.ctrl.GetCurrentPos()-1
6040        if start_pos is None:
6041            start_pos = pos
6042        char = self.GetUnichrAt(pos)
6043
6044        page_length = self.ctrl.GetLength()
6045
6046        text = self.ctrl.GetText()[:pos]
6047
6048        n = -1
6049        for i in self.SENTENCE_ENDINGS:
6050            index = text.rfind(i)
6051            if index != -1 and index > n:
6052                n = index
6053        pos = n
6054
6055        if pos < 1:
6056            self.ctrl.GotoPos(0)
6057            return
6058
6059        sentence_end_pos =  pos
6060        forward_char = self.GetUnichrAt(pos+1)
6061        if forward_char in self.SENTENCE_ENDINGS_SUFFIXS:
6062            pos += 1
6063            while forward_char in self.SENTENCE_ENDINGS_SUFFIXS:
6064                pos += 1
6065                forward_char = self.GetUnichrAt(pos)
6066        if forward_char in string.whitespace:
6067            while forward_char in string.whitespace:
6068                pos += 1
6069                forward_char = self.GetUnichrAt(pos)
6070        else:
6071            self._MoveCaretSentenceStart(pos-1, start_pos)
6072            return
6073
6074        if start_pos >= pos:
6075            self.ctrl.GotoPos(pos)
6076        else:
6077            self._MoveCaretSentenceStart(sentence_end_pos-1, start_pos)
6078
6079    def GotoNextSentence(self, count=None):
6080        self.AddJumpPosition()
6081        self.Repeat(self._MoveCaretNextSentence, count)
6082
6083    def GotoSentenceEnd(self, count=None):
6084        self.Repeat(self._MoveCaretNextSentence, count, False)
6085
6086    def GotoSentenceEndCountWhitespace(self, count=None):
6087        if count is None: count = self.count
6088
6089        if count % 2:
6090            include_whitespace = False
6091            count = count / 2 + 1
6092            move_left = False
6093        else:
6094            include_whitespace = True
6095            count = count / 2
6096            move_left = True
6097       
6098        self.Repeat(self._MoveCaretNextSentence, count, include_whitespace)
6099
6100        if move_left:
6101            self.ctrl.CharLeftExtend()
6102
6103        #self.ctrl.CharLeftExtend()
6104
6105    def _MoveCaretNextSentence(self, include_whitespace=True,
6106                                        pos=None, start_pos=None):
6107        # Could be combined with _MoveCaretBySentence func
6108        if pos is None:
6109            pos = self.ctrl.GetCurrentPos()+1
6110        if start_pos is None:
6111            start_pos = pos
6112        char = self.GetUnichrAt(pos)
6113
6114        page_length = self.ctrl.GetLength()
6115
6116        text = self.ctrl.GetText()[pos:]
6117
6118        n = page_length
6119        for i in self.SENTENCE_ENDINGS:
6120            index = text.find(i)
6121            if index != -1 and index < n:
6122                n = index
6123        pos = pos + n
6124
6125        if pos+1 >= page_length:
6126            self.ctrl.GotoPos(page_length)
6127            return
6128
6129        sentence_end_pos = pos
6130        forward_char = self.GetUnichrAt(pos+1)
6131        if forward_char in self.SENTENCE_ENDINGS_SUFFIXS:
6132            pos += 1
6133            while forward_char in self.SENTENCE_ENDINGS_SUFFIXS:
6134                pos += 1
6135                forward_char = self.GetUnichrAt(pos)
6136            sentence_end_pos = pos-1
6137        if forward_char in string.whitespace:
6138            while forward_char in string.whitespace:
6139                pos += 1
6140                forward_char = self.GetUnichrAt(pos)
6141        else:
6142            self._MoveCaretNextSentence(include_whitespace, pos+1, start_pos)
6143            return
6144
6145        if start_pos <= pos:
6146            if include_whitespace:
6147                self.ctrl.GotoPos(pos)
6148            else:
6149                self.ctrl.GotoPos(sentence_end_pos)
6150        else:
6151            self._MoveCaretNextSentence(include_whitespace,
6152                                            sentence_end_pos, start_pos)
6153
6154    def MoveCaretRight(self):
6155        self.MoveCaretPos(self.count)
6156
6157    #def MoveCaretLineUp(self, count=None):
6158    #    """Make long lines behave as in vim"""
6159    #    count = count if count is not None else self.count
6160    #    new_line_number = max(0, self.ctrl.GetCurrentLine()-count)
6161    #    self.ctrl.GotoLine(new_line_number)
6162
6163    #def MoveCaretLineDown(self, count=None):
6164    #    """Make long lines behave as in vim"""
6165    #    count = count if count is not None else self.count
6166    #    new_line_number = min(self.ctrl.GetLineCount(), self.ctrl.GetCurrentLine()+count)
6167    #    self.ctrl.GotoLine(new_line_number)
6168
6169    def MoveCaretUp(self, count=None):
6170        self.Repeat(self.ctrl.LineUp, count)
6171        self.CheckLineEnd()
6172
6173    def MoveCaretDown(self, count=None):
6174        self.Repeat(self.ctrl.LineDown, count)
6175        self.CheckLineEnd()
6176
6177    def MoveCaretToLine(self, line):
6178        current_line = self.ctrl.GetCurrentLine()
6179        if line == current_line:
6180            return
6181
6182        if line < current_line:
6183            scroll_func = self.ctrl.LineUp
6184        else:
6185            scroll_func = self.ctrl.LineDown
6186
6187        to_move = abs(current_line - line)
6188
6189        for i in range(to_move):
6190            scroll_func()
6191
6192
6193    def MoveCaretDownAndIndent(self, count=None):
6194        self.Repeat(self.ctrl.LineDown, count)
6195        self.GotoLineIndent()
6196
6197    def MoveCaretLeft(self):
6198        self.MoveCaretPos(-self.count)
6199
6200    def MoveCaretPos(self, offset):
6201        """
6202        Move caret by a given offset
6203        """
6204        line, line_pos = self.ctrl.GetCurLine()
6205        line_no = self.ctrl.GetCurrentLine()
6206
6207        if self.mode == ViHelper.VISUAL:
6208            if offset > 0:
6209                move_right = True
6210                move = self.ctrl.CharRightExtend
6211                stop_pos = self.GetLineStartPos(line_no) + \
6212                                self.ctrl.LineLength(line_no)-1
6213            else:
6214                move_right = False
6215                move = self.ctrl.CharLeftExtend
6216                stop_pos = self.GetLineStartPos(line_no)
6217        else:
6218            if offset > 0:
6219                move_right = True
6220                move = self.ctrl.CharRight
6221                stop_pos = self.GetLineStartPos(line_no) + \
6222                                self.ctrl.LineLength(line_no)-2
6223
6224                # Fix for last line (no EOL char present)
6225                if line_no+1 == self.ctrl.GetLineCount():
6226                    stop_pos += 1
6227            else:
6228                move_right = False
6229                move = self.ctrl.CharLeft
6230                stop_pos = self.GetLineStartPos(line_no)
6231
6232        for i in range(abs(offset)):
6233            if (move_right and self.ctrl.GetCurrentPos() < stop_pos) or \
6234               (not move_right and self.ctrl.GetCurrentPos() > stop_pos):
6235                move()
6236            else:
6237                break
6238
6239        ## The code below is faster but does not handle
6240        ## unicode charcters nicely
6241        #line, line_pos = self.ctrl.GetCurLine()
6242        #line_no = self.ctrl.GetCurrentLine()
6243        #pos = max(line_pos + offset, 0)
6244        #if self.mode == ViHelper.VISUAL:
6245        #    pos = min(pos, self.ctrl.LineLength(line_no)-1)
6246        #else:
6247        #    pos = min(pos, self.ctrl.LineLength(line_no)-2)
6248        #self.ctrl.GotoPos(self.GetLineStartPos(line_no) + pos)
6249        #self.ctrl.ChooseCaretX()
6250
6251    def MoveCaretLinePos(self, offset):
6252        """
6253        Move caret line position by a given offset
6254
6255        Faster but does not maintain line position
6256        """
6257        self.ctrl.ChooseCaretX()
6258        line = max(self.ctrl.GetCurrentLine() + offset, 0)
6259        line = min(line, self.ctrl.GetLineCount())
6260        self.ctrl.GotoLine(line)
6261        line_start_pos = self.ctrl.GetCurrentPos()
6262
6263        pos = max(index, 0)
6264        pos = min(pos, self.ctrl.GetLineEndPosition(line)-line_start_pos)
6265        self.ctrl.GotoPos(line_start_pos+pos)
6266        #self.ctrl.ChooseCaretX()
6267
6268    def MoveCaretToLinePos(self, line, index):
6269        line = max(line, 0)
6270        line = min(line, self.ctrl.GetLineCount())
6271        self.ctrl.GotoLine(line)
6272        line_start_pos = self.ctrl.GetCurrentPos()
6273        pos = max(index, 0)
6274        pos = min(pos, self.ctrl.GetLineEndPosition(line)-line_start_pos)
6275        self.ctrl.GotoPos(line_start_pos+pos)
6276
6277    def SaveCaretPos(self):
6278        page = self.ctrl.presenter.getWikiWord()
6279        self.previous_positions[page].append(self.ctrl.GetCurrentPos())
6280        self.future_positions[page] = []
6281
6282# word-motions
6283    def MoveCaretWordEndCountWhitespace(self, count=None):
6284        self.Repeat(self._MoveCaretWord, count,
6285                        { "recursion" : False, "count_whitespace" : True, \
6286                                    "only_whitespace" : False })
6287
6288    def MoveCaretNextWord(self, count=None):
6289        self.Repeat(self.ctrl.WordRight, count)
6290
6291    def MoveCaretPreviousWordEnd(self, count=None):
6292        # TODO: complete
6293        self.Repeat(self._MoveCaretWord, count, {
6294                                    "recursion" : False,
6295                                    "count_whitespace" : False,
6296                                    "only_whitespace" : False,
6297                                    "reverse" : True
6298                                    })
6299
6300    def MoveCaretWordEnd(self, count=None):
6301        self.Repeat(self._MoveCaretWord, count)
6302
6303    def _MoveCaretWord(self, recursion=False, count_whitespace=False,
6304                                        only_whitespace=False, reverse=False):
6305        """
6306        wxStyledTextCtrl's WordEnd function behaves differently to
6307        vims so it need to be replaced to get equivalent function
6308
6309        """
6310        pos = start_pos = self.ctrl.GetCurrentPos()
6311        char = self.GetUnichrAt(pos)
6312
6313        if char is None:
6314            return
6315
6316        text_length = self.ctrl.GetTextLength()
6317
6318        if reverse:
6319            offset = -1
6320            move = self.ctrl.CharLeft
6321            move_extend = self.ctrl.CharLeftExtend
6322        else:
6323            offset = 1
6324            move = self.ctrl.CharRight
6325            move_extend = self.ctrl.CharRightExtend
6326
6327        # If the current char is whitespace we either skip it or count
6328        # it depending on "count_whitespace"
6329        if char in string.whitespace:
6330            char = self.GetUnichrAt(pos + offset)
6331            if char is not None:
6332                while char is not None and char in string.whitespace:
6333                    pos = pos + offset
6334                    char = self.GetUnichrAt(pos + offset)
6335            if not count_whitespace:
6336                self._GotoPos(pos)
6337                move()
6338                self._MoveCaretWord(recursion=True,
6339                                   count_whitespace=count_whitespace,
6340                                   only_whitespace=only_whitespace,
6341                                   reverse=reverse)
6342                return
6343        # If we want a minor word end and start in punctuation we goto
6344        # end of the punctuation
6345        elif not only_whitespace and char in self.WORD_BREAK:
6346            char = self.GetUnichrAt(pos + offset)
6347            if char is not None:
6348                while char is not None and char in self.WORD_BREAK:
6349                    pos = pos + offset
6350                    char = self.GetUnichrAt(pos + offset)
6351        # Else offset forwards to first punctuation or whitespace char
6352        # (or just whitespace char if only_whitespace = True)
6353        else:
6354            char = self.GetUnichrAt(pos + offset)
6355            if char is not None:
6356                while char is not None and \
6357                        ((only_whitespace or char not in self.WORD_BREAK) and \
6358                        char not in string.whitespace) or char in ("_"):
6359                    pos = pos + offset
6360                    char = self.GetUnichrAt(pos + offset)
6361 
6362        if pos != start_pos or recursion:
6363            self._GotoPos(pos)
6364        else:
6365            move_extend()
6366            self._MoveCaretWord(True, count_whitespace=count_whitespace,
6367                        only_whitespace=only_whitespace, reverse=reverse)
6368            return
6369
6370    def MoveCaretToWhitespaceStart(self):
6371        start = self.ctrl.GetCurrentPos()
6372        while self.ctrl.GetCharAt(start-1) in self.SINGLE_LINE_WHITESPACE:
6373            start -= 1
6374        self.ctrl.GotoPos(start)
6375
6376    def MoveCaretBackWord(self, count=None):
6377        self.Repeat(self._MoveCaretWord, count, {
6378                                    "recursion" : False,
6379                                    "count_whitespace" : False,
6380                                    "only_whitespace" : False,
6381                                    "reverse" : True
6382                                    })
6383
6384    def MoveCaretBackWORD(self, count=None):
6385        self.Repeat(self._MoveCaretWord, count, {
6386                                    "recursion" : False,
6387                                    "count_whitespace" : False,
6388                                    "only_whitespace" : True,
6389                                    "reverse" : True
6390                                    })
6391
6392    def MoveCaretNextWORD(self, count=None):
6393        """Wordbreaks are spaces"""
6394        def func():
6395            self.ctrl.WordRight()
6396            while self.GetChar(-1) and not self.GetChar(-1).isspace():
6397                self.ctrl.WordRight()
6398        self.Repeat(func, count)
6399
6400    def MoveCaretWordEND(self, count=None):
6401        self.Repeat(self._MoveCaretWord, count, {
6402                                    "recursion" : False,
6403                                    "count_whitespace" : False,
6404                                    "only_whitespace" : True
6405                                    })
6406
6407    def MoveCaretWordENDCountWhitespace(self, count=None):
6408        self.Repeat(self._MoveCaretWord, count, {
6409                                    "recursion" : False,
6410                                    "count_whitespace" : True,
6411                                    "only_whitespace" : True
6412                                    })
6413
6414    def MoveCaretParaUp(self, count=None):
6415        self.AddJumpPosition()
6416        self.Repeat(self.ctrl.ParaUp, count)
6417
6418    def MoveCaretParaDown(self, count=None):
6419        self.AddJumpPosition()
6420        self.Repeat(self.ctrl.ParaDown, count)
6421
6422    def _GotoPos(self, pos):
6423        """
6424        Save caret position
6425        """
6426        self.ctrl.GotoPos(pos)
6427        self.ctrl.ChooseCaretX()
6428
6429    def DocumentNavigation(self, key):
6430        """
6431        It may be better to seperate this into multiple functions
6432        """
6433        if key in [71, (103, 103), 37]:
6434            self.AddJumpPosition()
6435       
6436        # %, G or gg
6437        if self.true_count:
6438            if key in [71, (103, 103)]:
6439                # Correct for line 0
6440                self.MoveCaretToLinePos(self.count-1, self.ctrl.GetCurLine()[1])
6441            elif key == 37: # %
6442                max_lines = self.ctrl.GetLineCount()
6443                # Same as   int(self.count / 100 * max_lines)  but needs only
6444                #   integer arithmetic
6445                line_percentage = (self.count * max_lines) // 100
6446                self.MoveCaretToLinePos(line_percentage, self.ctrl.GetCurLine()[1])
6447
6448        elif key == 37:
6449            # If key is % but no count it is used for brace matching
6450            self.MatchBraceUnderCaret()
6451
6452        elif key == (103, 103):
6453            self.ctrl.GotoLine(0)
6454
6455        elif key == (71):
6456            self.ctrl.GotoLine(self.ctrl.GetLineCount())
6457
6458    def GotoViewportTop(self):
6459        self.GotoLineIndent(self.ctrl.GetFirstVisibleLine())
6460       
6461    def GotoViewportMiddle(self):
6462        self.GotoLineIndent(self.ctrl.GetMiddleVisibleLine())
6463
6464    def GotoViewportBottom(self):
6465        self.GotoLineIndent(self.ctrl.GetLastVisibleLine())
6466
6467    def ScrollViewportTop(self):
6468        self._PositionViewport(0)
6469
6470    def ScrollViewportMiddle(self):
6471        self._PositionViewport(0.5)
6472
6473    def ScrollViewportBottom(self):
6474        self._PositionViewport(1)
6475
6476    def ScrollViewportUpHalfScreen(self):
6477        self._ScrollViewport(-0.5)
6478
6479    def ScrollViewportUpFullScreen(self):
6480        # vim has a 2 line offset
6481        # see *03.7*
6482        self._ScrollViewport(-1)
6483
6484    def ScrollViewportDownHalfScreen(self):
6485        self._ScrollViewport(0.5)
6486
6487    def ScrollViewportDownFullScreen(self):
6488        self._ScrollViewport(1)
6489
6490    def ScrollViewportLineDown(self):
6491        self._ScrollViewportByLines(1)
6492
6493    def ScrollViewportLineUp(self):
6494        self._ScrollViewportByLines(-1)
6495
6496    def Undo(self, count=None):
6497        if self.ctrl.CanUndo():
6498            self.visualBell("GREEN")
6499            self.Repeat(self._Undo, count)
6500        else:
6501            self.visualBell("RED")
6502
6503    def Redo(self, count=None):
6504        if self.ctrl.CanRedo():
6505            self.visualBell("GREEN")
6506            self.Repeat(self._Redo, count)
6507        else:
6508            self.visualBell("RED")
6509
6510# The following commands are basic ways to enter insert mode
6511    def Insert(self):
6512        self.BeginUndo(True)
6513        self.SetMode(ViHelper.INSERT)
6514
6515    def Append(self):
6516        if self.ctrl.GetCurrentPos() != self.ctrl.GetLineEndPosition(self.ctrl.GetCurrentLine()):
6517            self.ctrl.CharRight()
6518        self.Insert()
6519
6520    def InsertAtLineStart(self):
6521        # Goto line places the caret at the start of the line
6522        self.GotoLineIndent(self.ctrl.GetCurrentLine())
6523        self.ctrl.ChooseCaretX()
6524        self.Insert()
6525
6526    def AppendAtLineEnd(self):
6527        self.ctrl.GotoPos(self.ctrl.GetLineEndPosition(
6528                                    self.ctrl.GetCurrentLine()))
6529        self.Append()
6530
6531    def OpenNewLine(self, above, indent=None):
6532        self.BeginUndo(True)
6533        self.BeginUndo()
6534
6535        if indent is None:
6536            indent = self.ctrl.GetLineIndentation(self.ctrl.GetCurrentLine())
6537
6538        if above:
6539            self.MoveCaretUp(1)
6540        self.GotoLineEnd()
6541        self.ctrl.AddText(self.ctrl.GetEOLChar())
6542        self.ctrl.SetLineIndentation(self.ctrl.GetCurrentLine(), indent)
6543        self.EndUndo()
6544        self.AppendAtLineEnd()
6545
6546    def TruncateLine(self):
6547        self.ctrl.LineEndExtend()
6548        self.ctrl.CharLeftExtend()
6549        self.ExtendSelectionIfRequired()
6550        self.DeleteSelection()
6551
6552    def TruncateLineAndInsert(self):
6553        self.TruncateLine()
6554        self.Insert()
Note: See TracBrowser for help on using the browser.