//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 2 Sep 08 Brian Frank Creation
//
using concurrent
using fwt
using flux
**
** TextEditorController manages user events on the text editor.
**
internal class TextEditorController : TextEditorSupport
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(TextEditor editor) { this.editor = editor }
//////////////////////////////////////////////////////////////////////////
// Eventing
//////////////////////////////////////////////////////////////////////////
Void register()
{
richText.onVerifyKey.add { onVerifyKey(it) }
richText.onVerify.add { onVerify(it) }
richText.onModify.add { onModified(it) }
richText.onCaret.add { onCaret(it) }
richText.onFocus.add {onFocus(it) }
}
Void onVerifyKey(Event event)
{
checkBlockIndent(event)
}
Void onVerify(Event event)
{
clearBraceMatch
checkTabConvert(event.data)
checkAutoIndent(event)
}
Void onModified(Event event)
{
pushUndo(event.data)
editor.dirty = true
}
Void onCaret(Event event)
{
updateCaretPos
updateCaretStatus
checkBraceMatch(event)
}
Void onFocus(Event event)
{
checkFileOutOfDate
}
//////////////////////////////////////////////////////////////////////////
// Update Caret
//////////////////////////////////////////////////////////////////////////
Void updateCaretPos()
{
offset := editor.richText.caretOffset
this.caretLine = doc.lineAtOffset(offset)
this.caretCol = offset-doc.offsetAtLine(caretLine)
doc.caretLine = this.caretLine
}
Void updateCaretStatus()
{
try
{
editor.caretField.text = "${(caretLine+1)}:${caretCol+1}"
editor.caretField.parent?.relayout
}
catch (Err e) e.trace
}
//////////////////////////////////////////////////////////////////////////
// Tab-to-Spaces
//////////////////////////////////////////////////////////////////////////
Void checkTabConvert(TextChange tc)
{
if (!options.convertTabsToSpaces) return
if (!tc.newText.containsChar('\t')) return
line := doc.line(tc.startLine)
tab := options.tabSpacing
numSpaces := tab - ((line.size + tab) % tab)
tc.newText = tc.newText.replace("\t", Str.spaces(numSpaces))
}
//////////////////////////////////////////////////////////////////////////
// Indenting
//////////////////////////////////////////////////////////////////////////
internal Void checkAutoIndent(Event event)
{
// we only auto-indent on return/enter
TextChange tc := event.data
if (tc.newText != "\n") return
// get the last previous line above the insert point
lastNewLine := doc.line(tc.startLine+tc.newNumNewlines-1)
// compute leading whitespace
pos := 0
while (pos < lastNewLine.size && lastNewLine[pos].isSpace) pos++
if (pos == 0) return
ws := lastNewLine[0..<pos]
// insert leading whitespace into text to modify
tc.newText += ws
}
Void checkBlockIndent(Event event)
{
// check if tab or shift+tab
indent := event.key == blockIndentKey
unindent := event.key == blockUnindentKey
if (!indent && !unindent) return
// we only block indent if multiple lines are selected, although
// we don't really count the last line if the selection is at first col
selStart := richText.selectStart
selEnd := selStart + richText.selectSize
startLine := doc.lineAtOffset(selStart)
endLine := doc.lineAtOffset(selEnd)
if (startLine == endLine) return
if (selEnd == doc.offsetAtLine(endLine)) --endLine
// consume this event to prevent further propagation
event.consume
// build a replacement string for lines
s := StrBuf()
ws := options.convertTabsToSpaces ? Str.spaces(options.tabSpacing) : "\t"
(startLine..endLine).each |Int i|
{
line := doc.line(i)
if (indent)
{
// indent
s.add(ws).add(line).addChar('\n')
}
else
{
// unindent
if (line.startsWith(ws)) line = line[ws.size..-1]
else line = line.trimStart
s.add(line).addChar('\n')
}
}
s.remove(-1) // last newline
// replace the existing lines and re-select
start := doc.offsetAtLine(startLine)
end := doc.offsetAtLine(endLine) + doc.line(endLine).size
doc.modify(start, end-start, s.toStr)
richText.select(start, s.size)
}
private static const Key blockIndentKey := Key("Tab")
private static const Key blockUnindentKey := Key("Shift+Tab")
//////////////////////////////////////////////////////////////////////////
// Undo
//////////////////////////////////////////////////////////////////////////
Void pushUndo(TextChange tc)
{
if (!inUndo) editor.commandStack.push(TextChangeCommand(tc))
}
//////////////////////////////////////////////////////////////////////////
// Brace Matching
//////////////////////////////////////////////////////////////////////////
Void clearBraceMatch()
{
if (doc.bracketLine1 == null) return
oldLine1 := doc.bracketLine1
oldLine2 := doc.bracketLine2
doc.bracketLine1 = doc.bracketCol1 = null
doc.bracketLine2 = doc.bracketCol2 = null
richText.repaintLine(oldLine1)
richText.repaintLine(oldLine2)
}
Void checkBraceMatch(Event event)
{
// clear old brace match
clearBraceMatch
// get character before caret
offset := event.offset
lineIndex := doc.lineAtOffset(offset)
lineOffset := doc.offsetAtLine(lineIndex)
col := offset-lineOffset-1
if (lineOffset >= event.offset) return
ch := doc.line(lineIndex)[col]
if (!rules.brackets.containsChar(ch)) return
// attempt to find match
matchOffset := doc.matchBracket(offset-1)
if (matchOffset == null) return
matchLine := doc.lineAtOffset(matchOffset)
// cache bracket locations doc and repaint
matchCol := matchOffset-doc.offsetAtLine(matchLine)
doc.setBracketMatch(lineIndex, col, matchLine, matchCol)
richText.repaintLine(doc.bracketLine1)
richText.repaintLine(doc.bracketLine2)
}
//////////////////////////////////////////////////////////////////////////
// File Out-of-Date
//////////////////////////////////////////////////////////////////////////
Void checkFileOutOfDate()
{
// on focus always check if the file has been modified
// from out from under us and ask user if they want to reload
if (editor.fileTimeAtLoad == editor.file.modified) return
editor.fileTimeAtLoad = editor.file.modified
// prompt user to reload
r := Dialog.openQuestion(editor.window,
"File has been modified by another application:
$editor.file.name
Reload the file?", Dialog.yesNo)
if (r == Dialog.yes) editor.reload
}
//////////////////////////////////////////////////////////////////////////
// Mark
//////////////////////////////////////////////////////////////////////////
Void onGotoMark(Mark mark)
{
if (mark.line == null) return
line := doc.lines[mark.line-1] // line num is one based
offset := line.offset
if (mark.col != null) offset += mark.col-1 // col num is one based
richText.focus
richText.select(offset, 0)
richText.caretOffset = offset
}
//////////////////////////////////////////////////////////////////////////
// Commands
//////////////////////////////////////////////////////////////////////////
Void onFind(Event event) { editor.find.showFind }
Void onFindNext(Event event) { editor.find.next }
Void onFindPrev(Event event) { editor.find.prev }
Void onReplace(Event event) { editor.find.showFindReplace }
Void onGoto(Event event)
{
Str last := Actor.locals.get("fluxText.gotoLast", "1")
r := Dialog.openPromptStr(frame, "Goto Line:", last, 6)
if (r == null) return
line := r.toInt(10, false)
if (line == null) return
Actor.locals.set("fluxText.gotoLast", r)
line -= 1
if (line >= doc.lineCount) line = doc.lineCount-1
if (line < 0) line = 0
richText.select(doc.offsetAtLine(line), 0)
}
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
override TextEditor editor { private set }
Int caretLine
Int caretCol
Bool inUndo := false
}