/* * $Id$ */ /* * Harbour Project source code: * Editor Class (base for Memoedit(), debugger, etc.) * * Copyright 2000 Maurilio Longo * 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