' vibreoffice - Vi Mode for LibreOffice/OpenOffice ' ' The MIT License (MIT) ' ' Copyright (c) 2014 Sean Yeh ' ' Permission is hereby granted, free of charge, to any person obtaining a copy ' of this software and associated documentation files (the "Software"), to deal ' in the Software without restriction, including without limitation the rights ' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ' copies of the Software, and to permit persons to whom the Software is ' furnished to do so, subject to the following conditions: ' ' The above copyright notice and this permission notice shall be included in ' all copies or substantial portions of the Software. ' ' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ' OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ' THE SOFTWARE. Option Explicit ' -------- ' Globals ' -------- global VIBREOFFICE_STARTED as boolean ' Defaults to False global VIBREOFFICE_ENABLED as boolean ' Defaults to False global oXKeyHandler as object ' Global State global MODE as string global VIEW_CURSOR as object global MULTIPLIER as integer ' ----------- ' Singletons ' ----------- Function getCursor getCursor = VIEW_CURSOR End Function Function getTextCursor dim oTextCursor oTextCursor = getCursor().getText.createTextCursorByRange(getCursor()) ' oTextCursor.gotoRange(oTextCursor.getStart(), False) getTextCursor = oTextCursor End Function ' ----------------- ' Helper Functions ' ----------------- Sub restoreStatus 'restore original statusbar dim oLayout oLayout = thisComponent.getCurrentController.getFrame.LayoutManager oLayout.destroyElement("private:resource/statusbar/statusbar") oLayout.createElement("private:resource/statusbar/statusbar") End Sub Sub setRawStatus(rawText) thisComponent.Currentcontroller.StatusIndicator.Start(rawText, 0) End Sub Sub setStatus(statusText) setRawStatus(MODE & " | " & statusText & " | special: " & getSpecial()) End Sub Sub setMode(modeName) MODE = modeName setRawStatus(modeName) End Sub Function gotoMode(sMode) Select Case sMode Case "NORMAL": setMode("NORMAL") Case "INSERT": setMode("INSERT") Case "VISUAL": setMode("VISUAL") dim oTextCursor oTextCursor = getTextCursor() ' Deselect TextCursor oTextCursor.gotoRange(oTextCursor.getStart(), False) ' Show TextCursor selection thisComponent.getCurrentController.Select(oTextCursor) End Select End Function Sub cursorReset(oTextCursor) oTextCursor.gotoRange(oTextCursor.getStart(), False) oTextCursor.goRight(1, False) oTextCursor.goLeft(1, True) thisComponent.getCurrentController.Select(oTextCursor) End Sub ' ----------------------------------- ' Special Mode (for chained commands) ' ----------------------------------- global SPECIAL_MODE As string global SPECIAL_COUNT As integer Sub setSpecial(specialName) SPECIAL_MODE = specialName If specialName = "" Then SPECIAL_COUNT = 0 Else SPECIAL_COUNT = 2 End If End Sub Function getSpecial() getSpecial = SPECIAL_MODE End Function Sub delaySpecialReset() SPECIAL_COUNT = SPECIAL_COUNT + 1 End Sub Sub resetSpecial(Optional bForce) If IsMissing(bForce) Then bForce = False SPECIAL_COUNT = SPECIAL_COUNT - 1 If SPECIAL_COUNT <= 0 Or bForce Then setSpecial("") End If End Sub ' -------------------- ' Multiplier functions ' -------------------- Sub _setMultiplier(n as integer) MULTIPLIER = n End Sub Sub resetMultiplier() _setMultiplier(0) End Sub Sub addToMultiplier(n as integer) dim sMultiplierStr as String dim iMultiplierInt as integer ' Max multiplier: 10000 (stop accepting additions after 1000) If MULTIPLIER <= 1000 then sMultiplierStr = CStr(MULTIPLIER) & CStr(n) _setMultiplier(CInt(sMultiplierStr)) End If End Sub ' Should only be used if you need the raw value Function getRawMultiplier() getRawMultiplier = MULTIPLIER End Function ' Same as getRawMultiplier, but defaults to 1 if it is unset (0) Function getMultiplier() If MULTIPLIER = 0 Then getMultiplier = 1 Else getMultiplier = MULTIPLIER End If End Function ' ------------- ' Key Handling ' ------------- Sub sStartXKeyHandler sStopXKeyHandler() oXKeyHandler = CreateUnoListener("KeyHandler_", "com.sun.star.awt.XKeyHandler") thisComponent.CurrentController.AddKeyHandler(oXKeyHandler) End Sub Sub sStopXKeyHandler thisComponent.CurrentController.removeKeyHandler(oXKeyHandler) End Sub Sub XKeyHandler_Disposing(oEvent) End Sub ' -------------------- ' Main Key Processing ' -------------------- function KeyHandler_KeyPressed(oEvent) as boolean ' Exit if plugin is not enabled If IsMissing(VIBREOFFICE_ENABLED) Or Not VIBREOFFICE_ENABLED Then KeyHandler_KeyPressed = False Exit Function End If dim bConsumeInput, bIsMultiplier, bIsModified, bIsSpecial, oTextCursor bConsumeInput = True ' Block all inputs by default bIsMultiplier = False ' reset multiplier by default bIsModified = oEvent.Modifiers > 1 ' If Ctrl or Alt is held down. (Shift=1) bIsSpecial = getSpecial() <> "" ' -------------------------- ' Process global shortcuts, exit if matched (like ESC) If ProcessGlobalKey(oEvent) Then ' Pass ' If INSERT mode, allow all inputs ElseIf MODE = "INSERT" Then bConsumeInput = False ' If Change Mode ElseIf MODE = "NORMAL" And Not bIsSpecial And ProcessModeKey(oEvent) Then ' Pass ' Multiplier Key ElseIf ProcessNumberKey(oEvent) Then bIsMultiplier = True delaySpecialReset() ' Normal Key ElseIf ProcessNormalKey(oEvent) Then ' Pass ' If is modified but doesn't match a normal command, allow input ' (Useful for built-in shortcuts like Ctrl+s, Ctrl+w) ElseIf bIsModified Then bConsumeInput = False ' If bIsSpecial but nothing matched, return to normal mode ElseIf bIsSpecial Then gotoMode("NORMAL") End If ' -------------------------- ' Reset Special resetSpecial() ' Reset multiplier if last input was not number and not in special mode If not bIsMultiplier and getSpecial() = "" Then resetMultiplier() End If setStatus(getMultiplier()) ' Show terminal-like cursor oTextCursor = getTextCursor() If MODE = "NORMAL" Then cursorReset(oTextCursor) ElseIf MODE = "INSERT" Then oTextCursor.gotoRange(oTextCursor.getStart(), False) thisComponent.getCurrentController.Select(oTextCursor) End If KeyHandler_KeyPressed = bConsumeInput End Function Function KeyHandler_KeyReleased(oEvent) As boolean KeyHandler_KeyReleased = (MODE = "NORMAL") 'cancel KeyReleased End Function ' ---------------- ' Processing Keys ' ---------------- Function ProcessGlobalKey(oEvent) dim bMatched bMatched = True Select Case oEvent.KeyCode ' PRESSED ESCAPE Case 1281: ' Move cursor back if was in INSERT (but stay on same line) If MODE <> "NORMAL" And Not getCursor().isAtStartOfLine() Then getCursor().goLeft(1, False) End If resetSpecial(True) gotoMode("NORMAL") Case Else: bMatched = False End Select ProcessGlobalKey = bMatched End Function Function ProcessNumberKey(oEvent) dim c c = CStr(oEvent.KeyChar) If c >= "0" and c <= "9" Then addToMultiplier(CInt(c)) ProcessNumberKey = True Else ProcessNumberKey = False End If End Function Function ProcessModeKey(oEvent) dim bMatched bMatched = True Select Case oEvent.KeyChar ' Insert modes Case "i", "a", "I", "A": If oEvent.KeyChar = "a" Then getCursor().goRight(1, False) If oEvent.KeyChar = "I" Then ProcessMovementKey("^") If oEvent.KeyChar = "A" Then ProcessMovementKey("$") gotoMode("INSERT") Case "v": gotoMode("VISUAL") Case Else: bMatched = False End Select ProcessModeKey = bMatched End Function Function ProcessNormalKey(oEvent) dim i, bMatched, bIsVisual, iIterations bIsVisual = (MODE = "VISUAL") ' is this hardcoding bad? what about visual block? ' ---------------------- ' 1. Check Movement Key ' ---------------------- iIterations = getMultiplier() bMatched = False For i = 1 To iIterations dim bMatchedMovement ' Movement Key bMatchedMovement = ProcessMovementKey(oEvent.KeyChar, bIsVisual, oEvent.Modifiers) bMatched = bMatched or bMatchedMovement Next i If bMatched Then ' If Special: d/c + movement If bMatched And (getSpecial() = "d" Or getSpecial() = "c") Then getTextCursor().setString("") If getSpecial() = "d" Then gotoMode("NORMAL") If getSpecial() = "c" Then gotoMode("INSERT") End If ProcessNormalKey = True Exit Function End If ' -------------------- ' 2. Check Delete Key ' -------------------- ' Only 'x' or Special (dd, cc) can be done more than once If oEvent.KeyChar <> "x" and getSpecial() = "" Then iIterations = 1 End If For i = 1 To iIterations dim bMatchedDelete ' Delete Key bMatchedDelete = ProcessDeleteKey(oEvent.KeyChar) ' Selection Modifier Key ?? bMatched = bMatched or bMatchedDelete Next i ProcessNormalKey = bMatched End Function Function ProcessDeleteKey(keyChar) dim oTextCursor, bMatched, bIsSpecial bMatched = True bIsSpecial = getSpecial() <> "" If keyChar = "d" Or keyChar = "c" Then ' Special Cases: 'dd' and 'cc' If bIsSpecial Then dim bIsSpecialCase bIsSpecialCase = (keyChar = "d" And getSpecial() = "d") Or (keyChar = "c" And getSpecial() = "c") If bIsSpecialCase Then ProcessMovementKey("^", False) ProcessMovementKey("j", True) oTextCursor = getTextCursor() thisComponent.getCurrentController.Select(oTextCursor) oTextCursor.setString("") Else bMatched = False End If ' Go to INSERT mode after 'cc', otherwise NORMAL If bIsSpecialCase And keyChar = "c" Then gotoMode("INSERT") Else gotoMode("NORMAL") End If ' d or c in visual mode: delete selection ElseIf MODE = "VISUAL" Then oTextCursor = getTextCursor() thisComponent.getCurrentController.Select(oTextCursor) oTextCursor.setString("") If keyChar = "c" Then gotoMode("INSERT") If keyChar = "d" Then gotoMode("NORMAL") ' Enter Special mode: 'd' or 'c' ElseIf MODE = "NORMAL" Then setSpecial(keyChar) gotoMode("VISUAL") End If ' Otherwise, ignore if bIsSpecial ElseIf bIsSpecial Then bMatched = False ElseIf keyChar = "x" Then oTextCursor = getTextCursor() thisComponent.getCurrentController.Select(oTextCursor) oTextCursor.setString("") ' Reset Cursor cursorReset(oTextCursor) ' Goto NORMAL mode (in the case of VISUAL mode) gotoMode("NORMAL") ElseIf keyChar = "D" Or keyChar = "C" Then If MODE = "VISUAL" Then ProcessMovementKey("^", False) ProcessMovementKey("$", True) ProcessMovementKey("l", True) Else ' Deselect oTextCursor = getTextCursor() oTextCursor.gotoRange(oTextCursor.getStart(), False) thisComponent.getCurrentController.Select(oTextCursor) ProcessMovementKey("$", True) End If getTextCursor().setString("") If keyChar = "D" Then gotoMode("NORMAL") ElseIf keyChar = "C" Then gotoMode("INSERT") End IF Else bMatched = False End If ProcessDeleteKey = bMatched End Function ' ----------------------- ' Main Movement Function ' ----------------------- ' Default: bExpand = False, keyModifiers = 0 Function ProcessMovementKey(keyChar, Optional bExpand, Optional keyModifiers) dim oTextCursor, bSetCursor, bMatched oTextCursor = getTextCursor() bMatched = True If IsMissing(bExpand) Then bExpand = False If IsMissing(keyModifiers) Then keyModifiers = 0 ' Check for modified keys (Ctrl, Alt, not Shift) If keyModifiers > 1 Then dim bIsControl bIsControl = (keyModifiers = 2) or (keyModifiers = 8) ' Ctrl+d and Ctrl+u If bIsControl and keyChar = "d" Then getCursor().ScreenDown(bExpand) ElseIf bIsControl and keyChar = "u" Then getCursor().ScreenUp(bExpand) Else bMatched = False End If ProcessMovementKey = bMatched Exit Function End If ' Set global cursor to oTextCursor's new position if moved bSetCursor = True Select Case keyChar Case "l": oTextCursor.goRight(1, bExpand) Case "h": oTextCursor.goLeft(1, bExpand) ' oTextCursor.goUp and oTextCursor.goDown SHOULD work, but doesn't (I dont know why). ' So this is a weird hack Case "k": 'oTextCursor.goUp(1, False) getCursor().goUp(1, bExpand) bSetCursor = False Case "j": 'oTextCursor.goDown(1, False) getCursor().goDown(1, bExpand) bSetCursor = False ' ---------- Case "^": getCursor().gotoStartOfLine(bExpand) bSetCursor = False Case "$": dim oldPos, newPos oldPos = getCursor().getPosition() getCursor().gotoEndOfLine(bExpand) newPos = getCursor().getPosition() ' If the result is at the start of the line, then it must have ' jumped down a line; goLeft to return to the previous line. ' Except for: Empty lines (check for oldPos = newPos) If getCursor().isAtStartOfLine() And oldPos.Y() <> newPos.Y() Then getCursor().goLeft(1, bExpand) End If ' maybe eventually cursorGoto... should return True/False for bsetCursor bSetCursor = False Case "w", "W": oTextCursor.gotoNextWord(bExpand) Case "b", "B": oTextCursor.gotoPreviousWord(bExpand) Case "e": oTextCursor.gotoEndOfWord(bExpand) Case ")": oTextCursor.gotoNextSentence(bExpand) Case "(": oTextCursor.gotoPreviousSentence(bExpand) Case "}": oTextCursor.gotoNextParagraph(bExpand) Case "{": oTextCursor.gotoPreviousParagraph(bExpand) Case Else: bSetCursor = False bMatched = False End Select ' If oTextCursor was moved, set global cursor to its position If bSetCursor Then getCursor().gotoRange(oTextCursor.getStart(), False) End If ' If oTextCursor was moved and is in VISUAL mode, update selection if bSetCursor and bExpand then thisComponent.getCurrentController.Select(oTextCursor) end if ProcessMovementKey = bMatched End Function Sub initVibreoffice dim oTextCursor ' Initializing VIBREOFFICE_STARTED = True VIEW_CURSOR = thisComponent.getCurrentController.getViewCursor resetMultiplier() gotoMode("NORMAL") ' Show terminal cursor oTextCursor = getTextCursor() cursorReset(oTextCursor) sStartXKeyHandler() End Sub Sub Main If Not VIBREOFFICE_STARTED Then initVibreoffice() End If ' Toggle enable/disable VIBREOFFICE_ENABLED = Not VIBREOFFICE_ENABLED ' Restore statusbar If Not VIBREOFFICE_ENABLED Then restoreStatus() End Sub