TextEditor

In my previous post I revisited the topic about the signalling methods between the QML and C++ domains. Next I’m going to apply those methods by creating a simple text editor application.

The editor should have just the basic functionalities to make it useful, e.g.

  1. The user can type text in an edit window.
  2. The user can save the file, open a new file, open an existing file and save the file with a new name.
  3. The user can browse the file system for opening and saving the files.

The figures below show how the user interface looks like. On the left is the edit window with two toolbar buttons, one for saving the file and one for opening the file menu. The figure in the middle shows the file menu. If Open is selected the file browser is displayed as shown in the figure on the right.

TextEditor-Edit-View TextEditor-Menu-View TextEditor-Browse-View

Simple main

The main.cpp source code is shown below. To avoid cluttering the main function with the connect statements I will pass a pointer to the QML root object to my TextEditor object. The TextEditor class contructor will then make the necessary connections between the QML elements and the signals and slots defined in the TextEditor class (to access the QML root object we need to include the QDeclarativeItem header). Finally the TextEditor class will be instantiated. I will pass the viewer object as the parent object to the TextEditor. This will make sure that when the viewer is deleted also the TextEditor will be removed from memory.

#include <QtGui/QApplication>
#include <QDeclarativeItem>
#include "qmlapplicationviewer.h"
#include "texteditor.h"

Q_DECL_EXPORT int main(int argc, char *argv[])
{
    QScopedPointer<QApplication> app(createApplication(argc, argv));

    QmlApplicationViewer viewer;

    viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
    viewer.setMainQmlFile("qml/texteditor/main.qml");
    viewer.showExpanded();

    // Get the QML root object for signal-slot connections.
    QObject *qml = viewer.rootObject();

    // Create the back-end processor and pass in the root object.
    // Make viewer the parent object.
    new TextEditor(qml,&viewer);

    return app->exec();
}

Application constants

I will collect all the user interface parameters into one javascript library called appDefaults.js. The parameters can then be used in the QML files with the import statement (import “appDefaults.js” as AppDefaults).

.pragma library

var FILE_BG_COLOR_NORMAL = "#00AAAAAA";
var FILE_BG_COLOR_PRESSED = "steelblue";

var VERSION = "1.0" ;
var HOMEPAGE = "http://n9dyfi.github.io/TextEditor"
var COLOR_SCHEME = 18;
var DEFAULT_MARGIN = 16;

var HEADER_DEFAULT_HEIGHT_PORTRAIT = 72;
var HEADER_DEFAULT_TOP_SPACING_PORTRAIT = 15;
var HEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT = 15;

var HEADER_DEFAULT_HEIGHT_LANDSCAPE = 64;
var HEADER_DEFAULT_TOP_SPACING_LANDSCAPE = 9;
var HEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE = 13;

var FONT_FAMILY = "Nokia Pure Text Light";
var FONT_SIZE_LARGE = 32;
var FONT_SIZE_SMALL = 24;
var FONT_SIZE_TINY = 18;
var FONT_FAMILY_BUTTON = "Nokia Pure Text"
var FONT_SIZE_BUTTON = 20;

var TEXT_AREA_HEIGHT = 60

The main QML file

The main.qml source code is shown below. It contains one PageStackWindow element (appWindow). The code is a bit lengthy but not too complicated. It can basically be divided into 6 parts:

  1. Property settings, like the UI constants.
  2. Signal declarations for passing information from the QML side to the C++ side (using slots).
  3. Signal declarations for passing information from the C++ side to the QML side. Note that as there are no slots in the QML side we need to connect C++ signals to QML signals and then use the QML event handlers to process the signal.
  4. QML element instantiations (like the edit page and the browse page). We will use the page stack to change the active page.
  5. Event handlers to process the signals coming from the C++ side (signal named “signal” will be processed by an event handler called “onSignal”).
  6. QML dialog elements e.g. to get confirmation for certain actions or to display error messages.
import QtQuick 1.1
import com.nokia.meego 1.0
import "appDefaults.js" as AppDefaults

PageStackWindow {

    id: appWindow

    // UI constants
    property int defaultMargin : AppDefaults.DEFAULT_MARGIN
    property bool orientationIsPortrait
    property string aboutInfo: "A simple text editor for the Nokia N9.\n\n"+
                               AppDefaults.HOMEPAGE+"\n"+
                               "License: GPL3\n"+
                               "Contact: <n9dyfi@gmail.com>\n";

    // Select the color scheme before instantiating any QML elements
    // that need the color...
    onDefaultMarginChanged: theme.colorScheme = AppDefaults.COLOR_SCHEME

    showStatusBar: false;

    property alias content: editPage.content
    //property alias folderPath: browsePage.folderPath

    property bool appIsClosing: false
    signal browseCancelled

    // These QML signals will be connected to the corresponding TextEditor slots
    signal menuOpenClicked(string content)
    signal menuSaveClicked(string content)
    signal menuSaveAsClicked()
    signal currentFolderChanged(string newFolder)
    signal saveAsRequested(string content, string fileName)
    signal fileOpenRequested(string fileName)
    signal fileNewRequested(string content)
    signal saveAsConfirmed(string content)
    signal openConfirmed
    signal newConfirmed
    signal appCloseRequested(string content)
    signal saveBeforeClosed(string content)

    // The corresponding TextEditor signals will be connected to these QML signals
    signal browseRequested(string currentFolder, bool saveRequested)
    signal openCompleted(string content,string currentFolder, string currentFile)
    signal openFailed(string fileName, string errorString)
    signal saveCompleted
    signal saveFailed(string fileName, string errorString)
    signal saveAsCompleted(string currentFolder, string currentFile)
    signal saveAsToBeConfirmed(string fileName)
    signal openToBeConfirmed(string fileName)
    signal newToBeConfirmed(string fileName)
    signal appCloseToBeConfirmed(string fileName)
    signal appToBeClosed
    signal editorCleared(string folderPath,string fileName)

    // QML element instantiations

    // Instantiate the EditPage component (defined in EditPage.qml)
    initialPage: editPage
    EditPage {
        id: editPage
    }

    // Instantiate the BrowsePage component (defined in BrowsePage.qml)
    BrowsePage {
        id: browsePage;
    }

    // Instantiate the EditMenu component (defined in EditMenu.qml)
    EditMenu {
        id: editMenu
    }

    // Instantiate the BrowseMenu component (defined in BrowseMenu.qml)
    BrowseMenu {
        id: browseMenu
    }

    // Event handlers

    onEditorCleared: {
        editPage.content = ""
        editPage.currentFile = fileName
        editPage.currentFolder = folderPath
        editPage.textAreaHeight = AppDefaults.TEXT_AREA_HEIGHT
    }

    // Common exit point when Menu>Quit was selected
    onAppToBeClosed: {
        Qt.quit()
    }

    // Menu>Cancel was selected in the BrowsePage
    onBrowseCancelled: {
        pageStack.pop()
    }

    // Menu>Quit was selected but editor contents were not saved.
    onAppCloseToBeConfirmed: {
        appCloseConfirmDialog.titleText = fileName+" changed."
        appCloseConfirmDialog.open();
    }

    // TextEditor requested BrowsePage to be opened for selecting a file.
    onBrowseRequested: {
        pageStack.push(browsePage,{folderPath:currentFolder,saveAs:saveRequested});
    }

    // TextEditor could not open a file for reading.
    onOpenFailed: {
        openFailedDialog.titleText = "Cannot open "+fileName+"."
        openFailedDialog.message = errorString;
        openFailedDialog.open();
    }

    // TextEditor could not save the file.
    onSaveFailed: {
        saveFailedDialog.titleText = "Cannot save "+fileName+"."
        saveFailedDialog.message = errorString;
        saveFailedDialog.open();
    }

    // TextEditor successfully saved the editor contents to the selected file.
    // Go back to EditPage and flash "Saving...".
    onSaveAsCompleted: {
        editPage.currentFolder = currentFolder;
        editPage.currentFile = currentFile;
        pageStack.pop();
        editPage.showSave = true
        // Was this the save as before close?
        if(appIsClosing) {
            appToBeClosed()
        }
    }

    // TextEditor successfully saved the editor contents to the current file.
    // Stay in EditPage and flash "Saving...".
    onSaveCompleted: {
        editPage.showSave = true
        // Was this the save before close?
        if(appIsClosing) {
            appToBeClosed()
        }
    }

    // TextEditor successfully loaded the editor contents from the selected file.
    onOpenCompleted: {
        editPage.content = content;
        editPage.currentFolder = currentFolder;
        editPage.currentFile = currentFile;
        pageStack.pop();
    }

    // TextEditor will ask before overwriting an existing file.
    onSaveAsToBeConfirmed: {
        saveConfirmDialog.titleText = fileName+" already exists.";
        saveConfirmDialog.open();
    }

    // TextEditor will ask before opening a new file when editor contents changed.
    onOpenToBeConfirmed: {
        openConfirmDialog.titleText = fileName+" changed."
        openConfirmDialog.open();
    }

    // TextEditor will ask before starting a new file when editor contents changed.
    onNewToBeConfirmed: {
        newConfirmDialog.titleText = fileName+" changed."
        newConfirmDialog.open();
    }

    // Dialogs

    // About TextEditor
    QueryDialog {
        id: aboutDialog
        titleText: "TextEditor "+AppDefaults.VERSION
        message: aboutInfo
        acceptButtonText: "Go to homepage"
        rejectButtonText: "Close"
        onAccepted: {
            Qt.openUrlExternally(AppDefaults.HOMEPAGE)
        }
    }

    // File open error
    QueryDialog {
        id: openFailedDialog
        acceptButtonText: "OK"
    }

    // File save error
    QueryDialog {
        id: saveFailedDialog
        acceptButtonText: "OK"
    }

    // Overwrite confirmation
    QueryDialog {
        id: saveConfirmDialog
        message: "Do you want to replace it?"
        acceptButtonText: "Yes"
        rejectButtonText: "No"
        onAccepted: {
            saveAsConfirmed(editPage.content)
        }
    }

    // Save editor contents confirmation
    QueryDialog {
        id: appCloseConfirmDialog
        message: "Save before exiting?"
        acceptButtonText: "Yes"
        rejectButtonText: "No"
        onAccepted: {
            appIsClosing = true
            saveBeforeClosed(editPage.content)
        }
        onRejected: {
            appToBeClosed()
        }
    }

    // Discard changes confirmation when Open is selected
    QueryDialog {
        id: openConfirmDialog
        message: "Discard changes?"
        acceptButtonText: "Yes"
        rejectButtonText: "No"
        onAccepted: {
            openConfirmed()
        }
    }

    // Discard changes confirmation when New is selected
    QueryDialog {
        id: newConfirmDialog
        message: "Discard changes?"
        acceptButtonText: "Yes"
        rejectButtonText: "No"
        onAccepted: {
            newConfirmed()
        }
    }
}

Edit page

The editPage.qml file contains the main editor page. It is defined as the initialPage in main.qml and so it will be the initial page stack page and displayed when the application starts. The page layout is pretty simple as shown below. On the top is the header (defined in Header.qml) containing the file name and the folder name. In the middle is a TextArea QML component (embedded into a Flickable element) that is used as the editor buffer. And on the bottom there is the toolbar (defined in EditTools.qml) that has two buttons: Quick Save and File Menu.

TextEditor-01

Below is the EditPage.qml source code. To give some visual feedback for the save operation there is also a simple state transition that will shortly flash the word “Saving…” in the middle of the page. The transition will change the label opacity so that the word will eventually fade away (at the same time the Flickable element opacity will change to the opposite direction making the editor window visible again).

import QtQuick 1.1
import com.nokia.meego 1.0
import "appDefaults.js" as AppDefaults

Page {

    property alias content: textArea.text
    property string currentFolder: ""
    property string currentFile: ""
    property bool showSave: false
    property alias textAreaHeight: textArea.height
    property string placeHolder: qsTr("Click here to start typing.")

    tools: commonTools

    // Instantiate the Tools component (defined in EditTools.qml)
    EditTools{
        id: commonTools
    }

    Header {
        id: header
        headerText: "TextEditor"
        singleLineHeader: (currentFile=="")
        infoTopText: currentFile
        infoBottomText: currentFolder
    }

    Flickable {
        id: flickable
        anchors.top: header.bottom
        width: parent.width
        height: parent.height-header.height
        clip: true
        contentHeight: textArea.height
        contentWidth: textArea.width

        TextArea {
            id: textArea
            width: editPage.width
            placeholderText: placeHolder
            smooth: true
            wrapMode: TextEdit.Wrap
            textFormat: TextEdit.PlainText
            font.pixelSize: AppDefaults.FONT_SIZE_SMALL
        }
    }

    // Called twice: 1) showSave=true 2) showSave=false
    // Change state from "saving" to "saved"
    onShowSaveChanged: {
        state = (showSave)?"saving":"saved"
        showSave = false
    }

    state: "saved"
    states: [
        State {
            name: "saving"
            PropertyChanges {target: statusLabel; opacity: 1.0}
            PropertyChanges {target: flickable; opacity: 0}
        },
        State {
            name: "saved"
        }
    ]

    // Status label to give some visual feedback for the "save" button click
    Label {
        id: statusLabel
        opacity: 0
        color:  "orange"
        font.pixelSize: 42
        text: "Saving..."
        anchors.centerIn: parent
    }

    // Animations to give some visual feedback for the "save" button click
    transitions: [
        Transition {
            from: "saving"
            to: "saved"
            PropertyAnimation {
                target: statusLabel
                property: "opacity"
                to: 0
                duration: 1000
            }
            PropertyAnimation {
                target: flickable
                property: "opacity"
                to: 1
                duration: 1000
            }
        }
    ]
}

Edit Tools

The file EditTools.qml defines the toolbar for the edit page. When the save button is clicked the signal menuSaveClicked is emitted and the edit page content is passed to the C++ domain. The menu button will open and close the file menu (the menu is defined in EditMenu.qml and instantiated in the main.qml file).

import QtQuick 1.1
import com.nokia.meego 1.0

// Toolbar for the Edit page
ToolBar {
    anchors.bottom: parent.bottom
    tools:
        ToolBarLayout {
        // Quick save button
        ToolIcon {
            platformIconId: "toolbar-directory-move-to"
            anchors.left: (parent === undefined) ? undefined : parent.left
            onClicked: {
                menuSaveClicked(editPage.content)
            }
        }
        // Menu button
        ToolIcon {
            platformIconId: "toolbar-view-menu"
            anchors.right: (parent === undefined) ? undefined : parent.right
            onClicked: (editMenu.status === DialogStatus.Closed) ?
                           editMenu.open() : editMenu.close()
        }
    }
}

Edit Menu

The edit page menu is defined in the file EditMenu.qml.  Each menu item has an event handler onClicked that either emits a signal or opens a dialog (“About”). The signals are defined in main.qml and the connections are done in texteditor.cpp (in the TextEditor class constructor). For the File>New and File>Open actions we will pass the current editor content to the C++ side to check if the content has changed before loading new content.

import QtQuick 1.1
import com.nokia.meego 1.0

Menu {

    MenuLayout {

        MenuItem {
            text: "New"
            onClicked: {
                fileNewRequested(editPage.content)
            }
        }

        MenuItem {
            text: "Open"
            onClicked: {
                menuOpenClicked(editPage.content)
            }
        }

        MenuItem {
            text: "Save As"
            onClicked: {
                menuSaveAsClicked()
            }
        }

        MenuItem {
            text: "About"
            onClicked: {
                aboutDialog.open()
            }
        }

        MenuItem {
            text: "Quit"
            onClicked: {
                appCloseRequested(editPage.content)
            }
        }
    }
}

Page Header

The Header.qml file contains the page header code. It is parameterized (using properties as a kind of API – see the highlighted section below) so that the header text can be easily changed. Also it is possible to switch between a single line header and a double line header. The header dimensions are defined in the appDefaults.js file.

import QtQuick 1.1
import com.nokia.meego 1.0
import "appDefaults.js" as AppDefaults

Item {
    property alias headerText: titleLabel.text
    property alias infoTopText: infoTop.text
    property alias infoBottomText: infoBottom.text
    property bool singleLineHeader: true

    property string viewHeader: "image://theme/color"+theme.colorScheme+"-meegotouch-view-header-fixed"
    property int headerTopSpacing : (inPortrait)?AppDefaults.HEADER_DEFAULT_TOP_SPACING_PORTRAIT:
                         AppDefaults.HEADER_DEFAULT_TOP_SPACING_LANDSCAPE
    property int headerBottomSpacing : (inPortrait)?AppDefaults.HEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT:
                         AppDefaults.HEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE
    property int infoBottomSpacing : (inPortrait)?
                                         0.75*AppDefaults.HEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT:
                                         0.5*AppDefaults.HEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE
    property string headerFontFamily : AppDefaults.FONT_FAMILY
    property int headerFontSize : AppDefaults.FONT_SIZE_LARGE
    property int infoTopFontSize: AppDefaults.FONT_SIZE_SMALL
    property int infoBottomFontSize: AppDefaults.FONT_SIZE_TINY

    // Header dimensions
    height: (inPortrait)?AppDefaults.HEADER_DEFAULT_HEIGHT_PORTRAIT:
                          AppDefaults.HEADER_DEFAULT_HEIGHT_LANDSCAPE
    width: parent.width

    // Header background image
    Image {
        id: titleImage
        height: parent.height
        width: parent.width
        source: viewHeader
    }

    // Define the label styling
    LabelStyle {
        id: labelStyle
        textColor: "white"
        fontFamily: headerFontFamily
        fontPixelSize: headerFontSize
    }
    LabelStyle {
        id: infoTopStyle
        textColor: "white"
        fontFamily: headerFontFamily
        fontPixelSize: infoTopFontSize
    }
    LabelStyle {
        id: infoBottomStyle
        textColor: "black"
        fontFamily: headerFontFamily
        fontPixelSize: infoBottomFontSize
    }

    // Header text
    Label {
        id: titleLabel
        platformStyle: labelStyle
        visible: singleLineHeader
        anchors {
            top:parent.top; topMargin:headerTopSpacing;
            left:parent.left; leftMargin:defaultMargin
        }
    }
    Label {
        id: infoTop
        platformStyle: infoTopStyle
        visible: !singleLineHeader
        anchors {
            bottom: infoBottom.top
            left:parent.left; leftMargin:defaultMargin
        }
    }
    Label {
        id: infoBottom
        platformStyle: infoBottomStyle
        visible: !singleLineHeader
        anchors {
            bottom:parent.bottom; bottomMargin:infoBottomSpacing;
            left:parent.left; leftMargin:defaultMargin
        }
    }
}

Browse Page

When the user selects File>Open or File>Save As the browse page will be opened (by pushing the browse page component to the page stack). The browse page will list the currently selected folder contents in a scrollable list. By clicking a folder icon will open the sub-folder. By clicking a file icon will open the file (or select the file as the “Save As” target). The toolbar contains a “back” button that will go back to the previous folder, a refresh button for updating the current view (e.g. when a new file has been created) and the file menu containing a “cancel” action for cancelling the currently selected operation and to go back to the editor page (by popping the browse page from the page stack).

TextEditor-03
Below is the browse page source code from the file BrowsePage.qml. The page header is using the same Header component (from Header.qml) as the edit page. There is just one additional embedded button that is needed for saving files. Below the header is a TextField component that is only visible during the Save As action to enter a file name. The main part of the page is occupied by a ListView component that is using the FolderListModel to access the file system. The FolderListModel provides two roles called ‘fileName’ and ‘filePath’ that contain the currently selected file and folder names. They will be used inside the delegate component (FileDelegate defined in the file FileDelegate.qml). Finally there is a state transition similar to the edit page showing the text “Refreshing…” when the refresh button is clicked.

import QtQuick 1.1
import Qt.labs.folderlistmodel 1.0
import com.nokia.meego 1.0
import "appDefaults.js" as AppDefaults

Page {

    property variant content: content
    property alias folderPath: folderModel.folder;
    property bool saveAs: false

    property string buttonBackground: "image://theme/color"+theme.colorScheme+"-meegotouch-button-accent-background"
    property string buttonFontFamily : AppDefaults.FONT_FAMILY_BUTTON
    property int buttonFontSize : AppDefaults.FONT_SIZE_BUTTON

    signal folderChanged(string path);

    property bool refresh: false

    onFolderChanged: {
        header.infoBottomText = path
        folderModel.folder = path
        currentFolderChanged(path)
    }

    // Instantiate the BrowseTools component (defined in BrowseTools.qml)
    BrowseTools{
        id: browseTools
        visible: true
    }

    Column {
        width: parent.width
        height: parent.height

        // Button style for the page header Save As button
        ButtonStyle {
            id: buttonStyle
            textColor: "white"
            fontFamily: buttonFontFamily
            fontPixelSize: buttonFontSize
            background: buttonBackground
            pressedBackground: buttonBackground+"-pressed"
        }

        // The page header (with a Save As button if saveAs = true)
        Header {
            id: header
            singleLineHeader: false
            infoTopText: (saveAs)?"Save as":"Open"
            infoBottomText: folderPath
            // Save As button
            Button {
                id: saveAsButton
                platformStyle: buttonStyle
                visible: saveAs
                width: 130; height: 40
                anchors { right: parent.right; rightMargin: defaultMargin
                    verticalCenter: parent.verticalCenter}
                text: "Save"
                onClicked: {
                    saveAsRequested(editPage.content,saveasfile.text);
                }
            }
        }

        // Prompt for a file name for Save As
        TextField {
            id: saveasfile
            visible: saveAs
            width:parent.width;
            placeholderText: "File to save"
            Keys.onReturnPressed: {
                saveAsRequested(editPage.content,saveasfile.text);
            }
        }

        // Folder list
        ListView {
            id: listView
            clip: true
            height: (saveAs?-saveasfile.height:0) + parent.height - header.height - AppDefaults.DEFAULT_MARGIN - browseTools.height;
            //height: parent.height - header.height - AppDefaults.DEFAULT_MARGIN - browseTools.height;
            width: parent.width;

            delegate: FileDelegate {
                isDir: folderModel.isFolder(index)
            }

            // property 'folder' specifies the folder to list
            // roles 'fileName' and 'filePath' provide access to the current item
            model: FolderListModel {
                id: folderModel
                nameFilters: ["*"]
                showDirs: true
                showDotAndDotDot: false
            }
        }
    }

    // Called twice: 1) refresh=true 2) refresh=false
    // Change state from "refreshing" to "refreshed"
    onRefreshChanged: {
        state = (refresh)?"refreshing":"refreshed"
        refresh = false
    }

    state: "refreshed"
    states: [
        State {
            name: "refreshing"
            PropertyChanges {target: statusLabel; opacity: 1.0}
            PropertyChanges {target: listView; opacity: 0}
            PropertyChanges {target: folderModel; nameFilters: [""]}
        },
        State {
            name: "refreshed"
            PropertyChanges {target: folderModel; nameFilters: ["*"]}
        }
    ]

    // Status label to give some visual feedback for the "refresh" button click
    Label {
        id: statusLabel
        opacity: 0
        color:  "orange"
        font.pixelSize: 42
        text: "Refreshing..."
        anchors.centerIn: parent
    }

    // Animations to give some visual feedback for the "refresh" button click
    transitions: [
        Transition {
            from: "refreshing"
            to: "refreshed"
            PropertyAnimation {
                target: statusLabel
                property: "opacity"
                to: 0
                duration: 1000
            }
            PropertyAnimation {
                target: listView
                property: "opacity"
                to: 1
                duration: 1000
            }
        }
    ]
}

File delegate

The FileDelegate component defines how the file list entries are displayed. The source code is in the file FileDelegate.qml. The delegate consists of an Image, a Text and an associated MouseArea. The current file name and folder name are provided by the FolderListModel roles ‘fileName’ and ‘filePath’, respectively. The property isDir selects whether a file or a folder icon will be shown. When the MouseArea is clicked a signal will be emitted (for opening the selected file or folder) or the TextField will be updated with the chosen file name (for Save As).

import QtQuick 1.1
import "appDefaults.js" as AppDefaults
Rectangle {
    id: fileDelegate
    property bool isDir: false

    width: parent.width
    height: fileNameView.height * 1.5
    color: ( mouseArea.pressed )
           ? AppDefaults.FILE_BG_COLOR_PRESSED
           : AppDefaults.FILE_BG_COLOR_NORMAL
    Image
    {
        id: icon
        anchors.verticalCenter: parent.verticalCenter
        x: AppDefaults.DEFAULT_MARGIN
        smooth: true
        source: isDir
                ? "image://theme/icon-m-toolbar-directory-white"
                : "image://theme/icon-s-notes";
        visible: source != ''
    }

    Text {
        id: fileNameView
        text: fileName
        font.pixelSize: AppDefaults.FONT_SIZE_SMALL
        anchors.verticalCenter: parent.verticalCenter
        anchors.left:icon.right
        anchors.leftMargin: AppDefaults.DEFAULT_MARGIN
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent

        onClicked: {
            fileNameView.color="red"
            if ( isDir ) {
                folderChanged(filePath);
            } else {
                if (saveAs) {
                    saveasfile.text = fileName;
                } else {
                    fileOpenRequested(fileName)
                }
            }
        }
    }
}

Browse Tools

The browse page toolbar is defined in the file BrowseTools.qml (and instantiated in the main.qml file). Here we just define the three toolbar icons and the associated actions (i.e. the onClicked event handlers). When the “back” button is clicked we will emit the folderChanged signal that will trigger the onFolderChanged event handler in the BrowsePage component to update the current folder (and also notify the C++ side with the signal currentFolderChanged). When the “refresh” button is clicked the BrowsePage component property refresh will be set to true which will further trigger the property transitions. The transitions contain a temporary change in the FolderListModel nameFilters property which will force the FolderListModel to rescan the current folder. Finally the “menu” button will open and close the file menu.

import QtQuick 1.1
import com.nokia.meego 1.0

// Toolbar for the browse page
ToolBar {
    anchors.bottom: parent.bottom
    tools:
        ToolBarLayout {
        // Back to the parent folder
        ToolIcon {
            platformIconId: inPortrait?"toolbar-back-dimmed-white":
                                        "toolbar-back-landscape-dimmed-white"
            onClicked: folderChanged(folderModel.parentFolder);
        }
        // Refresh
        ToolIcon {
            platformIconId: "toolbar-refresh"
            onClicked: {
                refresh=true
            }
        }
        // Menu
        ToolIcon {
            platformIconId: "toolbar-view-menu"
            anchors.right: (parent === undefined) ? undefined : parent.right
            onClicked: (browseMenu.status === DialogStatus.Closed) ?
                           browseMenu.open() :
                           browseMenu.close()
        }
    }
}

Browse menu

The menu is defined in the file BrowseMenu.qml (and also instantiated in the main.qml). The only entry in the menu is “Cancel” that will cancel the current operation and return user back to the edit page.

import QtQuick 1.1
import com.nokia.meego 1.0

Menu {
    MenuLayout {
        MenuItem {
            text: "Cancel"
            onClicked: browseCancelled()
        }
    }
}

This concludes the QML user interface code. What is left is the C++ back-end.

Back-end processor

The C++ back-end consists of one class called TextEditor. The header file texteditor.h contains the class declaration. Here we just declare the member variables, the slots and the signals. Each slot and signal has a counterpart in the QML side.

#ifndef TEXTEDITOR_H
#define TEXTEDITOR_H

#include <QDeclarativeItem>
#include <QFile>

#define SAVE 0
#define SAVE_AS 1

class TextEditor : public QObject
{
    Q_OBJECT

public:
    explicit TextEditor(QObject *qml, QObject *parent = 0);

private:
    static const char *UNTITLED;    // default file name for File>New

    QString currentFolder;
    QString currentFile;
    QString currentContent;

private slots:
    void saveCurrentContent(int);

public slots:
    void menuOpenClicked(QString);
    void menuSaveClicked(QString);
    void menuSaveAsClicked();
    void saveAsRequested(QString,QString);
    void currentFolderChanged(QString);
    void fileOpenRequested(QString);
    void saveAsConfirmed(QString);
    void openConfirmed();
    void newConfirmed();
    void appCloseRequested(QString);
    void saveBeforeClosed(QString);
    void fileNewRequested(QString);

signals:
    void browseRequested(QString currentFolder, bool saveRequested);
    void openCompleted(QString content, QString currentFolder, QString currentFile);
    void openFailed(QString fileName, QString errorString);
    void saveCompleted();
    void saveFailed(QString fileName, QString errorString);
    void saveAsCompleted(QString currentFolder, QString currentFile);
    void saveAsToBeConfirmed(QString fileName);
    void openToBeConfirmed(QString fileName);
    void newToBeConfirmed(QString fileName);
    void appCloseToBeConfirmed(QString fileName);
    void appToBeClosed();
    void editorCleared(QString currentFolder, QString currentFile);
};

#endif // TEXTEDITOR_H

The file texteditor.cpp contains the TextEditor class implementation. Here we define what the slot methods do. In addition the class constructor will connect the slots and signals to their counterparts in the QML side. The QML root object is passed to the constructor from the main.cpp file.

#include "texteditor.h"

const char *TextEditor::UNTITLED = "Untitled";

TextEditor::TextEditor(QObject *qml, QObject *parent) :
      QObject(parent)
{
    currentFolder = "file:///home/user";
    currentFile = UNTITLED;
    currentContent = "";

    // connect QML signals to TextEditor slots
    connect(qml, SIGNAL(menuOpenClicked(QString)),
             this, SLOT(menuOpenClicked(QString)));
    connect(qml, SIGNAL(menuSaveClicked(QString)),
             this, SLOT(menuSaveClicked(QString)));
    connect(qml, SIGNAL(menuSaveAsClicked()),
             this, SLOT(menuSaveAsClicked()));
    connect(qml, SIGNAL(saveAsRequested(QString,QString)),
             this, SLOT(saveAsRequested(QString,QString)));
    connect(qml, SIGNAL(currentFolderChanged(QString)),
             this, SLOT(currentFolderChanged(QString)));
    connect(qml, SIGNAL(fileOpenRequested(QString)),
             this, SLOT(fileOpenRequested(QString)));
    connect(qml, SIGNAL(saveAsConfirmed(QString)),
             this, SLOT(saveAsConfirmed(QString)));
    connect(qml, SIGNAL(openConfirmed()),
             this, SLOT(openConfirmed()));
    connect(qml, SIGNAL(newConfirmed()),
             this, SLOT(newConfirmed()));
    connect(qml, SIGNAL(appCloseRequested(QString)),
             this, SLOT(appCloseRequested(QString)));
    connect(qml, SIGNAL(saveBeforeClosed(QString)),
             this, SLOT(saveBeforeClosed(QString)));
    connect(qml, SIGNAL(fileNewRequested(QString)),
             this, SLOT(fileNewRequested(QString)));

    // connect TextEditor signals to QML signals
    connect(this, SIGNAL(browseRequested(QString,bool)),
               qml, SLOT(browseRequested(QString,bool)));
    connect(this, SIGNAL(openCompleted(QString,QString,QString)),
               qml, SLOT(openCompleted(QString,QString,QString)));
    connect(this, SIGNAL(openFailed(QString,QString)),
               qml, SLOT(openFailed(QString,QString)));
    connect(this, SIGNAL(saveCompleted()),
               qml, SLOT(saveCompleted()));
    connect(this, SIGNAL(saveFailed(QString,QString)),
               qml, SLOT(saveFailed(QString,QString)));
    connect(this, SIGNAL(saveAsCompleted(QString,QString)),
               qml, SLOT(saveAsCompleted(QString,QString)));
    connect(this, SIGNAL(saveAsToBeConfirmed(QString)),
               qml, SLOT(saveAsToBeConfirmed(QString)));
    connect(this, SIGNAL(openToBeConfirmed(QString)),
               qml, SLOT(openToBeConfirmed(QString)));
    connect(this, SIGNAL(newToBeConfirmed(QString)),
               qml, SLOT(newToBeConfirmed(QString)));
    connect(this, SIGNAL(appCloseToBeConfirmed(QString)),
               qml, SLOT(appCloseToBeConfirmed(QString)));
    connect(this, SIGNAL(appToBeClosed()),
               qml, SLOT(appToBeClosed()));
    connect(this, SIGNAL(editorCleared(QString,QString)),
               qml, SLOT(editorCleared(QString,QString)));
}

// QML menu: Open was clicked
void TextEditor::menuOpenClicked(QString content)
{
    if(content!=currentContent)
    {
        emit openToBeConfirmed(currentFile);
        return;
    }
    bool saveRequested = false;
    emit browseRequested(currentFolder, saveRequested);
}

// QML menu: New was clicked
void TextEditor::fileNewRequested(QString content)
{
    if(content!=currentContent)
    {
        emit newToBeConfirmed(currentFile);
        return;
    }
    newConfirmed();
}

// QML toolbar: Save was clicked
void TextEditor::menuSaveClicked(QString content)
{
    if(currentFile==UNTITLED)
    {
        menuSaveAsClicked();
        return;
    }
    currentContent = content;
    saveCurrentContent(SAVE);
}

// QML menu: Save As was clicked
void TextEditor::menuSaveAsClicked()
{
    bool saveRequested = true;
    emit browseRequested(currentFolder, saveRequested);
}

// Overwrite during Menu>Save As was confirmed.
void TextEditor::saveAsConfirmed(QString content)
{
    currentContent = content;
    saveCurrentContent(SAVE_AS);
}

// Discard current content was confirmed during Menu>Open.
void TextEditor::openConfirmed()
{
    bool saveRequested = false;
    emit browseRequested(currentFolder, saveRequested);
}

// Discard current content was confirmed during Menu>New.
void TextEditor::newConfirmed()
{
    currentFile = UNTITLED;
    currentContent = "";
    emit editorCleared(currentFolder,currentFile);
}

// QML menu: Quit was clicked.
void TextEditor::appCloseRequested(QString content)
{
    // Check if the content was changed.
    if(content!=currentContent)
    {
        emit appCloseToBeConfirmed(currentFile);
        return;
    }
    // Not changed -> ok to quit.
    emit appToBeClosed();
}

// Saving changed content before quitting was confirmed.
void TextEditor::saveBeforeClosed(QString content)
{
    // Did we have a valid file name?
    if(currentFile==UNTITLED)
    {
        menuSaveAsClicked();
        return;
    }
    currentContent = content;
    saveCurrentContent(SAVE);
}

// Save the current editor content to the current folder/file.
void TextEditor::saveCurrentContent(int saveMode)
{
    bool fileIsWritable;

    QUrl url(currentFolder+"/"+currentFile);
    QString filename = url.toLocalFile();
    QFile file(filename);
    fileIsWritable = file.open(QIODevice::WriteOnly | QIODevice::Text);

    if ( fileIsWritable ) {
        QTextStream stream( &file );
        stream<<currentContent;
        file.close();
        if(saveMode==SAVE)
            emit saveCompleted();
        else
            emit saveAsCompleted(currentFolder,currentFile);
    } else {
        emit saveFailed(currentFile,file.errorString());
    }
}

// QML listView: file name was selected for saving
void TextEditor::saveAsRequested(QString content, QString fileName)
{
    currentFile = fileName;

    QUrl url(currentFolder+"/"+currentFile);
    QString filename = url.toLocalFile();
    QFile file(filename);
    if(file.exists())
    {
        emit saveAsToBeConfirmed(currentFile);
    } else {
        currentContent = content;
        saveCurrentContent(SAVE_AS);
    }
}

// QML listView: folder was selected
void TextEditor::currentFolderChanged(QString path)
{
    currentFolder = path;
}

// QML listView: file name was selected for reading
void TextEditor::fileOpenRequested(QString fileName)
{
    bool fileIsReadable = false;

    QUrl url(currentFolder+"/"+fileName);
    QString localFile = url.toLocalFile();
    QFile file(localFile);
    fileIsReadable = file.open(QIODevice::ReadOnly | QIODevice::Text);
    if (fileIsReadable)
    {
        QTextStream stream( &file );
        currentContent = stream.readAll();
        file.close();
        // pass the content to the TextArea component
        currentFile = fileName;
        emit openCompleted(currentContent,currentFolder,currentFile);
    } else {
        emit openFailed(fileName,file.errorString());
    }
}

Source codes on Github

All the TextEditor source codes are available on Github at

https://github.com/n9dyfi/TextEditor

The version presented in this post should correspond to branch-v1.0. In QtCreator you can open the project directly from Github by clicking the Create Project button and selecting Project from Version Control and Git Repository Clone. Then fill in the Clone URL and Branch as shown below.

TextEditor-10

Finally select the targets and you are ready to go.

TextEditor-11

1 2