552 lines
16 KiB
Plaintext
552 lines
16 KiB
Plaintext
/*
|
|
* $Id$
|
|
*/
|
|
|
|
/*
|
|
* Harbour Project source code:
|
|
* Editor Class (base for Memoedit(), debugger, etc.)
|
|
*
|
|
* Copyright 2000 Maurilio Longo <maurilio.longo@libero.it>
|
|
* www - http://www.harbour-project.org
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version, with one exception:
|
|
*
|
|
* The exception is that if you link the Harbour Runtime Library (HRL)
|
|
* and/or the Harbour Virtual Machine (HVM) with other files to produce
|
|
* an executable, this does not by itself cause the resulting executable
|
|
* to be covered by the GNU General Public License. Your use of that
|
|
* executable is in no way restricted on account of linking the HRL
|
|
* and/or HVM code into it.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA (or visit
|
|
* their web site at http://www.gnu.org/).
|
|
*
|
|
*/
|
|
|
|
/* TODO: Very minimal and little tested. To finish and refine */
|
|
|
|
#include "hbclass.ch"
|
|
#include "error.ch"
|
|
#include "fileio.ch"
|
|
#include "inkey.ch"
|
|
#include "setcurs.ch"
|
|
|
|
CLASS TEditor
|
|
|
|
DATA cFile INIT "" // name of file being edited
|
|
|
|
DATA aText INIT {} // array with lines of text being edited
|
|
DATA naTextLen INIT 0 // number of lines of text inside aText.
|
|
|
|
DATA nTop // boundaries of editor window, without box around
|
|
DATA nLeft
|
|
DATA nBottom
|
|
DATA nRight
|
|
|
|
DATA nFirstCol INIT 1 // FirstCol/Row of current text visible inside editor window
|
|
DATA nFirstRow INIT 1
|
|
DATA nRow INIT 1 // Cursor position inside aText (nRow) and inside current line of text (nCol)
|
|
DATA nCol INIT 1
|
|
DATA nNumCols INIT 1 // How many columns / rows can be displayed inside editor window
|
|
DATA nNumRows INIT 1
|
|
|
|
DATA lInsert INIT .F. // Is editor in Insert mode or in Overstrike one?
|
|
DATA nTabWidth INIT 8 // Size of Tab chars
|
|
DATA lEditAllow INIT .T. // Are changes to text allowed?
|
|
DATA lSaved INIT .F. // True if user exited editor with K_CTRL_W
|
|
DATA lWordWrap INIT .F. // True if word wrapping is active
|
|
DATA nWordWrapCol INIT 0 // At which column word wrapping occurs
|
|
|
|
METHOD New(cString, nTop, nLeft, nBottom, nRight, lEditMode, cUdF, nLineLength, nTabSize)
|
|
METHOD GetText() // Returns aText as a string (for MemoEdit())
|
|
METHOD RefreshWindow()
|
|
METHOD RefreshLine()
|
|
METHOD RefreshColumn()
|
|
METHOD MoveCursor(nKey)
|
|
METHOD InsertState(lInsState) // Changes lInsert value and insertion / overstrike mode of editor
|
|
METHOD Edit(nPassedKey)
|
|
|
|
ENDCLASS
|
|
|
|
/*
|
|
METHOD New(cFile, nTop, nLeft, nBottom, nRight) CLASS TEditor
|
|
|
|
LOCAL oFile := TFileRead():New(cFile)
|
|
|
|
oFile:Open()
|
|
if oFile:Error()
|
|
Alert(oFile:ErrorMsg("FileRead: "))
|
|
else
|
|
while oFile:MoreToRead()
|
|
AAdd(::aText, oFile:ReadLine())
|
|
end while
|
|
oFile:Close()
|
|
|
|
return Self
|
|
*/
|
|
|
|
|
|
STATIC function Text2Array(cString)
|
|
|
|
LOCAL cLine, i, nLastEOL, aArray
|
|
|
|
nLastEOL := 1
|
|
aArray := {}
|
|
|
|
while nLastEOL > 0
|
|
cLine := Left(cString, (nLastEOL := at(HB_OSNewLine(), cString)) - 1)
|
|
cLine := StrTran(cLine, HB_OSNewLine(), "")
|
|
if nLastEOL > 0
|
|
AAdd(aArray, cLine)
|
|
nLastEOL += Len(HB_OSNewLine())
|
|
cString := SubStr(cString, nLastEOL)
|
|
endif
|
|
enddo
|
|
|
|
return aArray
|
|
|
|
|
|
METHOD GetText() CLASS TEditor
|
|
|
|
LOCAL cString := ""
|
|
|
|
AEval(::aText, {|cItem| cString += cItem + HB_OSNewLine() })
|
|
|
|
return cString
|
|
|
|
|
|
METHOD New(cString, nTop, nLeft, nBottom, nRight, lEditMode, cUdF, nLineLength, nTabSize) CLASS TEditor
|
|
|
|
::aText := Text2Array(cString)
|
|
::naTextLen := Len(::aText)
|
|
|
|
// editor window boundaries
|
|
::nTop := nTop
|
|
::nLeft := nLeft
|
|
::nBottom := nBottom
|
|
::nRight := nRight
|
|
|
|
// How many cols and rows are available
|
|
::nNumCols := nRight - nLeft + 1
|
|
::nNumRows := nBottom - nTop + 1
|
|
|
|
if !lEditMode == NIL
|
|
::lEditAllow := lEditMode
|
|
endif
|
|
|
|
// set correct insert state
|
|
if ::lEditAllow
|
|
::InsertState(::lInsert)
|
|
endif
|
|
|
|
// is word wrap required?
|
|
if !nLineLength == NIL
|
|
::lWordWrap := .T.
|
|
::nWordWrapCol := nLineLength
|
|
endif
|
|
|
|
// how many spaces for each tab?
|
|
if !nTabSize == NIL
|
|
::nTabWidth := nTabSize
|
|
endif
|
|
|
|
// Empty area of screen which will hold editor window
|
|
Scroll(nTop, nLeft, nBottom, nRight)
|
|
|
|
// Set cursor upper left corner
|
|
SetPos(::nTop, ::nLeft)
|
|
|
|
return Self
|
|
|
|
|
|
// Redraws a screenfull of text (or part of it if nFromRow is not nil)
|
|
METHOD RefreshWindow() CLASS TEditor
|
|
|
|
LOCAL i, nOCol, nORow
|
|
|
|
nOCol := Col()
|
|
nORow := Row()
|
|
|
|
for i := 0 to Min(::nNumRows - 1, ::naTextLen - 1)
|
|
DispOutAt(::nTop + i, ::nLeft, PadR(SubStr(::aText[::nFirstRow + i], ::nFirstCol, ::nNumCols), ::nNumCols, " "))
|
|
next
|
|
|
|
// Clear rest of editor window (needed when deleting lines of text)
|
|
if ::naTextLen < ::nNumRows
|
|
Scroll(::nTop + ::naTextLen, ::nLeft, ::nBottom, ::nRight)
|
|
endif
|
|
|
|
SetPos(nORow, nOCol)
|
|
|
|
return Self
|
|
|
|
|
|
// Redraws current screen line
|
|
METHOD RefreshLine() CLASS TEditor
|
|
|
|
LOCAL nOCol, nORow
|
|
|
|
nOCol := Col()
|
|
nORow := Row()
|
|
|
|
DispOutAt(Row(), ::nLeft, PadR(SubStr(::aText[::nRow], ::nFirstCol, ::nNumCols), ::nNumCols, " "))
|
|
|
|
SetPos(nORow, nOCol)
|
|
|
|
return Self
|
|
|
|
|
|
// Refreshes only one screen column of text (for Left() and Right() movements)
|
|
METHOD RefreshColumn() CLASS TEditor
|
|
|
|
LOCAL i, nOCol, nORow
|
|
|
|
nOCol := Col()
|
|
nORow := Row()
|
|
|
|
for i := 0 to Min(::nNumRows - 1, ::naTextLen - 1)
|
|
DispOutAt(::nTop + i, nOCol, SubStr(::aText[::nFirstRow + i], ::nCol, 1))
|
|
next
|
|
|
|
SetPos(nORow, nOCol)
|
|
|
|
return Self
|
|
|
|
|
|
// Handles cursor movements inside text array
|
|
METHOD MoveCursor(nKey) CLASS TEditor
|
|
|
|
LOCAL lMoveKey := .T.
|
|
|
|
do case
|
|
case (nKey == K_DOWN)
|
|
if !::lEditAllow
|
|
while Row() < ::nBottom .AND. ::nRow < ::naTextLen
|
|
::nRow++
|
|
SetPos(Row() + 1, Col())
|
|
enddo
|
|
endif
|
|
if Row() == ::nBottom
|
|
if ::nRow < ::naTextLen
|
|
Scroll(::nTop, ::nLeft, ::nBottom, ::nRight, 1)
|
|
::nFirstRow++
|
|
::nRow++
|
|
::RefreshLine()
|
|
endif
|
|
else
|
|
if ::nRow < ::naTextLen
|
|
::nRow++
|
|
SetPos(Row() + 1, Col())
|
|
endif
|
|
endif
|
|
|
|
case (nKey == K_PGDN)
|
|
if ::nRow + ::nNumRows < ::naTextLen
|
|
::nRow += ::nNumRows
|
|
::nFirstRow += ::nNumRows
|
|
if ::nFirstRow + ::nNumRows > ::naTextLen
|
|
::nFirstRow -= ((::nFirstRow + ::nNumRows) - ::naTextLen) + 1
|
|
endif
|
|
else
|
|
::nFirstRow := Max(::naTextLen - ::nNumRows + 1, 1)
|
|
::nRow := ::naTextLen
|
|
SetPos(Min(::nTop + ::naTextLen - 1, ::nBottom), Col())
|
|
endif
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_CTRL_PGDN)
|
|
::nRow := ::naTextLen
|
|
::nCol := Max(Len(::aText[::nRow]), 1)
|
|
::nFirstRow := Max(::naTextLen - ::nNumRows + 1, 1)
|
|
::nFirstCol := Max(::nCol - ::nNumCols + 1, 1)
|
|
SetPos(Min(::nBottom, ::naTextLen), Min(::nLeft + ::nCol - 1, ::nRight))
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_UP)
|
|
if !::lEditAllow
|
|
while Row() > ::nTop .AND. ::nRow > 1
|
|
::nRow--
|
|
SetPos(Row() -1, Col())
|
|
enddo
|
|
endif
|
|
if Row() == ::nTop
|
|
if ::nRow > 1
|
|
Scroll(::nTop, ::nLeft, ::nBottom, ::nRight, -1)
|
|
::nFirstRow--
|
|
::nRow--
|
|
::RefreshLine()
|
|
endif
|
|
else
|
|
::nRow--
|
|
SetPos(Row() - 1, Col())
|
|
endif
|
|
|
|
case (nKey == K_PGUP)
|
|
if (::nRow - ::nNumRows) > 1
|
|
::nRow -= ::nNumRows
|
|
::nFirstRow -= ::nNumRows
|
|
if ::nFirstRow < 1
|
|
::nFirstRow := 1
|
|
endif
|
|
else
|
|
::nFirstRow := 1
|
|
::nRow := 1
|
|
SetPos(::nTop, Col())
|
|
endif
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_CTRL_PGUP)
|
|
::nRow := 1
|
|
::nCol := 1
|
|
::nFirstCol := 1
|
|
::nFirstRow := 1
|
|
SetPos(::nTop, ::nLeft)
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_RIGHT)
|
|
if Col() == ::nRight
|
|
if ::nCol <= iif(::lWordWrap, ::nWordWrapCol, Len(::aText[::nRow]))
|
|
Scroll(::nTop, ::nLeft, ::nBottom, ::nRight,, 1)
|
|
::nFirstCol++
|
|
::nCol++
|
|
::RefreshColumn()
|
|
endif
|
|
else
|
|
::nCol++
|
|
SetPos(Row(), Col() + 1)
|
|
endif
|
|
|
|
case (nKey == K_CTRL_RIGHT)
|
|
while ::nCol <= Len(::aText[::nRow]) .AND. SubStr(::aText[::nRow], ::nCol, 1) <> " "
|
|
::MoveCursor(K_RIGHT)
|
|
enddo
|
|
while ::nCol <= Len(::aText[::nRow]) .AND. SubStr(::aText[::nRow], ::nCol, 1) == " "
|
|
::MoveCursor(K_RIGHT)
|
|
enddo
|
|
|
|
case (nKey == K_LEFT)
|
|
if Col() == ::nLeft
|
|
if ::nCol > 1
|
|
Scroll(::nTop, ::nLeft, ::nBottom, ::nRight,, -1)
|
|
::nFirstCol--
|
|
::nCol--
|
|
::RefreshColumn()
|
|
endif
|
|
else
|
|
::nCol--
|
|
SetPos(Row(), Col() - 1)
|
|
endif
|
|
|
|
case (nKey == K_CTRL_LEFT)
|
|
while ::nCol > 1 .AND. SubStr(::aText[::nRow], ::nCol, 1) <> " "
|
|
::MoveCursor(K_LEFT)
|
|
enddo
|
|
while ::nCol > 1 .AND. SubStr(::aText[::nRow], ::nCol, 1) == " "
|
|
::MoveCursor(K_LEFT)
|
|
enddo
|
|
|
|
case (nKey == K_HOME)
|
|
::nCol := 1
|
|
::nFirstCol := 1
|
|
SetPos(Row(), ::nLeft)
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_CTRL_HOME)
|
|
::nCol := 1
|
|
::nFirstCol := 1
|
|
::nRow -= (Row() - ::nTop)
|
|
SetPos(::nTop, ::nLeft)
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_END)
|
|
// Empty lines have 0 len
|
|
::nCol := Max(Len(::aText[::nRow]), 1)
|
|
::nFirstCol := Max(::nCol - ::nNumCols + 1, 1)
|
|
SetPos(Row(), Min(::nLeft + ::nCol - 1, ::nRight))
|
|
::RefreshWindow()
|
|
|
|
case (nKey == K_CTRL_END)
|
|
::nRow += (::nBottom - Row())
|
|
if ::nRow > ::naTextLen
|
|
::nRow := ::naTextLen
|
|
endif
|
|
::nCol := Max(Len(::aText[::nRow]), 1)
|
|
::nFirstCol := Max(::nCol - ::nNumCols + 1, 1)
|
|
SetPos(Min(::nBottom, ::naTextLen), Min(::nLeft + ::nCol - 1, ::nRight))
|
|
::RefreshWindow()
|
|
|
|
otherwise
|
|
lMoveKey := .F.
|
|
|
|
endcase
|
|
|
|
return lMoveKey
|
|
|
|
|
|
// Changes lInsert value and insertion / overstrike mode of editor
|
|
METHOD InsertState(lInsState) CLASS TEditor
|
|
|
|
::lInsert := lInsState
|
|
SET(_SET_INSERT, lInsState)
|
|
|
|
return Self
|
|
|
|
|
|
// if editing isn't allowed we enter this loop which
|
|
// handles only movement keys and discards all the others
|
|
STATIC procedure BrowseText(oSelf)
|
|
|
|
LOCAL nKey
|
|
|
|
while (nKey := InKey(0)) <> K_ESC
|
|
oSelf:MoveCursor(nKey)
|
|
enddo
|
|
|
|
return
|
|
|
|
|
|
// Edits text
|
|
METHOD Edit(nPassedKey) CLASS TEditor
|
|
|
|
LOCAL i, nKey, lOldInsert
|
|
LOCAL lKeepGoing := .T.
|
|
|
|
if !::lEditAllow
|
|
BrowseText(Self)
|
|
|
|
else
|
|
|
|
while lKeepGoing
|
|
|
|
// If I've been called with a key already preset, evaluate this key and then exit
|
|
if nPassedKey == NIL
|
|
nKey := InKey(0)
|
|
else
|
|
lKeepGoing := .F.
|
|
nKey := nPassedKey
|
|
endif
|
|
|
|
do case
|
|
case (nKey >= 32 .AND. nKey <= 255)
|
|
// If I'm past EOL I need to add as much spaces as I need to reach ::nCol
|
|
if ::nCol > Len(::aText[::nRow])
|
|
::aText[::nRow] += Space(::nCol - Len(::aText[::nRow]))
|
|
endif
|
|
// insert char if in insert mode or at end of current line
|
|
if ::lInsert .OR. (::nCol > Len(::aText[::nRow]))
|
|
::aText[::nRow] := Stuff(::aText[::nRow], ::nCol, 0, Chr(nKey))
|
|
else
|
|
::aText[::nRow] := Stuff(::aText[::nRow], ::nCol, 1, Chr(nKey))
|
|
endif
|
|
::MoveCursor(K_RIGHT)
|
|
::RefreshLine()
|
|
|
|
// if we need to word wrap a word we simulate a word left + return
|
|
if ::lWordWrap .AND. (::nCol > ::nWordWrapCol)
|
|
::MoveCursor(K_CTRL_LEFT)
|
|
::MoveCursor(K_CTRL_RIGHT)
|
|
lOldInsert := ::lInsert
|
|
::lInsert := .T.
|
|
::Edit(K_RETURN)
|
|
::lInsert := lOldInsert
|
|
::MoveCursor(K_CTRL_RIGHT)
|
|
endif
|
|
|
|
case (nKey == K_RETURN)
|
|
if ::lInsert .OR. ::nRow == ::naTextLen
|
|
AAdd(::aText, "")
|
|
if ::nRow <= ::naTextLen
|
|
AIns(::aText, ::nRow + 1)
|
|
if Len(::aText[::nRow]) > 0
|
|
// Split current line at cursor position
|
|
::aText[::nRow + 1] := Right(::aText[::nRow], Len(::aText[::nRow]) - ::nCol + 1)
|
|
::aText[::nRow] := Left(::aText[::nRow], ::nCol - 1)
|
|
else
|
|
::aText[::nRow + 1] := ""
|
|
endif
|
|
endif
|
|
// I increment naTextLen only here because now there is a "real" line of text, before
|
|
// this point we have only added some "space" to split current line
|
|
::naTextLen++
|
|
endif
|
|
::MoveCursor(K_DOWN)
|
|
::MoveCursor(K_HOME)
|
|
|
|
case (nKey == K_INS)
|
|
::InsertState(!::lInsert)
|
|
|
|
case (nKey == K_DEL)
|
|
::aText[::nRow] := Stuff(::aText[::nRow], ::nCol, 1, "")
|
|
::RefreshLine()
|
|
if Len(::aText[::nRow]) == 0
|
|
::Edit(K_CTRL_Y)
|
|
endif
|
|
|
|
case (nKey == K_TAB)
|
|
// insert char if in insert mode or at end of current line
|
|
if ::lInsert .OR. (::nCol == Len(::aText[::nRow]))
|
|
::aText[::nRow] := Stuff(::aText[::nRow], ::nCol, 0, Space(::nTabWidth))
|
|
endif
|
|
for i := 1 to ::nTabWidth
|
|
::MoveCursor(K_RIGHT)
|
|
next
|
|
::RefreshLine()
|
|
|
|
case (nKey == K_BS)
|
|
// delete previous character
|
|
::aText[::nRow] := Stuff(::aText[::nRow], --::nCol, 1, "")
|
|
// correct column position for next call to MoveCursor()
|
|
::nCol++
|
|
::MoveCursor(K_LEFT)
|
|
::RefreshLine()
|
|
|
|
case (nKey == K_CTRL_Y)
|
|
if ::naTextLen > 1
|
|
// delete current line of text
|
|
ADel(::aText, ::nRow)
|
|
ASize(::aText, --::naTextLen)
|
|
// if we now have less than a screen full of text, adjust nFirstRow position
|
|
if ::nFirstRow + ::nNumRows > ::naTextLen
|
|
::nFirstRow := Max(::nFirstRow - 1, 1)
|
|
// if we have less lines of text than our current position on scree, up one line
|
|
if ::nRow > ::naTextLen
|
|
::nRow := Max(::nRow - 1, 1)
|
|
SetPos(Max(Row() -1, ::nTop), Col())
|
|
endif
|
|
endif
|
|
::RefreshWindow()
|
|
else
|
|
::aText[::nRow] := ""
|
|
::RefreshLine()
|
|
endif
|
|
|
|
case (::MoveCursor(nKey))
|
|
// if it's a movement key ::MoveCursor() handles it
|
|
|
|
case (nKey == K_ALT_W)
|
|
/* TOFIX: Not clipper compatible */
|
|
::lSaved := .T.
|
|
lKeepGoing := .F.
|
|
|
|
case (nKey == K_ESC)
|
|
lKeepGoing := .F.
|
|
|
|
otherwise
|
|
/* TODO: Here we should call an user defined function (to match memoedit() behaviour) */
|
|
endcase
|
|
enddo
|
|
endif
|
|
|
|
return Self
|
|
|