diff --git a/DeviceList.cxx b/DeviceList.cxx
index d60ad20..7c94496 100644
--- a/DeviceList.cxx
+++ b/DeviceList.cxx
@@ -1,584 +1,586 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
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());
}
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();
+ Solid::StorageVolume *volume = device.as();
+ Solid::StorageAccess *access = device.as();
- if ( !storage ) // storage can at least be mounted; others need some specific actions
+ if ( !volume ) // volume can at least be mounted; others need some specific actions
{
if ( deviceActions.isEmpty() )
{
//qDebug() << device.udi() << "no action found";
return nullptr;
}
+ if ( access && access->isAccessible() && !QFileInfo(access->filePath()).isReadable() )
+ {
+ //qDebug() << device.udi() << access->filePath() << "not readable";
+ return nullptr;
+ }
}
- else if ( storage->usage() != Solid::StorageVolume::FileSystem )
+ else if ( (volume->usage() != Solid::StorageVolume::FileSystem) || volume->isIgnored() )
{
- //qDebug() << device.udi() << "storage no filesystem";
+ //qDebug() << device.udi() << (access ? access->filePath() : QString()) << "no filesystem or ignored";
return nullptr;
}
// show only removable devices
- if ( device.is() &&
- !device.as()->isRemovable() )
- {
- //qDebug() << device.udi() << "not Removable";
- return nullptr;
- }
+ bool isRemovable = (device.is() &&
+ device.as()->isRemovable()) ||
+ (device.parent().is() &&
+ device.parent().as()->isRemovable());
- // show only removable devices
- if ( device.parent().is() &&
- !device.parent().as()->isRemovable() )
+ if ( !isRemovable )
{
- //qDebug() << device.parent().udi() << "parent() not Removable";
+ //qDebug() << device.udi() << "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/DeviceNotifier.cxx b/DeviceNotifier.cxx
index 1b06fc7..aef9589 100644
--- a/DeviceNotifier.cxx
+++ b/DeviceNotifier.cxx
@@ -1,103 +1,105 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/*
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
//--------------------------------------------------------------------------------
DeviceNotifier::DeviceNotifier(QWidget *parent)
: SysTrayItem(parent, "device-notifier")
{
setToolTip(i18n("Device Notifier"));
deviceList = new DeviceList(this);
deviceList->setWindowTitle(i18n("Device List"));
if ( deviceList->isEmpty() )
hide();
connect(deviceList, &DeviceList::deviceWasRemoved, this, &DeviceNotifier::checkDeviceList);
connect(deviceList, &DeviceList::deviceWasAdded,
[this]()
{
if ( !deviceList->isVisible() )
timer.start(); // auto-hide
showDetailsList();
});
// if the user did not activate the device list window, auto-hide it
// but keep it if the mouse is over it (e.g. the user wants to click)
timer.setInterval(4000);
timer.setSingleShot(true);
connect(&timer, &QTimer::timeout, this,
[this]()
{
- if ( !deviceList->underMouse() )
+ if ( deviceList->underMouse() )
+ timer.start();
+ else
deviceList->hide();
});
deviceList->installEventFilter(this);
}
//--------------------------------------------------------------------------------
QWidget *DeviceNotifier::getDetailsList()
{
deviceList->adjustSize();
deviceList->resize(deviceList->size().expandedTo(QSize(300, 100)));
setVisible(!deviceList->isEmpty());
return deviceList;
}
//--------------------------------------------------------------------------------
void DeviceNotifier::checkDeviceList()
{
if ( deviceList->isEmpty() )
{
deviceList->hide();
hide();
}
else if ( deviceList->isVisible() )
showDetailsList(); // reposition
}
//--------------------------------------------------------------------------------
bool DeviceNotifier::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched)
if ( event->type() == QEvent::WindowActivate )
timer.stop();
return false;
}
//--------------------------------------------------------------------------------