February 4, 2015

NCURSES Terminal Text Editor Tutorial Part 3

  1. Welcome Back
  2. Implementing the Editor class
  3. Basic Input Handling
  4. Moving it x2
  5. Time to Wrap it up

Welcome Back

Last time we left off, we finished the Buffer class. Here comes the fun stuff: Doing the functions for the Editor class! What fun! We will start immediately!

Implementing the Editor class

Create file Editor.cpp. Add some includes.

#include "Editor.h"

#include <fstream>
#include <iostream>
#include <sstream>          // If you are not using C++11; We'll get to that later!

Let's start off with the default constructor. For me, I want the default constructor to initialize the x and y coordinates, and also the mode to normal 'n'. I will want the status to be 'Normal Mode' and the filename to be 'untitled'. I also need to initialize the Buffer variable.

Editor::Editor()
{
    x=0;y=0;mode='n';
    status = "Normal Mode";
    filename = "untitled";

    /* Initializes buffer and appends line to
        prevent seg. faults */
    buff = new Buffer();
    buff->appendLine("");
}

If they give it a filename, read it into the buffer.

Editor::Editor(string fn)
{
    x=0;y=0;mode='n';
    status = "Normal Mode";
    filename = fn;

    buff = new Buffer();

    ifstream infile(fn.c_str());
    if(infile.is_open())
    {
        while(!infile.eof())
        {
            string temp;
            getline(infile, temp);
            buff->appendLine(temp);
        }
    }
    else
    {
        cerr << "Cannot open file: '" << fn << "'\n";
        buff->appendLine("");
    }
}

Now that we have done all that, it is time for us to get on with the core functions, starting with Editor::updateStatus().

void Editor::updateStatus()
{
    switch(mode)
    {
    case 'n':
        // Normal mode
        status = "Normal Mode";
        break;
    case 'i':
        // Insert mode
        status = "Insert Mode";
        break;
    case 'x':
        // Exiting
        status = "Exiting";
        break;
    }
    status += "\tCOL: " + tos(x) + "\tROW: " + tos(y);
}

Wait! We haven't defined tos(int) yet!!!

Just use stringstream!

string Editor::tos(int i)
{
    stringstream ss;
    ss << i;
    return ss.str();
}

Note that this isn't necessary if you have C++11 - you just use std::to_string(int). My school doesn't have it so I had to compensate.

Basic Input Handling

My input handler handles all inputs in normal mode as well as insert mode (but not exit mode, because you are exiting the program). This may be confusing because there are a bunch of switches, and you may be right. You could separate the input handler into two different functions - one for handling inputs in normal mode, the other handling inputs in insert mode.

void Editor::handleInput(int c)
{
    switch(c)
    {
    case KEY_LEFT:
        moveLeft();
        return;
    case KEY_RIGHT:
        moveRight();
        return;
    case KEY_UP:
        moveUp();
        return;
    case KEY_DOWN:
        moveDown();
        return;
    }
    switch(mode)
    {
    case 'n':
        switch(c)
        {
        case 'x':
            // Press 'x' to exit
            mode = 'x';
            break;
        case 'i':
            // Press 'i' to enter insert mode
            mode = 'i';
            break;
        case 's':
            // Press 's' to save the current file
            saveFile();
            break;
        }
        break;
    case 'i':
        switch(c)
        {
        case 27:
            // The Escape/Alt key
            mode = 'n';
            break;
        case 127:
        case KEY_BACKSPACE:
            // The Backspace key
            if(x == 0 && y > 0)
            {
                x = buff->lines[y-1].length();
                // Bring the line down
                buff->lines[y-1] += buff->lines[y];
                // Delete the current line
                deleteLine();
                moveUp();
            }
            else
            {
                // Removes a character
                buff->lines[y].erase(--x, 1);
            }
            break;
        case KEY_DC:
            // The Delete key
            if(x == buff->lines[y].length() && y != buff->lines.size() - 1)
            {
                // Bring the line down
                buff->lines[y] += buff->lines[y+1];
                // Delete the line
                deleteLine(y+1);
            }
            else
            {
                buff->lines[y].erase(x, 1);
            }
            break;
        case KEY_ENTER:
        case 10:
            // The Enter key
            // Bring the rest of the line down
            if(x < buff->lines[y].length())
            {
                // Put the rest of the line on a new line
                buff->insertLine(buff->lines[y].substr(x, buff->lines[y].length() - x), y + 1);
                // Remove that part of the line
                buff->lines[y].erase(x, buff->lines[y].length() - x);
            }
            else
            {
                buff->insertLine("", y+1);
            }
            x = 0;
            moveDown();
            break;
        case KEY_BTAB:
        case KEY_CTAB:
        case KEY_STAB:
        case KEY_CATAB:
        case 9:
            // The Tab key
            buff->lines[y].insert(x, 4, ' ');
            x += 4;
            break;
        default:
            // Any other character
            buff->lines[y].insert(x, 1, char(c));
            x++;
            break;
        }
        break;
    }
}

Whew! That sure was a lot of code! There is still a lot of stuff to implement! Let's do the move<Direction>() code first!

Moving it x2

void Editor::moveLeft()
{
    if(x-1 >= 0)
    {
        x--;
        move(y, x);
    }
}

void Editor::moveRight()
{
    if(x+1 < COLS && x+1 <= buff->lines[y].length())
    {
        x++;
        move(y, x);
    }
}

void Editor::moveUp()
{
    if(y-1 >= 0)
        y--;
    if(x >= buff->lines[y].length())
        x = buff->lines[y].length();
    move(y, x);
}

void Editor::moveDown()
{
    if(y+1 < LINES-1 && y+1 < buff->lines.size())
        y++;
    if(x >= buff->lines[y].length())
        x = buff->lines[y].length();
    move(y, x);
}

Pretty straight forward, although I think I need to do a bit of explaining on the if(x >= buff->lines[y].length()) part. You see, if it didn't have that, then when the user goes up or down, if the current line of text was longer than the one above/below it, then the cursor would be 'dangling in midair', so to speak. This little if-statement snaps the cursor to the end of the next/previous line if it were to be shorter than the current one.

Time to Wrap it up

Ha! You thought that I would do all of this in only a three-parter :trollface: ? There are still more to implement, though we are past the half-way point already. We still need the most important function: Editor::printBuff(), which is the function that actually lets you see the buffered text! I hope I will see you in Part 4!

Tags: ncurses tutorial c++ editor