Приложение РОСА Заметки

Разбор приложения “РОСА Заметки” для ОС РОСА Мобайл

Начало

Предустановленное приложение "Заметки" представляет собой редактор заметок, написанный с использованием Qt, QML и C++. На примере данного приложения можно изучить принципы работы с данными и взаимодействие между QML и C++ в контексте мобильных приложений для Rosa Mobile.

Структура приложения

Приложение состоит из части на C++ (логики, моделей и вспомогательных классов) и части на QML.

  • main.cpp – Точка входа в приложение. Здесь инициализируются настройки, регистрируются типы C++ для использования в QML, загружается основной QML-файл.
  • NotesModel – Модель данных (QAbstractListModel) для хранения и управления заметками.
  • DocumentHandler – Вспомогательный класс, связанный с редактированием и отображением содержимого заметки (работа с QTextDocument, загрузка и сохранение).
  • SelectionProxyModel – Прокси-модель над NotesModel, позволяющая выбирать несколько заметок, выделять их и обрабатывать операции с выделенными элементами.
  • QML-файлы (main.qml, EditPage.qml, NotesPage.qml, TabletNotesPage.qml и др.) –Определяют вид и логику пользовательского интерфейса.

Точка входа в приложение

main.cpp

Файл main.cpp содержит функцию main(), с которой начинается выполнение программы:

 int main(int argc, char *argv[])
 {
     #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
         QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
     #endif
 
     QApplication app(argc, argv);
 
     // Установка стиля
     if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
         QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
     }
 
     // Подключение перевода, установка домена локализации
     KLocalizedString::setApplicationDomain("rosanotes");
 
     // Установка иконки
     QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("org.kde.rosanotes")));
 
     // Инициализация движка QML
     QQmlApplicationEngine engine;
 
     // Регистрация C++ классов в пространстве имён QML
     qmlRegisterType("org.kde.rosanotes", 1, 0, "DocumentHandler");
     qmlRegisterType("org.kde.rosanotes", 1, 0, "NotesModel");
     qmlRegisterType("org.kde.rosanotes", 1, 0, "SelectionProxyModel");
 
     qmlRegisterAnonymousType("org.kde.rosanotes", 1);
     qmlRegisterType("org.kde.rosanotes", 1, 0, "SortFilterModel");
 
     // Регистрация KAboutData как синглтона для получения информации о приложении
     qmlRegisterSingletonType("org.kde.rosanotes", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
         return engine->toScriptValue(KAboutData::applicationData());
     });
 
     // Локализация
     engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
 
     // Путь к QML-ресурсам
     engine.addImportPath(QStringLiteral("qrc:/contents/ui"));
     engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
     if (engine.rootObjects().isEmpty()) {
         return -1;
     }
 
     return app.exec();
 }

Обратите внимание, что qmlRegisterType здесь используется чтобы "отдать" классы DocumentHandler, NotesModel, SelectionProxyModel в QML, чтобы там можно было использовать , и т. п.

Модель заметок

NotesModel

NotesModel наследуется от QAbstractListModel, хранит список заметок в локальной директории и предоставляет методы для добавления, удаления, переименования и проверки существования заметок. Основные методы модели включают в себя:

  • rowCount – возвращает количество существующих заметок
  • data – возвращает выбранные данные (путь, дату или имя) по индексу заметки
  • addNote, deleteNote, renameNote – управление заметками
  • path – возвращает путь к директории, где хранятся заметки

Пример объявления (фрагмент notesmodel.h):

 class NotesModel : public QAbstractListModel
 {
     Q_OBJECT
 
     Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
 
 public:
     enum Role { Path = Qt::UserRole + 1, Date, Name };
     Q_ENUM(Role)
 
     ...
 };

Здесь, Q_PROPERTY даёт возможность связывать свойство path с QML (чтение/запись, сигнал pathChanged), enum Role определяет роли, которые будут возвращаться в методе data (Name, Path, Date).

Реализация

Рассмотрим фрагмент notesmodel.cpp:

 int NotesModel::rowCount(const QModelIndex &index) const
 {
     return m_path.isEmpty() ? 0 : directory.entryList(QDir::Files).count();
 }
 
 QVariant NotesModel::data(const QModelIndex &index, int role) const
 {
     switch (role) {
     case Role::Path:
         return QUrl::fromLocalFile(directory.entryInfoList(QDir::Files).at(index.row()).filePath());
     case Role::Date:
         return directory.entryInfoList(QDir::Files).at(index.row()).birthTime();
     case Role::Name:
         return directory.entryInfoList(QDir::Files).at(index.row()).fileName().replace(".md", "");
     }
     ...
 }

rowCount возвращает количество файлов .md в директории с заметками, data возвращает либо путь к файлу, либо дата, либо имя файла (без расширения .md) по заданному индексу.

Методы манипуляции заметками:

 void NotesModel::addNote(const QString &name)
 {
     beginResetModel();
     QFile file(m_path + QDir::separator() + name + ".md");
     if (file.open(QFile::WriteOnly)) {
         file.write("");
     }
     endResetModel();
 }
 
 void NotesModel::deleteNote(const QUrl &path)
 {
     beginResetModel();
     QFile::remove(path.toLocalFile());
     endResetModel();
 }
 
 void NotesModel::renameNote(const QUrl &path, const QString &name)
 {
     QString newPath = QFileInfo(path.toLocalFile()).absolutePath() + QDir::separator() + name + ".md";
     beginResetModel();
     QFile::rename(path.toLocalFile(), newPath);
     endResetModel();
 }

Методы newNotePath и newNoteName создают корректный путь/имя файла, если есть повторяющиеся названия. isNameExist проверяет, есть ли уже заметка с таким именем.В этих методах вызывается beginResetModel() и endResetModel(), чтобы QML мог обновить представления, связанные с моделью.

Обработка содержимого заметок

DocumentHandler

Класс DocumentHandler предназначен для работы с QTextDocument, который лежит в основе TextArea (RichText-редактор в QML). Он управляет:

  • Загрузкой и сохранением текста в/из файла
  • Форматированием (bold, italic, underline и т. д.)
  • Отслеживает изменения (свойство modified)

Объявление в documenthandler.h:

 Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
 Q_PROPERTY(bool bold READ bold WRITE setBold NOTIFY boldChanged)
 Q_PROPERTY(bool italic READ italic WRITE setItalic NOTIFY italicChanged)
 // ...

Вся логика форматирования (установка жирного/курсива/подчёркивания) сводится к методам, которые меняют QTextCharFormat текущего выделения:

 void DocumentHandler::setBold(bool bold)
 {
     QTextCharFormat format;
     format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
     mergeFormatOnWordOrSelection(format);
     Q_EMIT boldChanged();
 }

Загрузка/сохранение:

 void DocumentHandler::load(const QUrl &fileUrl)
 {
     // Получаем путь
     const QString fileName = QQmlFile::urlToLocalFileOrQrc(fileUrl);
     // Считываем данные и устанавливаем в QTextDocument
     ...
 }
 
 void DocumentHandler::saveAs(const QUrl &fileUrl)
 {
     // Записываем данные из QTextDocument в HTML (toHtml().toUtf8())
     ...
 }

В QML этот класс подключается как DocumentHandler { id: document ... }, а затем свойства, такие как, document.bold связываются с кнопками, которые меняют форматирование текста.

Выделение в списке

SelectionProxyModel

SelectionProxyModel – это QSortFilterProxyModel, который поверх NotesModel даёт возможность выбора нескольких элементов. Он хранит набор выбранных индексов (m_selectedIndexes) и предоставляет соответствующие методы:

  • select(int sourceIndex, bool on)
  • toggleSelected(int sourceIndex)
  • selectAll(), deselectAll()
  • data(..., SelectedRole)

В QML мы получаем доступ к роли "selected", которая показывает, отмечен ли элемент. Пример:

 QVariant SelectionProxyModel::data(const QModelIndex &index, int role) const
 {
     if (role == SelectedRole) {
         return m_selectedIndexes.contains(mapToSource(index).row());
     }
     return QSortFilterProxyModel::data(index, role);
 }

Когда пользователь "долго жмёт" на заметку или иным способом отмечает элемент, вызывается toggleSelected – индекс попадает/исчезает из m_selectedIndexes.

QML-интерфейс

Файл main.qml

main.qml определяет главное окно Kirigami:

 Kirigami.ApplicationWindow {
     id: root
     pageStack.initialPage: isTablet ? "qrc:/TabletNotesPage.qml" : "qrc:/NotesPage.qml"
     ...
 }

Здесь создается приложение на основе Kirigami.ApplicationWindow. В зависимости от того, запущено ли приложение на планшете или нет, загружается различный начальный QML-файл (TabletNotesPage.qml или NotesPage.qml).

Файл NotesPage.qml

Файл NotesPage.qml содержит список заметок (ListView), связанный с моделью NotesModel через прокси-модель SelectionProxyModel и SortFilterModel. Основные элементы:

  • ActionButton в правом нижнем углу для добавления новой заметки.
  • ListView (id: notesList), делегат которой отображает имя заметки, дату и даёт возможность при долгом нажатии перейти в режим выбора (selectionMode = true).
  • SearchField (TextField), которая фильтрует список по имени (через filterModel.setFilterFixedString(text)).

Файл EditPage.qml

EditPage.qml – страница редактирования выбранной заметки. Здесь присутствует TextArea и объект DocumentHandler:

 QQC.TextArea.flickable: QQC.TextArea {
     id: textArea
     ...
     DocumentHandler {
         id: document
         document: textArea.textDocument
         cursorPosition: textArea.cursorPosition
         selectionStart: textArea.selectionStart
         selectionEnd: textArea.selectionEnd
         Component.onCompleted: document.load(path)
 
         onLoaded: {
             textArea.text = text
         }
         onError: (message) => {
             print(message)
         }
     }
 }

Благодаря этому document.saveAs(path) и document.load(path) вызывают C++ код из DocumentHandler. Кроме того, на странице есть кнопки жирного, курсивного и подчёркнутого форматирования, которые меняют свойства document.bold, document.italic, document.underline. Также реализована логика переименования заметки и проверки, что имя не пустое и не занято (методы из NotesModel).

Файл TabletNotesPage.qml

В этом QML-файле предусмотрен двухпанельный интерфейс, который используется, если устройство - планшет в горизонтальной (landscape) ориентации. Справа показывается EditPage, слева - NotesPage. При этом используется States, чтобы в портретной ориентации список был на весь экран, а при выборе заметки - скрывался, и показывалась страница редактирования.

 states: [
     State {
         when: landscapeTablet
         PropertyChanges { target: notesPage; width: root.width * 0.368; visible: true }
         PropertyChanges { target: editPage; anchors.leftMargin: Rosa.Theme.scale(20); visible: true }
     },
     State {
         when: !landscapeTablet && (!notesModel || selectMode || srchMode)
         PropertyChanges { target: notesPage; width: root.width; visible: true }
     },
     State {
         when: !landscapeTablet && notesModel
         PropertyChanges { target: notesPage; width: 0; visible: false }
     }
 ]