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.
- The user can type text in an edit window.
- The user can save the file, open a new file, open an existing file and save the file with a new name.
- 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.
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:
- Property settings, like the UI constants.
- Signal declarations for passing information from the QML side to the C++ side (using slots).
- 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.
- QML element instantiations (like the edit page and the browse page). We will use the page stack to change the active page.
- Event handlers to process the signals coming from the C++ side (signal named “signal” will be processed by an event handler called “onSignal”).
- 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.
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).
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.
Finally select the targets and you are ready to go.