Tab the page with a repeater

When writing the hextool app I used the Repeater element a lot. When there are many similar elements in the QML user interface, the repeater makes it very easy to instantiate and customize the elements. I have previously done some simple experiments with the tabbed user interface (e.g. see Tab the page!). I was wondering if I could also utilize the repeater when creating the tabbed pages. After some experimenting I found out that the repeater suits very well for this kind of task.

I created a generic application “About” page that could be added to any application by simply customizing the tab names and page contents. Here is how it looks like when running in the simulator. There are currently four pages in this app, but with the repeater it is easy to change the number of pages/tabs. The header line contains the active tab name and the close button.

C++ part

The main.cpp is basically the same as used in many previous apps. I fetch the QML root object and pass it to the C++ MainPage class instance for signal/slot connections.

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

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

QmlApplicationViewer viewer;
viewer.setOrientation(QmlApplicationViewer::ScreenOrientationAuto);
viewer.setMainQmlFile(QLatin1String("qml/TestApp15/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 MainPage(qml,&viewer);

return app->exec();
}

The MainPage class is defined in the files mainpage.h and mainpage.cpp. The header file defines the signals and slots. I have defined two signals: openTabMain opens the main tab page and setPageContent sets the page content for the tabbed pages. The openTabMain will pass the tab names (defined in QStringList) to the QML side. The setPageContent signal has two parameters: the page number and the page content (as QString). There is one slot: openTabMainRequested which will process the open request for the tabbed page.

#ifndef MAINPAGE_H
#define MAINPAGE_H

#include <QObject>
#include <QStringList>
#include <QVariant>

class MainPage : public QObject
{
    Q_OBJECT
public:
    explicit MainPage(QObject *qml, QObject *parent = 0);
    
signals:
    void openTabMain(QVariant);
    void setPageContent(int,QString);
    
public slots:
    void openTabMainRequested();
    
};

#endif // MAINPAGE_H

The mainpage.cpp connects the signals and slots and implements the slot openTabMainRequested. It defines the tab names and content and opens the tab pages and sets the content to each page.

#include "mainpage.h"

MainPage::MainPage(QObject *qml, QObject *parent) :
    QObject(parent)
{
    // connect QML signals to MainPage slots
    connect(qml, SIGNAL(openTabMainRequested()), this, SLOT(openTabMainRequested()));

    // connect MainPage signals to QML signals
    connect(this, SIGNAL(openTabMain(QVariant)), qml, SIGNAL(openTabMain(QVariant)));
    connect(this, SIGNAL(setPageContent(int,QString)), qml, SIGNAL(setPageContent(int,QString)));

}

void MainPage::openTabMainRequested()
{
    enum TabName {ABOUT,CREDITS,TRANSLATION,LICENSE};
    QStringList tabNames;

    tabNames << "About" << "Credits" << "Translation" << "License";

    QString aboutContent;
    aboutContent += "<b>TestApp15</b> is a simple example application that ";
    aboutContent += "creates a tabbed page selector. The tab names and ";
    aboutContent += "page contents are set from the C++ code.<p>";
    aboutContent += "The text size is controlled by the screen orientation.<p>";
    aboutContent += "Source code is available at https://github.com/n9dyfi/TestApp15.git";

    QString creditsContent;
    creditsContent +="<b>Thanks To</b><br>";
    creditsContent +="<div style='margin-left:15px;'>";
    creditsContent +="<u>John Doe</u><br>Main Application development<br>john.doe@example.com<br>";
    creditsContent +="<br>";
    creditsContent +="<u>Jane Doe</u><br>Main Application Icon<br>jane.doe@example.com<br></div>";


    QString translationContent;
    translationContent += "Currently there are no translations available.";

    QString licenseContent;

    licenseContent += "This package is free software; you can redistribute it and/or modify ";
    licenseContent += "it under the terms of the GNU General Public License as published by ";
    licenseContent += "the Free Software Foundation; either version 2 of the License, or ";
    licenseContent += "(at your option) any later version.";

    licenseContent += "<p>This package is distributed in the hope that it will be useful, ";
    licenseContent += "but WITHOUT ANY WARRANTY; without even the implied warranty of ";
    licenseContent += "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the ";
    licenseContent += "GNU General Public License for more details.";

    licenseContent += "<p>You should have received a copy of the GNU General Public License ";
    licenseContent += "along with this package; if not, write to the Free Software ";
    licenseContent += "Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA";

    licenseContent += "<p>On Debian systems, the complete text of the GNU General ";
    licenseContent += "Public License can be found in `/usr/share/common-licenses/GPL'.";

    licenseContent += "<p>The Debian packaging is (C) 2018, N9dyfi &lt;n9dyfi@gmail.com&gt; and ";
    licenseContent += "is licensed under the GPL, see above.";

    // Open the TabMain page
    emit(openTabMain(QVariant::fromValue(tabNames)));

    // Set content for each TabPage
    emit(setPageContent(ABOUT,aboutContent));
    emit(setPageContent(CREDITS,creditsContent));
    emit(setPageContent(TRANSLATION,translationContent));
    emit(setPageContent(LICENSE,licenseContent));
}

QML part

The main.cpp loads the main QML file main.qml. Here we need to define the counterparts for the C++ signals and slots. Note that the parameter names used in the signal definitions will be used in the signal handlers to access the data coming from the C++ side. In the C++ side we do not need to name the signal parameters. The AppDefaults component contains default values for font sizes, margins, etc. The MainPage contains the main user interface. In addition there is the standard toolbar with a menu.

import QtQuick 1.1
import com.nokia.meego 1.0

PageStackWindow {
    id: appWindow

    signal openTabMain(variant tabNames)
    signal openTabMainRequested
    signal setPageContent(int pageNumber, string pageContent)

    initialPage: mainPage

    AppDefaults {
        id: appDefaults
    }

    MainPage {
        id: mainPage
    }

    ToolBarLayout {
        id: commonTools
        visible: true
        ToolIcon {
            platformIconId: "toolbar-view-menu"
            anchors.right: (parent === undefined) ? undefined : parent.right
            onClicked: (myMenu.status === DialogStatus.Closed) ? myMenu.open() : myMenu.close()
        }
    }

    Menu {
        id: myMenu
        visualParent: pageStack
        MenuLayout {
            MenuItem {
                text: qsTr("Quit")
                onClicked: Qt.quit()
            }
        }
    }
}

The AppDefaults component is pretty much the same as what I have been using previously e.g. in the hextool app. The nice thing with this kind of a component based default settings file is that I can dynamically change different dimensions based on the display orientation (e.g. text size or header height). Also the color scheme can be easily set.

import QtQuick 1.1

Item {

    property int cCOLOR_SCHEME : 4
    property int cDEFAULT_MARGIN : 16

    property int cHEADER_DEFAULT_HEIGHT_PORTRAIT : 72
    property int cHEADER_DEFAULT_TOP_SPACING_PORTRAIT : 15
    property int cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT : 15

    property int cHEADER_DEFAULT_HEIGHT_LANDSCAPE : 64
    property int cHEADER_DEFAULT_TOP_SPACING_LANDSCAPE : 9
    property int cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE : 13

    property string cFONT_FAMILY : "Nokia pure Text Light"
    property int cFONT_SIZE_LARGE : 32
    property int cFONT_SIZE_SMALL : 22
    property int cFONT_SIZE_TINY : 18
    property string cFONT_FAMILY_BUTTON : "Nokia pure Text"
    property int cFONT_SIZE_BUTTON : 20

    property int cTEXT_AREA_HEIGHT : 60
    property string cVIEW_HEADER : "image://theme/color"+cCOLOR_SCHEME+"-meegotouch-view-header-fixed"
    property string cBUTTON_BACKGROUND: "image://theme/color"+cCOLOR_SCHEME+"-meegotouch-button-accent-background"


    property int cHEADER_HEIGHT : (inPortrait)?cHEADER_DEFAULT_HEIGHT_PORTRAIT:
                          cHEADER_DEFAULT_HEIGHT_LANDSCAPE
    property int cHEADER_TOP_SPACING : (inPortrait)?cHEADER_DEFAULT_TOP_SPACING_PORTRAIT:
                         cHEADER_DEFAULT_TOP_SPACING_LANDSCAPE
    property int cHEADER_BOTTOM_SPACING : (inPortrait)?cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT:
                         cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE
    property int cHEADER_REDUCED_BOTTOM_SPACING : (inPortrait)?
                         0.75*cHEADER_DEFAULT_BOTTOM_SPACING_PORTRAIT:
                         0.5*cHEADER_DEFAULT_BOTTOM_SPACING_LANDSCAPE
    property int cFONT_SIZE : (inPortrait)?cFONT_SIZE_TINY:cFONT_SIZE_SMALL

    Component.onCompleted:
        theme.colorScheme = cCOLOR_SCHEME

}

The MainPage will just instantiate the Header component and one button to display the tab page.

When the button is clicked the openTabMainRequested signal will be emitted. The signal was connected to the openTabMainRequested slot in the MainPage C++ class. The Connections element connects the onOpenTabMain handler to the openTabMain signal defined in main.qml (and connected to the openTabMain signal coming from the C++ side). When the signal is received the TabMain page will be pushed into the page stack. Note that the TabMain.qml file is not loaded until the tab page is requested. This will speed up the application startup time as we don’t need to load all the components at once. The tabNames parameter coming with the signal will be passed to the TabMain page.

import QtQuick 1.1
import com.nokia.meego

Page {
    tools: commonTools

    Header {
        id: header
        headerText: "TestApp15"
    }

    Button{
        anchors.centerIn: parent
        anchors.verticalCenterOffset: header.height/2
        text: qsTr("Tab the page!")
        onClicked: openTabMainRequested()
    }

    Connections {
        target: appWindow
        onOpenTabMain: pageStack.push(Qt.resolvedUrl("TabMain.qml"),{tabNames:tabNames})
    }

}

The TabMain component creates the tabbed page with the tabs and contents defined in the MainPage.cpp. The TabGroup element defines the tabbed pages. But instead of instantiating each page explicitly I can use the Repeater element to instantiate any number of pages I want (as defined with the tabNames string list). I will use the TabPage component as my page definition. The Repeater under TabGroup uses the model property to select the number of pages. For each page I will create a TabButton into the ButtonRow element inside the toolbar. Here the buttons are also instantiated with the Repeater element. Here the Repeater model property contains the tab name list which is used to set the button text (using the modelData context property). Finally I will connect the event handler onSetPageContent to the setPageContent signal defined in the main.qml file. When the setPageContent signal is received from the C++ side the selected page content will be set based on the provided signal parameters.

import QtQuick 1.1
import com.nokia.meego 1.0

// This is a generic tab page with a header
// API
// - property tabNames : a string list containing the tab names
// - signal setPageContent(int pageNumber, string pageContent)

Page {

    property int defaultMargin: appDefaults.cDEFAULT_MARGIN
    property int textSize: appDefaults.cFONT_SIZE
    property int headerHeight: appDefaults.cHEADER_HEIGHT

    // Define the tab names
    property variant tabNames

    // Page header
    //
    Header {
        id: header
        headerText: "TestApp15"
        // Close button
        ToolIcon {
            platformIconId: "browser-stop"
            onClicked: pageStack.pop()
            anchors { right: parent.right; rightMargin: defaultMargin/2
                verticalCenter: parent.verticalCenter}
        }
    }

    // Instantiate the tab pages
    // Use the tab name as a placeholder text
    //
    TabGroup {
        id: tabGroup
        Repeater {
            id: tabPages
            model: tabNames.length  // number of tabs pages to create
            TabPage {
                textWidth: header.width-2*defaultMargin
                // Initial content can be replaced with the setPageContent signal
                content: tabNames[index]
                // Copy the tab name to the header when tab is activated
                onStatusChanged: {
                    if(status===PageStatus.Active)
                        header.headerText = tabNames[index]
                }
            }
        }
    }

    // Define the tab buttons in the toolbar
    //
    tools: ToolBarLayout {
        id: tabTools
        ButtonRow {
            id: buttonRow
            Repeater {
                id: tabButtons
                model: tabNames
                TabButton {
                    text: modelData
                    font.pixelSize: textSize
                    tab: tabPages.itemAt(index)
                }
            }
        }
    }

    // Select the active tab
    //
    Component.onCompleted: {
        tabGroup.currentTab = tabPages.itemAt(0)
        buttonRow.checkedButton = tabButtons.itemAt(0)
    }

    // Connect the setPageContent signal
    //
    Connections {
        target: appWindow
        onSetPageContent: {
            tabPages.itemAt(pageNumber).content = pageContent
        }
    }

}

The last part of the QML code is the TabPage component. The same component is used for all the tabs. Here we need just a TextEdit element to hold the text and a Flickable element to be able to scroll the text.

import QtQuick 1.1
import com.nokia.meego 1.0

// About - Credits - Translation - License

Page {

    property int textWidth
    property string content

    Flickable {
        id: flick
        x: defaultMargin
        y: headerHeight+defaultMargin
        width: parent.width-2*defaultMargin
        height: parent.height-headerHeight-defaultMargin
        contentWidth: textbox.width
        contentHeight: textbox.height
        clip: true

        TextEdit {
            id: textbox
            text: content
            width: textWidth
            font.pixelSize: textSize
            wrapMode: TextEdit.Wrap
        }
    }
}