Explore the Notebook sample app

The Sample Apps page includes many Cascades apps that you can download, import into the Momentics IDE, and try out. These apps demonstrate how to use different Cascades controls (such as lists, images, and custom controls) and C++ platform APIs (such as BBM, data access and storage, and sensors). You can use these examples to learn about specific features that you want to use while you're developing your own apps, and you can even use the code from these examples directly in your apps.

There are several sample apps that demonstrate how to use PIM APIs. You can download samples that show you how to use accounts, contacts, calendars, messages, and notebooks. All of these sample apps share a similar structure and similar features, so we're going to focus on the PIM Notebook sample app and highlight some of the important areas of the code. The PIM Notebook app lets you manipulate notebook entries in the notebook database on a device. You can add new notebook entries and edit existing ones, all within a custom UI.

You will learn to:

  • Set up a custom UI to work with notes
  • Filter notes based on certain criteria
  • Retrieve a specific note from the notebook database
  • Edit a note and save the changes to the notebook database
Screen showing the Notebook sample app.

Download the PIM Notebook sample app

Visit the Sample Apps page and download the PIM Notebook sample app (located in the Cascades API samples section). Before we start exploring the app, import it into the Momentics IDE and try it out. To learn how, see Import an existing project.

As we explore the different areas of the app, you can follow along in the code in the IDE. The app includes comments that provide additional information to help you understand what's happening in the code.

Exploring the UI

When you run the PIM Notebook app, you'll see the main screen of the app's UI. This screen includes any notebook entries (which we'll call notes from now on) that exist in the notebook database already. These notes might have been added by using the sample app, or they could have been added using the Remember app on the device. Both of these apps interact with the common notebook database on the device, and we'll demonstrate how to access this database later, in the Examining the NoteBook class section.

The sample app UI includes three screens: the main screen, a viewer screen, and an editor screen.

Main screen

On the main screen, you can view all of the notes that have been added to the notebook database. When you tap one of the notes, the viewer screen is displayed with the details of that note. You can add new notes by tapping the New action at the bottom of the main screen. You can also filter the notes that are displayed by typing text in the text field at the top. When you type filter text, only those notes that include the filter text are displayed on the main screen.

The main screen is defined in the main.qml file in your project (located in the assets folder). The root element in this file is a NavigationPane, which makes it easy to add Page controls that represent each screen in the app. The first Page (representing the main screen) is added directly to the NavigationPane and includes an ImageView for the background image, a TextField for the filter field, and a ListView for the notes.

Screen showing the main screen of the Notebook sample app.

Let's take a closer look at the ListView that's used to display the notes. Here's the code for the ListView:

ListView {
    dataModel: _noteBook.model

    listItemComponents: ListItemComponent {
        type: "item"

        StandardListItem {
            title: ListItemData.title
            description: ListItemData.status
        }
    }

    onTriggered: {
        clearSelection()
        select(indexPath)

        _noteBook.setCurrentNote(indexPath)

        _noteBook.viewNote();
        navigationPane.push(noteViewer.createObject())
    }
}

The first thing to notice is that we set the data model of the ListView to _noteBook.model. In this app, _noteBook is a context property that we specify in main.cpp, and it refers to the main Notebook object of our app. By setting it as a context property, we can access the Notebook properties and functions right from QML. The model property represents the data model for our app, and we can manipulate that data model in C++ and the changes are reflected in our ListView.

When a list item is tapped, the triggered() signal is emitted and we handle it using the onTriggered signal handler, as follows:

  • Clear any previous selection of items and select the item that was tapped. When we select an item using select(), the item is highlighted in the list.
  • Set the current note to the item that was tapped. It's important that we keep track of the current note that's selected, so that we know which note to open in the viewer or editor. The setCurrentNote() function retrieves the ID of the selected note and stores it as a NotebookEntryId.
  • Prepare the note for viewing by calling viewNote(), and then push a new Page on to the NavigationPane stack. This new Page is an instance of NoteViewer.qml, which represents the viewer screen in our app. At the bottom of main.qml, you'll see that both NoteViewer.qml and NoteEditor.qml are added to the attached objects list inside ComponentDefinition components. This approach lets us create these screens dynamically using createObject() whenever they're needed.

There's a lot more to learn about lists, data models, and navigation. If you want to learn more about working with lists and data models, see Lists. If you want to learn more about adding navigation to your apps, see Navigation.

Viewer screen

The viewer screen displays the details about a note that you select from the main screen. You can view the title, description, due date, and status of a note, and you can tap the Edit action (located in the action menu) to change the values of any of these fields. You can also delete the note by tapping the Delete action.

The viewer screen is defined in the NoteViewer.qml file. The root element is a Page, and it contains an ImageView for the background image and Container controls to lay out each field on the screen. To represent each field, we use a ViewerField. This control is a simple custom component (defined in ViewerField.qml) that makes it easier to set the values of each field independently. If you take a look inside ViewerField.qml, you'll see that it just consists of a couple of Label controls in a stack layout.

Screen showing the viewer screen of the Notebook sample app.

Here's how one of the ViewerField controls is used in NoteViewer.qml:

ViewerField {
    horizontalAlignment: HorizontalAlignment.Fill
    title: qsTr ("title")
    value: _noteBook.noteViewer.title
}

A ViewerField includes two aliased properties, title and value, that we specify to display the information we want. It's important to note that NoteViewer.qml takes care of only the visual representation of the viewer screen. It doesn't contain any information about the values of the note fields (such as the title or description fields), nor does it include any logic to interact with the notebook database. Instead, an underlying C++ class (defined in NoteViewer.cpp) is used, and you'll learn how this class works in the Using the viewer class section below.

To retrieve the title of the note we're viewing, we access the _noteBook context property again. This time, we're interested in the noteViewer property, which contains a reference to the underlying NoteViewer object in C++. This object has a title property that specifies the title of the current note, so we use that value in our ViewerField.

If you want to learn more about custom components and properties, see Custom QML components.

Editor screen

The editor screen lets you change the field values for a note and save the changes to the notebook database. This screen is used both to create a new note and to edit an existing note. It includes text fields for the note title and description, a picker for the due date and time, and a check box indicating whether the note is considered completed or not.

The editor screen is defined in the NoteEditor.qml file. Similar to NoteViewer.qml that we looked at above, NoteEditor.qml includes the visual elements of the editor screen (such as Container, TextArea, and TextField controls), while an underlying C++ class (defined in NoteEditor.cpp) stores the actual information for the note that's being edited.

Screen showing the editor screen of the Notebook sample app.

There are a couple of interesting parts of NoteEditor.qml to consider. First, take a look at the implementation of the onCreationCompleted signal handler, which is called when the root Page of the editor screen is created:

onCreationCompleted: {
    if ( _noteBook.noteEditor.mode == NoteEditor.EditMode) {
        // Fill the editor fields after the UI has been created
        titleField.text = _noteBook.noteEditor.title
        descriptionField.text = _noteBook.noteEditor.description
        dueDateTimeField.value = _noteBook.noteEditor.dueDateTime
        completedField.checked = _noteBook.noteEditor.completed
    }
}

Remember that the editor screen is used both to create a new note and to edit an existing note. There's an enumeration in the NoteEditor class called Mode, which specifies the modes for the editor screen (CreateMode or EditMode). If we're editing an existing note, there are existing values in at least one of the note fields, so we need to retrieve these values and populate the fields accordingly.

We use the _noteBook context property again and access the noteEditor property, which contains a reference to the underlying NoteEditor object. This object includes title, description, dueDateTime, and completed properties, so we retrieve the values of these properties and populate the corresponding fields.

Next, consider the description field in NoteEditor.qml:

TextArea {
    id: descriptionField

    hintText: qsTr ("Description")

    onTextChanging: _noteBook.noteEditor.description = text
}

This field includes a signal handler for the textChanging() signal, which is emitted when text in the TextArea changes. When the text is updated, we want to store that updated text so that we can save it to the notebook database later. We update the value of the description property of the NoteEditor object. Other fields (such as the title and due date) use the same approach to update their values.

Examining the NoteBook class

So far, we've explored some of the important UI aspects of the PIM Notebook sample app. Now, we'll start looking at the underlying C++ classes that support the UI and provide the real functionality of the sample app.

All of the source files for these classes are located in the src folder in your project.

Let's start with the NoteBook class. This class provides centralized access to all of the notes and note operations for the app, and is defined in the NoteBook.hpp and NoteBook.cpp files. Open NoteBook.hpp and you'll see the following areas in the code:

  • Properties that provide access to the data model, filter criteria, and the note viewer and editor classes. Remember that we exposed the app's NoteBook object to QML using a context property (_noteBook), so we can access these properties in QML.
  • Slot functions that provide support operations for our app. For example, the app includes a createNote() function that prepares the note editor to create a new note. If you take a look at the corresponding definition of this function in NoteBook.cpp, you'll see that it resets the note editor (clearing the values of all of its fields) and sets the editor mode to CreateMode.
  • A signal (filterChanged()) and a private slot function (filterNotes()) that implement filtering behavior in our app. These elements, along with the filter property, are responsible for filtering the list of displayed notes.
  • Accessor functions for the properties of NoteBook. These functions are marked as private because they're used only within the NoteBook class.
  • Instance variables for NoteBook. We store values for each of the properties, and we also store the ID of the current note.

Next, open NoteBook.cpp. Most of the functions that are implemented in this file are straightforward, but several of them are more complicated. Take a look at the constructor for NoteBook:

NoteBook::NoteBook(QObject *parent)
    : QObject(parent)
    , m_notebookService(new NotebookService(this))
    , m_model(new GroupDataModel(this))
    , m_noteViewer(new NoteViewer(m_notebookService, this))
    , m_noteEditor(new NoteEditor(m_notebookService, this))
{
    // Disable grouping in data model
    m_model->setGrouping(ItemGrouping::None);

    // Ensure to invoke the filterNotes() method whenever a note has been added,
    // changed or removed
    
    // If any Q_ASSERT statement(s) indicate that the slot failed to connect to 
    // the signal, make sure you know exactly why this has happened. This is not
    // normal, and will cause your app to stop working!!
    bool connectResult;
    
    // Since the variable is not used in the app, this is added to avoid a 
    // compiler warning.
    Q_UNUSED(connectResult);
    
    connectResult = connect(m_notebookService, SIGNAL(notebookEntriesAdded(
                            QList<bb::pim::notebook::NotebookEntryId>)),
                            SLOT(filterNotes()));
                            
    // This is only available in Debug builds.
    Q_ASSERT(connectResult);
                            
    connectResult = connect(m_notebookService, SIGNAL(notebookEntriesUpdated(
                            QList<bb::pim::notebook::NotebookEntryId>)),
                            SLOT(filterNotes()));
                            
    // This is only available in Debug builds.
    Q_ASSERT(connectResult);
                            
    connectResult = connect(m_notebookService, SIGNAL(notebookEntriesDeleted(
                            QList<bb::pim::notebook::NotebookEntryId>)),
                            SLOT(filterNotes()));
                            
    // This is only available in Debug builds.
    Q_ASSERT(connectResult);

    // Fill the data model with notes initially
    filterNotes();
}

We start by initializing our instance variables, which use the prefix m_. The NotebookService class provides fundamental operations on the notebook database, such as adding, updating, and removing notes. By using an instance variable for a NotebookService object, we can ensure that this object is created and used only once throughout our app. It's considered a best practice to minimize the number of times that you create PIM service objects, because creating these objects can be expensive in terms of memory consumption and processing cost. We also pass this same NotebookService instance variable to the constructors of our NoteViewer and NoteEditor classes.

Whenever a note is added to, updated in, or removed from the notebook database, we want to refresh the list of notes that's displayed. The filterNotes() function takes care of this by repopulating our data model with entries from the notebook database. So, we connect the appropriate signals to the filterNotes() function. When our sample app starts, we want to populate the data model with any existing notes, so we call filterNotes() at the end of the NoteBook constructor to accomplish this.

Let's take a closer look at the filterNotes() function. This function is defined in NoteBook.cpp, and it looks like this:

void NoteBook::filterNotes()
{
    NotebookEntryFilter filter;

    // Use the entered filter string as search string
    filter.setSearchString(m_filter);

    const QList<NotebookEntry> notes = m_notebookService->notebookEntries(filter);

    // Clear the old note information from the model
    m_model->clear();

    // Iterate over the list of notes
    foreach (const NotebookEntry &note, notes) {
        // Copy the data into a model entry
        QVariantMap entry;
        entry["noteId"] = QVariant::fromValue(note.id());
        entry["title"] = note.title();
        entry["status"] = NoteViewer::statusToString(note.status());

        // Add the entry to the model
        m_model->insert(entry);
    }
}

The Notebook API includes a class called NotebookEntryFilter that you can use to define filter criteria for notes. When you call NotebookService::notebookEntries() to retrieve a list of notes, and you provide a NotebookEntryFilter object, you'll receive only those entries that match the criteria that you specify.

Screen showing the filter field in the Notebook class.

In the sample app, we want to filter our results based on the text string that a user types in the filter field on the main screen of the app. So, we store this text in an instance variable (m_filter), use it to create a NotebookEntryFilter, and then pass this object to notebookEntries(). After we receive our filtered list of notes, we clear any existing entries in the data model and copy the notes into the model.

The last function in NoteBook that we'll look at is setCurrentNote(). This function is designed to solve a common problem when working with lists and data models. Given the index path of an item in a list (say, the item that a user selects), we want to determine the value of a property in the data model for that item. In the case of our sample app, we want to retrieve the ID of the selected note and store it in an instance variable to use later.

Here's what the setCurrentNote() function looks like:

void NoteBook::setCurrentNote(const QVariantList &indexPath)
{
    // Extract the ID of the selected note from the model
    if (indexPath.isEmpty()) {
        m_currentNoteId = NotebookEntryId();
    } else {
        const QVariantMap entry = m_model->data(indexPath).toMap();
        m_currentNoteId = entry.value("noteId").value<NotebookEntryId>();
    }
}

If the index path is empty (meaning that no note is selected), we simply create a NotebookEntryId to represent a new note. Otherwise, we access the data model and retrieve the data at the provided index path, and store it as a QVariantMap. Then, we can retrieve the value of the "noteId" property and store it as a NotebookEntryId.

To learn more about data models and index paths, see Lists.

Using the viewer class

We've taken a look at the NoteBook class, which provides a lot of the notebook-related functionality for our sample app. The NoteBook class is supported by two other custom classes, NoteViewer and NoteEditor. We'll examine some of the important parts of NoteViewer next.

The NoteViewer class provides all of the logic that's required to view a note. This class includes properties that represent each field in the note viewer (such as title, description, and due date), as well as accessor functions for each of these properties (such as title() and description()). There are also signals (such as titleChanged() and descriptionChanged()) that are emitted when the values of the properties change.

It's important to remember that the implementation of NoteViewer is completely UI-independent. The class includes information and functions that are useful for a note viewer, but it doesn't contain any UI elements or other visual representation. You can build a custom UI to display the note information and use NoteViewer to help you retrieve that information from the notebook database. In our sample app, we created our UI in NoteViewer.qml.

The implementation of the accessor functions is straightforward. However, the updateNote() function is more interesting, so let's take a closer look at it. When we use the note viewer to display the details of a note, we need to fetch the note information from the notebook database. Also, when a note is updated in the notebook database, we might need to refresh the note viewer to display the updated information. The updateNote() function takes care of both of these situations.

Here's what updateNote() looks like:

void NoteViewer::updateNote()
{
    // Store previous values
    const QString oldTitle = m_title;
    const QString oldDescription = m_description;
    const QDateTime oldDueDateTime = m_dueDateTime;
    const NotebookEntryStatus::Type oldStatus = m_status;

    // Fetch new values from persistent storage
    const NotebookEntry note = m_notebookService->notebookEntry(m_noteId);

    m_title = note.title();
    m_description = note.description().plainText();
    m_dueDateTime = note.dueDateTime();
    m_status = note.status();

    // Check whether values have changed
    if (oldTitle != m_title)
        emit titleChanged();

    if (oldDescription != m_description)
        emit descriptionChanged();

    if (oldDueDateTime != m_dueDateTime)
        emit dueDateTimeChanged();

    if (oldStatus != m_status)
        emit statusChanged();
}

First, we store the previous values of the note fields so that we can determine if these values have been updated. Then, we retrieve the note from the notebook database by calling notebookEntry() and specifying the note ID. After we update the instance variables (m_title, m_description, and so on) with the new values that we retrieved, we compare the old values with the new values and, if the values are different, we emit the appropriate signals.

Using the editor class

The final class that we'll examine is the NoteEditor class. This class supports our main NoteBook class by providing the logic to create a new note or edit an existing one. Similar to the NoteViewer class, NoteEditor includes properties that correspond to each field of a note (title, description, and so on), as well as functions to set and get these property values. An additional property, mode, indicates whether the note editor should create a new note (indicated by Mode::CreateMode enumeration value) or change the values of an existing note (indicated by the Mode::EditMode enumeration value). The value of this property changes the way that we save notes to the notebook database, as you'll see later when we discuss the saveNote() function.

Like NoteViewer, the NoteEditor class is UI-independent. It provides the functionality that a note editor needs, but doesn't have a visual representation. In our sample app, we created the UI for the note editor in NoteEditor.qml.

Once again, the accessor functions for NoteEditor are straightforward; the real work is done in the loadNote() and saveNote() functions. We use loadNote() to retrieve note information from the notebook database, which is necessary if we're editing an existing note. Here's the implementation of loadNote():

void NoteEditor::loadNote(const NotebookEntryId &noteId)
{
    m_noteId = noteId;

    // Load the note from the persistent storage
    const NotebookEntry note = m_notebookService->notebookEntry(m_noteId);

    // Update the properties with the data from the note
    m_title = note.title();
    m_description = note.description();
    m_dueDateTime = note.dueDateTime();
    m_completed = (note.status() == NotebookEntryStatus::Completed);

    // Emit the change notifications
    emit titleChanged();
    emit descriptionChanged();
    emit dueDateTimeChanged();
    emit completedChanged();
}

At the start of this function, we set the current note ID to the one that's passed in as a parameter. Then, we retrieve the note information from the database by calling notebookEntry() and specifying the ID, and we update each of the fields with the new information. Finally, we emit signals to indicate that the values have changed.

The saveNote() function takes care of creating or updating a note and committing the changes to the notebook database. Here's what saveNote() looks like:

void NoteEditor::saveNote()
{
    if (m_mode == CreateMode) {
        NotebookEntry *note = new NotebookEntry;

        note->setTitle(m_title);
        note->setDescription(m_description);
        note->setDueDateTime(m_dueDateTime);
        note->setStatus(m_completed ? NotebookEntryStatus::Completed
                                    : NotebookEntryStatus::NotCompleted);

        // Save the note to persistent storage (always store in system default
        // notebook)
        m_notebookService->addNotebookEntry(note,
                               m_notebookService->defaultNotebook().id());

    } else if (m_mode == EditMode) {
        // Load the note from persistent storage
        NotebookEntry note = m_notebookService->notebookEntry(m_noteId);

        if (note.isValid()) {
            // Update the single attributes
            note.setTitle(m_title);
            note.setDescription(m_description);
            note.setDueDateTime(m_dueDateTime);
            note.setStatus(m_completed ? NotebookEntryStatus::Completed
                                       : NotebookEntryStatus::NotCompleted);

            // Save the updated note back to persistent storage
            m_notebookService->updateNotebookEntry(note);
        }
    }
}

If we're creating a new note (that is, the note editor is in CreateMode), we create a new NotebookEntry object and set the values of the note fields. Then, we add the note to the database by calling addNotebookEntry(). We need to specify the notebook that we want to add the note to, so we use the default notebook on the device. This notebook always exists on a device and can't be deleted. You can retrieve the default notebook by calling NotebookService::defaultNotebook().

If we're editing an existing note (that is, the note editor is in EditMode), we fetch the information about the current note by calling notebookEntry(). If the note that we retrieved is valid, we update the values of each field. Finally, we save our changes to the database by calling updateNotebookEntry().

There are a couple of details to consider about this function:

  • The completion status of a note is represented in the notebook database by one of two enumeration values: NotebookEntryStatus::Completed or NotebookEntryStatus::NotCompleted. However, we represent the status in NoteEditor as a Boolean value. When we save a note to the database, we need to convert the Boolean value to the appropriate enumeration value. We perform this conversion when we call note->setStatus() and note.setStatus() above.
  • When you change the values of a NotebookEntry, those changes aren't stored in the notebook database until you call the appropriate NotebookService function. In our saveNote() function, when we call notebookEntry() to retrieve an existing note and then update the values of that note, the updated values aren't committed to the database until we call updateNotebookEntry().

Learning more

In this document, you've explored the PIM Notebook sample app and learned how to retrieve and manipulate notebook entries in the database. We've also introduced several other concepts, such as lists, data models, and navigation, that you might want to learn more about. Here are some additional resources that you might find useful as you develop your apps:

Last modified: 2013-12-21

comments powered by Disqus