diff --git a/DeviceList.cxx b/DeviceList.cxx
index e9a249f..d60ad20 100644
--- a/DeviceList.cxx
+++ b/DeviceList.cxx
@@ -1,568 +1,584 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
- Copyright 2017, 2019 Martin Koller, kollix@aon.at
+ Copyright 2017 - 2020 Martin Koller, kollix@aon.at
This file is part of liquidshell.
liquidshell is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
liquidshell is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with liquidshell. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//--------------------------------------------------------------------------------
DeviceItem::DeviceItem(Solid::Device dev, const QVector &deviceActions)
: device(dev)
{
setFrameShape(QFrame::StyledPanel);
QVBoxLayout *vbox = new QVBoxLayout(this);
QHBoxLayout *hbox = new QHBoxLayout;
vbox->addLayout(hbox);
QLabel *iconLabel = new QLabel;
iconLabel->setPixmap(QIcon::fromTheme(device.icon()).pixmap(32));
iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
newFlagLabel = new QLabel;
newFlagLabel->setPixmap(QIcon::fromTheme("emblem-important").pixmap(16));
newFlagLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
newFlagLabel->hide();
textLabel = new QLabel;
textLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
hbox->addWidget(iconLabel, 0, Qt::AlignLeft | Qt::AlignVCenter);
hbox->addWidget(newFlagLabel, 0, Qt::AlignCenter);
hbox->addWidget(textLabel, 0, Qt::AlignVCenter);
Solid::StorageAccess *storage = device.as();
if ( storage )
{
connect(storage, &Solid::StorageAccess::teardownDone, this, &DeviceItem::teardownDone);
connect(storage, &Solid::StorageAccess::setupDone, this, &DeviceItem::setupDone);
mountBusyTimer.setInterval(500);
connect(&mountBusyTimer, &QTimer::timeout, this,
[this]() { mountButton->setVisible(!mountButton->isVisible()); });
mountButton = new QToolButton;
mountButton->setIconSize(QSize(32, 32));
connect(mountButton, &QToolButton::clicked, mountButton,
[this]()
{
statusLabel->hide();
mountButton->setEnabled(false);
mountBusyTimer.start();
Solid::StorageAccess *storage = device.as();
if ( storage->isAccessible() ) // mounted -> unmount it
storage->teardown();
else
storage->setup();
}
);
hbox->addWidget(mountButton);
}
statusLabel = new QLabel;
statusLabel->setWordWrap(true);
statusLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
statusLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
vbox->addWidget(statusLabel);
statusLabel->hide();
statusTimer.setSingleShot(true);
statusTimer.setInterval(60000);
connect(&statusTimer, &QTimer::timeout, statusLabel, &QLabel::hide);
fillData();
// append actions
for (const DeviceAction &action : deviceActions)
{
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addSpacing(iconLabel->sizeHint().width());
IconButton *button = new IconButton;
button->setIcon(QIcon::fromTheme(action.action.icon()));
button->setText(action.action.text() + " (" + QFileInfo(action.path).baseName() + ")");
connect(button, &IconButton::clicked, button,
[action, this]()
{
QString command = action.action.exec();
+ if ( device.is() )
+ command.replace("%d", device.as()->device());
+
+ command.replace("%i", device.udi());
+
Solid::StorageAccess *storage = device.as();
if ( storage )
{
if ( !storage->isAccessible() )
{
statusLabel->hide();
storage->setup();
+ pendingCommand = command;
return;
}
command.replace("%f", storage->filePath());
}
- if ( device.is() )
- command.replace("%d", device.as()->device());
-
- command.replace("%i", device.udi());
-
KRun::runCommand(command, this);
window()->hide();
}
);
hbox->addWidget(button);
vbox->addLayout(hbox);
}
}
//--------------------------------------------------------------------------------
DeviceItem::DeviceItem(const KdeConnect::Device &dev)
{
setFrameShape(QFrame::StyledPanel);
QVBoxLayout *vbox = new QVBoxLayout(this);
QHBoxLayout *hbox = new QHBoxLayout;
vbox->addLayout(hbox);
QLabel *iconLabel = new QLabel;
iconLabel->setPixmap(dev->icon.pixmap(32));
iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
newFlagLabel = new QLabel;
newFlagLabel->setPixmap(QIcon::fromTheme("emblem-important").pixmap(16));
newFlagLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
newFlagLabel->hide();
textLabel = new QLabel;
textLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
hbox->addWidget(iconLabel, 0, Qt::AlignLeft | Qt::AlignVCenter);
hbox->addWidget(newFlagLabel, 0, Qt::AlignCenter);
hbox->addWidget(textLabel, 0, Qt::AlignVCenter);
statusLabel = new QLabel;
hbox->addWidget(statusLabel, 0, Qt::AlignVCenter);
QLabel *chargeIcon = new QLabel;
hbox->addWidget(chargeIcon, 0, Qt::AlignVCenter);
if ( dev->charge >= 0 )
{
statusLabel->setText(QString::number(dev->charge) + '%');
chargeIcon->setPixmap(dev->chargeIcon.pixmap(22));
}
connect(dev.data(), &KdeConnectDevice::changed, this,
[this, chargeIcon, dev]()
{
textLabel->setText(dev->name);
if ( dev->charge >= 0 )
{
statusLabel->setText(QString::number(dev->charge) + '%');
chargeIcon->setPixmap(dev->chargeIcon.pixmap(22));
}
});
if ( dev->plugins.contains("kdeconnect_findmyphone") )
{
QToolButton *ringButton = new QToolButton;
ringButton->setIcon(QIcon::fromTheme("preferences-desktop-notification-bell"));
connect(ringButton, &QToolButton::clicked, dev.data(), [dev]() { dev->ringPhone(); });
hbox->addWidget(ringButton, 0, Qt::AlignVCenter);
}
QToolButton *configure = new QToolButton;
configure->setIcon(QIcon::fromTheme("configure"));
connect(configure, &QToolButton::clicked, configure,
[this]()
{
if ( !dialog )
{
dialog = new KCMultiDialog(nullptr);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->addModule("kcm_kdeconnect");
dialog->setWindowTitle(i18n("KDE Connect"));
dialog->adjustSize();
}
dialog->show();
});
hbox->addWidget(configure, 0, Qt::AlignVCenter);
textLabel->setText(dev->name);
if ( dev->plugins.contains("kdeconnect_sftp") )
{
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addSpacing(iconLabel->sizeHint().width());
IconButton *button = new IconButton;
button->setIcon(QIcon::fromTheme("system-file-manager"));
button->setText(i18n("Open with File Manager"));
connect(button, &IconButton::clicked,
[dev]() { new KRun(QUrl(QLatin1String("kdeconnect://") + dev->id), nullptr); });
hbox->addWidget(button);
vbox->addLayout(hbox);
}
}
//--------------------------------------------------------------------------------
void DeviceItem::markAsNew()
{
newFlagLabel->show();
QTimer::singleShot(5000, newFlagLabel, &QLabel::hide);
}
//--------------------------------------------------------------------------------
void DeviceItem::fillData()
{
Solid::StorageAccess *storage = device.as();
QString text = device.description();
if ( !device.product().isEmpty() && (device.product() != text) )
text += " (" + device.product() + ")";
else if ( !device.vendor().isEmpty() && (device.vendor() != text) )
text += " (" + device.vendor() + ")";
Solid::StorageVolume *volume = device.as();
if ( volume )
text += " " + KIO::convertSize(volume->size());
if ( storage && !storage->filePath().isEmpty() )
text += '\n' + storage->filePath();
textLabel->setText(text);
if ( mountButton )
{
if ( storage && storage->isAccessible() )
mountButton->setIcon(QIcon::fromTheme("media-eject"));
else
{
if ( device.emblems().count() )
mountButton->setIcon(QIcon::fromTheme(device.emblems()[0]));
else
mountButton->setIcon(QIcon::fromTheme("emblem-unmounted"));
}
if ( storage )
{
mountButton->setToolTip(storage->isAccessible() ?
i18n("Device is mounted.\nClick to unmount/eject") :
i18n("Device is unmounted.\nClick to mount"));
}
}
}
//--------------------------------------------------------------------------------
QString DeviceItem::errorToString(Solid::ErrorType error)
{
switch ( error )
{
case Solid::UnauthorizedOperation: return i18n("Unauthorized Operation");
case Solid::DeviceBusy: return i18n("Device Busy");
case Solid::OperationFailed: return i18n("Operation Failed");
case Solid::UserCanceled: return i18n("User Canceled");
case Solid::InvalidOption: return i18n("Invalid Option");
case Solid::MissingDriver: return i18n("Missing Driver");
default: return QString();
}
}
//--------------------------------------------------------------------------------
void DeviceItem::mountDone(Action action, Solid::ErrorType error, QVariant errorData, const QString &udi)
{
Q_UNUSED(udi)
mountBusyTimer.stop();
mountButton->setEnabled(true);
mountButton->setVisible(true);
if ( error == Solid::NoError )
{
fillData();
if ( action == Unmount )
{
statusLabel->setText(i18n("The device can now be safely removed"));
statusLabel->show();
statusTimer.start();
}
}
else
{
QString text = (action == Mount) ? i18n("Mount failed:") : i18n("Unmount failed:");
statusLabel->setText("" + text + "" + errorToString(error) + "
" + errorData.toString());
statusLabel->show();
statusTimer.start();
}
}
//--------------------------------------------------------------------------------
void DeviceItem::teardownDone(Solid::ErrorType error, QVariant errorData, const QString &udi)
{
mountDone(Unmount, error, errorData, udi);
}
//--------------------------------------------------------------------------------
void DeviceItem::setupDone(Solid::ErrorType error, QVariant errorData, const QString &udi)
{
mountDone(Mount, error, errorData, udi);
+
+ if ( !pendingCommand.isEmpty() )
+ {
+ if ( error == Solid::NoError )
+ {
+ Solid::StorageAccess *storage = device.as();
+ if ( storage ) // should always be true. paranoid check
+ {
+ pendingCommand.replace("%f", storage->filePath());
+ KRun::runCommand(pendingCommand, this);
+ window()->hide();
+ }
+ }
+ pendingCommand.clear();
+ }
}
//--------------------------------------------------------------------------------
//--------------------------------------------------------------------------------
//--------------------------------------------------------------------------------
DeviceList::DeviceList(QWidget *parent)
: QScrollArea(parent)
{
setWindowFlags(windowFlags() | Qt::Tool);
setFrameShape(QFrame::StyledPanel);
setAttribute(Qt::WA_AlwaysShowToolTips);
setWidgetResizable(true);
loadActions();
QWidget *widget = new QWidget;
vbox = new QVBoxLayout(widget);
vbox->setContentsMargins(QMargins());
vbox->addStretch();
vbox->setSizeConstraint(QLayout::SetMinAndMaxSize);
setWidget(widget);
predicate = Solid::Predicate(Solid::DeviceInterface::StorageAccess);
predicate |= Solid::Predicate(Solid::DeviceInterface::StorageDrive);
predicate |= Solid::Predicate(Solid::DeviceInterface::StorageVolume);
predicate |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive);
predicate |= Solid::Predicate(Solid::DeviceInterface::OpticalDisc);
predicate |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer);
predicate |= Solid::Predicate(Solid::DeviceInterface::Camera);
QList devices = Solid::Device::listFromQuery(predicate);
for (Solid::Device device : devices)
addDevice(device);
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded,
this, &DeviceList::deviceAdded);
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
this, &DeviceList::deviceRemoved);
connect(&kdeConnect, &KdeConnect::deviceAdded, this, &DeviceList::kdeConnectDeviceAdded);
connect(&kdeConnect, &KdeConnect::deviceRemoved, this, &DeviceList::deviceRemoved);
}
//--------------------------------------------------------------------------------
QSize DeviceList::sizeHint() const
{
// avoid horizontal scrollbar when the list is higher than 2/3 of the screen
// where a vertical scrollbar will be shown, reducing the available width
// leading to also getting a horizontal scrollbar
QSize s = widget()->sizeHint() + QSize(2 * frameWidth(), 2 * frameWidth());
s.setWidth(s.width() + verticalScrollBar()->sizeHint().width());
return s;
}
//--------------------------------------------------------------------------------
void DeviceList::loadActions()
{
actions.clear();
const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "solid/actions", QStandardPaths::LocateDirectory);
for (const QString &dirPath : dirs)
{
QDir dir(dirPath);
for (const QString &file : dir.entryList(QStringList(QLatin1String("*.desktop")), QDir::Files))
{
QString path = dir.absoluteFilePath(file);
KDesktopFile cfg(path);
const QString predicateString = cfg.desktopGroup().readEntry("X-KDE-Solid-Predicate");
QList actionList = KDesktopFileActions::userDefinedServices(path, true);
if ( !actionList.isEmpty() && !predicateString.isEmpty() )
actions.append(DeviceAction(path, Solid::Predicate::fromString(predicateString), actionList[0]));
}
}
}
//--------------------------------------------------------------------------------
DeviceItem *DeviceList::addDevice(Solid::Device device)
{
if ( items.contains(device.udi()) )
{
//qDebug() << device.udi() << "already known";
return nullptr;
}
QVector deviceActions;
for (const DeviceAction &action : actions)
{
if ( action.predicate.matches(device) )
deviceActions.append(action);
}
Solid::StorageVolume *storage = device.as();
if ( !storage ) // storage can at least be mounted; others need some specific actions
{
if ( deviceActions.isEmpty() )
{
//qDebug() << device.udi() << "no action found";
return nullptr;
}
}
else if ( storage->usage() != Solid::StorageVolume::FileSystem )
{
//qDebug() << device.udi() << "storage no filesystem";
return nullptr;
}
// show only removable devices
if ( device.is() &&
!device.as()->isRemovable() )
{
//qDebug() << device.udi() << "not Removable";
return nullptr;
}
// show only removable devices
if ( device.parent().is() &&
!device.parent().as()->isRemovable() )
{
//qDebug() << device.parent().udi() << "parent() not Removable";
return nullptr;
}
DeviceItem *item = new DeviceItem(device, deviceActions);
vbox->insertWidget(vbox->count() - 1, item); // insert before stretch
items.insert(device.udi(), item);
return item;
}
//--------------------------------------------------------------------------------
void DeviceList::deviceAdded(const QString &dev)
{
Solid::Device device(dev);
if ( !predicate.matches(device) )
return;
DeviceItem *item = addDevice(device);
// when we added a new device, make sure the DeviceNotifier shows and places this window
if ( item )
{
item->markAsNew();
item->show();
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
emit deviceWasAdded();
}
}
//--------------------------------------------------------------------------------
void DeviceList::deviceRemoved(const QString &dev)
{
if ( items.contains(dev) )
{
delete items.take(dev);
emit deviceWasRemoved();
}
}
//--------------------------------------------------------------------------------
void DeviceList::kdeConnectDeviceAdded(const KdeConnect::Device &device)
{
if ( items.contains(device->id) )
{
//qDebug() << device->id << "already known";
return;
}
DeviceItem *item = new DeviceItem(device);
vbox->insertWidget(vbox->count() - 1, item); // insert before stretch
items.insert(device->id, item);
item->markAsNew();
item->show();
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
// when we added a new device, make sure the DeviceNotifier shows and places this window
emit deviceWasAdded();
}
//--------------------------------------------------------------------------------
diff --git a/DeviceList.hxx b/DeviceList.hxx
index 7175636..594c795 100644
--- a/DeviceList.hxx
+++ b/DeviceList.hxx
@@ -1,120 +1,121 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
- Copyright 2017, 2019 Martin Koller, kollix@aon.at
+ Copyright 2017 - 2020 Martin Koller, kollix@aon.at
This file is part of liquidshell.
liquidshell is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
liquidshell is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with liquidshell. If not, see .
*/
#ifndef _DeviceList_H_
#define _DeviceList_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//--------------------------------------------------------------------------------
struct DeviceAction
{
DeviceAction() { }
DeviceAction(const QString &filePath, Solid::Predicate p, KServiceAction a)
: path(filePath), predicate(p), action(a) { }
QString path;
Solid::Predicate predicate;
KServiceAction action;
};
//--------------------------------------------------------------------------------
class DeviceItem : public QFrame
{
Q_OBJECT
public:
DeviceItem(Solid::Device dev, const QVector &deviceActions);
DeviceItem(const KdeConnect::Device &dev);
void markAsNew();
private:
static QString errorToString(Solid::ErrorType error);
void fillData();
enum Action { Mount, Unmount };
void mountDone(Action action, Solid::ErrorType error, QVariant errorData, const QString &udi);
private Q_SLOTS:
void teardownDone(Solid::ErrorType error, QVariant errorData, const QString &udi);
void setupDone(Solid::ErrorType error, QVariant errorData, const QString &udi);
private:
Solid::Device device;
QToolButton *mountButton = nullptr;
QLabel *textLabel = nullptr, *statusLabel = nullptr, *newFlagLabel = nullptr;
QTimer statusTimer, mountBusyTimer;
QPointer dialog;
+ QString pendingCommand; // used when click -> mount -> action
};
//--------------------------------------------------------------------------------
class DeviceList : public QScrollArea
{
Q_OBJECT
public:
DeviceList(QWidget *parent);
bool isEmpty() const { return items.isEmpty(); }
QSize sizeHint() const override;
Q_SIGNALS:
void deviceWasAdded();
void deviceWasRemoved();
private Q_SLOTS:
void deviceAdded(const QString &dev);
void deviceRemoved(const QString &dev);
void kdeConnectDeviceAdded(const KdeConnect::Device &dev);
private:
void loadActions();
DeviceItem *addDevice(Solid::Device device);
private:
QVBoxLayout *vbox;
QMap items;
Solid::Predicate predicate;
QVector actions;
KdeConnect kdeConnect;
};
#endif