[OpenDocString] kdeconnect-kde (cpp)
runcommand_config.cpp
RunCommandConfig::RunCommandConfig(QWidget *parent, const QVariantList &args)
    : KdeConnectPluginKcm(parent, args, QStringLiteral("kdeconnect_runcommand"))
{
    // The qdbus executable name is different on some systems
    QString qdbusExe = QStringLiteral("qdbus-qt5");
    if (QStandardPaths::findExecutable(qdbusExe).isEmpty()) {
        qdbusExe = QStringLiteral("qdbus");
    }

    QMenu *defaultMenu = new QMenu(this);

#ifdef Q_OS_WIN
    addSuggestedCommand(defaultMenu, i18n("Schedule a shutdown"), QStringLiteral("shutdown /s /t 60"));
    addSuggestedCommand(defaultMenu, i18n("Shutdown now"), QStringLiteral("shutdown /s /t 0"));
    addSuggestedCommand(defaultMenu, i18n("Cancel last shutdown"), QStringLiteral("shutdown /a"));
    addSuggestedCommand(defaultMenu, i18n("Schedule a reboot"), QStringLiteral("shutdown /r /t 60"));
    addSuggestedCommand(defaultMenu, i18n("Suspend"), QStringLiteral("rundll32.exe powrprof.dll,SetSuspendState 0,1,0"));
    addSuggestedCommand(defaultMenu, i18n("Lock Screen"), QStringLiteral("rundll32.exe user32.dll,LockWorkStation"));
    addSuggestedCommand(
        defaultMenu,
        i18n("Say Hello"),
        QStringLiteral("PowerShell -Command \"Add-Type –AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('hello');\""));
#else
    addSuggestedCommand(defaultMenu, i18n("Shutdown"), QStringLiteral("systemctl poweroff"));
    addSuggestedCommand(defaultMenu, i18n("Reboot"), QStringLiteral("systemctl reboot"));
    addSuggestedCommand(defaultMenu, i18n("Suspend"), QStringLiteral("systemctl suspend"));
    addSuggestedCommand(
        defaultMenu,
        i18n("Maximum Brightness"),
        QStringLiteral("%0 org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl "
                       "org.kde.Solid.PowerManagement.Actions.BrightnessControl.setBrightness `%0 org.kde.Solid.PowerManagement "
                       "/org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.brightnessMax`")
            .arg(qdbusExe));
    addSuggestedCommand(defaultMenu, i18n("Lock Screen"), QStringLiteral("loginctl lock-session"));
    addSuggestedCommand(defaultMenu, i18n("Unlock Screen"), QStringLiteral("loginctl unlock-session"));
    addSuggestedCommand(defaultMenu, i18n("Close All Vaults"), QStringLiteral("%0 org.kde.kded5 /modules/plasmavault closeAllVaults").arg(qdbusExe));
    addSuggestedCommand(defaultMenu,
                        i18n("Forcefully Close All Vaults"),
                        QStringLiteral("%0 org.kde.kded5 /modules/plasmavault forceCloseAllVaults").arg(qdbusExe));
#endif

    QTableView *table = new QTableView(this);
    table->horizontalHeader()->setStretchLastSection(true);
    table->verticalHeader()->setVisible(false);
    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->addWidget(table);
    QPushButton *button = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Sample commands"), this);
    button->setMenu(defaultMenu);
    layout->addWidget(button);

    QHBoxLayout *importExportLayout = new QHBoxLayout(this);
    QPushButton *exportButton = new QPushButton(i18n("Export"), this);
    importExportLayout->addWidget(exportButton);
    connect(exportButton, &QPushButton::clicked, this, &RunCommandConfig::exportCommands);
    QPushButton *importButton = new QPushButton(i18n("Import"), this);
    importExportLayout->addWidget(importButton);
    connect(importButton, &QPushButton::clicked, this, &RunCommandConfig::importCommands);
    layout->addLayout(importExportLayout);

    setLayout(layout);

    m_entriesModel = new QStandardItemModel(this);
    table->setModel(m_entriesModel);

    m_entriesModel->setHorizontalHeaderLabels(QStringList() << i18n("Name") << i18n("Command"));
}
This constructor builds a KdeConnectPlugin instance and its dll. It takes the args of the program and creates a menu, sets the default menu and buttons for the menu to be displayed. It also connects the export and import buttons to the table view.
nCommandConfig::~RunCommandConfig()
{
}
This implements the configuration for the command.
id RunCommandConfig::exportCommands()
{
    QString filePath = QFileDialog::getSaveFileName(this, i18n("Export Commands"), QDir::homePath(), QStringLiteral("JSON (*.json)"));
    if (filePath.isEmpty())
        return;

    QFile file(filePath);
    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        qWarning() << "Could not write to file:" << filePath;
        return;
    }

    QJsonArray jsonArray;
    for (int i = 0; i < m_entriesModel->rowCount(); i++) {
        QJsonObject jsonObj;
        jsonObj[QStringLiteral("name")] = m_entriesModel->index(i, 0).data().toString();
        jsonObj[QStringLiteral("command")] = m_entriesModel->index(i, 1).data().toString();
        jsonArray.append(jsonObj);
    }

    QJsonDocument jsonDocument(jsonArray);
    file.write(jsonDocument.toJson());
    file.close();
}
Exports the commands from the entries model to a json file.
id RunCommandConfig::importCommands()
{
    QString filePath = QFileDialog::getOpenFileName(this, i18n("Import Commands"), QDir::homePath(), QStringLiteral("JSON (*.json)"));
    if (filePath.isEmpty())
        return;

    QFile file(filePath);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        qWarning() << "Could not read file:" << filePath;
        return;
    }

    QByteArray jsonData = file.readAll();
    file.close();

    QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
    if (jsonDoc.isNull() || !jsonDoc.isArray()) {
        qWarning() << "Invalid JSON format.";
        return;
    }

    // Clear the current command list
    m_entriesModel->removeRows(0, m_entriesModel->rowCount());

    // Populate the model with the imported commands
    QJsonArray jsonArray = jsonDoc.array();
    for (const QJsonValue &jsonValue : jsonArray) {
        QJsonObject jsonObj = jsonValue.toObject();
        QString name = jsonObj.value(QStringLiteral("name")).toString();
        QString command = jsonObj.value(QStringLiteral("command")).toString();
        insertRow(m_entriesModel->rowCount(), name, command);
    }

    Q_EMIT changed(true);
}
This function imports commands from a json file. It opens the file with the "json" extension, and reads the file as a JSON document, and creates a QTableModel instance with the commands from it.
id RunCommandConfig::addSuggestedCommand(QMenu *menu, const QString &name, const QString &command)
{
    auto action = new QAction(name);
    connect(action, &QAction::triggered, action, [this, name, command]() {
        insertRow(0, name, command);
        Q_EMIT changed(true);
    });
    menu->addAction(action);
}
Adds a suggested command to the given menu. It creates an action object, sets the action s row number and command to execute. It also connects the triggered signal to the action.
id RunCommandConfig::defaults()
{
    KCModule::defaults();
    m_entriesModel->removeRows(0, m_entriesModel->rowCount());

    Q_EMIT changed(true);
}
This removes the defaults from the entries model and emits the signal changed.
id RunCommandConfig::load()
{
    KCModule::load();

    QJsonDocument jsonDocument = QJsonDocument::fromJson(config()->getByteArray(QStringLiteral("commands"), "{}"));
    QJsonObject jsonConfig = jsonDocument.object();
    const QStringList keys = jsonConfig.keys();
    for (const QString &key : keys) {
        const QJsonObject entry = jsonConfig[key].toObject();
        const QString name = entry[QStringLiteral("name")].toString();
        const QString command = entry[QStringLiteral("command")].toString();

        QStandardItem *newName = new QStandardItem(name);
        newName->setEditable(true);
        newName->setData(key);
        QStandardItem *newCommand = new QStandardItem(command);
        newName->setEditable(true);

        m_entriesModel->appendRow(QList() << newName << newCommand);
    }

    m_entriesModel->sort(0);

    insertEmptyRow();
    connect(m_entriesModel, &QAbstractItemModel::dataChanged, this, &RunCommandConfig::onDataChanged);

    Q_EMIT changed(false);
}
This loads the commands configuration from the json file. It creates two lists of items: the name and the command of the command, and connects the dataChanged signal to the signal.
id RunCommandConfig::save()
{
    QJsonObject jsonConfig;
    for (int i = 0; i < m_entriesModel->rowCount(); i++) {
        QString key = m_entriesModel->item(i, 0)->data().toString();
        const QString name = m_entriesModel->item(i, 0)->text();
        const QString command = m_entriesModel->item(i, 1)->text();

        if (name.isEmpty() || command.isEmpty()) {
            continue;
        }

        if (key.isEmpty()) {
            key = QUuid::createUuid().toString();
            DBusHelper::filterNonExportableCharacters(key);
        }
        QJsonObject entry;
        entry[QStringLiteral("name")] = name;
        entry[QStringLiteral("command")] = command;
        jsonConfig[key] = entry;
    }
    QJsonDocument document;
    document.setObject(jsonConfig);
    config()->set(QStringLiteral("commands"), document.toJson(QJsonDocument::Compact));

    KCModule::save();

    Q_EMIT changed(false);
}
This saves the config of the commands to the DB.
id RunCommandConfig::insertEmptyRow()
{
    insertRow(m_entriesModel->rowCount(), {}, {});
}
This inserts an empty row at the end of the list of entries.
id RunCommandConfig::insertRow(int i, const QString &name, const QString &command)
{
    QStandardItem *newName = new QStandardItem(name);
    newName->setEditable(true);
    QStandardItem *newCommand = new QStandardItem(command);
    newName->setEditable(true);

    m_entriesModel->insertRow(i, QList() << newName << newCommand);
}
Inserts a new command at the given index, with a new name.
id RunCommandConfig::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
{
    Q_EMIT changed(true);
    Q_UNUSED(topLeft);
    if (bottomRight.row() == m_entriesModel->rowCount() - 1) {
        // TODO check both entries are still empty
        insertEmptyRow();
    }
}
If the top left and bottom right indices have the same row number in the entries model, it will emit a signal changed. If the bottom right index is the last entry in the entries model, it will insert an empty row if the two entries are empty.