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