File indexing completed on 2024-04-28 17:06:05

0001 /*
0002     SPDX-FileCopyrightText: 2006 Csaba Karai <cskarai@freemail.hu>
0003     SPDX-FileCopyrightText: 2006-2022 Krusader Krew <https://krusader.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "mediabutton.h"
0009 
0010 #include "../MountMan/kmountman.h"
0011 #include "../icon.h"
0012 #include "../krglobal.h"
0013 
0014 // QtCore
0015 #include <QEvent>
0016 // QtGui
0017 #include <QCursor>
0018 #include <QMouseEvent>
0019 
0020 #include <KConfigCore/KConfigGroup>
0021 #include <KConfigCore/KSharedConfig>
0022 #include <KI18n/KLocalizedString>
0023 #include <KIO/Global>
0024 #include <KIOCore/KMountPoint>
0025 #include <KWidgetsAddons/KMessageBox>
0026 
0027 #include <Solid/DeviceInterface>
0028 #include <Solid/DeviceNotifier>
0029 #include <Solid/OpticalDisc>
0030 #include <Solid/OpticalDrive>
0031 #include <Solid/StorageAccess>
0032 #include <Solid/StorageVolume>
0033 #include <utility>
0034 
0035 QString MediaButton::remotePrefix = QLatin1String("remote:");
0036 
0037 MediaButton::MediaButton(QWidget *parent)
0038     : QToolButton(parent)
0039     , popupMenu(nullptr)
0040     , rightMenu(nullptr)
0041     , openInNewTab(false)
0042 {
0043     setAutoRaise(true);
0044     setIcon(Icon("system-file-manager"));
0045     setText(i18n("Open the available media list"));
0046     setToolTip(i18n("Open the available media list"));
0047     setPopupMode(QToolButton::InstantPopup);
0048     setAcceptDrops(false);
0049 
0050     popupMenu = new QMenu(this);
0051     popupMenu->installEventFilter(this);
0052     Q_CHECK_PTR(popupMenu);
0053 
0054     setMenu(popupMenu);
0055 
0056     connect(popupMenu, &QMenu::aboutToShow, this, &MediaButton::slotAboutToShow);
0057     connect(popupMenu, &QMenu::aboutToHide, this, &MediaButton::slotAboutToHide);
0058     connect(popupMenu, &QMenu::triggered, this, &MediaButton::slotPopupActivated);
0059 
0060     Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance();
0061     connect(notifier, &Solid::DeviceNotifier::deviceAdded, this, &MediaButton::slotDeviceAdded);
0062     connect(notifier, &Solid::DeviceNotifier::deviceRemoved, this, &MediaButton::slotDeviceRemoved);
0063 
0064     connect(&mountCheckerTimer, &QTimer::timeout, this, &MediaButton::slotCheckMounts);
0065 }
0066 
0067 MediaButton::~MediaButton() = default;
0068 
0069 void MediaButton::updateIcon(const QString &mountPoint)
0070 {
0071     if (!mountPoint.isEmpty() && mountPoint == currentMountPoint)
0072         return;
0073 
0074     currentMountPoint = mountPoint;
0075 
0076     QString iconName("system-file-manager");
0077     QStringList overlays;
0078 
0079     if (!mountPoint.isEmpty()) {
0080         Solid::Device device(krMtMan.findUdiForPath(mountPoint, Solid::DeviceInterface::StorageAccess));
0081         auto *vol = device.as<Solid::StorageVolume>();
0082 
0083         if (device.isValid())
0084             iconName = device.icon();
0085         if (vol && vol->usage() == Solid::StorageVolume::Encrypted)
0086             overlays << "security-high";
0087     }
0088     setIcon(Icon(iconName, overlays));
0089 }
0090 
0091 void MediaButton::slotAboutToShow()
0092 {
0093     emit aboutToShow();
0094 
0095     popupMenu->clear();
0096     udiNameMap.clear();
0097 
0098     createMediaList();
0099 }
0100 
0101 void MediaButton::slotAboutToHide()
0102 {
0103     if (rightMenu)
0104         rightMenu->close();
0105     mountCheckerTimer.stop();
0106 }
0107 
0108 void MediaButton::createMediaList()
0109 {
0110     // devices detected by solid
0111     storageDevices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
0112 
0113     for (int p = storageDevices.count() - 1; p >= 0; p--) {
0114         Solid::Device device = storageDevices[p];
0115         QString udi = device.udi();
0116 
0117         QString name;
0118         QIcon kdeIcon;
0119         if (!getNameAndIcon(device, name, kdeIcon))
0120             continue;
0121 
0122         QAction *act = popupMenu->addAction(kdeIcon, name);
0123         act->setData(QVariant(udi));
0124         udiNameMap[udi] = name;
0125 
0126         connect(device.as<Solid::StorageAccess>(), &Solid::StorageAccess::accessibilityChanged, this, &MediaButton::slotAccessibilityChanged);
0127     }
0128 
0129     KMountPoint::List possibleMountList = KMountPoint::possibleMountPoints();
0130     KMountPoint::List currentMountList = KMountPoint::currentMountPoints();
0131 
0132     for (auto &it : possibleMountList) {
0133         if (krMtMan.networkFilesystem(it->mountType())) {
0134             bool mounted = false;
0135 
0136             for (auto &it2 : currentMountList) {
0137                 if (krMtMan.networkFilesystem(it2->mountType()) && it->mountPoint() == it2->mountPoint()) {
0138                     mounted = true;
0139                     break;
0140                 }
0141             }
0142 
0143             QString name = i18nc("%1 is the mount point of the remote share", "Remote Share [%1]", it->mountPoint());
0144             QStringList overlays;
0145             if (mounted)
0146                 overlays << "emblem-mounted";
0147             QAction *act = popupMenu->addAction(Icon("network-wired", overlays), name);
0148             QString udi = remotePrefix + it->mountPoint();
0149             act->setData(QVariant(udi));
0150         }
0151     }
0152 
0153     mountCheckerTimer.setSingleShot(true);
0154     mountCheckerTimer.start(1000);
0155 }
0156 
0157 bool MediaButton::getNameAndIcon(Solid::Device &device, QString &name, QIcon &iconOut)
0158 {
0159     auto *access = device.as<Solid::StorageAccess>();
0160     if (access == nullptr)
0161         return false;
0162 
0163     QString label = i18nc("Unknown label", "Unknown");
0164     bool mounted = access->isAccessible();
0165     QString path = access->filePath();
0166     QString type = i18nc("Unknown media type", "Unknown");
0167     QString iconName = device.icon();
0168     QString fstype;
0169     QString size;
0170 
0171     auto *vol = device.as<Solid::StorageVolume>();
0172     if (vol) {
0173         label = vol->label();
0174         fstype = vol->fsType();
0175         size = KIO::convertSize(vol->size());
0176     }
0177 
0178     bool printSize = false;
0179 
0180     if (iconName == "media-floppy")
0181         type = i18n("Floppy");
0182     else if (iconName == "drive-optical")
0183         type = i18n("CD/DVD-ROM");
0184     else if (iconName == "drive-removable-media-usb-pendrive")
0185         type = i18n("USB pen drive"), printSize = true;
0186     else if (iconName == "drive-removable-media-usb")
0187         type = i18n("USB device"), printSize = true;
0188     else if (iconName == "drive-removable-media")
0189         type = i18n("Removable media"), printSize = true;
0190     else if (iconName == "drive-harddisk")
0191         type = i18n("Hard Disk"), printSize = true;
0192     else if (iconName == "camera-photo")
0193         type = i18n("Camera");
0194     else if (iconName == "media-optical-video")
0195         type = i18n("Video CD/DVD-ROM");
0196     else if (iconName == "media-optical-audio")
0197         type = i18n("Audio CD/DVD-ROM");
0198     else if (iconName == "media-optical")
0199         type = i18n("Recordable CD/DVD-ROM");
0200 
0201     KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("MediaMenu"));
0202 
0203     if (fstype == "squashfs" && cfg.readEntry("HideSquashFS", false)) {
0204         return false;
0205     }
0206 
0207     if (cfg.readEntry("Hidden Mountpoints", QStringList()).contains(path)) {
0208         return false;
0209     }
0210 
0211     if (printSize) {
0212         QString showSizeSetting = cfg.readEntry("ShowSize", "Always");
0213         if (showSizeSetting == "WhenNoLabel")
0214             printSize = label.isEmpty();
0215         else if (showSizeSetting == "Never")
0216             printSize = false;
0217     }
0218 
0219     if (printSize && !size.isEmpty())
0220         name += size + ' ';
0221     if (!label.isEmpty())
0222         name += label + ' ';
0223     else
0224         name += type + ' ';
0225     if (!fstype.isEmpty() && cfg.readEntry("ShowFSType", true))
0226         name += '(' + fstype + ") ";
0227     if (!path.isEmpty() && cfg.readEntry("ShowPath", true))
0228         name += '[' + path + "] ";
0229 
0230     name = name.trimmed();
0231 
0232     QStringList overlays;
0233     if (mounted) {
0234         overlays << "emblem-mounted";
0235     } else {
0236         overlays << QString(); // We have to guarantee the placement of the next emblem
0237     }
0238     if (vol && vol->usage() == Solid::StorageVolume::Encrypted) {
0239         overlays << "security-high";
0240     }
0241     iconOut = Icon(iconName, overlays);
0242     return true;
0243 }
0244 
0245 void MediaButton::slotPopupActivated(QAction *action)
0246 {
0247     if (action && action->data().canConvert<QString>()) {
0248         QString udi = action->data().toString();
0249         if (!udi.isEmpty()) {
0250             bool mounted = false;
0251             QString mountPoint;
0252             getStatus(udi, mounted, &mountPoint);
0253 
0254             if (mounted)
0255                 emit openUrl(QUrl::fromLocalFile(mountPoint));
0256             else
0257                 mount(udi, true);
0258         }
0259     }
0260 }
0261 
0262 void MediaButton::showMenu()
0263 {
0264     QMenu *pP = menu();
0265     if (pP) {
0266         menu()->exec(mapToGlobal(QPoint(0, height())));
0267     }
0268 }
0269 
0270 bool MediaButton::eventFilter(QObject *o, QEvent *e)
0271 {
0272     if (o == popupMenu) {
0273         if (e->type() == QEvent::ContextMenu) {
0274             QAction *act = popupMenu->activeAction();
0275             if (act && act->data().canConvert<QString>()) {
0276                 QString id = act->data().toString();
0277                 if (!id.isEmpty()) {
0278                     QPoint globalPos = popupMenu->mapToGlobal(popupMenu->actionGeometry(act).topRight());
0279                     rightClickMenu(id, globalPos);
0280                     return true;
0281                 }
0282             }
0283         } else if (e->type() == QEvent::KeyPress) {
0284             auto *ke = dynamic_cast<QKeyEvent *>(e);
0285             if (ke->key() == Qt::Key_Return && ke->modifiers() == Qt::ControlModifier) {
0286                 if (QAction *act = popupMenu->activeAction()) {
0287                     QString id = act->data().toString();
0288                     if (!id.isEmpty()) {
0289                         toggleMount(id);
0290                         return true;
0291                     }
0292                 }
0293             }
0294         } else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) {
0295             auto *m = dynamic_cast<QMouseEvent *>(e);
0296             if (m->button() == Qt::RightButton) {
0297                 if (e->type() == QEvent::MouseButtonPress) {
0298                     QAction *act = popupMenu->actionAt(m->pos());
0299                     if (act && act->data().canConvert<QString>()) {
0300                         QString id = act->data().toString();
0301                         if (!id.isEmpty())
0302                             rightClickMenu(id, m->globalPos());
0303                     }
0304                 }
0305                 m->accept();
0306                 return true;
0307             }
0308         }
0309     }
0310     return false;
0311 }
0312 
0313 void MediaButton::rightClickMenu(const QString &udi, QPoint pos)
0314 {
0315     if (rightMenu)
0316         rightMenu->close();
0317 
0318     bool ejectable = false;
0319     bool mounted = false;
0320     QString mountPoint;
0321 
0322     getStatus(udi, mounted, &mountPoint, &ejectable);
0323 
0324     QUrl openURL = QUrl::fromLocalFile(mountPoint);
0325 
0326     QMenu *myMenu = rightMenu = new QMenu(popupMenu);
0327     QAction *actOpen = myMenu->addAction(i18n("Open"));
0328     actOpen->setData(QVariant(1));
0329     QAction *actOpenNewTab = myMenu->addAction(i18n("Open in a new tab"));
0330     actOpenNewTab->setData(QVariant(2));
0331 
0332     myMenu->addSeparator();
0333     if (!mounted) {
0334         QAction *actMount = myMenu->addAction(i18n("Mount"));
0335         actMount->setData(QVariant(3));
0336     } else {
0337         QAction *actUnmount = myMenu->addAction(i18n("Unmount"));
0338         actUnmount->setData(QVariant(4));
0339     }
0340     if (ejectable) {
0341         QAction *actEject = myMenu->addAction(i18n("Eject"));
0342         actEject->setData(QVariant(5));
0343     }
0344 
0345     QAction *act = myMenu->exec(pos);
0346     int result = -1;
0347     if (act != nullptr && act->data().canConvert<int>())
0348         result = act->data().toInt();
0349 
0350     delete myMenu;
0351     if (rightMenu == myMenu)
0352         rightMenu = nullptr;
0353     else
0354         return;
0355 
0356     switch (result) {
0357     case 1:
0358     case 2:
0359         popupMenu->close();
0360         if (mounted) {
0361             if (result == 1)
0362                 emit openUrl(openURL);
0363             else
0364                 emit newTab(openURL);
0365         } else {
0366             mount(udi, true, result == 2); // mount first, when mounted open the tab
0367         }
0368         break;
0369     case 3:
0370         mount(udi);
0371         break;
0372     case 4:
0373         umount(udi);
0374         break;
0375     case 5:
0376         eject(udi);
0377         break;
0378     default:
0379         break;
0380     }
0381 }
0382 
0383 void MediaButton::toggleMount(const QString &udi)
0384 {
0385     bool mounted = false;
0386     getStatus(udi, mounted);
0387 
0388     if (mounted)
0389         umount(udi);
0390     else
0391         mount(udi);
0392 }
0393 
0394 void MediaButton::getStatus(const QString &udi, bool &mounted, QString *mountPointOut, bool *ejectableOut)
0395 {
0396     mounted = false;
0397     bool network = udi.startsWith(remotePrefix);
0398     bool ejectable = false;
0399     QString mountPoint;
0400 
0401     if (network) {
0402         mountPoint = udi.mid(remotePrefix.length());
0403 
0404         KMountPoint::List currentMountList = KMountPoint::currentMountPoints();
0405         for (auto &it : currentMountList) {
0406             if (krMtMan.networkFilesystem(it->mountType()) && it->mountPoint() == mountPoint) {
0407                 mounted = true;
0408                 break;
0409             }
0410         }
0411     } else {
0412         Solid::Device device(udi);
0413 
0414         auto *access = device.as<Solid::StorageAccess>();
0415         auto *optdisc = device.as<Solid::OpticalDisc>();
0416         if (access)
0417             mountPoint = access->filePath();
0418         if (access && access->isAccessible())
0419             mounted = true;
0420         if (optdisc)
0421             ejectable = true;
0422     }
0423 
0424     if (mountPointOut)
0425         *mountPointOut = mountPoint;
0426     if (ejectableOut)
0427         *ejectableOut = ejectable;
0428 }
0429 
0430 void MediaButton::mount(const QString &udi, bool open, bool newtab)
0431 {
0432     if (udi.startsWith(remotePrefix)) {
0433         QString mp = udi.mid(remotePrefix.length());
0434         krMtMan.mount(mp, true);
0435         if (newtab)
0436             emit newTab(QUrl::fromLocalFile(mp));
0437         else
0438             emit openUrl(QUrl::fromLocalFile(mp));
0439         return;
0440     }
0441     Solid::Device device(udi);
0442     auto *access = device.as<Solid::StorageAccess>();
0443     if (access && !access->isAccessible()) {
0444         if (open)
0445             udiToOpen = device.udi(), openInNewTab = newtab;
0446         connect(access, &Solid::StorageAccess::setupDone, this, &MediaButton::slotSetupDone);
0447         connect(access, &Solid::StorageAccess::teardownRequested, &krMtMan, &KMountMan::slotTeardownRequested, Qt::UniqueConnection);
0448         access->setup();
0449     }
0450 }
0451 
0452 void MediaButton::slotSetupDone(Solid::ErrorType error, const QVariant &errorData, const QString &udi)
0453 {
0454     if (error == Solid::NoError) {
0455         if (udi == udiToOpen) {
0456             auto *access = Solid::Device(udi).as<Solid::StorageAccess>();
0457             if (access && access->isAccessible()) {
0458                 if (openInNewTab)
0459                     emit newTab(QUrl::fromLocalFile(access->filePath()));
0460                 else
0461                     emit openUrl(QUrl::fromLocalFile(access->filePath()));
0462             }
0463             udiToOpen = QString(), openInNewTab = false;
0464         }
0465     } else {
0466         if (udi == udiToOpen)
0467             udiToOpen = QString(), openInNewTab = false;
0468         QString name;
0469         if (udiNameMap.contains(udi))
0470             name = udiNameMap[udi];
0471 
0472         if (errorData.isValid()) {
0473             KMessageBox::error(this, i18n("An error occurred while accessing '%1', the system responded: %2", name, errorData.toString()));
0474         } else {
0475             KMessageBox::error(this, i18n("An error occurred while accessing '%1'", name));
0476         }
0477     }
0478 }
0479 
0480 void MediaButton::umount(const QString &udi)
0481 {
0482     if (udi.startsWith(remotePrefix)) {
0483         krMtMan.unmount(udi.mid(remotePrefix.length()), false);
0484         return;
0485     }
0486     krMtMan.unmount(krMtMan.pathForUdi(udi), false);
0487 }
0488 
0489 void MediaButton::eject(QString udi)
0490 {
0491     krMtMan.eject(krMtMan.pathForUdi(std::move(udi)));
0492 }
0493 
0494 void MediaButton::slotAccessibilityChanged(bool /*accessible*/, const QString &udi)
0495 {
0496     QList<QAction *> actionList = popupMenu->actions();
0497     foreach (QAction *act, actionList) {
0498         if (act && act->data().canConvert<QString>() && act->data().toString() == udi) {
0499             Solid::Device device(udi);
0500 
0501             QString name;
0502             QIcon kdeIcon;
0503             if (getNameAndIcon(device, name, kdeIcon)) {
0504                 act->setText(name);
0505                 act->setIcon(kdeIcon);
0506             }
0507             break;
0508         }
0509     }
0510 }
0511 
0512 void MediaButton::slotDeviceAdded(const QString &udi)
0513 {
0514     if (popupMenu->isHidden())
0515         return;
0516 
0517     Solid::Device device(udi);
0518     auto *access = device.as<Solid::StorageAccess>();
0519     if (access == nullptr)
0520         return;
0521 
0522     QString name;
0523     QIcon kdeIcon;
0524     if (!getNameAndIcon(device, name, kdeIcon))
0525         return;
0526 
0527     QAction *act = popupMenu->addAction(kdeIcon, name);
0528     act->setData(QVariant(udi));
0529     udiNameMap[udi] = name;
0530 
0531     connect(device.as<Solid::StorageAccess>(), &Solid::StorageAccess::accessibilityChanged, this, &MediaButton::slotAccessibilityChanged);
0532 }
0533 
0534 void MediaButton::slotDeviceRemoved(const QString &udi)
0535 {
0536     if (popupMenu->isHidden())
0537         return;
0538 
0539     QList<QAction *> actionList = popupMenu->actions();
0540     foreach (QAction *act, actionList) {
0541         if (act && act->data().canConvert<QString>() && act->data().toString() == udi) {
0542             popupMenu->removeAction(act);
0543             delete act;
0544             break;
0545         }
0546     }
0547 }
0548 
0549 void MediaButton::slotCheckMounts()
0550 {
0551     if (isHidden())
0552         return;
0553 
0554     KMountPoint::List possibleMountList = KMountPoint::possibleMountPoints();
0555     KMountPoint::List currentMountList = KMountPoint::currentMountPoints();
0556     QList<QAction *> actionList = popupMenu->actions();
0557 
0558     foreach (QAction *act, actionList) {
0559         if (act && act->data().canConvert<QString>() && act->data().toString().startsWith(remotePrefix)) {
0560             QString mountPoint = act->data().toString().mid(remotePrefix.length());
0561             bool available = false;
0562 
0563             for (auto &it : possibleMountList) {
0564                 if (krMtMan.networkFilesystem(it->mountType()) && it->mountPoint() == mountPoint) {
0565                     available = true;
0566                     break;
0567                 }
0568             }
0569 
0570             if (!available) {
0571                 popupMenu->removeAction(act);
0572                 delete act;
0573             }
0574         }
0575     }
0576 
0577     for (auto &it : possibleMountList) {
0578         if (krMtMan.networkFilesystem(it->mountType())) {
0579             QString path = it->mountPoint();
0580             bool mounted = false;
0581             QString udi = remotePrefix + path;
0582 
0583             QAction *correspondingAct = nullptr;
0584             foreach (QAction *act, actionList) {
0585                 if (act && act->data().canConvert<QString>() && act->data().toString() == udi) {
0586                     correspondingAct = act;
0587                     break;
0588                 }
0589             }
0590             for (auto &it2 : currentMountList) {
0591                 if (krMtMan.networkFilesystem(it2->mountType()) && path == it2->mountPoint()) {
0592                     mounted = true;
0593                     break;
0594                 }
0595             }
0596 
0597             QString name = i18nc("%1 is the mount point of the remote share", "Remote Share [%1]", it->mountPoint());
0598             QStringList overlays;
0599             if (mounted)
0600                 overlays << "emblem-mounted";
0601             QIcon kdeIcon = Icon("network-wired", overlays);
0602 
0603             if (!correspondingAct) {
0604                 QAction *act = popupMenu->addAction(kdeIcon, name);
0605                 act->setData(QVariant(udi));
0606             } else {
0607                 correspondingAct->setText(name);
0608                 correspondingAct->setIcon(kdeIcon);
0609             }
0610         }
0611     }
0612 
0613     mountCheckerTimer.setSingleShot(true);
0614     mountCheckerTimer.start(1000);
0615 }