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

Revision 297, 233.4 kB (checked in by mbutscher, 22 months ago)

branches/mbutscher/work:
* My changes to Ross' changes: unbind scroll events when options changed before possible bind for vi support

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