Merge branch 'userapps' into 'master'

This commit is contained in:
Nicolas Mailloux 2022-06-23 01:02:55 -04:00
commit 0dfcc20409
17 changed files with 1486 additions and 274 deletions

230
apps.cpp
View file

@ -1,8 +1,14 @@
#include "apps.h"
#include "ui_apps.h"
#include "mainwindow.h"
#include "userapps.h"
#include <QFile>
#include <QProcess>
#include <QJsonParseError>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
apps::apps(QWidget *parent) :
QWidget(parent),
@ -29,6 +35,8 @@ apps::apps(QWidget *parent) :
ui->reversiLaunchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px");
ui->g2048LaunchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px");
ui->noUserAppsAvailableLabel->hide();
// Hiding KoBox apps button and label if X11 isn't enabled/wasn't started
if(checkconfig("/external_root/boot/flags/X11_START") == false or checkconfig("/external_root/boot/flags/X11_STARTED") == false) {
ui->label_5->hide();
@ -61,6 +69,15 @@ apps::apps(QWidget *parent) :
ui->lightmapsLaunchBtn->deleteLater();
}
ui->editUserAppsBtn->setProperty("type", "borderless");
ui->editUserAppsBtn->setIcon(QIcon(":/resources/edit.png"));
// Refresh because Qt shows the scrollbar for one second then hides it, leaving a mark on the eInk screen
QTimer::singleShot(1750, this, SLOT(refreshScreenNative()));
jsonParseSuccess = parseJson();
showUserApps(false);
QFile stylesheetFile("/mnt/onboard/.adds/inkbox/eink.qss");
stylesheetFile.open(QFile::ReadOnly);
this->setStyleSheet(stylesheetFile.readAll());
@ -160,3 +177,216 @@ void apps::on_g2048LaunchBtn_clicked()
void apps::showToastNative(QString messageToDisplay) {
emit showToast(messageToDisplay);
}
/*
* This function opens and reads the main GUI user applications' JSON file, parses and verifies its content to avoid issues in the future.
*/
bool apps::parseJson() {
// If the path changes, it is also used statically in showUserApps()
jsonFile.setFileName("/mnt/onboard/onboard/.apps/apps.json");
bool jsonCheckSuccess = true;
if(jsonFile.exists() == false) {
log("GUI user applications' main JSON file is missing", className);
jsonCheckSuccess = false;
}
else {
jsonFile.open(QIODevice::ReadOnly | QIODevice::Text);
QString fileRead = jsonFile.readAll();
jsonFile.close();
jsonDocument = QJsonDocument::fromJson(fileRead.toUtf8());
if(jsonDocument.isNull() == true) {
log("GUI user applications' main JSON file is invalid", className);
jsonCheckSuccess = false;
}
else {
QJsonObject jsonObject = jsonDocument.object();
if(jsonObject["list"].isArray() == true) {
QJsonArray jsonArray = jsonObject["list"].toArray();
for(QJsonValueRef refJsonObject: jsonArray) {
if(refJsonObject.isObject()) {
QJsonObject jsonMainObject = refJsonObject.toObject();
if(jsonMainObject.size() == 9) {
if(!jsonMainObject["Name"].isString()) {
QString function = __func__; log(function + ": Invalid 'Name' type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["Author"].isString()) {
QString function = __func__; log(function + ": Invalid 'Author' type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["AuthorContact"].isString()) {
QString function = __func__; log(function + ": Invalid 'AuthorContact' type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["Version"].isString()) {
QString function = __func__; log(function + ": Invalid 'Version' type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["IconPath"].isString()) {
log("JSON: Invalid IconPath type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["ExecPath"].isString()) {
QString function = __func__; log(function + ": Invalid 'ExecPath' type inside object", className);
jsonCheckSuccess = false;
}
else {
if(jsonMainObject["ExecPath"].toString().contains("..")) {
// Possible security risk
showToastNative("ERROR: 'ExecPath' has invalid path");
QString function = __func__; log(function + ": 'ExecPath' contains \"..\"", className);
jsonCheckSuccess = false;
}
}
if(!jsonMainObject["Enabled"].isBool()) {
QString function = __func__; log(function + ": Invalid 'Enabled' type inside object", className);
jsonCheckSuccess = false;
}
if(!jsonMainObject["SupportedDevices"].isArray()) {
QString function = __func__; log(function + ": Invalid 'SupportedDevices' type inside object", className);
jsonCheckSuccess = false;
}
else {
QJsonArray jsonArray = jsonMainObject["SupportedDevices"].toArray();
for(QJsonValueRef refJsonObject: jsonArray) {
// https://doc.qt.io/qt-5/qjsonvalue.html#toInt
if(!refJsonObject.isString()) {
QString function = __func__; log(function + ": Array from 'RequiredFeatures' contains a wrong type", className);
jsonCheckSuccess = false;
}
}
}
if(!jsonMainObject["RequiredFeatures"].isArray()) {
QString function = __func__; log(function + ": Invalid 'RequiredFeatures' type inside object", className);
jsonCheckSuccess = false;
}
else {
QJsonArray jsonArray = jsonMainObject["RequiredFeatures"].toArray();
for(QJsonValueRef refJsonObject: jsonArray) {
// https://doc.qt.io/qt-5/qjsonvalue.html#toInt
if(!refJsonObject.isDouble()) {
QString function = __func__; log(function + ": Array from 'RequiredFeatures' contains a wrong type", className);
jsonCheckSuccess = false;
}
}
}
}
else {
QString function = __func__; QString message = function + ": An object inside list array has too many items (or not enough): ";
message.append(QString::number(jsonMainObject.size()));
log(message, className);
jsonCheckSuccess = false;
}
}
else {
QString function = __func__; log(function + ": List array has an item of other type than object", className);
jsonCheckSuccess = false;
}
}
}
}
}
return jsonCheckSuccess;
}
void apps::on_editUserAppsBtn_clicked()
{
if(userAppsSecondPage == false) {
userAppsSecondPage = true;
// Settings page
ui->editUserAppsBtn->setIcon(QIcon(":/resources/save.png"));
showUserApps(userAppsSecondPage);
emit showUserAppsEdit(userAppsSecondPage);
QTimer::singleShot(500, this, SLOT(refreshScreenNative()));
}
else {
userAppsSecondPage = false;
userAppsAvailable = false;
// Launch page
ui->editUserAppsBtn->setIcon(QIcon(":/resources/edit.png"));
// It changed via updateJsonFileSlot, and now it writes it only once
jsonFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate);
jsonFile.write(jsonDocument.toJson());
jsonFile.flush();
jsonFile.close();
showUserApps(userAppsSecondPage);
emit showUserAppsEdit(userAppsSecondPage);
QTimer::singleShot(500, this, SLOT(refreshScreenNative()));
}
}
void apps::showUserApps(bool showDisabledJson)
{
emit clearAppsLayout();
if(jsonParseSuccess == true) {
QString function = __func__; log(function + ": Main user applications' JSON is valid", className);
QJsonArray jsonListArray = jsonDocument.object()["list"].toArray();
for(QJsonValueRef refJsonObject: jsonListArray) {
QJsonObject appInfo = refJsonObject.toObject();
if(appInfo["Enabled"].toBool() == true or showDisabledJson == true) {
userAppsAvailable = true;
userapps * newUserApp = new userapps;
newUserApp->provideInfo(appInfo);
connect(this, SIGNAL(clearAppsLayout()), newUserApp, SLOT(deleteLater()));
connect(this, SIGNAL(showUserAppsEdit(bool)), newUserApp, SLOT(changePageEnabling(bool)));
connect(this, SIGNAL(updateJsonFileSignal(QJsonDocument)), newUserApp, SLOT(updateJsonFileSlotUA(QJsonDocument)));
connect(newUserApp, SIGNAL(updateJsonFileSignalUA(QJsonDocument)), this, SLOT(updateJsonFileSlot(QJsonDocument)));
newUserApp->jsonDocument = jsonDocument;
newUserApp->jsonFilePath = jsonFile.fileName();
ui->verticalLayout_4->addWidget(newUserApp);
}
}
if(userAppsAvailable == false) {
ui->noUserAppsAvailableLabel->show();
}
else {
ui->noUserAppsAvailableLabel->hide();
}
}
else {
if(QFile::exists("/mnt/onboard/onboard/.apps/apps.json")) {
QString function = __func__; log(function + ": Main user applications' JSON file is invalid", className);
QTimer::singleShot(500, this, SLOT(showFailedToParseMainUserAppsJsonFile()));
}
else {
QString function = __func__; log(function + ": Main user applications' JSON file doesn't exist; assuming that no user applications are currently installed", className);
}
ui->editUserAppsBtn->hide();
ui->label_6->hide();
ui->line_2->hide();
ui->line_3->hide();
ui->editUserAppsBtn->hide();
ui->label_6->deleteLater();
ui->horizontalLayout->deleteLater();
ui->line_2->deleteLater();
ui->line_3->deleteLater();
}
}
void apps::updateJsonFileSlot(QJsonDocument jsonDocumentFunc)
{
jsonDocument = jsonDocumentFunc;
emit updateJsonFileSignal(jsonDocument);
}
void apps::showFailedToParseMainUserAppsJsonFile() {
emit showToast("Failed to parse 'apps.json'");
}

16
apps.h
View file

@ -7,6 +7,8 @@
#include <koboxappsdialog.h>
#include <generaldialog.h>
#include <QJsonDocument>
namespace Ui {
class apps;
}
@ -30,9 +32,14 @@ private slots:
void on_koboxAppsOpenButton_clicked();
void on_vncLaunchBtn_clicked();
void on_reversiLaunchBtn_clicked();
void on_editUserAppsBtn_clicked();
void refreshScreenNative();
void on_g2048LaunchBtn_clicked();
void showToastNative(QString messageToDisplay);
bool parseJson();
void showUserApps(bool showDisabledJson);
void updateJsonFileSlot(QJsonDocument jsonDocument);
void showFailedToParseMainUserAppsJsonFile();
private:
Ui::apps *ui;
@ -41,9 +48,18 @@ private:
koboxAppsDialog *koboxAppsDialogWindow;
generalDialog *generalDialogWindow;
QFile jsonFile;
QJsonDocument jsonDocument;
bool jsonParseSuccess = false;
bool userAppsSecondPage = false;
bool userAppsAvailable = false;
signals:
void refreshScreen();
void showToast(QString messageToDisplay);
void updateJsonFileSignal(QJsonDocument jsonDocument);
void clearAppsLayout();
void showUserAppsEdit(bool show);
};
#endif // APPS_H

606
apps.ui
View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>321</height>
<width>430</width>
<height>544</height>
</rect>
</property>
<property name="windowTitle">
@ -28,129 +28,66 @@
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item row="5" column="0">
<widget class="QScrollArea" name="scrollArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>-30</y>
<width>407</width>
<height>541</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Apps</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="0">
<item>
<layout class="QGridLayout" name="gridLayout_3">
<property name="bottomMargin">
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item row="7" column="1">
<widget class="QPushButton" name="calculatorLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
<property name="bottomMargin">
<number>15</number>
</property>
<property name="text">
<string>Launch</string>
<property name="verticalSpacing">
<number>10</number>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="calendarLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Light Maps</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="scribbleLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Scribble</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QPushButton" name="savedWordsLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="reversiLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Reversi</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Saved words</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="font">
<font>
@ -163,89 +100,7 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="calendarLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Calendar</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="lightmapsLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="reversiLaunchBtn">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QPushButton" name="vncLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="koboxAppsOpenButton">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Calculator</string>
</property>
</widget>
</item>
<item row="9" column="0">
<item row="12" column="0">
<widget class="QLabel" name="vncViewerLabel">
<property name="font">
<font>
@ -258,7 +113,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="4" column="1">
<widget class="QPushButton" name="scribbleLaunchBtn">
<property name="font">
<font>
@ -273,8 +128,101 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="g2048LaunchBtn">
<item row="9" column="0">
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Saved words</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="scribbleLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Scribble</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Calculator</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="calendarLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="koboxAppsOpenButton">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Light Maps</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="reversiLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Reversi</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QPushButton" name="reversiLaunchBtn">
<property name="font">
<font>
<weight>75</weight>
@ -286,7 +234,35 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="9" column="1">
<widget class="QPushButton" name="savedWordsLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QPushButton" name="calculatorLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="g2048Label">
<property name="font">
<font>
@ -299,21 +275,239 @@
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QPushButton" name="g2048LaunchBtn">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QPushButton" name="vncLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QPushButton" name="lightmapsLaunchBtn">
<property name="font">
<font>
<family>Inter</family>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="calendarLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Calendar</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<item>
<widget class="Line" name="line_3">
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>User apps</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="editUserAppsBtn">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line_2">
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_4">
<property name="topMargin">
<number>12</number>
</property>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4"/>
</item>
<item>
<widget class="QLabel" name="noUserAppsAvailableLabel">
<property name="font">
<font>
<family>Chivo</family>
</font>
</property>
<property name="text">
<string>
No user apps currently available</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Built-in apps</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Line" name="line">
<property name="frameShadow">

View file

@ -74,5 +74,7 @@
<file>resources/clock.png</file>
<file>resources/eink-square-encfs.qss</file>
<file>resources/tzlist</file>
<file>resources/edit.png</file>
<file>resources/save.png</file>
</qresource>
</RCC>

View file

@ -13,6 +13,12 @@
#include <QDebug>
#include <QRandomGenerator>
#include <QDateTime>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <stdio.h>
#include <fcntl.h>
@ -122,6 +128,13 @@ namespace global {
namespace logger {
inline bool status;
}
namespace userApps {
inline bool appCompatibilityDialog;
inline QString appCompatibilityText;
inline bool appCompatibilityLastContinueStatus = true; // This is for RequiredFeatures to show only one dialog if 'Cancel' is clicked.
inline bool appInfoDialog;
inline bool launchApp;
}
inline QString systemInfoText;
inline bool forbidOpenSearchDialog;
inline bool isN705 = false;
@ -846,6 +859,90 @@ namespace {
return false;
}
}
void updateUserAppsMainJsonFile() {
QDirIterator appsDir("/mnt/onboard/onboard/.apps", QDirIterator::NoIteratorFlags);
QFile newJsonFile = QFile{"/mnt/onboard/onboard/.apps/apps.json"};
QJsonDocument newJsonDocument;
QJsonArray array;
while (appsDir.hasNext())
{
QDir dir(appsDir.next());
if(dir.exists() == true) {
if(dir.path().split("/").last().contains(".") == false) {
QFile jsonSmall = QFile{dir.path() + "/app.json"};
if(jsonSmall.exists() == true) {
jsonSmall.open(QIODevice::ReadOnly | QIODevice::Text);
QString jsonString = jsonSmall.readAll();
jsonSmall.close();
QJsonDocument jsonSmallDoc = QJsonDocument::fromJson(jsonString.toUtf8());
if(jsonSmallDoc["app"].isObject() == true) {
QJsonObject jsonSmallMainObj = jsonSmallDoc["app"].toObject();
array.append(jsonSmallMainObj);
}
else {
log("Error: User application '" + appsDir.path() + "''s JSON file descriptor is missing main object 'app'", "main");
}
}
else {
QString message = "User application '" + appsDir.path() + "' does not contain any 'app.json' file: ";
message.append(jsonSmall.fileName());
log(message, "main");
}
}
}
}
// https://forum.qt.io/topic/104791/how-i-can-create-json-format-in-qt/5
QJsonObject root;
root["list"] = array;
newJsonDocument.setObject(root);
newJsonFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate);
newJsonFile.write(newJsonDocument.toJson());
newJsonFile.flush();
newJsonFile.close();
}
void updateUserAppsSmallJsonFiles() {
QFile jsonFile = QFile{"/mnt/onboard/onboard/.apps/apps.json"};
jsonFile.open(QIODevice::ReadOnly | QIODevice::Text);
QString fileRead = jsonFile.readAll();
jsonFile.close();
QJsonDocument jsonDocument = QJsonDocument::fromJson(fileRead.toUtf8());
if(jsonDocument["list"].isArray() == true) {
QJsonArray jsonArray = jsonDocument["list"].toArray();
for(QJsonValueRef refJsonObject: jsonArray) {
QJsonObject jsonMainObject = refJsonObject.toObject();
QString appName = jsonMainObject["Name"].toString();
// This needs to be here and not at the beggining of this function because it is an iterator
QDirIterator appsDir("/mnt/onboard/onboard/.apps", QDirIterator::NoIteratorFlags);
while (appsDir.hasNext()) {
QDir dir(appsDir.next());
if(dir.exists() == true) {
if(dir.path().split("/").last().toLower().contains(appName.toLower()) == true) {
QJsonObject root;
root["app"] = refJsonObject.toObject();;
QJsonDocument newJsonDocument;
newJsonDocument.setObject(root);
QFile newSmallJson = QFile{dir.path() + "/" + "app.json"};
newSmallJson.open(QIODevice::ReadWrite);
QTextStream stream(&newSmallJson);
stream << newJsonDocument.toJson() << Qt::endl;
newSmallJson.flush();
newSmallJson.close();
}
}
}
}
}
}
}
#endif // FUNCTIONS_H

View file

@ -13,6 +13,7 @@
#include <QStringListModel>
#include <QListView>
#include <QDateTime>
#include <QTextEdit>
generalDialog::generalDialog(QWidget *parent) :
QDialog(parent),
@ -131,6 +132,7 @@ generalDialog::generalDialog(QWidget *parent) :
}
else if(global::usbms::usbmsDialog == true) {
usbmsDialog = true;
ui->stackedWidget->setCurrentIndex(0);
ui->okBtn->setText("Connect");
ui->cancelBtn->setText("Not now");
ui->bodyLabel->setText("<font face='u001'>Do you want to connect your device to a computer to manage books</font><font face='Inter'>?</font>");
@ -187,9 +189,29 @@ generalDialog::generalDialog(QWidget *parent) :
ui->bodyLabel->setText("<font face='u001'>New files have been found in 'encfs-dropbox'. Would you want to repack your encrypted storage</font><font face='Inter'>?</font>");
QTimer::singleShot(50, this, SLOT(adjust_size()));
}
else if(global::userApps::appCompatibilityDialog == true) {
appCompatibilityDialog = true;
global::userApps::appCompatibilityLastContinueStatus = true;
ui->okBtn->setText("Continue");
ui->cancelBtn->setText("Cancel");
ui->bodyLabel->setText(global::userApps::appCompatibilityText);
ui->headerLabel->setText("Compatibility warning");
QTimer::singleShot(50, this, SLOT(adjust_size()));
}
else if(global::userApps::appInfoDialog == true) {
appInfoDialog = true;
textwidgetWindow = new textwidget();
ui->headerLabel->setText("App info");
ui->stackedWidget->setCurrentIndex(1);
ui->mainStackedWidget->insertWidget(1, textwidgetWindow);
ui->mainStackedWidget->setCurrentIndex(1);
yIncrease = 1.8;
QTimer::singleShot(50, this, SLOT(increaseSize()));
}
else {
// We shouldn't be there ;)
;
log("Launched without settings", className);
}
// Centering dialog
@ -248,6 +270,12 @@ void generalDialog::on_cancelBtn_clicked()
else if(global::encfs::repackDialog == true) {
global::encfs::repackDialog = false;
}
else if(global::userApps::appCompatibilityDialog == true) {
global::userApps::launchApp = false;
global::userApps::appCompatibilityLastContinueStatus = false;
global::userApps::appCompatibilityText = "";
global::userApps::appCompatibilityDialog = false;
}
generalDialog::close();
}
}
@ -507,6 +535,13 @@ void generalDialog::on_okBtn_clicked()
string_writeconfig("/external_root/run/encfs_repack", "true");
quit_restart();
}
else if(global::userApps::appCompatibilityDialog == true) {
global::userApps::launchApp = true;
global::userApps::appCompatibilityText = "";
global::userApps::appCompatibilityLastContinueStatus = true; // Not really necceserry, only if something fails horibly
global::userApps::appCompatibilityDialog = false;
generalDialog::close();
}
}
void generalDialog::on_acceptBtn_clicked()
{
@ -533,6 +568,11 @@ void generalDialog::on_acceptBtn_clicked()
global::text::textBrowserDialog = false;
}
if(appInfoDialog == true) {
global::text::textBrowserContents = "";
global::userApps::appInfoDialog = false;
}
// We don't have any other option ;p
generalDialog::close();
}
@ -552,6 +592,7 @@ void generalDialog::restartSearchDialog() {
}
void generalDialog::setupKeyboardDialog() {
ui->stackedWidget->setCurrentIndex(0);
keyboardDialog = true;
ui->stackedWidget->setVisible(true);
if(global::keyboard::searchDialog == true) {
@ -732,3 +773,25 @@ void generalDialog::waitForGutenbergSearchDone() {
}
}
}
void generalDialog::increaseSize()
{
log("Resizing generalDialog", className);
ui->topStackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
ui->mainStackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
ui->stackedWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
QRect screenGeometry = QGuiApplication::screens()[0]->geometry();
int wx = screenGeometry.width();
int x = wx - 25;
int y = this->height() * yIncrease;
this->setFixedWidth(x);
this->setFixedHeight(y);
ui->bodyLabel->sizePolicy().setVerticalPolicy(QSizePolicy::Expanding);
ui->bodyLabel->sizePolicy().setHorizontalPolicy(QSizePolicy::Expanding);
adjust_size();
}

View file

@ -32,6 +32,8 @@ public:
bool lowBatteryDialog = false;
bool usbmsDialog = false;
bool textBrowserDialog = false;
bool appCompatibilityDialog = false;
bool appInfoDialog = false;
bool resetKoboxDialog = false;
bool keyboardDialog = false;
bool keypadDialog = false;
@ -49,6 +51,10 @@ public:
QString wifiPassphrase;
void setupKeyboardDialog();
void startVNC(QString server, QString password, QString port);
float yIncrease;
public slots:
void increaseSize();
private slots:
void on_cancelBtn_clicked();

View file

@ -48,6 +48,7 @@ SOURCES += \
textwidget.cpp \
toast.cpp \
usbms_splash.cpp \
userapps.cpp \
virtualkeyboard.cpp \
virtualkeypad.cpp \
wifidialog.cpp
@ -77,6 +78,7 @@ HEADERS += \
textwidget.h \
toast.h \
usbms_splash.h \
userapps.h \
virtualkeyboard.h \
virtualkeypad.h \
wifidialog.h
@ -105,6 +107,7 @@ FORMS += \
textwidget.ui \
toast.ui \
usbms_splash.ui \
userapps.ui \
virtualkeyboard.ui \
virtualkeypad.ui \
wifidialog.ui

View file

@ -38,6 +38,9 @@ int main(int argc, char *argv[])
global::deviceID = readFile("/opt/inkbox_device");
log("Running on device " + global::deviceID, "main", true);
// Tell the OS that we're currently running
string_writeconfig("/tmp/inkbox_running", "true");
setDefaultWorkDir();
if(checkconfig("/run/wifi_able") == true) {
log("Device has Wi-Fi capabilities", "main");
@ -47,6 +50,31 @@ int main(int argc, char *argv[])
log("Device does not have Wi-Fi capabilities", "main");
global::device::isWifiAble = false;
}
if(QFile::exists("/tmp/rescan_userapps")) {
QFile::remove("/tmp/rescan_userapps");
log("Re-scanning user applications from explicit request", "main");
string_writeconfig("/opt/ibxd", "gui_apps_stop\n");
QThread::msleep(1000);
string_writeconfig("/opt/ibxd", "gui_apps_start\n");
while(true) {
if(QFile::exists("/tmp/gui_apps_started")) {
if(checkconfig("/tmp/gui_apps_started") == true) {
log("GUI apps service started successfully", "main");
QFile::remove("/tmp/gui_apps_started");
updateUserAppsMainJsonFile();
break;
}
else {
log("GUI apps service failed to start", "main");
QFile::remove("/tmp/gui_apps_started");
break;
}
}
}
updateUserAppsMainJsonFile();
}
if(checkconfig(".config/18-encrypted_storage/status") == true and checkconfig("/external_root/run/encfs_mounted") == false) {
// Open Encryption Manager to unlock encrypted storage
QApplication a(argc, argv);
@ -66,9 +94,6 @@ int main(int argc, char *argv[])
return a.exec();
}
else {
// Tell scripts that we're currently running
string_writeconfig("/tmp/inkbox_running", "true");
// Variables
global::reader::startBatteryWatchdog = false;
global::reader::startUsbmsPrompt = false;

View file

@ -51,6 +51,9 @@ void quit::on_pushButton_clicked()
global::battery::showCriticalBatteryAlert = false;
global::battery::showLowBatteryDialog = false;
// GUI apps
updateUserAppsMainJsonFile();
poweroff(true);
qApp->quit();
}
@ -60,6 +63,9 @@ void quit::on_pushButton_2_clicked()
global::battery::showCriticalBatteryAlert = false;
global::battery::showLowBatteryDialog = false;
// GUI apps
updateUserAppsMainJsonFile();
reboot(true);
qApp->quit();
}

BIN
resources/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
resources/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -822,6 +822,10 @@ void settings::on_showSystemInfoBtn_clicked()
// Show a system info dialog
log("Showing system info dialog", className);
generalDialogWindow = new generalDialog();
if(global::deviceID == "n306\n") {
generalDialogWindow->yIncrease = 2.6;
}
generalDialogWindow->increaseSize();
generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose);
}

View file

@ -85,6 +85,9 @@ void usbms_splash::usbms_launch()
string_writeconfig("/opt/ibxd", "usbnet_stop\n");
QThread::msleep(1000);
string_writeconfig("/opt/ibxd", "gui_apps_stop\n");
QThread::msleep(1000);
if(global::deviceID == "n306\n" or global::deviceID == "n873\n") {
QProcess::execute("insmod", QStringList() << "/external_root/lib/modules/fs/configfs/configfs.ko");
QProcess::execute("insmod", QStringList() << "/external_root/lib/modules/drivers/usb/gadget/libcomposite.ko");
@ -197,6 +200,22 @@ void usbms_splash::restartServices() {
string_writeconfig("/opt/ibxd", "update_inkbox_restart\n");
QThread::msleep(2500);
string_writeconfig("/tmp/in_usbms", "false");
// GUI apps: update main JSON file
string_writeconfig("/opt/ibxd", "gui_apps_start\n");
while(true) {
if(QFile::exists("/tmp/gui_apps_started")) {
if(checkconfig("/tmp/gui_apps_started") == true) {
QFile::remove("/tmp/gui_apps_started");
updateUserAppsMainJsonFile();
break;
}
else {
log("GUI apps service failed to start", className);
QFile::remove("/tmp/gui_apps_started");
break;
}
}
}
quit_restart();
}

274
userapps.cpp Normal file
View file

@ -0,0 +1,274 @@
#include "userapps.h"
#include "ui_userapps.h"
#include "mainwindow.h"
#include "generaldialog.h"
userapps::userapps(QWidget *parent) :
QWidget(parent),
ui(new Ui::userapps)
{
ui->setupUi(this);
ui->launchBtn->setProperty("type", "borderless");
ui->launchBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px");
ui->launchBtn->setProperty("type", "borderless");
ui->iconBtn->setProperty("type", "borderless");
ui->iconBtn->setStyleSheet("QPushButton[type='borderless']:pressed { background: white; color: white; border: none; }");
ui->infoBtn->setProperty("type", "borderless");
ui->statusBtn->setProperty("type", "borderless");
ui->statusBtn->setStyleSheet("background: lightGrey; font-size: 9pt; padding: 8px");
}
userapps::~userapps()
{
delete ui;
}
void userapps::provideInfo(QJsonObject jsonInfo)
{
QString name = jsonInfo["Name"].toString();
appName = name; // It is for searching for json entry while disabling / enabling
jsonObject = jsonInfo;
// Limit name size to avoid breaking the GUI
if(name.size() > 20) {
// If someone wants to break the GUI, they will do it ¯\^-^/¯
name.remove(16, 100);
}
ui->appNameLabel->setText(name);
appDir.setPath("/mnt/onboard/onboard/.apps/" + name + "/" + name);
QFile iconPath = QFile{appDir.path() + "/" + jsonInfo["IconPath"].toString()};
if(iconPath.exists() == true) {
QIcon appIcon = QIcon(iconPath.fileName());
ui->iconBtn->setIconSize(QSize(40, 40));
ui->iconBtn->setIcon(appIcon);
}
else {
QString function = __func__;
QString message = ": Warning: Icon not found: ";
message.append(iconPath.fileName());
log(function + message, className);
ui->iconBtn->deleteLater();
ui->gridLayout->deleteLater();
}
execPath.setFileName(jsonInfo["ExecPath"].toString());
userAppEnabled = jsonInfo["Enabled"].toBool();
if(userAppEnabled == true) {
ui->statusBtn->setText("Disable");
}
else {
ui->statusBtn->setText("Enable");
}
}
// This function is needed when we dont want to repaint all widgets, but only change the the page (when no changes to the main JSON file were applied)
void userapps::changePageEnabling(bool SecondPage)
{
if(SecondPage == true){
ui->stackedWidget->setCurrentIndex(1);
}
else {
ui->stackedWidget->setCurrentIndex(0);
}
}
void userapps::on_infoBtn_clicked()
{
log("Launching user application information dialog", className);
// https://stackoverflow.com/questions/28181627/how-to-convert-a-qjsonobject-to-qstring
QString jsonStringParsed = parseJsonShow(jsonObject);
global::text::textBrowserContents = jsonStringParsed;
global::userApps::appInfoDialog = true;
generalDialogWindow = new generalDialog();
generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose);
generalDialogWindow->show();
}
void userapps::on_statusBtn_clicked()
{
ui->statusBtn->setEnabled(false);
// Here the text on this button is used as a boolean; no need to create a new one
// Disable and Enable
if(userAppEnabled == false) {
userAppEnabled = true;
ui->statusBtn->setText("Disable");
}
else {
userAppEnabled = false;
ui->statusBtn->setText("Enable");
}
QJsonObject jsonRootObject = jsonDocument.object();
QJsonArray jsonArrayList = jsonRootObject["list"].toArray();
int arraySize = jsonArrayList.size();
for(int i = 0; i < arraySize; i++) {
QJsonObject jsonObject = jsonArrayList.at(i).toObject();
QString entryName = jsonObject["Name"].toString();
if(entryName == appName) {
jsonObject.insert("Enabled", QJsonValue(userAppEnabled));
QJsonArray sonArrayListNew = jsonDocument.object()["list"].toArray();
sonArrayListNew.replace(i, jsonObject);
jsonRootObject["list"] = sonArrayListNew;
jsonDocument.setObject(jsonRootObject);
emit updateJsonFileSignalUA(jsonDocument);
}
}
ui->statusBtn->setEnabled(true);
}
void userapps::updateJsonFileSlotUA(QJsonDocument jsonDocumentProvided)
{
jsonDocument = jsonDocumentProvided;
}
void userapps::on_launchBtn_clicked()
{
// Some command to execute script or binary at "ExecPath"
QJsonArray supportedDevices = jsonObject["SupportedDevices"].toArray();
// This will work even if we are looking for 'n306' and there is a device named 'n306b' because QJsonArray::contains() works that way
if(supportedDevices.contains("all") == false and supportedDevices.contains(global::deviceID.trimmed()) == false) {
log("Warning: User application '" + appName + "' does not support this device", className);
global::userApps::appCompatibilityDialog = true;
global::userApps::appCompatibilityText = "<font face='u001'>Your device is not compatible with this app.<br>Continue anyway</font><font face='Inter'>?</font>";
generalDialogWindow = new generalDialog();
generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose);
generalDialogWindow->exec();
}
else {
global::userApps::launchApp = true;
}
if(manageRequiredFeatures() == true) {
if(global::userApps::launchApp == true) {
global::userApps::launchApp = false;
QString message = "Launching user application at: ";
message.append("[" + appDir.path() + "]" + execPath.fileName());
log(message, className);
// Tell the OS that we're not running anymore
string_writeconfig("/tmp/inkbox_running", "false");
QProcess process;
QStringList args;
args << appDir.path() << execPath.fileName();
process.startDetached("launch_user_application.sh", args);
qApp->quit();
}
}
}
bool userapps::manageRequiredFeatures()
{
// This should be already set to 'true', but just in case
global::userApps::appCompatibilityLastContinueStatus = true;
QJsonArray jsonArray = jsonObject["RequiredFeatures"].toArray();
for(QJsonValueRef refJsonObject: jsonArray) {
bool launchDialog = false;
int featureId = refJsonObject.toInt();
// Wi-Fi connection required
if(featureId == 0) {
// Double 'if' conditions to avoid launching unnecesery testPing() in emu
if(global::deviceID != "emu\n") {
if(testPing(true) != 0) {
global::userApps::appCompatibilityText = "<font face='u001'>This app needs a Wi-Fi connection, continue anyway</font><font face='Inter'>?</font>";
launchDialog = true;
}
}
}
// Rooted kernel required
if(featureId == 1) {
if(checkconfig("/external_root/opt/root/rooted") == true) {
global::userApps::appCompatibilityText = "<font face='u001'>This app needs a rooted kernel, continue anyway</font><font face='Inter'>?</font>";
launchDialog = true;
}
}
// Pseudoterminal support (ID: 2) is managed by the 'gui_apps' service (https://github.com/Kobo-InkBox/rootfs/blob/master/etc/init.d/gui_apps)
if(launchDialog == true) {
global::userApps::appCompatibilityDialog = true;
generalDialogWindow = new generalDialog();
generalDialogWindow->setAttribute(Qt::WA_DeleteOnClose);
generalDialogWindow->exec();
}
if(global::userApps::appCompatibilityLastContinueStatus == false) {
return false;
}
}
return true;
}
QString userapps::parseJsonShow(QJsonObject json)
{
QString mainString;
foreach(const QString& key, json.keys()) {
QJsonValue value = json.value(key);
QString appendString;
appendString.append("<b>");
appendString.append(key);
appendString.append("</b>: ");
if(value.isString()) {
appendString.append(value.toString());
}
else if(value.isBool()) {
appendString.append(QVariant(value.toBool()).toString());
}
else if(value.isArray()) {
QJsonArray array = value.toArray();
if(key == "RequiredFeatures") {
bool foundRequiredFeature = false;
appendString.append("None");
for(QJsonValueRef ref: array) {
foundRequiredFeature = true;
appendString.remove(appendString.size() - 4, 4);
int id = ref.toInt();
if(id == 0) {
appendString.append("Wi-Fi connection");
}
else if(id == 1) {
appendString.append("Rooted kernel");
}
else if(id == 2) {
appendString.append("Pseudoterminal support");
}
appendString.append(", ");
}
if(foundRequiredFeature == true) {
appendString.remove(appendString.size() - 2, 2);
}
}
else if(key == "SupportedDevices") {
for(QJsonValueRef ref: array) {
QString name = ref.toString();
appendString.append(name);
appendString.append(", ");
}
appendString.remove(appendString.size() - 2, 2);
}
}
appendString.append("<br>");
mainString.append(appendString);
}
return mainString;
}

58
userapps.h Normal file
View file

@ -0,0 +1,58 @@
#ifndef USERAPPS_H
#define USERAPPS_H
#include "generaldialog.h"
#include <QWidget>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QDir>
#include <QFile>
namespace Ui {
class userapps;
}
class userapps : public QWidget
{
Q_OBJECT
public:
QString className = this->metaObject()->className();
explicit userapps(QWidget * parent = nullptr);
~userapps();
QJsonDocument jsonDocument; // TODO: Optimize for RAM usage
QJsonObject jsonObject; // Needed for 'App info' button
QString jsonFilePath;
signals:
// This is needed, because user application have their own JSON file. If the user changes it too fast it won't read it and overwrite previous changes
void updateJsonFileSignalUA(QJsonDocument jsonDocument);
public slots:
void provideInfo(QJsonObject jsonObject);
void changePageEnabling(bool goThere);
void updateJsonFileSlotUA(QJsonDocument jsonDocument);
bool manageRequiredFeatures();
QString parseJsonShow(QJsonObject json);
private slots:
void on_infoBtn_clicked();
void on_statusBtn_clicked();
void on_launchBtn_clicked();
private:
Ui::userapps * ui;
generalDialog * generalDialogWindow;
QDir appDir;
QFile execPath;
bool userAppEnabled;
QString appName;
};
#endif // USERAPPS_H

215
userapps.ui Normal file
View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>userapps</class>
<widget class="QWidget" name="userapps">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>461</width>
<height>79</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="1" column="1">
<widget class="QStackedWidget" name="stackedWidget">
<property name="lineWidth">
<number>0</number>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="pageLaunch">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="launchBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Launch</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2Edit">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="infoBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="eink.qrc">
<normaloff>:/resources/info.png</normaloff>:/resources/info.png</iconset>
</property>
<property name="iconSize">
<size>
<width>34</width>
<height>34</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="statusBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Disable</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="iconBtn">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="appNameLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>AppName</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>13</width>
<height>13</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="eink.qrc"/>
</resources>
<connections/>
</ui>