Getting The Look & Feel Of An Editor Right

As developers, we all have an editor of choice. We all have one that we consider "perfect for our way of working" and no others will do. Sure, in a bind we can make do with alternatives, but they never seem to "feel" right. For me, this editor is nedit. I guess it's because it was the graphical editor suggested by my professor in school. But I often feel crippled when I have to use something else. Unfortunately, nedit is showing its age. It was made in a time before Unicode was even a concept, using a widget toolkit that has simply not kept up with the times.


Sadly the developers do not seem to be working on it anymore, the last update was version 5.5 in 2004, nearly a decade ago :-(. Often I am still able to make use of nedit on my Linux install, but I'd love to see a spiritual successor developed using a modern toolkit. I've looked, nothing quite fits the bill. gedit comes close, but I find its keyboard bindings awkward and it is completely lacking some advanced features such as regular expression-based search and replace, and rectangular selection. So I've decided in my spare time to try to develop an editor using Qt, which is a work-alike for nedit and a spiritual successor. For now, the working title is "nedit2". I make no promises on a timeline, but I plan to do my best to create modern maintainable code resulting in an editor that any nedit user will feel comfortable using.

So why I am talking so much and not showing code yet? Well, this task is all about the details. In my initial tests, I encountered something which was driving me a bit batty, and I wanted to share with you my findings :-).

Here's a screenshot of my favorite editor.

nedit

And here's a screenshot of Qt's QTextEdit widget with the same font.

qt-edit

Let's ignore some things for the moment. I know that there is no "interface" yet, I know that the coloring is different.

You know what really, and I mean really was driving me nuts?

The way the highlighting was working.

In every code editor, I've ever used, when you highlight multiple lines, the highlight runs the width of the view, not the text. See how in Qt's editor the highlight just stops after the "{" instead of extending to the edge? It just feels wrong. Even the textarea box I'm typing into right now has the selection act the way I want it to. kate, nedit, gedit... They all get this right. Yet Qt's QTextEdit and QPlainTextEdit widgets sadly... don't. Since the whole point of this is not to just create another editor that falls short, but to create one which really feels like the original, I could not accept this.

I tried many things. I subclassed QTextEdit in the hopes that I could get access to the highlighting code (extra selections yes, but not the main selection). I tried toying with a custom QAbstractTextDocumentLayout, but it turns out that I just couldn't get it to work right. Heck, I even worked on creating my own text editing widget from scratch... and while I actually had success getting it to look right. Getting the rest of the text editing details right was difficult and the rendering code was way too slow. I needed a real solution. I was about ready to ask on the Qt mailing list about it when I finally found some code in qtextcontrol.cpp from Qt's codebase which revealed the simple and elegant solution:

if (style->styleHint(QStyle::SH_RichText_FullWidthSelection, &opt, widget))
        selection.format.setProperty(QTextFormat::FullWidthSelection, true);

Aha! There is code to do what I want, but how the heck do I activate it? The solution if you read that snippet carefully is of course using

QStyle. Once I realized that the rest was easy and not very much code at all. First, we need to create a new "Style" class to represent our custom style. Here's what it looks like:

Style.h

#ifndef STYLE_H_
#define STYLE_H_

#include <QCommonStyle>

class Style : public QCommonStyle {
    Q_OBJECT
public:
    Style();
    ~Style();
public:
    virtual int styleHint(StyleHint hint, const QStyleOption *option = 0, const QWidget *widget = 0, QStyleHintReturn *returnData = 0) const;
};

#endif

Nothing special there, just inherit a base class and implement a single virtual function (the one that QTextControl was calling...)

Style.cpp

#include "Style.h"
#include <QTextEdit>

Style::Style() {
}

Style::~Style() {
}

int Style::styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const {
    switch(hint) {
    case QStyle::SH_RichText_FullWidthSelection:
        if(qobject_cast<const QTextEdit *>(widget)) {
            return 1;
        }
    default:
        break;
    }

    return QCommonStyle::styleHint(hint, option, widget, returnData);
}

And here's where the magic happens. The code is simple, elegant, and most of all clear. We test which style hint we are being asked about. If it happens to be SH_RichText_FullWidthSelection, then we also test that the widget in question is a QTextEdit (sorry, this trick doesn't work on QPlainTextEdit). If both of those things are true, then we return 1 to indicate that the style hint should be in effect. Using the style couldn't be more simple, here's my complete code from main.cpp:

int main(int argc, char *argv[]) {

    QApplication app(argc, argv);       
    QTextEdit *const w = new QTextEdit;

    // set the style to and instance of our Style class
    w->setStyle(new Style);
    w->setFont(QFont("Courier", 10));
    w->setPlainText("\n#include <QApplication>\n\nint main(int argc, char *argv[]) {\n\tQApplication app(argc, argv);\n\treturn app.exec();\n}\n");
    w->resize(600, 400);
    w->show();

    return app.exec();
}

And there we have it, basic selection now behaves the way I want it! Behold, in all it's glory:

qt-edit-2

I kinda feel that the Qt folks should have documented these style hints a bit better (perhaps at least listing which widgets are affected by which and in what way?. What hints are enabled by default in which styles? Things like that). But it's OK. I found what I needed and now nedit2 can officially get underway. I look forward to posting more about my trials, tribulations, and progress on this project!