File indexing completed on 2024-03-24 17:23:07

0001 // SPDX-License-Identifier: GPL-3.0-or-later
0002 /*
0003   Copyright 2017 - 2023 Martin Koller, kollix@aon.at
0004 
0005   This file is part of liquidshell.
0006 
0007   liquidshell is free software: you can redistribute it and/or modify
0008   it under the terms of the GNU General Public License as published by
0009   the Free Software Foundation, either version 3 of the License, or
0010   (at your option) any later version.
0011 
0012   liquidshell is distributed in the hope that it will be useful,
0013   but WITHOUT ANY WARRANTY; without even the implied warranty of
0014   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0015   GNU General Public License for more details.
0016 
0017   You should have received a copy of the GNU General Public License
0018   along with liquidshell.  If not, see <http://www.gnu.org/licenses/>.
0019 */
0020 
0021 #include <DeviceList.hxx>
0022 #include <IconButton.hxx>
0023 #include <Battery.hxx>
0024 
0025 #include <Solid/DeviceNotifier>
0026 #include <Solid/DeviceInterface>
0027 #include <Solid/StorageAccess>
0028 #include <Solid/StorageVolume>
0029 #include <Solid/StorageDrive>
0030 #include <Solid/Block>
0031 #include <Solid/Battery>
0032 
0033 #include <QHBoxLayout>
0034 #include <QIcon>
0035 #include <QStandardPaths>
0036 #include <QDir>
0037 #include <QFileInfo>
0038 #include <QScrollBar>
0039 #include <QDebug>
0040 
0041 #include <KLocalizedString>
0042 #include <KDesktopFile>
0043 #include <KDesktopFileActions>
0044 #include <KConfigGroup>
0045 #include <KService>
0046 
0047 #include <kio_version.h>
0048 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0049 #  include <KIO/CommandLauncherJob>
0050 #  include <KIO/JobUiDelegateFactory>
0051 #endif
0052 #include <KRun>
0053 
0054 #include <kio/global.h>
0055 
0056 //--------------------------------------------------------------------------------
0057 
0058 DeviceItem::DeviceItem(Solid::Device dev, const QVector<DeviceAction> &deviceActions)
0059   : device(dev)
0060 {
0061   setFrameShape(QFrame::StyledPanel);
0062 
0063   QVBoxLayout *vbox = new QVBoxLayout(this);
0064   QHBoxLayout *hbox = new QHBoxLayout;
0065   vbox->addLayout(hbox);
0066 
0067   QLabel *iconLabel = new QLabel;
0068   iconLabel->setPixmap(QIcon::fromTheme(device.icon()).pixmap(32));
0069   iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0070 
0071   newFlagLabel = new QLabel;
0072   newFlagLabel->setPixmap(QIcon::fromTheme("emblem-important").pixmap(16));
0073   newFlagLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0074   newFlagLabel->hide();
0075 
0076   textLabel = new QLabel;
0077   textLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
0078   textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
0079 
0080   hbox->addWidget(iconLabel, 0, Qt::AlignLeft | Qt::AlignVCenter);
0081   hbox->addWidget(newFlagLabel, 0, Qt::AlignCenter);
0082   hbox->addWidget(textLabel, 0, Qt::AlignVCenter);
0083 
0084   Solid::StorageAccess *storage = device.as<Solid::StorageAccess>();
0085 
0086   if ( storage )
0087   {
0088     connect(storage, &Solid::StorageAccess::teardownDone, this, &DeviceItem::teardownDone);
0089     connect(storage, &Solid::StorageAccess::setupDone, this, &DeviceItem::setupDone);
0090 
0091     mountBusyTimer.setInterval(500);
0092     connect(&mountBusyTimer, &QTimer::timeout, this,
0093             [this]() { mountButton->setVisible(!mountButton->isVisible()); });
0094 
0095     mountButton = new QToolButton;
0096     mountButton->setIconSize(QSize(32, 32));
0097 
0098     connect(mountButton, &QToolButton::clicked, mountButton,
0099             [this]()
0100             {
0101               statusLabel->hide();
0102               mountButton->setEnabled(false);
0103               mountBusyTimer.start();
0104 
0105               Solid::StorageAccess *storage = device.as<Solid::StorageAccess>();
0106 
0107               if ( storage->isAccessible() )  // mounted -> unmount it
0108                 storage->teardown();
0109               else
0110                 storage->setup();
0111             }
0112            );
0113 
0114 
0115     hbox->addWidget(mountButton);
0116   }
0117 
0118   statusLabel = new QLabel;
0119 
0120   if ( device.is<Solid::Battery>() )
0121   {
0122     hbox->addWidget(statusLabel, 0, Qt::AlignVCenter);
0123 
0124     chargeIcon = new QLabel;
0125     hbox->addWidget(chargeIcon, 0, Qt::AlignVCenter);
0126 
0127     Solid::Battery *battery = device.as<Solid::Battery>();
0128 
0129     if ( battery->chargePercent() >= 0 )
0130     {
0131       statusLabel->setText(QString::number(battery->chargePercent()) + '%');
0132       chargeIcon->setPixmap(Battery::getStatusIcon(battery->chargePercent(),
0133                             battery->chargeState() == Solid::Battery::Charging).pixmap(22));
0134     }
0135 
0136     connect(battery, &Solid::Battery::chargePercentChanged, this,
0137             [this, battery]()
0138             {
0139               statusLabel->setText(QString::number(battery->chargePercent()) + '%');
0140               chargeIcon->setPixmap(Battery::getStatusIcon(battery->chargePercent(),
0141                                     battery->chargeState() == Solid::Battery::Charging).pixmap(22));
0142             });
0143   }
0144   else
0145   {
0146     statusLabel->setWordWrap(true);
0147     statusLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
0148     statusLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
0149     vbox->addWidget(statusLabel);
0150     statusLabel->hide();
0151 
0152     statusTimer.setSingleShot(true);
0153     statusTimer.setInterval(60000);
0154     connect(&statusTimer, &QTimer::timeout, statusLabel, &QLabel::hide);
0155   }
0156 
0157   fillData();
0158 
0159   // append actions
0160   for (const DeviceAction &action : deviceActions)
0161   {
0162     QHBoxLayout *hbox = new QHBoxLayout;
0163     hbox->addSpacing(iconLabel->sizeHint().width());
0164 
0165     IconButton *button = new IconButton;
0166     button->setIcon(QIcon::fromTheme(action.action.icon()));
0167     button->setText(action.action.text() + " (" + QFileInfo(action.path).baseName() + ")");
0168 
0169     connect(button, &IconButton::clicked, button,
0170             [action, this]()
0171             {
0172               QString command = action.action.exec();
0173 
0174               if ( device.is<Solid::Block>() )
0175                 command.replace("%d", device.as<Solid::Block>()->device());
0176 
0177               command.replace("%i", device.udi());
0178 
0179               Solid::StorageAccess *storage = device.as<Solid::StorageAccess>();
0180               if ( storage )
0181               {
0182                 if ( !storage->isAccessible() )
0183                 {
0184                   statusLabel->hide();
0185                   storage->setup();
0186                   pendingCommand = command;
0187                   return;
0188                 }
0189 
0190                 command.replace("%f", storage->filePath());
0191               }
0192 
0193 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0194               auto *job = new KIO::CommandLauncherJob(command);
0195               job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
0196               job->start();
0197 #else
0198               KRun::runCommand(command, this);
0199 #endif
0200               window()->hide();
0201             }
0202            );
0203 
0204     hbox->addWidget(button);
0205 
0206     vbox->addLayout(hbox);
0207   }
0208 }
0209 
0210 //--------------------------------------------------------------------------------
0211 
0212 DeviceItem::DeviceItem(const KdeConnect::Device &dev)
0213 {
0214   setFrameShape(QFrame::StyledPanel);
0215 
0216   QVBoxLayout *vbox = new QVBoxLayout(this);
0217   QHBoxLayout *hbox = new QHBoxLayout;
0218   vbox->addLayout(hbox);
0219 
0220   QLabel *iconLabel = new QLabel;
0221   iconLabel->setPixmap(dev->icon.pixmap(32));
0222   iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0223 
0224   newFlagLabel = new QLabel;
0225   newFlagLabel->setPixmap(QIcon::fromTheme("emblem-important").pixmap(16));
0226   newFlagLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0227   newFlagLabel->hide();
0228 
0229   textLabel = new QLabel;
0230   textLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
0231   textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
0232 
0233   hbox->addWidget(iconLabel, 0, Qt::AlignLeft | Qt::AlignVCenter);
0234   hbox->addWidget(newFlagLabel, 0, Qt::AlignCenter);
0235   hbox->addWidget(textLabel, 0, Qt::AlignVCenter);
0236 
0237   statusLabel = new QLabel;
0238   hbox->addWidget(statusLabel, 0, Qt::AlignVCenter);
0239 
0240   chargeIcon = new QLabel;
0241   hbox->addWidget(chargeIcon, 0, Qt::AlignVCenter);
0242 
0243   ringButton = new QToolButton;
0244   ringButton->setIcon(QIcon::fromTheme("preferences-desktop-notification-bell"));
0245   connect(ringButton, &QToolButton::clicked, dev.data(), [dev]() { dev->ringPhone(); });
0246   hbox->addWidget(ringButton, 0, Qt::AlignVCenter);
0247 
0248   kdeConnectDeviceChanged(dev);
0249 
0250   connect(dev.data(), &KdeConnectDevice::changed, this, [this, dev]() { kdeConnectDeviceChanged(dev); });
0251 
0252   QToolButton *configure = new QToolButton;
0253   configure->setIcon(QIcon::fromTheme("configure"));
0254   connect(configure, &QToolButton::clicked, configure,
0255           [this]()
0256           {
0257             if ( !dialog )
0258             {
0259               dialog = new KCMultiDialog(nullptr);
0260               dialog->setAttribute(Qt::WA_DeleteOnClose);
0261 
0262               KPluginMetaData data("kcm_kdeconnect");
0263 
0264               if ( data.isValid() )
0265                 dialog->addModule(data);
0266               else
0267                 dialog->addModule("kcm_kdeconnect");
0268 
0269               dialog->setWindowTitle(i18n("KDE Connect"));
0270               dialog->adjustSize();
0271             }
0272             dialog->show();
0273           });
0274 
0275   hbox->addWidget(configure, 0, Qt::AlignVCenter);
0276 
0277   textLabel->setText(dev->name);
0278 
0279   if ( dev->plugins.contains("kdeconnect_sftp") )
0280   {
0281     QHBoxLayout *hbox = new QHBoxLayout;
0282     hbox->addSpacing(iconLabel->sizeHint().width());
0283 
0284     IconButton *button = new IconButton;
0285     button->setIcon(QIcon::fromTheme("system-file-manager"));
0286     button->setText(i18n("Open with File Manager"));
0287 
0288     connect(button, &IconButton::clicked,
0289             [dev]() { new KRun(QUrl(QLatin1String("kdeconnect://") + dev->id), nullptr); });
0290 
0291     hbox->addWidget(button);
0292 
0293     vbox->addLayout(hbox);
0294   }
0295 }
0296 
0297 //--------------------------------------------------------------------------------
0298 
0299 void DeviceItem::kdeConnectDeviceChanged(const KdeConnect::Device &dev)
0300 {
0301   textLabel->setText(dev->name);
0302 
0303   if ( dev->charge >= 0 )
0304   {
0305     statusLabel->setText(QString::number(dev->charge) + '%');
0306     chargeIcon->setPixmap(dev->chargeIcon.pixmap(22));
0307     statusLabel->show();
0308     chargeIcon->show();
0309   }
0310   else
0311   {
0312     statusLabel->hide();
0313     chargeIcon->hide();
0314   }
0315 
0316   ringButton->setVisible(dev->plugins.contains("kdeconnect_findmyphone"));
0317 }
0318 
0319 //--------------------------------------------------------------------------------
0320 
0321 void DeviceItem::markAsNew()
0322 {
0323   newFlagLabel->show();
0324   QTimer::singleShot(5000, newFlagLabel, &QLabel::hide);
0325 }
0326 
0327 //--------------------------------------------------------------------------------
0328 
0329 void DeviceItem::fillData()
0330 {
0331   Solid::StorageAccess *storage = device.as<Solid::StorageAccess>();
0332   QString text = device.description();
0333 
0334   if ( !device.product().isEmpty() && (device.product() != text) )
0335     text += " (" + device.product() + ")";
0336   else if ( !device.vendor().isEmpty() && (device.vendor() != text) )
0337     text += " (" + device.vendor() + ")";
0338 
0339   Solid::StorageVolume *volume = device.as<Solid::StorageVolume>();
0340   if ( volume )
0341     text += " " + KIO::convertSize(volume->size());
0342 
0343   if ( storage && !storage->filePath().isEmpty() )
0344     text += '\n' + storage->filePath();
0345 
0346   textLabel->setText(text);
0347 
0348   if ( mountButton )
0349   {
0350     if ( storage && storage->isAccessible() )
0351       mountButton->setIcon(QIcon::fromTheme("media-eject"));
0352     else
0353     {
0354       if ( device.emblems().count() )
0355         mountButton->setIcon(QIcon::fromTheme(device.emblems()[0]));
0356       else
0357         mountButton->setIcon(QIcon::fromTheme("emblem-unmounted"));
0358     }
0359 
0360     if ( storage )
0361     {
0362       mountButton->setToolTip(storage->isAccessible() ?
0363                               i18n("Device is mounted.\nClick to unmount/eject") :
0364                               i18n("Device is unmounted.\nClick to mount"));
0365     }
0366   }
0367 }
0368 
0369 //--------------------------------------------------------------------------------
0370 
0371 QString DeviceItem::errorToString(Solid::ErrorType error)
0372 {
0373   switch ( error )
0374   {
0375     case Solid::UnauthorizedOperation: return i18n("Unauthorized Operation");
0376     case Solid::DeviceBusy: return i18n("Device Busy");
0377     case Solid::OperationFailed: return i18n("Operation Failed");
0378     case Solid::UserCanceled: return i18n("User Canceled");
0379     case Solid::InvalidOption: return i18n("Invalid Option");
0380     case Solid::MissingDriver: return i18n("Missing Driver");
0381 
0382     default: return QString();
0383   }
0384 }
0385 
0386 //--------------------------------------------------------------------------------
0387 
0388 void DeviceItem::mountDone(Action action, Solid::ErrorType error, QVariant errorData, const QString &udi)
0389 {
0390   Q_UNUSED(udi)
0391 
0392   mountBusyTimer.stop();
0393   mountButton->setEnabled(true);
0394   mountButton->setVisible(true);
0395 
0396   if ( error == Solid::NoError )
0397   {
0398     fillData();
0399 
0400     if ( action == Unmount )
0401     {
0402       statusLabel->setText(i18n("The device can now be safely removed"));
0403       statusLabel->show();
0404       statusTimer.start();
0405     }
0406   }
0407   else
0408   {
0409     QString text = (action == Mount) ? i18n("Mount failed:") : i18n("Unmount failed:");
0410     statusLabel->setText("<b>" + text + "</b>" + errorToString(error) + "<br>" + errorData.toString());
0411     statusLabel->show();
0412     statusTimer.start();
0413   }
0414 }
0415 
0416 //--------------------------------------------------------------------------------
0417 
0418 void DeviceItem::teardownDone(Solid::ErrorType error, QVariant errorData, const QString &udi)
0419 {
0420   mountDone(Unmount, error, errorData, udi);
0421 }
0422 
0423 //--------------------------------------------------------------------------------
0424 
0425 void DeviceItem::setupDone(Solid::ErrorType error, QVariant errorData, const QString &udi)
0426 {
0427   mountDone(Mount, error, errorData, udi);
0428 
0429   if ( !pendingCommand.isEmpty() )
0430   {
0431     if ( error == Solid::NoError )
0432     {
0433       Solid::StorageAccess *storage = device.as<Solid::StorageAccess>();
0434       if ( storage )  // should always be true. paranoid check
0435       {
0436         pendingCommand.replace("%f", storage->filePath());
0437 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0438         auto *job = new KIO::CommandLauncherJob(pendingCommand);
0439         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
0440         job->start();
0441 #else
0442         KRun::runCommand(pendingCommand, this);
0443 #endif
0444         window()->hide();
0445       }
0446     }
0447     pendingCommand.clear();
0448   }
0449 }
0450 
0451 //--------------------------------------------------------------------------------
0452 //--------------------------------------------------------------------------------
0453 //--------------------------------------------------------------------------------
0454 
0455 DeviceList::DeviceList(QWidget *parent)
0456   : QScrollArea(parent)
0457 {
0458   setWindowFlags(windowFlags() | Qt::Tool);
0459   setFrameShape(QFrame::StyledPanel);
0460   setAttribute(Qt::WA_AlwaysShowToolTips);
0461   setWidgetResizable(true);
0462 
0463   loadActions();
0464 
0465   QWidget *widget = new QWidget;
0466 
0467   vbox = new QVBoxLayout(widget);
0468   vbox->setContentsMargins(QMargins());
0469   vbox->addStretch();
0470   vbox->setSizeConstraint(QLayout::SetMinAndMaxSize);
0471   setWidget(widget);
0472 
0473   predicate = Solid::Predicate(Solid::DeviceInterface::StorageAccess);
0474   predicate |= Solid::Predicate(Solid::DeviceInterface::StorageDrive);
0475   predicate |= Solid::Predicate(Solid::DeviceInterface::StorageVolume);
0476   predicate |= Solid::Predicate(Solid::DeviceInterface::OpticalDrive);
0477   predicate |= Solid::Predicate(Solid::DeviceInterface::OpticalDisc);
0478   predicate |= Solid::Predicate(Solid::DeviceInterface::PortableMediaPlayer);
0479   predicate |= Solid::Predicate(Solid::DeviceInterface::Camera);
0480   predicate |= Solid::Predicate(Solid::DeviceInterface::Battery);  // non-primary batteries, e.g. mouse
0481 
0482   QList<Solid::Device> devices = Solid::Device::listFromQuery(predicate);
0483 
0484   for (Solid::Device device : devices)
0485     addDevice(device);
0486 
0487   connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded,
0488           this, &DeviceList::deviceAdded);
0489 
0490   connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
0491           this, &DeviceList::deviceRemoved);
0492 
0493   connect(&kdeConnect, &KdeConnect::deviceAdded, this, &DeviceList::kdeConnectDeviceAdded);
0494   connect(&kdeConnect, &KdeConnect::deviceRemoved, this, &DeviceList::deviceRemoved);
0495 }
0496 
0497 //--------------------------------------------------------------------------------
0498 
0499 QSize DeviceList::sizeHint() const
0500 {
0501   // avoid horizontal scrollbar when the list is higher than 2/3 of the screen
0502   // where a vertical scrollbar will be shown, reducing the available width
0503   // leading to also getting a horizontal scrollbar
0504   QSize s = widget()->sizeHint() + QSize(2 * frameWidth(), 2 * frameWidth());
0505   s.setWidth(s.width() + verticalScrollBar()->sizeHint().width());
0506   return s;
0507 }
0508 
0509 //--------------------------------------------------------------------------------
0510 
0511 void DeviceList::loadActions()
0512 {
0513   actions.clear();
0514 
0515   const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "solid/actions", QStandardPaths::LocateDirectory);
0516   for (const QString &dirPath : dirs)
0517   {
0518     QDir dir(dirPath);
0519 
0520     for (const QString &file : dir.entryList(QStringList(QLatin1String("*.desktop")), QDir::Files))
0521     {
0522       QString path = dir.absoluteFilePath(file);
0523       KDesktopFile cfg(path);
0524       const QString predicateString = cfg.desktopGroup().readEntry("X-KDE-Solid-Predicate");
0525 
0526 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0527       QList<KServiceAction> actionList = KDesktopFileActions::userDefinedServices(KService(path), true);
0528 #else
0529       QList<KServiceAction> actionList = KDesktopFileActions::userDefinedServices(path, true);
0530 #endif
0531 
0532       if ( !actionList.isEmpty() && !predicateString.isEmpty() )
0533         actions.append(DeviceAction(path, Solid::Predicate::fromString(predicateString), actionList[0]));
0534     }
0535   }
0536 }
0537 
0538 //--------------------------------------------------------------------------------
0539 
0540 DeviceItem *DeviceList::addDevice(Solid::Device device)
0541 {
0542   if ( items.contains(device.udi()) )
0543   {
0544     //qDebug() << device.udi() << "already known";
0545     return nullptr;
0546   }
0547 
0548   QVector<DeviceAction> deviceActions;
0549 
0550   for (const DeviceAction &action : actions)
0551   {
0552     if ( action.predicate.matches(device) )
0553       deviceActions.append(action);
0554   }
0555 
0556   if ( device.is<Solid::Battery>() && (device.as<Solid::Battery>()->type() != Solid::Battery::PrimaryBattery) )
0557   {
0558     // show also non-primary batteries
0559   }
0560   else
0561   {
0562     Solid::StorageVolume *volume = device.as<Solid::StorageVolume>();
0563     Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
0564 
0565     if ( !volume )  // volume can at least be mounted; others need some specific actions
0566     {
0567       if ( deviceActions.isEmpty() )
0568       {
0569         //qDebug() << device.udi() << "no action found";
0570         return nullptr;
0571       }
0572       if ( access && access->isAccessible() && !QFileInfo(access->filePath()).isReadable() )
0573       {
0574         //qDebug() << device.udi() << access->filePath() << "not readable";
0575         return nullptr;
0576       }
0577     }
0578     else if ( (volume->usage() != Solid::StorageVolume::FileSystem) || volume->isIgnored() )
0579     {
0580       //qDebug() << device.udi() << (access ? access->filePath() : QString()) << "no filesystem or ignored";
0581       return nullptr;
0582     }
0583 
0584     // show only removable devices
0585     bool isRemovable = (device.is<Solid::StorageDrive>() &&
0586                         device.as<Solid::StorageDrive>()->isRemovable()) ||
0587                        (device.parent().is<Solid::StorageDrive>() &&
0588                         device.parent().as<Solid::StorageDrive>()->isRemovable());
0589 
0590     if ( !isRemovable )
0591     {
0592       //qDebug() << device.udi() << "not Removable";
0593       return nullptr;
0594     }
0595   }
0596 
0597   DeviceItem *item = new DeviceItem(device, deviceActions);
0598   vbox->insertWidget(vbox->count() - 1, item);  // insert before stretch
0599 
0600   items.insert(device.udi(), item);
0601   return item;
0602 }
0603 
0604 //--------------------------------------------------------------------------------
0605 
0606 void DeviceList::deviceAdded(const QString &dev)
0607 {
0608   Solid::Device device(dev);
0609 
0610   if ( !predicate.matches(device) )
0611     return;
0612 
0613   DeviceItem *item = addDevice(device);
0614 
0615   // when we added a new device, make sure the DeviceNotifier shows and places this window
0616   if ( item )
0617   {
0618     item->markAsNew();
0619     item->show();
0620     verticalScrollBar()->setValue(verticalScrollBar()->maximum());
0621 
0622     emit deviceWasAdded();
0623   }
0624 }
0625 
0626 //--------------------------------------------------------------------------------
0627 
0628 void DeviceList::deviceRemoved(const QString &dev)
0629 {
0630   if ( items.contains(dev) )
0631   {
0632     delete items.take(dev);
0633     emit deviceWasRemoved();
0634   }
0635 }
0636 
0637 //--------------------------------------------------------------------------------
0638 
0639 void DeviceList::kdeConnectDeviceAdded(const KdeConnect::Device &device)
0640 {
0641   if ( items.contains(device->id) )
0642   {
0643     //qDebug() << device->id << "already known";
0644     return;
0645   }
0646 
0647   DeviceItem *item = new DeviceItem(device);
0648   vbox->insertWidget(vbox->count() - 1, item);  // insert before stretch
0649 
0650   items.insert(device->id, item);
0651   item->markAsNew();
0652   item->show();
0653   verticalScrollBar()->setValue(verticalScrollBar()->maximum());
0654 
0655   // when we added a new device, make sure the DeviceNotifier shows and places this window
0656   emit deviceWasAdded();
0657 }
0658 
0659 //--------------------------------------------------------------------------------