C PROGRAMMING

D-Flat++ Editor Class

Al Stevens

The D-Flat++ library is close to being finished. I'm working on some touch-ups--the clipboard, some common dialogs, and a help system--and there are still a few small bugs underfoot. I'll have a new version ready for download by the time you read this.

This month's topic is the Editor class, which implements a multiple-line text editor. Last month I described the EditBox class, the single-line text-editor control that dialog boxes use. The Editor class is derived from the EditBox class, adding the things that a multiline editor needs, such as word wrapping. The base TextBox class takes care of paging and scrolling. The EditBox class is derived from the TextBox class. The TextBox class also handles marking selected text blocks for clipboard operations.

Text editors are a favorite topic of conversation among programmers. To see why, post a CompuServe message about your favorite editor. You will launch a thread of many messages and many differing and unshakable opinions. The Middle East peace accord is a strawberry festival compared to getting programmers to agree about editors. Writers get similarly chauvinistic about their word processors, although to a lesser extent. No matter which features you build into an editor, they will be either not enough, too much, or implemented incorrectly. The editor will be too big, too slow, and too much unlike the [your editor's name here] program. That said, let's get on with the D-Flat++ editor.

Listings One and Two, page 123, are editor.h and editor.cpp, the source files that declare and define the Editor class. The class adds three data members, a tab-expansion value, a flag to indicate whether or not the editor is in word-wrap mode, and an integer with the text document's row number. The base EditBox class already takes care of the column.

The Editor class has member functions to handle moving the cursor up, down, forward, and backward and to handle paging. These operations move the editor's insertion pointer, which supports keyboard input as well as other functions. They also change the display by paging and scrolling when necessary. The EditBox class has forward and backward cursor operations, and the Editor class uses them, but it adds logic to move the insertion pointer forward past the end of a line to the next one and backward past the beginning of a line to the previous one. The TextBox class handles paging, but the Editor class intercepts the operations to update its insertion pointers.

Editor Tabs

The Editor class has private member functions to manage tabs in the text. Readers were always asking why the D-Flat C library's editor doesn't handle tab expansion, so I decided to build the feature into D-Flat++. An understanding of what that required reveals why I didn't hasten to put the feature in the C library.

It's not that easy to keep track of tabs in text while the text is in memory. DF++ text classes store text as a null-terminated buffer of lines, each one terminated by a newline character. Paragraphs are terminated by blank lines. Each tab character in the text is followed by pseudotab characters that represent the number of spaces to the next tab stop. The program computes the number of such insertions, which depends on how far it is to the next tab. That number is a function of both where in the text line the tab character occurs and the width of the tabs themselves. The Paint function recognizes these characters and displays them as spaces. The cursor-movement functions make sure that the cursor does not land on one of the pseudotab expansion positions on the screen. The delete-character functions delete the expansion characters whenever the user deletes a tab character.

The tab-expansion logic gets hairy when the user inserts or deletes characters on a line or reforms a paragraph. In the first case, the expansions for subsequent tabs on the same line have to be adjusted. In the case of paragraph reforms, which occur during word wrapping or by user command, the program would have to adjust the tabs for the whole paragraph to maintain tab integrity. For now, it simply "de-tabs" the paragraph, which means the tabs and tab-insertion characters are replaced by spaces.

There are trade-offs between the way that the Editor class stores text and the way that other editors do it. One technique stores paragraphs as newline-terminated lines, word-wraps on the fly, and does tab expansion and collapsing only when painting the screen. This technique involves more complex text line and tab management and cursor movement than the one that I used. If I were to build a serious word processor with D-Flat++ (not likely, I might add) I'd probably build such a class just to get the performance improvements it would offer. The DF++ Editor class is more than adequate for simple text-editing applications, however, and uses simpler code than other editors.

The D-Flat++ Source Code

D-Flat and D-Flat++ are available to download from the DDJ Forum on CompuServe and on the Internet by anonymous ftp. See page 3 in this magazine for details. If you cannot get to one of the online sources, send a diskette and a stamped, addressed mailer to me at Dr. Dobb's Journal, 411 Borel, San Mateo, CA 94402. I'll send you a copy of the source code. It's free, but if you want to support my Careware charity, include a dollar for the Brevard County Food Bank. They help hungry and homeless citizens.

C++ Book Report

Practical C++ by Mark Terribile (McGraw-Hill, 1994) is addressed to the C programmer who wants to learn C++ and to "write it well." The book is not a tutorial. It isn't a reference book either. Nor is it a treatise on programming issues. At different times it is all of those things, but never only one of them, and certainly none of them comprehensively. If the work seems to lack focus and organization, perhaps it does, but that is neither its weakness nor its strength. It's just how things are. This is clearly the best C++ book about the language itself that I've run across.

The book explains many of the more abstruse language features and programming issues. For example, we used to have a clear understanding of the difference between declaration and definition in our programming jargon because the two were distinct. C++ muddies that distinction by allowing declarations to be definitions and vice versa depending on the context in which a declarator is used. It is not uncommon for declare and define to be used inconsistently and incorrectly in C++ conversations and literature (mine included). Practical C++ has the best explanation of these terms that I've seen.

The author is apparently active in the ANSI and ISO C++ committees. The book describes several proposed and/or pending changes to the language, including some inventions of the committees. One example is called Namespace Management, which organizes external names into named groups and introduces new keywords. Another is the addition of const_cast<>, static_cast<>, reinterpret_cast<>, and dynamic_cast<>, all used to invoke or sidestep a particular implicit conversion. Just when you thought you knew it all.

Although targeted at the C programmer who is learning C++, a more appropriate audience for this book might be C++ programmers who want to understand the language better. It isn't an easy read, but you can select a subject at random from the table of contents, open to the discussion, and almost always find something new to learn, if only a different perspective on the topic. But you need to understand the language before you start. Everything is covered, but it takes some plowing through the text to find it all. You probably won't start at the beginning and proceed to the end. If you are not already a C++ programmer with some object-oriented design knowledge, you'll get mired down early on.

There are no programs in the book for you to type in and run. The code consists of fragments that demonstrate the points under discussion. As such, most of them would not compile except in the larger context of a program where you used them. Probably the author did not always take the time to do that because there are a few code errors. Some are due to the author's oversight. Others are production problems, such as when the page layout word-wraps a double-slash comment. Fortunately, an experienced reader will readily recognize the code problems and make allowances.

The Great Debate: To Preprocess or Not

Much C++ literature tells us that C++ makes the C preprocessor obsolete. As a rule, the preprocessor is to be avoided, except for #include and compile-time directives that change the compiled output (to exclude debug code, for example). Many authors argue that inline functions make macros unnecessary and undesirable because they eliminate side effects and type-check the parameters. So they do. The authors state further that const objects can replace #define for associating values with global symbols. As with inline function parameter lists, they contend, const objects include types and, therefore, enjoy type-safety, too. So they do. (I always wondered why they call them "const variables." How can a constant object be variable?)

Given all this warm, comfortable type-checking, most programmers understandably resist a return to the old days of reckless C programming. They conclude that something as bad as the typeless #define is never again necessary.

But there are always exceptions. Sometimes you run into them just when you are trying to do the right thing. Consider this familiar macro:

#define min(a,b) ((a)<(b)?(a):(b))

There can be side effects, as seen when you code this use of the macro:

a = min(b++, c++);

Depending on which value is greater, one of the variables is incremented twice. C programmers learned early on not to write code that could generate such side effects. Sometimes, though, you can't be sure. For example, some compilers implement the ctype family of functions as macros; others build them as functions.

C++ programmers solve the side-effect issue by using inline functions such as this one:

inline int min(int a, int b)
{
  return (a < b ? a : b);
}

There are no side effects with the inline version of the macro/function. Furthermore, there is type-checking, which is both good and bad. Type-checking is inherently good. However, you can't use the inline min function unless both argument types can be implicitly converted to integer values. Other types that do not have conversion constructors to convert them to numeric types do not work with this inline min function. You would need an overloaded min function for each such type.

C++ solves the many-function problem with the template class, as shown here.

template<class T>
T min(T a, T b)
{
  return (a < b ? a : b);
}

Now, one little macro fits all types as long as they have the less-than relational operator. You can code the following statements by using the same template macro.

int a, b, c;
a = min(b, c);

String s1, s2;
s1 = min(s2, s3);

You don't save any room by using templates. Each different type usage generates its own copy of the run-time code, but at least you don't have to maintain several different source-code versions of the same macro.

The template solution is not perfect. The objects being compared must be of the same type, regardless of implicit or programmed conversion rules. And that's where the const vs. #define issue comes in.

I use the template version of the min macro in D-Flat++. It rose up and bit me when I tried to use it with a const argument. The example editor program that I use to test and demonstrate D-Flat++ derives its specific application class from the DF++ generic Application class. Users can resize application windows, and I needed to establish a minimum size so that the window didn't get too small to hold its menu bar and other things. That's easy enough to do: Intercept the Size message function and adjust its height and width parameters before letting it pass. I coded it something like this:

const int MinimumWidth = 40;
Ted::Size(int x, int y)
{
  // _
  x = min(x, MinimumWidth);
}

The compiler rejected the usage. There is, it said, no min function that expects an int and a const int as parameters. Casting the x argument to const int didn't work either. The Borland compiler ignored the cast. I'm not sure why, but that's what it did.

Incidentally, this discussion is based on how Borland C++ 3.1 works. Other compilers exhibit different behavior, and I'll discuss them next month.

There are other solutions, one very simple one and some others that are somewhat contrived. (Even if the cast had worked, it, too, would have been a contrivance.) Assume for this discussion that the x variable needs to be non-const. You could declare the MinimumWidth object as non-const, but then you would have to put it into an executable source code file. You'd need an extern declaration in the header to make it globally visible. You can assign the const object to a non-const variable and use the non-const variable in the min call. Or vice versa. You can return the const int value from a non-const member function of the class and use that value. You can find many such workarounds, each of which involves additional code. You can even rewrite the template function like this:

template<class T, class U>
T min(T a, U b)
{
  return (a < b ? a : b);
}

On the surface, this would appear to be the most elegant of the contrivances, and it works in some cases, but it can turn up some nasty side effects of its own. Whichever type you code as the first argument in a call to this macro becomes the type of the result. There could be some unwanted conversion truncation as a result. For example, the following expression, when using that macro, would assign the value 1 to the foo variable.

float foo = min(4, 1.23);

Reverse the arguments, and the expression returns 1.23. That's the kind of inconsistent behavior that makes for deep-space, black-hole bugs.

Those are the contrived solutions, and you can use one of them. On the other hand, you can choose the simple solution--the unsafe, old-fashioned, obsolete one that you are not supposed to need or want anymore--and the one that works. You can change the MinimumWidth declaration to this statement:

#define MinimumWidth 40

Sometimes a little serendipity just beats the pants off of a lot of conventional wisdom.

[LISTING ONE]


// -------- editor.h

#ifndef EDITOR_H
#define EDITOR_H

#include "editbox.h"

const unsigned char Ptab = '\t'+0x80; // pseudo tab expansion
#define MaxTab 12                     // maximum tab width

class Editor : public EditBox    {
    int tabs;           // tab expansion value
    Bool wordwrapmode;  // True = wrap words
    int row;            // Current row
    void OpenWindow();
    void ScrollCursor();
    void AdjustCursorTabs(int key = 0);
    void ExtendBlock(int x, int y);
    void InsertTab();
    void AdjustTabInsert();
    void AdjustTabDelete();
    void WordWrap();
    Bool AtBufferStart()
        { return (Bool) (column == 0 && row == 0); }
protected:
    virtual void Upward();
    virtual void Downward();
    virtual void Forward();
    virtual void Backward();
    virtual void DeleteCharacter();
    virtual void BeginDocument();
    virtual void EndDocument();
    virtual Bool PageUp();
    virtual Bool PageDown();
    virtual void InsertCharacter(int key);
    virtual void PaintCurrentLine()
        { WriteTextLine(row); }
    virtual Bool ResetCursor();
    virtual void WriteString(String &ln,
                        int x, int y, int fg, int bg);
    virtual void LeftButton(int mx, int my);
public:
    Editor(const char *ttl, int lf, int tp, int ht, int wd,
                                            DFWindow *par=0)
                        : EditBox(ttl, lf, tp, ht, wd, par)
            { OpenWindow(); }
    Editor(const char *ttl, int ht, int wd, DFWindow *par=0)
                        : EditBox(ttl, ht, wd, par)
            { OpenWindow(); }
    Editor(int lf, int tp, int ht, int wd, DFWindow *par=0)
                        : EditBox(lf, tp, ht, wd, par)
            { OpenWindow(); }
    Editor(int ht, int wd, DFWindow *par=0)
                        : EditBox(ht, wd, par)
            { OpenWindow(); }
    Editor(const char *ttl) : EditBox(ttl)
            { OpenWindow(); }
    virtual void Keyboard(int key);
    virtual unsigned char CurrentChar()
        { return *(TextLine(row) + column); }
    virtual unsigned CurrentCharPosition()
        { return (unsigned)
            ((const char *)
            (TextLine(row)+column) - (const char *) *text);
        }
    virtual void FormParagraph();
    virtual void AddText(const String& txt);
    virtual const String GetText();
    virtual void ClearText();
    virtual int GetRow() const { return row; }
    int Tabs()
        { return tabs; }
    void SetTabs(int t);
    Bool WordWrapMode()
        { return wordwrapmode; }
    void SetWordWrapMode(Bool wmode)
        { wordwrapmode = wmode; }
    virtual void DeleteSelectedText();
    virtual void InsertText(const String& txt);
};

#endif


[LISTING TWO]



// ----- editor.cpp

#include "editor.h"
#include "desktop.h"

// ----------- common constructor code
void Editor::OpenWindow()
{
    windowtype = EditorWindow;
    row = 0;
    tabs = 4;
    insertmode = desktop.keyboard().InsertMode();
    wordwrapmode = True;
    DblBorder = False;
}
// ---- keep the cursor out of tabbed space
void Editor::AdjustCursorTabs(int key)
{
    while (CurrentChar() == Ptab)
        key == FWD ? column++ : --column;
    ResetCursor();
}
// -------- process keystrokes
void Editor::Keyboard(int key)
{
    int svwtop = wtop;
    int svwleft = wleft;
    switch (key)    {
        case '\t':
            InsertTab();
            BuildTextPointers();
            PaintCurrentLine();
            ResetCursor();
            break;
        case ALT_P:
            FormParagraph();
            break;
        case UP:
            Upward();
            TestMarking();
            break;
        case DN:
            Downward();
            TestMarking();
            break;
        case CTRL_HOME:
            BeginDocument();
            TestMarking();
            break;
        case CTRL_END:
            EndDocument();
            TestMarking();
            break;
        case '\r':
            InsertCharacter('\n');
            BuildTextPointers();
            ResetCursor();
            Paint();
            break;
        case DEL:
        case RUBOUT:
            visible = False;
            EditBox::Keyboard(key);
            visible = True;
            BuildTextPointers();
            PaintCurrentLine();
            ResetCursor();
            break;
        default:
            EditBox::Keyboard(key);
            break;
    }
    if (svwtop != wtop || svwleft != wleft)
        Paint();
}
// --- move the cursor forward one character
void Editor::Forward()
{
    if (CurrentChar())    {
        if (CurrentChar() == '\n')    {
            Home();
            Downward();
        }
        else
            EditBox::Forward();
        AdjustCursorTabs(FWD);
    }
}
// --- move the cursor back one character
void Editor::Backward()
{
    if (column)
        EditBox::Backward();
    else if (row)    {
        Upward();
        End();
    }
    AdjustCursorTabs();
}
// ---- if cursor moves out of the window, scroll
void Editor::ScrollCursor()
{
    if (column < wleft || column >= wleft + ClientWidth())    {
        wleft = column;
        Paint();
    }
}
// --- move the cursor up one line
void Editor::Upward()
{
    if (row)    {
        if (row == wtop)
            ScrollDown();
        --row;
        AdjustCursorTabs();
        ScrollCursor();
    }
}
// --- move the cursor down one line
void Editor::Downward()
{
    if (row < wlines)    {
        if (row == wtop + ClientHeight() - 1)
            ScrollUp();
        row++;
        AdjustCursorTabs();
        ScrollCursor();
    }
}
// --- move the cursor to the beginning of the document
void Editor::BeginDocument()
{
    row = 0;
    wtop = 0;
    EditBox::Home();
    AdjustCursorTabs();
}
// --- move the cursor to the end of the document
void Editor::EndDocument()
{
    TextBox::End();
    row = wlines-1;
    End();
    AdjustCursorTabs();
}
// --- keep cursor in the window, in text and out of empty space
Bool Editor::ResetCursor()
{
    KeepInText(column, row);
    if (EditBox::ResetCursor())    {
        if (!(row >= wtop && row < wtop+ClientHeight()))    {
            desktop.cursor().Hide();
            return False;
        }
    }
    return True;
}
// ------- page up one screenfull
Bool Editor::PageUp()
{
    if (wlines)    {
        row -= ClientHeight();
        if (row < 0)
            row = 0;
        EditBox::PageUp();
        AdjustCursorTabs();
        return True;
    }
    return False;
}
// ------- page down one screenfull
Bool Editor::PageDown()
{
    if (wlines)    {
        row += ClientHeight();
        if (row >= wlines)
            row = wlines-1;
        EditBox::PageDown();
        AdjustCursorTabs();
        return True;
    }
    return False;
}
// --- insert a tab into the edit buffer
void Editor::InsertTab()
{
    visible = False;
    if (insertmode)    {
        EditBox::InsertCharacter('\t');
        while ((column % tabs) != 0)
            EditBox::InsertCharacter(Ptab);
    }
    else
        do
            Forward();
        while ((column % tabs) != 0);
    visible = True;
}
// --- When inserting char, adjust next following tab, same line
void Editor::AdjustTabInsert()
{
    visible = False;
    // ---- test if there is a tab beyond this character
    int savecol = column;
    while (CurrentChar() && CurrentChar() != '\n')    {
        if (CurrentChar() == '\t')    {
            column++;
            if (CurrentChar() == Ptab)
                EditBox::DeleteCharacter();
            else
                for (int i = 0; i < tabs-1; i++)
                    EditBox::InsertCharacter(Ptab);
            break;
        }
        column++;
    }
    column = savecol;
    visible = True;
}
// --- test for wrappable word and wrap it
void Editor::WordWrap()
{
    // --- test for word wrap
    int len = LineLength(row);
    int wd = ClientWidth()-1;
    if (len >= wd)    {
        const char *cp = TextLine(row);
        char ch = *(cp + wd);
        // --- test words beyond right margin
        if (len > wd || (ch && ch != ' ' && ch != '\n'))    {
            // --- test typing in last word in window's line
            const char *cw = cp + wd;
            cp += column;
            while (cw > cp)    {
                if (*cw == ' ')
                    break;
                --cw;
            }
            int newcol = 0;
            if (cw <= cp)    {
                // --- user was typing last word on line
                // --- find beginning of the word
                const char *cp1 = TextLine(row);
                const char *cw1 = cw;
                while (*cw1 != ' ' && cw1 > cp1)
                    --cw1, newcol++;
                wleft = 0;
            }
            FormParagraph();
            if (cw <= cp)    {
                // --- user was typing last word on line
                column = newcol;
                if (cw == cp)
                    --column;
                row++;
                if (row - wtop >= ClientHeight())
                    ScrollUp();
                ResetCursor();
            }
        }
    }
}
// --- insert a character at the current cursor position
void Editor::InsertCharacter(int key)
{
    if (insertmode)    {
        if (key != '\n')
            AdjustTabInsert();
    }
    else if (CurrentChar() == '\t')    {
        // --- overtyping a tab
        visible = False;
        column++;
        while (CurrentChar() == Ptab)
            EditBox::DeleteCharacter();
        --column;
    }
    visible = False;
    EditBox::InsertCharacter(key);
    visible = True;
    ResetCursor();
    if (wordwrapmode)
        WordWrap();
}
// --- When deleting char, adjust next following tab, same line
void Editor::AdjustTabDelete()
{
    visible = False;
    // ---- test if there is a tab beyond this character
    int savecol = column;
    while (CurrentChar() && CurrentChar() != '\n')    {
        if (CurrentChar() == '\t')    {
            column++;
            // --- count pseudo tabs
            int pct = 0;
            while (CurrentChar() == Ptab)
                pct++, column++;
            if (pct == tabs-1)    {
                column -= tabs-1;
                for (int i = 0; i < tabs-1; i++)
                    EditBox::DeleteCharacter();
            }
            else
                EditBox::InsertCharacter(Ptab);
            break;
        }
        column++;
    }
    column = savecol;
    visible = True;
}
// --- delete the character at the current cursor position
void Editor::DeleteCharacter()
{
    if (CurrentChar() == '\0')
        return;
    if (insertmode)
        AdjustTabDelete();
    if (CurrentChar() == '\t')    {
        // --- deleting a tab
        EditBox::DeleteCharacter();
        while (CurrentChar() == Ptab)
            EditBox::DeleteCharacter();
        return;
    }
    const char *cp = TextLine(row);
    const char *cw = cp + column;
    const Bool delnewline = (Bool) (*cw == '\n');
    const Bool reform = (Bool) (delnewline && *(cw+1) != '\n');
    const Bool lastnewline =
                        (Bool) (delnewline && *(cw+1) == '\0');
    int newcol = 0;
    if (reform && !lastnewline)    {
        // --- user is deleting /n, find beginning of last word
        while (*--cw != ' ' && cw > cp)
            newcol++;
    }
    EditBox::DeleteCharacter();
    if (lastnewline)
        return;
    if (delnewline && !reform)    {
        // --- user deleted a blank line
        visible = True;
        BuildTextPointers();
        Paint();
        return;
    }
    if (wordwrapmode && reform)    {
        // --- user deleted /n
        wleft = 0;
        FormParagraph();
        if (CurrentChar() == '\n')    {
            // ---- joined the last word with next line's
            //      first word and then wrapped the result
            column = newcol;
            row++;
        }
    }
}
// --- form a paragraph from the current cursor position
//    through one line before the next blank line or end of text
void Editor::FormParagraph()
{
    int BegCol, FirstLine;
    const char *blkBegLine, *blkEndLine, *blkBeg;

    // ---- forming paragraph from cursor position
    FirstLine = wtop + row;
    blkBegLine = blkEndLine = TextLine(row);
    if ((BegCol = column) >= ClientWidth())
        BegCol = 0;
    // ---- locate the end of the paragraph
    while (*blkEndLine)    {
        Bool blank = True;
        const char *BlankLine = blkEndLine;
        // --- blank line marks end of paragraph
        while (*blkEndLine && *blkEndLine != '\n')    {
            if (*blkEndLine != ' ')
                blank = False;
            blkEndLine++;
        }
        if (blank)    {
            blkEndLine = BlankLine;
            break;
        }
        if (*blkEndLine)
            blkEndLine++;
    }
    if (blkEndLine == blkBegLine)    {
        visible = True;
        Downward();
        return;
    }
    if (*blkEndLine == '\0')
        --blkEndLine;
    if (*blkEndLine == '\n')
        --blkEndLine;
    // --- change newlines, tabs, and tab expansions to spaces
    blkBeg = blkBegLine;
    while (blkBeg < blkEndLine)    {
        if (*blkBeg == '\n' || ((*blkBeg) & 0x7f) == '\t')    {
            int off = blkBeg - (const char *)*text;
            (*text)[off] = ' ';
        }
        blkBeg++;
    }
    // ---- insert newlines at new margin boundaries
    blkBeg = blkBegLine;
    while (blkBegLine < blkEndLine)    {
        blkBegLine++;
        if ((int)(blkBegLine - blkBeg) == ClientWidth()-1)    {
            while (*blkBegLine != ' ' && blkBegLine > blkBeg)
                --blkBegLine;
            if (*blkBegLine != ' ')    {
                blkBegLine = strchr(blkBegLine, ' ');
                if (blkBegLine == NULL ||
                        blkBegLine >= blkEndLine)
                    break;
            }
            int off = blkBegLine - (const char *)*text;
            (*text)[off] = '\n';
            blkBeg = blkBegLine+1;
        }
    }
    BuildTextPointers();
    changed = True;
    // --- put cursor back at beginning
    column = BegCol;
    if (FirstLine < wtop)
        wtop = FirstLine;
    row = FirstLine - wtop;
    visible = True;
    Paint();
    ResetCursor();
}
// --------- add a line of text to the editor textbox
void Editor::AddText(const String& txt)
{
    // --- compute the buffer size based on tabs in the text
    const char *tp = txt;
    int x = 0;
    int sz = 0;
    while (*tp)    {
        if (*tp == '\t')    {
            // --- tab, adjust the buffer length
            int sps = Tabs() - (x % Tabs());
            sz += sps;
            x += sps;
        }
        else    {
            // --- not a tab, count the character
            sz++;
            x++;
        }
        if (*tp == '\n')
            x = 0;    // newline, reset x
        tp++;
    }
    // --- allocate a buffer
    char *ep = new char[sz];
    // --- detab the input file
    tp = txt;
    char *ttp = ep;
    x = 0;
    while (*tp)    {
        // --- put the character (\t, too) into the buffer
        *ttp++ = *tp;
        x++;
        // --- expand tab into \t and expansions (\t + 0x80)
        if (*tp == '\t')
            while ((x % Tabs()) != 0)
                *ttp++ = Ptab, x++;
        else if (*tp == '\n')
            x = 0;
        tp++;
    }
    *ttp = '\0';
    // ---- add the text to the editor window
    EditBox::AddText(String(ep));
}
// ------- retrieve editor text collapsing tabs
const String Editor::GetText()
{
    char *tx = new char[text->Strlen()+1];
    const char *tp = (const char *) *text;
    char *nt = tx;
    while (*tp)    {
        if (*(const unsigned char *)tp != Ptab)
            *tx++ = *tp;
        tp++;
    }
    *tx = '\0';
    String temp(nt);
    return nt;
}
// --- write a string to the editor window
void Editor::WriteString(String &ln,int x,int y,int fg,int bg)
{
    String nln(ln.Strlen());
    int ch;
    for (int i = 0; i < ln.Strlen(); i++)    {
        ch = ln[i];
        nln[i] = (ch & 0x7f) == '\t' ? ' ' : ch;
    }
    EditBox::WriteString(nln, x, y, fg, bg);
}

// ---- left mouse button pressed
void Editor::LeftButton(int mx, int my)
{
    if (ClientRect().Inside(mx, my))    {
        column = mx-ClientLeft()+wleft;
        row = my-ClientTop()+wtop;
        ResetCursor();
    }
    TextBox::LeftButton(mx, my);
}
// --------- clear the text from the editor window
void Editor::ClearText()
{
    row = 0;
    EditBox::ClearText();
}
// ------ extend the marked block
void Editor::ExtendBlock(int x, int y)
{
    row = y;
    EditBox::ExtendBlock(x, y);
}
// ---- delete a marked block
void Editor::DeleteSelectedText()
{
    if (TextBlockMarked())    {
        row = BlkBegLine;
        column = BlkBegCol;
        if (row < wtop || row >= wtop + ClientHeight())
            wtop = row;
        if (column < wleft || column >= wleft + ClientWidth())
            wleft = column;
        EditBox::DeleteSelectedText();
        FormParagraph();
    }
}
// ---- insert a string into the text
void Editor::InsertText(const String& txt)
{
    EditBox::InsertText(txt);
    FormParagraph();
}
// ---- set tab width
void Editor::SetTabs(int t)
{
    if (t && tabs != t && t <= MaxTab)    {
        tabs = t;
        if (text != 0)    {
            String ln;
            // ------- retab the text
            for (int lno = 0; lno < wlines; lno++)    {
                // --- retrieve a line at a time
                ln = ExtractTextLine(lno);
                int len = ln.Strlen();
                String newln(len * t);
                // --- copy string, collapsing old tabs
                //     and expanding new tabs
                unsigned int ch;
                for (int x2 = 0, x1 = 0; x2 < len; x2++)    {
                    ch = ln[x2] & 0xff;
                    if (ch == Ptab)
                        // --- collapse old tab expansion
                        continue;
                    // --- copy text
                    newln[x1++] = ch;
                    if (ch == '\t')
                        // --- expand new tabs
                        while ((x1 % t) != 0)
                            newln[x1++] = Ptab;
                }
                newln[x1] = '\0';
                newln += "\n";

                // --- compute left segment length
                unsigned seg1 = (unsigned)
                    (TextLine(lno) - (const char *) *text);
                // --- compute right segment length
                unsigned seg2 = 0;
                if (lno < wlines+1)
                    seg2 = (unsigned) text->Strlen() -
                    (TextLine(lno+1) - (const char *) *text);
                // --- rebuild the text from the three parts
                String lft = text->left(seg1);
                String rht = text->right(seg2);
                *text = lft+newln+rht;
                BuildTextPointers();
            }
            Paint();
        }
    }
}


Copyright © 1994, Dr. Dobb's Journal