diff --git a/src/tuxclocker-qt/MainWindow.cpp b/src/tuxclocker-qt/MainWindow.cpp index 086b779..bb539bd 100644 --- a/src/tuxclocker-qt/MainWindow.cpp +++ b/src/tuxclocker-qt/MainWindow.cpp @@ -1,4 +1,9 @@ +#include "MainWindow.hpp" + #include +#include +#include +#include #include #include #include @@ -10,10 +15,6 @@ #include #include -#include -#include -#include "MainWindow.hpp" - namespace TCDBus = TuxClocker::DBus; using namespace TuxClocker; @@ -48,9 +49,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto root = flatTree.toTree(flatTree); - auto view = new QTreeView; + /*auto view = new QTreeView; view->setItemDelegate(new DeviceModelDelegate); - auto model = new DeviceModel(root); view->setModel(model); - setCentralWidget(view); + setCentralWidget(view);*/ + + auto model = new DeviceModel(root); + auto browser = new DeviceBrowser(*model); + setCentralWidget(browser); } diff --git a/src/tuxclocker-qt/data/AssignableItem.cpp b/src/tuxclocker-qt/data/AssignableItem.cpp new file mode 100644 index 0000000..8b6a067 --- /dev/null +++ b/src/tuxclocker-qt/data/AssignableItem.cpp @@ -0,0 +1,16 @@ +#include "AssignableItem.hpp" + +Q_DECLARE_METATYPE(AssignableItemData) + +void AssignableItem::setData(const QVariant &v, int role) { + if (role == DeviceModel::AssignableRole) { + auto data = v.value().value(); + // Value is empty + if (data.isValid()) { + QVariant vr; + vr.setValue(data); + emit assignableDataChanged(vr); + } + } + QStandardItem::setData(v, role); +} diff --git a/src/tuxclocker-qt/data/AssignableItem.hpp b/src/tuxclocker-qt/data/AssignableItem.hpp index bf5ec35..36cea71 100644 --- a/src/tuxclocker-qt/data/AssignableItem.hpp +++ b/src/tuxclocker-qt/data/AssignableItem.hpp @@ -1,6 +1,6 @@ #pragma once -//#include "DeviceModel.hpp" +#include "DeviceModel.hpp" #include #include @@ -12,11 +12,14 @@ class AssignableItem : public QObject, public QStandardItem { public: AssignableItem(QObject *parent = nullptr) : QObject(parent), QStandardItem() { } - void setData(const QVariant &v, int role = Qt::UserRole + 1) { - //if (role == DeviceModel::AssignableRole) ; - } + bool committal() {return m_committed;} + // Whether or not the set value shall be applied. Doesn't reset or change it. + void setCommittal(bool on) {m_committed = on;} + void setData(const QVariant &v, int role = Qt::UserRole + 1); signals: void assignableDataChanged(QVariant value); private: Q_OBJECT + + bool m_committed = false; }; diff --git a/src/tuxclocker-qt/data/AssignableItemData.hpp b/src/tuxclocker-qt/data/AssignableItemData.hpp index 80b0a88..0c13ed1 100644 --- a/src/tuxclocker-qt/data/AssignableItemData.hpp +++ b/src/tuxclocker-qt/data/AssignableItemData.hpp @@ -12,6 +12,9 @@ public: m_enabled = false; } TC::Device::AssignableInfo assignableInfo() {return m_info;} + bool committal() {return m_enabled;} + // Whether or not the set value shall be applied. Doesn't reset or change it. + void setCommittal(bool on) {m_enabled = on;} void setValue(QVariant v) {m_targetValue = v;} QVariant value() {return m_targetValue;} private: @@ -19,4 +22,3 @@ private: TC::Device::AssignableInfo m_info; QVariant m_targetValue; }; -Q_DECLARE_METATYPE(TC::Device::AssignableInfo) diff --git a/src/tuxclocker-qt/data/AssignableProxy.cpp b/src/tuxclocker-qt/data/AssignableProxy.cpp new file mode 100644 index 0000000..901a21b --- /dev/null +++ b/src/tuxclocker-qt/data/AssignableProxy.cpp @@ -0,0 +1,43 @@ +#include "AssignableProxy.hpp" + +#include +#include +#include + +namespace TCD = TuxClocker::DBus; + +using namespace TuxClocker::Device; + +Q_DECLARE_METATYPE(TCD::Result) + +AssignableProxy::AssignableProxy(QString path, QDBusConnection conn, + QObject *parent) : QObject(parent) { + qDBusRegisterMetaType>(); + m_iface = new QDBusInterface("org.tuxclocker", + path, "org.tuxclocker.Assignable", conn, this); +} + +void AssignableProxy::apply() { + // A value hasn't been set yet + if (m_value.isNull()) + return; + + // Use QDBusVariant since otherwise tries to call with the wrong signature + QDBusVariant dv(m_value); + QVariant v; + v.setValue(dv); + QDBusReply> reply = m_iface->call("assign", v); + if (reply.isValid()) { + qDebug() << reply.value().error << reply.value().value; + + TCD::Result ar{reply.value().error, + static_cast(reply.value().value)}; + qDebug("Success!"); + emit applied(ar.toOptional()); + } + // TODO: Maybe indicate that calling dbus errored out + else + emit applied(AssignmentError::UnknownError); + // Indicate that there's no pending value to applied by making value invalid + m_value = QVariant(); +} diff --git a/src/tuxclocker-qt/data/AssignableProxy.hpp b/src/tuxclocker-qt/data/AssignableProxy.hpp new file mode 100644 index 0000000..ce82f46 --- /dev/null +++ b/src/tuxclocker-qt/data/AssignableProxy.hpp @@ -0,0 +1,28 @@ +#pragma once + +// Acts as a buffer for unapplied assignables and applies them asynchronously + +#include +#include +#include +#include +#include +#include +#include + +namespace TC = TuxClocker; + +class AssignableProxy : public QObject { +public: + AssignableProxy(QString path, QDBusConnection conn, + QObject *parent = nullptr); + void apply(); + void setValue(QVariant v) {m_value = v;} +signals: + void applied(std::optional); +private: + Q_OBJECT + + QVariant m_value; + QDBusInterface *m_iface; +}; diff --git a/src/tuxclocker-qt/data/DeviceModel.cpp b/src/tuxclocker-qt/data/DeviceModel.cpp index 8a97497..b60839a 100644 --- a/src/tuxclocker-qt/data/DeviceModel.cpp +++ b/src/tuxclocker-qt/data/DeviceModel.cpp @@ -1,5 +1,12 @@ #include "DeviceModel.hpp" +#include "AssignableItem.hpp" +#include "AssignableProxy.hpp" +#include +#include + +using namespace mpark::patterns; + Q_DECLARE_METATYPE(AssignableItemData) Q_DECLARE_METATYPE(TCDBus::Enumeration) Q_DECLARE_METATYPE(TCDBus::Range) @@ -16,10 +23,12 @@ DeviceModel::DeviceModel(TC::TreeNode root, QObject *parent) setColumnCount(2); - std::function node, QStandardItem*)> traverse; - traverse = [&traverse](auto node, auto item) { + std::function node, + QStandardItem*)> traverse; + traverse = [&traverse, this](auto node, auto item) { auto conn = QDBusConnection::systemBus(); - QDBusInterface nodeIface("org.tuxclocker", node.value().path, "org.tuxclocker.Node", conn); + QDBusInterface nodeIface("org.tuxclocker", + node.value().path,"org.tuxclocker.Node", conn); auto nodeName = nodeIface.property("name").toString(); QList rowItems; @@ -40,14 +49,44 @@ DeviceModel::DeviceModel(TC::TreeNode root, QObject *parent) auto d_arg = qvariant_cast(a_info); switch (d_arg.currentType()) { case QDBusArgument::StructureType: { - auto ifaceItem = new QStandardItem; + auto ifaceItem = new AssignableItem; + ifaceItem->setEditable(true); + //ifaceItem->setCheckable(true); + auto proxy = new AssignableProxy(node.value().path, conn, this); + connect(ifaceItem, &AssignableItem::assignableDataChanged, + [=](QVariant v) { + proxy->setValue(v); + ifaceItem->setData(unappliedColor(), Qt::BackgroundRole); + }); + + connect(proxy, &AssignableProxy::applied, [=](auto err) { + // Fade out result color + auto startColor = (err.has_value()) ? errorColor() + : successColor(); + auto anim = new QVariantAnimation; + anim->setDuration(fadeOutTime()); + anim->setStartValue(startColor); + anim->setEndValue(QPalette().color(QPalette::Base)); + + connect(anim, &QVariantAnimation::valueChanged, [=](QVariant v) { + QVariant iv; + iv.setValue(v.value()); + ifaceItem->setData(iv, Qt::BackgroundRole); + }); + anim->start(QAbstractAnimation::DeleteWhenStopped); + }); + + connect(this, &DeviceModel::changesApplied, [=] { + proxy->apply(); + }); + TCDBus::Range r; d_arg >> r; QVariant v; AssignableItemData data(r.toAssignableInfo()); v.setValue(data); ifaceItem->setData(v, Role::AssignableRole); - ifaceItem->setText(r.min.variant().toString()); + //ifaceItem->setText(r.min.variant().toString()); rowItems.append(ifaceItem); break; } diff --git a/src/tuxclocker-qt/data/DeviceModel.hpp b/src/tuxclocker-qt/data/DeviceModel.hpp index 36baaf8..0493b11 100644 --- a/src/tuxclocker-qt/data/DeviceModel.hpp +++ b/src/tuxclocker-qt/data/DeviceModel.hpp @@ -1,6 +1,6 @@ #pragma once -//#include "AssignableItem.hpp" +#include "AssignableItem.hpp" #include "AssignableItemData.hpp" #include @@ -10,6 +10,7 @@ #include #include #include +#include namespace p = mpark::patterns; namespace TC = TuxClocker; @@ -32,4 +33,17 @@ public: enum class FilterFlag { }; + // For decoupling AssignableItems created in the model + void applyChanges() {emit changesApplied();} +signals: + void changesApplied(); +private: + Q_OBJECT + + constexpr int fadeOutTime() {return 5000;} + constexpr int transparency() {return 120;} + // Colors for items + QColor errorColor() {return QColor(255, 0, 0, transparency());} // red + QColor unappliedColor() {return QColor(255, 255, 0, transparency());} // yellow + QColor successColor() {return QColor(0, 255, 0, transparency());} // green }; diff --git a/src/tuxclocker-qt/data/DeviceModelDelegate.cpp b/src/tuxclocker-qt/data/DeviceModelDelegate.cpp index c6bd897..b3c6eed 100644 --- a/src/tuxclocker-qt/data/DeviceModelDelegate.cpp +++ b/src/tuxclocker-qt/data/DeviceModelDelegate.cpp @@ -56,3 +56,15 @@ void DeviceModelDelegate::setModelData(QWidget *editor, model->setData(index, v, DeviceModel::AssignableRole); } } + +void DeviceModelDelegate::updateEditorGeometry(QWidget *editor, + const QStyleOptionViewItem &option, const QModelIndex&) const { + // Why do I need to override this to perform such a basic task? + editor->setGeometry(option.rect); +} + +/*void DeviceModelDelegate::drawCheck(QPainter *painter, + const QStyleOptionViewItem &option, const QRect &rect, + Qt::CheckState state) const { + QStyledItemDelegate::drawCheck(painter, option, rect, state) +}*/ diff --git a/src/tuxclocker-qt/data/DeviceModelDelegate.hpp b/src/tuxclocker-qt/data/DeviceModelDelegate.hpp index 2fc6def..ac91012 100644 --- a/src/tuxclocker-qt/data/DeviceModelDelegate.hpp +++ b/src/tuxclocker-qt/data/DeviceModelDelegate.hpp @@ -2,13 +2,14 @@ #include +// TODO: align checkbox to the right class DeviceModelDelegate : public QStyledItemDelegate { public: DeviceModelDelegate(QObject *parent = nullptr); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; - //void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, - // const QModelIndex &index) const; + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, + const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; diff --git a/src/tuxclocker-qt/meson.build b/src/tuxclocker-qt/meson.build index 825d320..744463d 100644 --- a/src/tuxclocker-qt/meson.build +++ b/src/tuxclocker-qt/meson.build @@ -2,12 +2,17 @@ qt5 = import('qt5') qt5_dep = dependency('qt5', modules: ['DBus', 'Charts', 'Widgets']) moc_files = qt5.preprocess(moc_headers: ['MainWindow.hpp', - 'data/AssignableItem.hpp'], + 'data/AssignableItem.hpp', + 'data/AssignableProxy.hpp', + 'data/DeviceModel.hpp'], dependencies: qt5_dep) sources = ['main.cpp', + 'data/AssignableItem.cpp', + 'data/AssignableProxy.cpp', 'data/DeviceModel.cpp', 'data/DeviceModelDelegate.cpp', + 'widgets/DeviceTreeView.cpp', 'MainWindow.cpp'] local_incdir = include_directories(['data', @@ -17,6 +22,7 @@ local_incdir = include_directories(['data', executable('tuxclocker-qt', moc_files, sources, + override_options : ['cpp_std=c++17'], dependencies: qt5_dep, include_directories: [incdir, local_incdir], install: true) diff --git a/src/tuxclocker-qt/widgets/DeviceBrowser.hpp b/src/tuxclocker-qt/widgets/DeviceBrowser.hpp new file mode 100644 index 0000000..0d54cff --- /dev/null +++ b/src/tuxclocker-qt/widgets/DeviceBrowser.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Class for viewing/editing the main tuxclocker tree +class DeviceBrowser : public QWidget { +public: + DeviceBrowser(DeviceModel &model, + QWidget *parent = nullptr) : QWidget(parent), m_deviceModel(model) { + m_layout = new QGridLayout(this); + m_treeView = new DeviceTreeView(model); + m_apply = new QPushButton("Apply changes"); + m_apply->setEnabled(true); + + connect(m_apply, &QPushButton::pressed, &m_deviceModel, &DeviceModel::applyChanges); + + m_layout->addWidget(m_treeView, 0, 0); + m_layout->addWidget(m_apply, 1, 0); + + setLayout(m_layout); + } + +private: + DeviceModel &m_deviceModel; + DeviceTreeView *m_treeView; + QPushButton *m_apply; + QGridLayout *m_layout; +}; diff --git a/src/tuxclocker-qt/widgets/DeviceTreeView.cpp b/src/tuxclocker-qt/widgets/DeviceTreeView.cpp new file mode 100644 index 0000000..dc34643 --- /dev/null +++ b/src/tuxclocker-qt/widgets/DeviceTreeView.cpp @@ -0,0 +1,39 @@ +#include "DeviceTreeView.hpp" + +#include +#include + +Q_DECLARE_METATYPE(AssignableItemData) + +DeviceTreeView::DeviceTreeView(DeviceModel &model, QWidget *parent) + : QTreeView(parent), m_deviceModel(model) { + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QTreeView::customContextMenuRequested, [this](QPoint point) { + auto index = indexAt(point); + auto data = index.data(DeviceModel::AssignableRole); + QMenu menu; + if (data.canConvert()) { + auto a_data = data.value(); + QCheckBox commitCb("Commit"); + auto commitAct = new QWidgetAction(&menu); + commitAct->setDefaultWidget(&commitCb); + commitCb.setChecked(a_data.committal()); + connect(&commitCb, &QCheckBox::toggled, [&](bool toggled) { + a_data.setCommittal(toggled); + }); + // Write the committal value to the model only on menu close + connect(&menu, &QMenu::aboutToHide, [&] { + qDebug() << a_data.committal(); + QVariant v; + v.setValue(a_data); + m_deviceModel.setData(index, v, DeviceModel::AssignableRole); + }); + menu.addAction(commitAct); + menu.exec(QCursor::pos()); + } + }); + m_delegate = new DeviceModelDelegate(this); + + setItemDelegate(m_delegate); + setModel(&m_deviceModel); +} diff --git a/src/tuxclocker-qt/widgets/DeviceTreeView.hpp b/src/tuxclocker-qt/widgets/DeviceTreeView.hpp new file mode 100644 index 0000000..4447aa2 --- /dev/null +++ b/src/tuxclocker-qt/widgets/DeviceTreeView.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Class for handling menus on DeviceModel +class DeviceTreeView : public QTreeView { +public: + DeviceTreeView(DeviceModel &model, QWidget *parent = nullptr); + // Accessor method for connecting everything in the browser + const DeviceModel &deviceModel() {return m_deviceModel;} +private: + DeviceModel &m_deviceModel; + DeviceModelDelegate *m_delegate; +};