File indexing completed on 2024-04-21 05:45:41

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2020 Felix Ernst <felixernst@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "dolphinnavigatorswidgetaction.h"
0009 
0010 #include "trash/dolphintrash.h"
0011 
0012 #include <KLocalizedString>
0013 #include <KNotificationJobUiDelegate>
0014 #include <KService>
0015 
0016 #include <KIO/ApplicationLauncherJob>
0017 
0018 #include <QApplication>
0019 #include <QHBoxLayout>
0020 #include <QPushButton>
0021 #include <QToolBar>
0022 
0023 #include <limits>
0024 
0025 DolphinNavigatorsWidgetAction::DolphinNavigatorsWidgetAction(QWidget *parent)
0026     : QWidgetAction{parent}
0027     , m_splitter{new QSplitter(Qt::Horizontal)}
0028     , m_adjustSpacingTimer{new QTimer(this)}
0029     , m_viewGeometriesHelper{m_splitter.get(), this}
0030 {
0031     updateText();
0032     setIcon(QIcon::fromTheme(QStringLiteral("dialog-scripts")));
0033 
0034     m_splitter->setChildrenCollapsible(false);
0035 
0036     m_splitter->addWidget(createNavigatorWidget(Primary));
0037 
0038     m_adjustSpacingTimer->setInterval(100);
0039     m_adjustSpacingTimer->setSingleShot(true);
0040     connect(m_adjustSpacingTimer.get(), &QTimer::timeout, this, &DolphinNavigatorsWidgetAction::adjustSpacing);
0041 }
0042 
0043 void DolphinNavigatorsWidgetAction::adjustSpacing()
0044 {
0045     m_previousWindowWidth = qobject_cast<QWidget *>(parent())->window()->width();
0046     auto viewGeometries = m_viewGeometriesHelper.viewGeometries();
0047     const int widthOfSplitterPrimary = viewGeometries.globalXOfPrimary + viewGeometries.widthOfPrimary - viewGeometries.globalXOfNavigatorsWidget;
0048     const QList<int> splitterSizes = {widthOfSplitterPrimary, m_splitter->width() - widthOfSplitterPrimary - m_splitter->handleWidth()};
0049     m_splitter->setSizes(splitterSizes);
0050 
0051     // primary side of m_splitter
0052     int leadingSpacing = viewGeometries.globalXOfPrimary - viewGeometries.globalXOfNavigatorsWidget;
0053     if (leadingSpacing < 0) {
0054         leadingSpacing = 0;
0055     }
0056     int trailingSpacing = (viewGeometries.globalXOfNavigatorsWidget + m_splitter->width()) - (viewGeometries.globalXOfPrimary + viewGeometries.widthOfPrimary);
0057     if (trailingSpacing < 0 || emptyTrashButton(Primary)->isVisible() || networkFolderButton(Primary)->isVisible()) {
0058         trailingSpacing = 0;
0059     }
0060     const int widthLeftForUrlNavigator = m_splitter->widget(0)->width() - leadingSpacing - trailingSpacing;
0061     const int widthNeededForUrlNavigator = primaryUrlNavigator()->sizeHint().width() - widthLeftForUrlNavigator;
0062     if (widthNeededForUrlNavigator > 0) {
0063         trailingSpacing -= widthNeededForUrlNavigator;
0064         if (trailingSpacing < 0) {
0065             leadingSpacing += trailingSpacing;
0066             trailingSpacing = 0;
0067         }
0068         if (leadingSpacing < 0) {
0069             leadingSpacing = 0;
0070         }
0071     }
0072     spacing(Primary, Leading)->setMinimumWidth(leadingSpacing);
0073     spacing(Primary, Trailing)->setFixedWidth(trailingSpacing);
0074 
0075     // secondary side of m_splitter
0076     if (viewGeometries.globalXOfSecondary == INT_MIN) {
0077         Q_ASSERT(viewGeometries.widthOfSecondary == INT_MIN);
0078         return;
0079     }
0080     spacing(Primary, Trailing)->setFixedWidth(0);
0081 
0082     trailingSpacing = (viewGeometries.globalXOfNavigatorsWidget + m_splitter->width()) - (viewGeometries.globalXOfSecondary + viewGeometries.widthOfSecondary);
0083     if (trailingSpacing < 0 || emptyTrashButton(Secondary)->isVisible() || networkFolderButton(Secondary)->isVisible()) {
0084         trailingSpacing = 0;
0085     } else {
0086         const int widthLeftForUrlNavigator2 = m_splitter->widget(1)->width() - trailingSpacing;
0087         const int widthNeededForUrlNavigator2 = secondaryUrlNavigator()->sizeHint().width() - widthLeftForUrlNavigator2;
0088         if (widthNeededForUrlNavigator2 > 0) {
0089             trailingSpacing -= widthNeededForUrlNavigator2;
0090             if (trailingSpacing < 0) {
0091                 trailingSpacing = 0;
0092             }
0093         }
0094     }
0095     spacing(Secondary, Trailing)->setMinimumWidth(trailingSpacing);
0096 }
0097 
0098 void DolphinNavigatorsWidgetAction::createSecondaryUrlNavigator()
0099 {
0100     Q_ASSERT(m_splitter->count() == 1);
0101     m_splitter->addWidget(createNavigatorWidget(Secondary));
0102     Q_ASSERT(m_splitter->count() == 2);
0103     updateText();
0104 }
0105 
0106 void DolphinNavigatorsWidgetAction::followViewContainersGeometry(QWidget *primaryViewContainer, QWidget *secondaryViewContainer)
0107 {
0108     m_viewGeometriesHelper.setViewContainers(primaryViewContainer, secondaryViewContainer);
0109     adjustSpacing();
0110 }
0111 
0112 bool DolphinNavigatorsWidgetAction::isInToolbar() const
0113 {
0114     return qobject_cast<QToolBar *>(m_splitter->parentWidget());
0115 }
0116 
0117 DolphinUrlNavigator *DolphinNavigatorsWidgetAction::primaryUrlNavigator() const
0118 {
0119     Q_ASSERT(m_splitter);
0120     return m_splitter->widget(0)->findChild<DolphinUrlNavigator *>();
0121 }
0122 
0123 DolphinUrlNavigator *DolphinNavigatorsWidgetAction::secondaryUrlNavigator() const
0124 {
0125     Q_ASSERT(m_splitter);
0126     if (m_splitter->count() < 2) {
0127         return nullptr;
0128     }
0129     return m_splitter->widget(1)->findChild<DolphinUrlNavigator *>();
0130 }
0131 
0132 void DolphinNavigatorsWidgetAction::setSecondaryNavigatorVisible(bool visible)
0133 {
0134     if (visible) {
0135         Q_ASSERT(m_splitter->count() == 2);
0136         m_splitter->widget(1)->setVisible(true);
0137     } else if (m_splitter->count() > 1) {
0138         m_splitter->widget(1)->setVisible(false);
0139         // Fix an unlikely event of wrong trash button visibility.
0140         emptyTrashButton(Secondary)->setVisible(false);
0141     }
0142     updateText();
0143 }
0144 
0145 QWidget *DolphinNavigatorsWidgetAction::createWidget(QWidget *parent)
0146 {
0147     QWidget *oldParent = m_splitter->parentWidget();
0148     if (oldParent && oldParent->layout()) {
0149         oldParent->layout()->removeWidget(m_splitter.get());
0150         QGridLayout *layout = qobject_cast<QGridLayout *>(oldParent->layout());
0151         if (qobject_cast<QToolBar *>(parent) && layout) {
0152             // in DolphinTabPage::insertNavigatorsWidget the minimumHeight of this row was
0153             // set to fit the m_splitter. Since we are now removing it again, the
0154             // minimumHeight can be reset to 0.
0155             layout->setRowMinimumHeight(0, 0);
0156         }
0157     }
0158     m_splitter->setParent(parent);
0159     return m_splitter.get();
0160 }
0161 
0162 void DolphinNavigatorsWidgetAction::deleteWidget(QWidget *widget)
0163 {
0164     Q_UNUSED(widget)
0165     m_splitter->setParent(nullptr);
0166 }
0167 
0168 QWidget *DolphinNavigatorsWidgetAction::createNavigatorWidget(Side side) const
0169 {
0170     auto navigatorWidget = new QWidget(m_splitter.get());
0171     auto layout = new QHBoxLayout{navigatorWidget};
0172     layout->setSpacing(0);
0173     layout->setContentsMargins(0, 0, 0, 0);
0174     if (side == Primary) {
0175         auto leadingSpacing = new QWidget{navigatorWidget};
0176         layout->addWidget(leadingSpacing);
0177     }
0178     auto urlNavigator = new DolphinUrlNavigator(navigatorWidget);
0179     layout->addWidget(urlNavigator);
0180 
0181     auto emptyTrashButton = newEmptyTrashButton(urlNavigator, navigatorWidget);
0182     layout->addWidget(emptyTrashButton);
0183 
0184     auto networkFolderButton = newNetworkFolderButton(urlNavigator, navigatorWidget);
0185     layout->addWidget(networkFolderButton);
0186 
0187     connect(
0188         urlNavigator,
0189         &KUrlNavigator::urlChanged,
0190         this,
0191         [urlNavigator, this]() {
0192             // Update URL navigator to show a server URL entry placeholder text if we
0193             // just loaded the remote:/ page, to make it easier for users to figure out
0194             // that they can enter arbitrary remote URLs. See bug 414670
0195             if (urlNavigator->locationUrl().scheme() == QLatin1String("remote")) {
0196                 if (!urlNavigator->isUrlEditable()) {
0197                     urlNavigator->setUrlEditable(true);
0198                 }
0199                 urlNavigator->clearText();
0200                 urlNavigator->setPlaceholderText(i18n("Enter server URL (e.g. smb://[ip address])"));
0201             } else {
0202                 urlNavigator->setPlaceholderText(QString());
0203             }
0204 
0205             // We have to wait for DolphinUrlNavigator::sizeHint() to update which
0206             // happens a little bit later than when urlChanged is emitted.
0207             this->m_adjustSpacingTimer->start();
0208         },
0209         Qt::QueuedConnection);
0210 
0211     auto trailingSpacing = new QWidget{navigatorWidget};
0212     layout->addWidget(trailingSpacing);
0213     return navigatorWidget;
0214 }
0215 
0216 QPushButton *DolphinNavigatorsWidgetAction::emptyTrashButton(DolphinNavigatorsWidgetAction::Side side)
0217 {
0218     int sideIndex = (side == Primary ? 0 : 1);
0219     if (side == Primary) {
0220         return static_cast<QPushButton *>(m_splitter->widget(sideIndex)->layout()->itemAt(2)->widget());
0221     }
0222     return static_cast<QPushButton *>(m_splitter->widget(sideIndex)->layout()->itemAt(1)->widget());
0223 }
0224 
0225 QPushButton *DolphinNavigatorsWidgetAction::newEmptyTrashButton(const DolphinUrlNavigator *urlNavigator, QWidget *parent) const
0226 {
0227     auto emptyTrashButton = new QPushButton(QIcon::fromTheme(QStringLiteral("user-trash")), i18nc("@action:button", "Empty Trash"), parent);
0228     emptyTrashButton->setToolTip(i18n("Empties Trash to create free space"));
0229 
0230     emptyTrashButton->setFlat(true);
0231     connect(emptyTrashButton, &QPushButton::clicked, this, [parent]() {
0232         Trash::empty(parent);
0233     });
0234     connect(&Trash::instance(), &Trash::emptinessChanged, emptyTrashButton, &QPushButton::setDisabled);
0235     emptyTrashButton->hide();
0236     connect(urlNavigator, &KUrlNavigator::urlChanged, this, [emptyTrashButton, urlNavigator]() {
0237         emptyTrashButton->setVisible(urlNavigator->locationUrl().scheme() == QLatin1String("trash"));
0238     });
0239     emptyTrashButton->setDisabled(Trash::isEmpty());
0240     return emptyTrashButton;
0241 }
0242 
0243 QPushButton *DolphinNavigatorsWidgetAction::networkFolderButton(DolphinNavigatorsWidgetAction::Side side)
0244 {
0245     int sideIndex = (side == Primary ? 0 : 1);
0246     if (side == Primary) {
0247         return static_cast<QPushButton *>(m_splitter->widget(sideIndex)->layout()->itemAt(3)->widget());
0248     }
0249     return static_cast<QPushButton *>(m_splitter->widget(sideIndex)->layout()->itemAt(2)->widget());
0250 }
0251 
0252 QPushButton *DolphinNavigatorsWidgetAction::newNetworkFolderButton(const DolphinUrlNavigator *urlNavigator, QWidget *parent) const
0253 {
0254     auto networkFolderButton = new QPushButton(QIcon::fromTheme(QStringLiteral("folder-add")), i18nc("@action:button", "Add Network Folder"), parent);
0255     networkFolderButton->setFlat(true);
0256     KService::Ptr service = KService::serviceByDesktopName(QStringLiteral("org.kde.knetattach"));
0257     connect(networkFolderButton, &QPushButton::clicked, this, [networkFolderButton, service]() {
0258         auto *job = new KIO::ApplicationLauncherJob(service, networkFolderButton);
0259         auto *delegate = new KNotificationJobUiDelegate;
0260         delegate->setAutoErrorHandlingEnabled(true);
0261         job->setUiDelegate(delegate);
0262         job->start();
0263     });
0264     networkFolderButton->hide();
0265     connect(urlNavigator, &KUrlNavigator::urlChanged, this, [networkFolderButton, urlNavigator, service]() {
0266         networkFolderButton->setVisible(service && urlNavigator->locationUrl().scheme() == QLatin1String("remote"));
0267     });
0268     return networkFolderButton;
0269 }
0270 
0271 QWidget *DolphinNavigatorsWidgetAction::spacing(Side side, Position position) const
0272 {
0273     int sideIndex = (side == Primary ? 0 : 1);
0274     if (position == Leading) {
0275         Q_ASSERT(side == Primary); // The secondary side of the splitter has no leading spacing.
0276         return m_splitter->widget(sideIndex)->layout()->itemAt(0)->widget();
0277     }
0278     if (side == Primary) {
0279         return m_splitter->widget(sideIndex)->layout()->itemAt(4)->widget();
0280     }
0281     return m_splitter->widget(sideIndex)->layout()->itemAt(3)->widget();
0282 }
0283 
0284 void DolphinNavigatorsWidgetAction::updateText()
0285 {
0286     const int urlNavigatorsAmount = m_splitter->count() > 1 && m_splitter->widget(1)->isVisible() ? 2 : 1;
0287     setText(i18ncp("@action:inmenu", "Location Bar", "Location Bars", urlNavigatorsAmount));
0288 }
0289 
0290 DolphinNavigatorsWidgetAction::ViewGeometriesHelper::ViewGeometriesHelper(QWidget *navigatorsWidget, DolphinNavigatorsWidgetAction *navigatorsWidgetAction)
0291     : m_navigatorsWidget{navigatorsWidget}
0292     , m_navigatorsWidgetAction{navigatorsWidgetAction}
0293 {
0294     Q_CHECK_PTR(navigatorsWidget);
0295     Q_CHECK_PTR(navigatorsWidgetAction);
0296 }
0297 
0298 bool DolphinNavigatorsWidgetAction::ViewGeometriesHelper::eventFilter(QObject *watched, QEvent *event)
0299 {
0300     if (event->type() == QEvent::Resize) {
0301         if (qobject_cast<QWidget*>(m_navigatorsWidgetAction->parent())->window()->width() != m_navigatorsWidgetAction->m_previousWindowWidth) {
0302             // The window is being resized which means not all widgets have gotten their new sizes yet.
0303             // Let's wait a bit so the sizes of the navigatorsWidget and the viewContainers have all
0304             // had a chance to be updated.
0305             m_navigatorsWidgetAction->m_adjustSpacingTimer->start();
0306         } else {
0307             m_navigatorsWidgetAction->adjustSpacing();
0308             // We could always use the m_adjustSpacingTimer instead of calling adjustSpacing() directly
0309             // here but then the navigatorsWidget doesn't fluently align with the viewContainers when
0310             // the DolphinTabPage::m_expandViewAnimation is animating.
0311         }
0312         return false;
0313     }
0314     return QObject::eventFilter(watched, event);
0315 }
0316 
0317 void DolphinNavigatorsWidgetAction::ViewGeometriesHelper::setViewContainers(QWidget *primaryViewContainer, QWidget *secondaryViewContainer)
0318 {
0319     Q_CHECK_PTR(primaryViewContainer);
0320     if (m_primaryViewContainer) {
0321         m_primaryViewContainer->removeEventFilter(this);
0322     }
0323     primaryViewContainer->installEventFilter(this);
0324     m_primaryViewContainer = primaryViewContainer;
0325 
0326     // It is not possible to resize the secondaryViewContainer without simultaneously
0327     // resizing the primaryViewContainer so we don't have to installEventFilter() here.
0328     m_secondaryViewContainer = secondaryViewContainer;
0329 }
0330 
0331 DolphinNavigatorsWidgetAction::ViewGeometriesHelper::Geometries DolphinNavigatorsWidgetAction::ViewGeometriesHelper::viewGeometries()
0332 {
0333     Q_ASSERT(m_primaryViewContainer);
0334     Geometries geometries;
0335 
0336     // width
0337     geometries.widthOfPrimary = m_primaryViewContainer->width();
0338     if (m_secondaryViewContainer) {
0339         geometries.widthOfSecondary = m_secondaryViewContainer->width();
0340     } else {
0341         geometries.widthOfSecondary = INT_MIN;
0342     }
0343 
0344     // globalX
0345     if (QApplication::layoutDirection() == Qt::LeftToRight) {
0346         geometries.globalXOfNavigatorsWidget = m_navigatorsWidget->mapToGlobal(QPoint(0, 0)).x();
0347         geometries.globalXOfPrimary = m_primaryViewContainer->mapToGlobal(QPoint(0, 0)).x();
0348         geometries.globalXOfSecondary = !m_secondaryViewContainer ? INT_MIN : m_secondaryViewContainer->mapToGlobal(QPoint(0, 0)).x();
0349     } else {
0350         // When the direction is reversed, globalX does not change.
0351         // For the adjustSpacing() code to work we need globalX to measure from right to left
0352         // and to measure up to the rightmost point of a widget instead of the leftmost.
0353         geometries.globalXOfNavigatorsWidget = (-1) * (m_navigatorsWidget->mapToGlobal(QPoint(0, 0)).x() + m_navigatorsWidget->width());
0354         geometries.globalXOfPrimary = (-1) * (m_primaryViewContainer->mapToGlobal(QPoint(0, 0)).x() + geometries.widthOfPrimary);
0355         geometries.globalXOfSecondary =
0356             !m_secondaryViewContainer ? INT_MIN : (-1) * (m_secondaryViewContainer->mapToGlobal(QPoint(0, 0)).x() + geometries.widthOfSecondary);
0357     }
0358     return geometries;
0359 }
0360 
0361 #include "moc_dolphinnavigatorswidgetaction.cpp"