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

Revision 295, 217.9 kB (checked in by mbutscher, 22 months ago)

branches/mbutscher/work:
* vi improvements (3) (Ross' repSVN.: bf203990adeb2e706175fcb7c657af5df0dd3af9:21e5b31bc7d722c6ce4a7d02ff1baf2295d7b537)

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