Streamline context menus of regular tray application and Plasmoid

* Support triggering actions via the context menu in the regular tray like
  it is already possible in the Plasmoid
* Support copying via the context menu in the Plasmoid like it is already
  possible in the regular tray
* Reduce repetition of coding patterns using templates
This commit is contained in:
Martchus 2020-10-07 21:34:37 +02:00
parent 7cf98e97a7
commit aac87621dc
14 changed files with 212 additions and 115 deletions

View File

@ -33,11 +33,12 @@ public Q_SLOTS:
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
const SyncthingDev *devInfo(const QModelIndex &index) const;
const SyncthingDev *info(const QModelIndex &index) const;
private Q_SLOTS:
void devStatusChanged(const SyncthingDev &, int index);
@ -50,6 +51,11 @@ private:
const std::vector<SyncthingDev> &m_devs;
};
inline const SyncthingDev *SyncthingDeviceModel::info(const QModelIndex &index) const
{
return devInfo(index);
}
} // namespace Data
#endif // DATA_SYNCTHINGDEVICEMODEL_H

View File

@ -34,11 +34,12 @@ public Q_SLOTS:
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
const SyncthingDir *dirInfo(const QModelIndex &index) const;
const SyncthingDir *info(const QModelIndex &index) const;
private Q_SLOTS:
void dirStatusChanged(const SyncthingDir &dir, int index);
@ -55,6 +56,11 @@ private:
std::vector<int> m_rowCount;
};
inline const SyncthingDir *SyncthingDirectoryModel::info(const QModelIndex &index) const
{
return dirInfo(index);
}
} // namespace Data
Q_DECLARE_METATYPE(QModelIndex)

View File

@ -27,12 +27,13 @@ public Q_SLOTS:
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
const SyncthingDir *dirInfo(const QModelIndex &index) const;
const SyncthingItemDownloadProgress *progressInfo(const QModelIndex &index) const;
QPair<const SyncthingDir *, const SyncthingItemDownloadProgress *> info(const QModelIndex &index) const;
unsigned int pendingDownloads() const;
bool singleColumnMode() const;
void setSingleColumnMode(bool singleColumnModeEnabled);
@ -62,6 +63,11 @@ private:
bool m_singleColumnMode;
};
inline QPair<const SyncthingDir *, const SyncthingItemDownloadProgress *> SyncthingDownloadModel::info(const QModelIndex &index) const
{
return qMakePair(dirInfo(index), progressInfo(index));
}
inline unsigned int SyncthingDownloadModel::pendingDownloads() const
{
return m_pendingDownloads;

View File

@ -23,6 +23,8 @@ Item {
delegate: TopLevelItem {
id: item
width: deviceView.width
readonly property string devName: name
readonly property string devId: devId
property alias resumePauseButton: resumePauseButton
ColumnLayout {
@ -95,6 +97,19 @@ Item {
resumePauseItem.icon = item.resumePauseButton.icon
}
PlasmaComponents.MenuItem {
text: qsTr("Copy name")
icon: "edit-copy"
onClicked: deviceView.copyCurrentItemData("devName")
}
PlasmaComponents.MenuItem {
text: qsTr("Copy ID")
icon: "edit-copy"
onClicked: deviceView.copyCurrentItemData("devId")
}
PlasmaComponents.MenuItem {
separator: true
}
PlasmaComponents.MenuItem {
id: resumePauseItem
text: qsTr("Pause")

View File

@ -31,6 +31,8 @@ ColumnLayout {
delegate: TopLevelItem {
id: item
width: directoryView.width
readonly property string dirName: name
readonly property string dirPath: path
property alias errorsButton: errorsButton
property alias rescanButton: rescanButton
property alias resumePauseButton: resumePauseButton
@ -139,9 +141,22 @@ ColumnLayout {
resumePauseItem.icon = item.resumePauseButton.icon
}
PlasmaComponents.MenuItem {
text: qsTr("Copy label/ID")
icon: "edit-copy"
onClicked: directoryView.copyCurrentItemData("dirName")
}
PlasmaComponents.MenuItem {
text: qsTr("Copy path")
icon: "edit-copy"
onClicked: directoryView.copyCurrentItemData("dirPath")
}
PlasmaComponents.MenuItem {
separator: true
}
PlasmaComponents.MenuItem {
id: rescanItem
text: qsTr('Rescan')
text: qsTr("Rescan")
icon: "view-refresh"
onClicked: directoryView.clickCurrentItemButton(
"rescanButton")
@ -155,7 +170,7 @@ ColumnLayout {
}
PlasmaComponents.MenuItem {
id: openItem
text: qsTr('Open in file browser')
text: qsTr("Open in file browser")
icon: "folder"
onClicked: directoryView.clickCurrentItemButton(
"openButton")

View File

@ -23,6 +23,7 @@ Item {
delegate: TopLevelItem {
id: item
width: downloadView.width
readonly property string downloadName: name
property alias openButton: openButton
ColumnLayout {
@ -134,9 +135,17 @@ Item {
PlasmaComponents.Menu {
id: contextMenu
PlasmaComponents.MenuItem {
text: qsTr("Copy label/ID")
icon: "edit-copy"
onClicked: downloadView.copyCurrentItemData("downloadName")
}
PlasmaComponents.MenuItem {
separator: true
}
PlasmaComponents.MenuItem {
id: openItem
text: qsTr('Open in file browser')
text: qsTr("Open in file browser")
icon: "folder"
onClicked: downloadView.clickCurrentItemButton("openButton")
}

View File

@ -33,6 +33,16 @@ ListView {
}
}
function copyCurrentItemData(fieldName) {
if (!currentItem) {
return
}
var data = currentItem[fieldName]
if (data) {
plasmoid.nativeInterface.copyToClipboard(data)
}
}
function showContextMenu(item, x, y) {
if (typeof contextMenu === "undefined") {
return

View File

@ -2,6 +2,7 @@
#include "./devbuttonsitemdelegate.h"
#include "./helper.h"
#include "../../connector/syncthingdev.h"
#include "../../model/syncthingdevicemodel.h"
#include <QClipboard>
@ -48,59 +49,39 @@ void DevView::mouseReleaseEvent(QMouseEvent *event)
void DevView::showContextMenu(const QPoint &position)
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
const auto selectedRow = SelectedRow(this);
const auto &selectedIndex = selectedRow.index;
if (!selectedRow) {
return;
}
QMenu menu(this);
if (selectionModel()->selectedRows(0).at(0).parent().isValid()) {
if (selectedIndex.parent().isValid()) {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy value")),
&QAction::triggered, this, &DevView::copySelectedItem);
&QAction::triggered,
copyToClipboard(selectedRow.model->data(selectedRow.model->index(selectedIndex.row(), 1, selectedIndex.parent())).toString()));
} else {
const auto *const dev = selectedRow.data;
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy name")),
&QAction::triggered, this, &DevView::copySelectedItem);
&QAction::triggered, copyToClipboard(dev->displayName()));
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy ID")),
&QAction::triggered, this, &DevView::copySelectedItemId);
&QAction::triggered, copyToClipboard(dev->id));
menu.addSeparator();
if (dev->paused) {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("media-playback-start"),
QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/media-playback-start.svg"))),
tr("Resume")),
&QAction::triggered, triggerActionForSelectedRow(this, &DevView::pauseResumeDev));
} else {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("media-playback-pause"),
QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/media-playback-pause.svg"))),
tr("Pause")),
&QAction::triggered, triggerActionForSelectedRow(this, &DevView::pauseResumeDev));
}
}
showViewMenu(position, *this, menu);
}
void DevView::copySelectedItem()
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
return;
}
const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0);
QString text;
if (selectedIndex.parent().isValid()) {
// dev attribute
text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString();
} else {
// dev name/id
text = model()->data(selectedIndex).toString();
}
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
}
}
void DevView::copySelectedItemId()
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
return;
}
const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0);
QString text;
if (selectedIndex.parent().isValid()) {
// dev attribute: should be handled by copySelectedItemId()
} else {
// dev name/id
text = model()->data(model()->index(0, 1, selectedIndex)).toString();
}
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
}
}
} // namespace QtGui

View File

@ -5,13 +5,16 @@
namespace Data {
struct SyncthingDev;
}
class SyncthingDeviceModel;
} // namespace Data
namespace QtGui {
class DevView : public QTreeView {
Q_OBJECT
public:
using ModelType = Data::SyncthingDeviceModel;
DevView(QWidget *parent = nullptr);
Q_SIGNALS:
@ -22,8 +25,6 @@ protected:
private Q_SLOTS:
void showContextMenu(const QPoint &position);
void copySelectedItem();
void copySelectedItemId();
};
} // namespace QtGui

View File

@ -72,59 +72,46 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
void DirView::showContextMenu(const QPoint &position)
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
const auto selectedRow = SelectedRow(this);
const auto &selectedIndex = selectedRow.index;
if (!selectedRow) {
return;
}
QMenu menu(this);
if (selectionModel()->selectedRows(0).at(0).parent().isValid()) {
if (selectedIndex.parent().isValid()) {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy value")),
&QAction::triggered, this, &DirView::copySelectedItem);
&QAction::triggered,
copyToClipboard(selectedRow.model->data(selectedRow.model->index(selectedIndex.row(), 1, selectedIndex.parent())).toString()));
} else {
const auto *const dir = selectedRow.data;
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy label/ID")),
&QAction::triggered, this, &DirView::copySelectedItem);
&QAction::triggered, copyToClipboard(dir->displayName()));
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy path")),
&QAction::triggered, this, &DirView::copySelectedItemPath);
&QAction::triggered, copyToClipboard(dir->path));
menu.addSeparator();
connect(menu.addAction(
QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg"))),
tr("Rescan")),
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::scanDir));
if (dir->paused) {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("media-playback-start"),
QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/media-playback-start.svg"))),
tr("Resume")),
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::pauseResumeDir));
} else {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("media-playback-pause"),
QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/media-playback-pause.svg"))),
tr("Pause")),
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::pauseResumeDir));
}
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))),
tr("Open in file browser")),
&QAction::triggered, triggerActionForSelectedRow(this, &DirView::openDir));
}
showViewMenu(position, *this, menu);
}
void DirView::copySelectedItem()
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
return;
}
const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0);
QString text;
if (selectedIndex.parent().isValid()) {
// dev attribute
text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString();
} else {
// dev label/id
text = model()->data(selectedIndex).toString();
}
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
}
}
void DirView::copySelectedItemPath()
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
return;
}
const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0);
QString text;
if (selectedIndex.parent().isValid()) {
// dev attribute: should be handled by copySelectedItem() only
} else {
// dev path
text = model()->data(model()->index(1, 1, selectedIndex)).toString();
}
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
}
}
} // namespace QtGui

View File

@ -5,13 +5,16 @@
namespace Data {
struct SyncthingDir;
}
class SyncthingDirectoryModel;
} // namespace Data
namespace QtGui {
class DirView : public QTreeView {
Q_OBJECT
public:
using ModelType = Data::SyncthingDirectoryModel;
DirView(QWidget *parent = nullptr);
Q_SIGNALS:
@ -24,8 +27,6 @@ protected:
private Q_SLOTS:
void showContextMenu(const QPoint &position);
void copySelectedItem();
void copySelectedItemPath();
};
} // namespace QtGui

View File

@ -2,6 +2,7 @@
#include "./downloaditemdelegate.h"
#include "./helper.h"
#include "../../connector/syncthingdir.h"
#include "../../model/syncthingdownloadmodel.h"
#include <QClipboard>
@ -53,38 +54,36 @@ void DownloadView::mouseReleaseEvent(QMouseEvent *event)
void DownloadView::showContextMenu(const QPoint &position)
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
const auto selectedRow = SelectedRow(this);
const auto &selectedIndex = selectedRow.index;
if (!selectedRow) {
return;
}
QMenu menu(this);
if (selectionModel()->selectedRows(0).at(0).parent().isValid()) {
if (selectedIndex.parent().isValid()) {
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy value")),
&QAction::triggered, this, &DownloadView::copySelectedItem);
&QAction::triggered, copyToClipboard(model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString()));
} else {
const auto [dir, progress] = selectedRow.data;
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy label/ID")),
&QAction::triggered, this, &DownloadView::copySelectedItem);
&QAction::triggered, copyToClipboard(dir->displayName()));
menu.addSeparator();
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder-open.svg"))),
tr("Open in file browser")),
&QAction::triggered, triggerActionForSelectedRow(this, &DownloadView::emitOpenDir));
}
showViewMenu(position, *this, menu);
}
void DownloadView::copySelectedItem()
void DownloadView::emitOpenDir(QPair<const SyncthingDir *, const SyncthingItemDownloadProgress *> info)
{
if (!selectionModel() || selectionModel()->selectedRows(0).size() != 1) {
return;
}
const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0);
QString text;
if (selectedIndex.parent().isValid()) {
// dev attribute
text = model()->data(model()->index(selectedIndex.row(), 1, selectedIndex.parent())).toString();
if (info.second) {
emit openItemDir(*info.second);
} else {
// dev label/id
text = model()->data(selectedIndex).toString();
}
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
emit openDir(*info.first);
}
}
} // namespace QtGui

View File

@ -6,6 +6,7 @@
namespace Data {
struct SyncthingItemDownloadProgress;
struct SyncthingDir;
class SyncthingDownloadModel;
} // namespace Data
namespace QtGui {
@ -13,6 +14,8 @@ namespace QtGui {
class DownloadView : public QTreeView {
Q_OBJECT
public:
using ModelType = Data::SyncthingDownloadModel;
DownloadView(QWidget *parent = nullptr);
Q_SIGNALS:
@ -24,7 +27,9 @@ protected:
private Q_SLOTS:
void showContextMenu(const QPoint &position);
void copySelectedItem();
private:
void emitOpenDir(QPair<const Data::SyncthingDir *, const Data::SyncthingItemDownloadProgress *> info);
};
} // namespace QtGui

View File

@ -1,16 +1,72 @@
#ifndef TRAY_GUI_HELPER_H
#define TRAY_GUI_HELPER_H
#include <QtGlobal>
#include <c++utilities/misc/traits.h>
#include <QClipboard>
#include <QGuiApplication>
#include <QModelIndex>
#include <QTreeView>
QT_FORWARD_DECLARE_CLASS(QPoint)
QT_FORWARD_DECLARE_CLASS(QTreeView)
QT_FORWARD_DECLARE_CLASS(QMenu)
namespace QtGui {
void showViewMenu(const QPoint &position, const QTreeView &view, QMenu &menu);
inline auto copyToClipboard(const QString &text)
{
return [=] { QGuiApplication::clipboard()->setText(text); };
}
template <typename ViewType> struct SelectedRow {
explicit SelectedRow(ViewType *view);
operator bool() const;
typename ViewType::ModelType *model = nullptr;
QModelIndex index;
decltype(model->info(index)) data = decltype(model->info(index))();
};
template <typename ViewType> SelectedRow<ViewType>::SelectedRow(ViewType *view)
{
auto *const selectionModel = view->selectionModel();
if (!selectionModel) {
return;
}
const auto selectedRows = selectionModel->selectedRows(0);
if (selectedRows.size() != 1) {
return;
}
index = selectedRows.at(0);
model = qobject_cast<typename ViewType::ModelType *>(view->model());
if (model) {
data = model->info(index);
}
}
template <typename ViewType> SelectedRow<ViewType>::operator bool() const
{
if (!model || !index.isValid()) {
return false;
}
if constexpr (CppUtilities::Traits::IsSpecializationOf<decltype(data), QPair>::value) {
return data.first;
} else {
return data;
}
}
template <typename ViewType, typename ActionType> inline auto triggerActionForSelectedRow(ViewType *view, ActionType action)
{
return [=] {
if (const auto selectedRow = SelectedRow(view)) {
std::invoke(action, view, CppUtilities::Traits::dereferenceMaybe(selectedRow.data));
}
};
}
} // namespace QtGui
#endif // TRAY_GUI_HELPER_H