File indexing completed on 2024-09-15 03:38:43
0001 // -*- c++ -*- 0002 /* 0003 This file is part of the KDE libraries 0004 SPDX-FileCopyrightText: 1997, 1998 Richard Moore <rich@kde.org> 0005 SPDX-FileCopyrightText: 1998 Stephan Kulow <coolo@kde.org> 0006 SPDX-FileCopyrightText: 1998 Daniel Grana <grana@ie.iwi.unibe.ch> 0007 SPDX-FileCopyrightText: 1999, 2000, 2001, 2002, 2003 Carsten Pfeiffer <pfeiffer@kde.org> 0008 SPDX-FileCopyrightText: 2003 Clarence Dang <dang@kde.org> 0009 SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org> 0010 SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org> 0011 0012 SPDX-License-Identifier: LGPL-2.0-or-later 0013 */ 0014 0015 #include "kfilewidget.h" 0016 0017 #include "../utils_p.h" 0018 #include "kfilebookmarkhandler_p.h" 0019 #include "kfileplacesmodel.h" 0020 #include "kfileplacesview.h" 0021 #include "kfilepreviewgenerator.h" 0022 #include "kfilewidgetdocktitlebar_p.h" 0023 #include "kurlcombobox.h" 0024 #include "kurlnavigator.h" 0025 0026 #include <config-kiofilewidgets.h> 0027 0028 #include <defaults-kfile.h> 0029 #include <kdiroperator.h> 0030 #include <kfilefiltercombo.h> 0031 #include <kfileitemdelegate.h> 0032 #include <kio/job.h> 0033 #include <kio/jobuidelegate.h> 0034 #include <kio/statjob.h> 0035 #include <kprotocolmanager.h> 0036 #include <krecentdirs.h> 0037 #include <krecentdocument.h> 0038 #include <kurlauthorized.h> 0039 #include <kurlcompletion.h> 0040 0041 #include <KActionMenu> 0042 #include <KConfigGroup> 0043 #include <KDirLister> 0044 #include <KFileItem> 0045 #include <KFilePlacesModel> 0046 #include <KIconLoader> 0047 #include <KJobWidgets> 0048 #include <KLocalizedString> 0049 #include <KMessageBox> 0050 #include <KMessageWidget> 0051 #include <KSharedConfig> 0052 #include <KShell> 0053 #include <KStandardAction> 0054 0055 #include <QAbstractProxyModel> 0056 #include <QApplication> 0057 #include <QCheckBox> 0058 #include <QDebug> 0059 #include <QDockWidget> 0060 #include <QFormLayout> 0061 #include <QHelpEvent> 0062 #include <QIcon> 0063 #include <QLabel> 0064 #include <QLayout> 0065 #include <QLineEdit> 0066 #include <QLoggingCategory> 0067 #include <QMenu> 0068 #include <QMimeDatabase> 0069 #include <QPushButton> 0070 #include <QScreen> 0071 #include <QSplitter> 0072 #include <QStandardPaths> 0073 #include <QTimer> 0074 #include <QToolBar> 0075 0076 #include <algorithm> 0077 #include <array> 0078 0079 Q_DECLARE_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW) 0080 Q_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW, "kf.kio.kfilewidgets.kfilewidget", QtInfoMsg) 0081 0082 class KFileWidgetPrivate 0083 { 0084 public: 0085 explicit KFileWidgetPrivate(KFileWidget *qq) 0086 : q(qq) 0087 { 0088 } 0089 0090 ~KFileWidgetPrivate() 0091 { 0092 delete m_bookmarkHandler; // Should be deleted before m_ops! 0093 // Must be deleted before m_ops, otherwise the unit test crashes due to the 0094 // connection to the QDockWidget::visibilityChanged signal, which may get 0095 // emitted after this object is destroyed 0096 delete m_placesDock; 0097 delete m_ops; 0098 } 0099 0100 QSize screenSize() const 0101 { 0102 return q->parentWidget() ? q->parentWidget()->screen()->availableGeometry().size() // 0103 : QGuiApplication::primaryScreen()->availableGeometry().size(); 0104 } 0105 0106 void initDirOpWidgets(); 0107 void initToolbar(); 0108 void initZoomWidget(); 0109 void initLocationWidget(); 0110 void initFilterWidget(); 0111 void updateLocationWhatsThis(); 0112 void updateAutoSelectExtension(); 0113 void initPlacesPanel(); 0114 void setPlacesViewSplitterSizes(); 0115 void initGUI(); 0116 void readViewConfig(); 0117 void writeViewConfig(); 0118 void setNonExtSelection(); 0119 void setLocationText(const QUrl &); 0120 void setLocationText(const QList<QUrl> &); 0121 void appendExtension(QUrl &url); 0122 void updateLocationEditExtension(const QString &); 0123 QString findMatchingFilter(const QString &filter, const QString &filename) const; 0124 void updateFilter(); 0125 void updateFilterText(); 0126 /** 0127 * Parses the string "line" for files. If line doesn't contain any ", the 0128 * whole line will be interpreted as one file. If the number of " is odd, 0129 * an empty list will be returned. Otherwise, all items enclosed in " " 0130 * will be returned as correct urls. 0131 */ 0132 QList<QUrl> tokenize(const QString &line) const; 0133 /** 0134 * Reads the recent used files and inserts them into the location combobox 0135 */ 0136 void readRecentFiles(); 0137 /** 0138 * Saves the entries from the location combobox. 0139 */ 0140 void saveRecentFiles(); 0141 /** 0142 * called when an item is highlighted/selected in multiselection mode. 0143 * handles setting the m_locationEdit. 0144 */ 0145 void multiSelectionChanged(); 0146 0147 /** 0148 * Returns the absolute version of the URL specified in m_locationEdit. 0149 */ 0150 QUrl getCompleteUrl(const QString &) const; 0151 0152 /** 0153 * Asks for overwrite confirmation using a KMessageBox and returns 0154 * true if the user accepts. 0155 * 0156 */ 0157 bool toOverwrite(const QUrl &); 0158 0159 // private slots 0160 void slotLocationChanged(const QString &); 0161 void urlEntered(const QUrl &); 0162 void enterUrl(const QUrl &); 0163 void enterUrl(const QString &); 0164 void locationAccepted(const QString &); 0165 void slotFilterChanged(); 0166 void fileHighlighted(const KFileItem &); 0167 void fileSelected(const KFileItem &); 0168 void slotLoadingFinished(); 0169 void togglePlacesPanel(bool show, QObject *sender = nullptr); 0170 void toggleBookmarks(bool); 0171 void slotAutoSelectExtClicked(); 0172 void placesViewSplitterMoved(int, int); 0173 void activateUrlNavigator(); 0174 0175 enum ZoomState { 0176 ZoomOut, 0177 ZoomIn, 0178 }; 0179 void changeIconsSize(ZoomState zoom); 0180 void slotDirOpIconSizeChanged(int size); 0181 void slotIconSizeSliderMoved(int); 0182 void slotIconSizeChanged(int); 0183 void slotViewDoubleClicked(const QModelIndex &); 0184 void slotViewKeyEnterReturnPressed(); 0185 0186 void addToRecentDocuments(); 0187 0188 QString locationEditCurrentText() const; 0189 0190 /** 0191 * KIO::NetAccess::mostLocalUrl local replacement. 0192 * This method won't show any progress dialogs for stating, since 0193 * they are very annoying when stating. 0194 */ 0195 QUrl mostLocalUrl(const QUrl &url); 0196 0197 void setInlinePreviewShown(bool show); 0198 0199 KFileWidget *const q; 0200 0201 // the last selected url 0202 QUrl m_url; 0203 0204 // now following all kind of widgets, that I need to rebuild 0205 // the geometry management 0206 QBoxLayout *m_boxLayout = nullptr; 0207 QFormLayout *m_lafBox = nullptr; 0208 0209 QLabel *m_locationLabel = nullptr; 0210 QWidget *m_opsWidget = nullptr; 0211 QVBoxLayout *m_opsWidgetLayout = nullptr; 0212 0213 QLabel *m_filterLabel = nullptr; 0214 KUrlNavigator *m_urlNavigator = nullptr; 0215 KMessageWidget *m_messageWidget = nullptr; 0216 QPushButton *m_okButton = nullptr; 0217 QPushButton *m_cancelButton = nullptr; 0218 QDockWidget *m_placesDock = nullptr; 0219 KFilePlacesView *m_placesView = nullptr; 0220 QSplitter *m_placesViewSplitter = nullptr; 0221 // caches the places view width. This value will be updated when the splitter 0222 // is moved. This allows us to properly set a value when the dialog itself 0223 // is resized 0224 int m_placesViewWidth = -1; 0225 0226 QWidget *m_labeledCustomWidget = nullptr; 0227 QWidget *m_bottomCustomWidget = nullptr; 0228 0229 // Automatically Select Extension stuff 0230 QCheckBox *m_autoSelectExtCheckBox = nullptr; 0231 QString m_extension; // current extension for this filter 0232 0233 QList<QUrl> m_urlList; // the list of selected urls 0234 0235 KFileWidget::OperationMode m_operationMode = KFileWidget::Opening; 0236 0237 // The file class used for KRecentDirs 0238 QString m_fileClass; 0239 0240 KFileBookmarkHandler *m_bookmarkHandler = nullptr; 0241 0242 KActionMenu *m_bookmarkButton = nullptr; 0243 0244 QToolBar *m_toolbar = nullptr; 0245 KUrlComboBox *m_locationEdit = nullptr; 0246 KDirOperator *m_ops = nullptr; 0247 KFileFilterCombo *m_filterWidget = nullptr; 0248 QTimer m_filterDelayTimer; 0249 0250 KFilePlacesModel *m_model = nullptr; 0251 0252 // whether or not the _user_ has checked the above box 0253 bool m_autoSelectExtChecked = false; 0254 0255 // indicates if the location edit should be kept or cleared when changing 0256 // directories 0257 bool m_keepLocation = false; 0258 0259 // the KDirOperators view is set in KFileWidget::show(), so to avoid 0260 // setting it again and again, we have this nice little boolean :) 0261 bool m_hasView = false; 0262 0263 bool m_hasDefaultFilter = false; // necessary for the m_operationMode 0264 bool m_inAccept = false; // true between beginning and end of accept() 0265 bool m_confirmOverwrite = false; 0266 bool m_differentHierarchyLevelItemsEntered = false; 0267 0268 const std::array<short, 8> m_stdIconSizes = { 0269 KIconLoader::SizeSmall, 0270 KIconLoader::SizeSmallMedium, 0271 KIconLoader::SizeMedium, 0272 KIconLoader::SizeLarge, 0273 KIconLoader::SizeHuge, 0274 KIconLoader::SizeEnormous, 0275 256, 0276 512, 0277 }; 0278 0279 QSlider *m_iconSizeSlider = nullptr; 0280 QAction *m_zoomOutAction = nullptr; 0281 QAction *m_zoomInAction = nullptr; 0282 0283 // The group which stores app-specific settings. These settings are recent 0284 // files and urls. Visual settings (view mode, sorting criteria...) are not 0285 // app-specific and are stored in kdeglobals 0286 KConfigGroup m_configGroup; 0287 0288 KToggleAction *m_toggleBookmarksAction = nullptr; 0289 KToggleAction *m_togglePlacesPanelAction = nullptr; 0290 }; 0291 0292 Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path 0293 0294 // returns true if the string contains "<a>:/" sequence, where <a> is at least 2 alpha chars 0295 static bool containsProtocolSection(const QString &string) 0296 { 0297 int len = string.length(); 0298 static const char prot[] = ":/"; 0299 for (int i = 0; i < len;) { 0300 i = string.indexOf(QLatin1String(prot), i); 0301 if (i == -1) { 0302 return false; 0303 } 0304 int j = i - 1; 0305 for (; j >= 0; j--) { 0306 const QChar &ch(string[j]); 0307 if (ch.toLatin1() == 0 || !ch.isLetter()) { 0308 break; 0309 } 0310 if (ch.isSpace() && (i - j - 1) >= 2) { 0311 return true; 0312 } 0313 } 0314 if (j < 0 && i >= 2) { 0315 return true; // at least two letters before ":/" 0316 } 0317 i += 3; // skip : and / and one char 0318 } 0319 return false; 0320 } 0321 0322 // this string-to-url conversion function handles relative paths, full paths and URLs 0323 // without the http-prepending that QUrl::fromUserInput does. 0324 static QUrl urlFromString(const QString &str) 0325 { 0326 if (Utils::isAbsoluteLocalPath(str)) { 0327 return QUrl::fromLocalFile(str); 0328 } 0329 QUrl url(str); 0330 if (url.isRelative()) { 0331 url.clear(); 0332 url.setPath(str); 0333 } 0334 return url; 0335 } 0336 0337 KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent) 0338 : QWidget(parent) 0339 , d(new KFileWidgetPrivate(this)) 0340 { 0341 QUrl startDir(_startDir); 0342 // qDebug() << "startDir" << startDir; 0343 QString filename; 0344 0345 d->m_okButton = new QPushButton(this); 0346 KGuiItem::assign(d->m_okButton, KStandardGuiItem::ok()); 0347 d->m_okButton->setDefault(true); 0348 d->m_cancelButton = new QPushButton(this); 0349 KGuiItem::assign(d->m_cancelButton, KStandardGuiItem::cancel()); 0350 // The dialog shows them 0351 d->m_okButton->hide(); 0352 d->m_cancelButton->hide(); 0353 0354 d->initDirOpWidgets(); 0355 0356 // Resolve this now so that a 'kfiledialog:' URL, if specified, 0357 // does not get inserted into the urlNavigator history. 0358 d->m_url = getStartUrl(startDir, d->m_fileClass, filename); 0359 startDir = d->m_url; 0360 0361 const auto operatorActions = d->m_ops->allActions(); 0362 for (QAction *action : operatorActions) { 0363 addAction(action); 0364 } 0365 0366 QAction *goToNavigatorAction = new QAction(this); 0367 0368 connect(goToNavigatorAction, &QAction::triggered, this, [this]() { 0369 d->activateUrlNavigator(); 0370 }); 0371 0372 goToNavigatorAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L)); 0373 0374 addAction(goToNavigatorAction); 0375 0376 KUrlComboBox *pathCombo = d->m_urlNavigator->editor(); 0377 KUrlCompletion *pathCompletionObj = new KUrlCompletion(KUrlCompletion::DirCompletion); 0378 pathCombo->setCompletionObject(pathCompletionObj); 0379 pathCombo->setAutoDeleteCompletionObject(true); 0380 0381 connect(d->m_urlNavigator, &KUrlNavigator::urlChanged, this, [this](const QUrl &url) { 0382 d->enterUrl(url); 0383 }); 0384 connect(d->m_urlNavigator, &KUrlNavigator::returnPressed, d->m_ops, qOverload<>(&QWidget::setFocus)); 0385 0386 // Location, "Name:", line-edit and label 0387 d->initLocationWidget(); 0388 0389 // "Filter:" line-edit and label 0390 d->initFilterWidget(); 0391 0392 // the Automatically Select Extension checkbox 0393 // (the text, visibility etc. is set in updateAutoSelectExtension(), which is called by readConfig()) 0394 d->m_autoSelectExtCheckBox = new QCheckBox(this); 0395 connect(d->m_autoSelectExtCheckBox, &QCheckBox::clicked, this, [this]() { 0396 d->slotAutoSelectExtClicked(); 0397 }); 0398 0399 d->initGUI(); // activate GM 0400 0401 // read our configuration 0402 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0403 config->reparseConfiguration(); // grab newly added dirs by other processes (#403524) 0404 KConfigGroup group(config, ConfigGroup); 0405 readConfig(group); 0406 0407 d->m_ops->action(KDirOperator::ShowPreview)->setChecked(d->m_ops->isInlinePreviewShown()); 0408 d->slotDirOpIconSizeChanged(d->m_ops->iconSize()); 0409 0410 KFilePreviewGenerator *pg = d->m_ops->previewGenerator(); 0411 if (pg) { 0412 d->m_ops->action(KDirOperator::ShowPreview)->setChecked(pg->isPreviewShown()); 0413 } 0414 0415 // getStartUrl() above will have resolved the startDir parameter into 0416 // a directory and file name in the two cases: (a) where it is a 0417 // special "kfiledialog:" URL, or (b) where it is a plain file name 0418 // only without directory or protocol. For any other startDir 0419 // specified, it is not possible to resolve whether there is a file name 0420 // present just by looking at the URL; the only way to be sure is 0421 // to stat it. 0422 bool statRes = false; 0423 if (filename.isEmpty()) { 0424 KIO::StatJob *statJob = KIO::stat(startDir, KIO::HideProgressInfo); 0425 KJobWidgets::setWindow(statJob, this); 0426 statRes = statJob->exec(); 0427 // qDebug() << "stat of" << startDir << "-> statRes" << statRes << "isDir" << statJob->statResult().isDir(); 0428 if (!statRes || !statJob->statResult().isDir()) { 0429 filename = startDir.fileName(); 0430 startDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0431 // qDebug() << "statJob -> startDir" << startDir << "filename" << filename; 0432 } 0433 } 0434 0435 d->m_ops->setUrl(startDir, true); 0436 d->m_urlNavigator->setLocationUrl(startDir); 0437 if (d->m_placesView) { 0438 d->m_placesView->setUrl(startDir); 0439 } 0440 0441 // We have a file name either explicitly specified, or have checked that 0442 // we could stat it and it is not a directory. Set it. 0443 if (!filename.isEmpty()) { 0444 QLineEdit *lineEdit = d->m_locationEdit->lineEdit(); 0445 // qDebug() << "selecting filename" << filename; 0446 if (statRes) { 0447 d->setLocationText(QUrl(filename)); 0448 } else { 0449 lineEdit->setText(filename); 0450 // Preserve this filename when clicking on the view (cf fileHighlighted) 0451 lineEdit->setModified(true); 0452 } 0453 lineEdit->selectAll(); 0454 } 0455 0456 d->m_locationEdit->setFocus(); 0457 0458 const QAction *showHiddenAction = d->m_ops->action(KDirOperator::ShowHiddenFiles); 0459 Q_ASSERT(showHiddenAction); 0460 d->m_urlNavigator->setShowHiddenFolders(showHiddenAction->isChecked()); 0461 connect(showHiddenAction, &QAction::toggled, this, [this](bool checked) { 0462 d->m_urlNavigator->setShowHiddenFolders(checked); 0463 }); 0464 0465 const QAction *hiddenFilesLastAction = d->m_ops->action(KDirOperator::SortHiddenFilesLast); 0466 Q_ASSERT(hiddenFilesLastAction); 0467 d->m_urlNavigator->setSortHiddenFoldersLast(hiddenFilesLastAction->isChecked()); 0468 connect(hiddenFilesLastAction, &QAction::toggled, this, [this](bool checked) { 0469 d->m_urlNavigator->setSortHiddenFoldersLast(checked); 0470 }); 0471 } 0472 0473 KFileWidget::~KFileWidget() 0474 { 0475 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0476 config->sync(); 0477 d->m_ops->removeEventFilter(this); 0478 d->m_locationEdit->removeEventFilter(this); 0479 } 0480 0481 void KFileWidget::setLocationLabel(const QString &text) 0482 { 0483 d->m_locationLabel->setText(text); 0484 } 0485 0486 void KFileWidget::setFilters(const QList<KFileFilter> &filters, const KFileFilter &activeFilter) 0487 { 0488 d->m_ops->clearFilter(); 0489 d->m_filterWidget->setFilters(filters, activeFilter); 0490 d->m_ops->updateDir(); 0491 d->m_hasDefaultFilter = false; 0492 d->m_filterWidget->setEditable(true); 0493 d->updateFilterText(); 0494 0495 d->updateAutoSelectExtension(); 0496 } 0497 0498 KFileFilter KFileWidget::currentFilter() const 0499 { 0500 return d->m_filterWidget->currentFilter(); 0501 } 0502 0503 void KFileWidget::clearFilter() 0504 { 0505 d->m_filterWidget->setFilters({}, KFileFilter()); 0506 d->m_ops->clearFilter(); 0507 d->m_hasDefaultFilter = false; 0508 d->m_filterWidget->setEditable(true); 0509 0510 d->updateAutoSelectExtension(); 0511 } 0512 0513 void KFileWidget::setPreviewWidget(KPreviewWidgetBase *w) 0514 { 0515 d->m_ops->setPreviewWidget(w); 0516 d->m_ops->clearHistory(); 0517 d->m_hasView = true; 0518 } 0519 0520 QUrl KFileWidgetPrivate::getCompleteUrl(const QString &_url) const 0521 { 0522 // qDebug() << "got url " << _url; 0523 0524 const QString url = KShell::tildeExpand(_url); 0525 QUrl u; 0526 0527 if (Utils::isAbsoluteLocalPath(url)) { 0528 u = QUrl::fromLocalFile(url); 0529 } else { 0530 QUrl relativeUrlTest(m_ops->url()); 0531 relativeUrlTest.setPath(Utils::concatPaths(relativeUrlTest.path(), url)); 0532 if (!m_ops->dirLister()->findByUrl(relativeUrlTest).isNull() || !KProtocolInfo::isKnownProtocol(relativeUrlTest)) { 0533 u = relativeUrlTest; 0534 } else { 0535 // Try to preserve URLs if they have a scheme (for example, 0536 // "https://example.com/foo.txt") and otherwise resolve relative 0537 // paths to absolute ones (e.g. "foo.txt" -> "file:///tmp/foo.txt"). 0538 u = QUrl(url); 0539 if (u.isRelative()) { 0540 u = relativeUrlTest; 0541 } 0542 } 0543 } 0544 0545 return u; 0546 } 0547 0548 QSize KFileWidget::sizeHint() const 0549 { 0550 int fontSize = fontMetrics().height(); 0551 const QSize goodSize(48 * fontSize, 30 * fontSize); 0552 const QSize scrnSize = d->screenSize(); 0553 const QSize minSize(scrnSize / 2); 0554 const QSize maxSize(scrnSize * qreal(0.9)); 0555 return (goodSize.expandedTo(minSize).boundedTo(maxSize)); 0556 } 0557 0558 static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url); 0559 0560 /** 0561 * Escape the given Url so that is fit for use in the selected list of file. This 0562 * mainly handles double quote (") characters. These are used to separate entries 0563 * in the list, however, if `"` appears in the filename (or path), this will be 0564 * escaped as `\"`. Later, the tokenizer is able to understand the difference 0565 * and do the right thing 0566 */ 0567 static QString escapeDoubleQuotes(QString &&path); 0568 0569 // Called by KFileDialog 0570 void KFileWidget::slotOk() 0571 { 0572 // qDebug() << "slotOk\n"; 0573 0574 const QString locationEditCurrentText(KShell::tildeExpand(d->locationEditCurrentText())); 0575 0576 QList<QUrl> locationEditCurrentTextList(d->tokenize(locationEditCurrentText)); 0577 KFile::Modes mode = d->m_ops->mode(); 0578 0579 // if there is nothing to do, just return from here 0580 if (locationEditCurrentTextList.isEmpty()) { 0581 return; 0582 } 0583 0584 // Make sure that one of the modes was provided 0585 if (!((mode & KFile::File) || (mode & KFile::Directory) || (mode & KFile::Files))) { 0586 mode |= KFile::File; 0587 // qDebug() << "No mode() provided"; 0588 } 0589 0590 // Clear the list as we are going to refill it 0591 d->m_urlList.clear(); 0592 0593 const bool directoryMode = (mode & KFile::Directory); 0594 const bool onlyDirectoryMode = directoryMode && !(mode & KFile::File) && !(mode & KFile::Files); 0595 0596 // if we are on file mode, and the list of provided files/folder is greater than one, inform 0597 // the user about it 0598 if (locationEditCurrentTextList.count() > 1) { 0599 if (mode & KFile::File) { 0600 KMessageBox::error(this, i18n("You can only select one file"), i18n("More than one file provided")); 0601 return; 0602 } 0603 0604 /** 0605 * Logic of the next part of code (ends at "end multi relative urls"). 0606 * 0607 * We allow for instance to be at "/" and insert '"home/foo/bar.txt" "boot/grub/menu.lst"'. 0608 * Why we need to support this ? Because we provide tree views, which aren't plain. 0609 * 0610 * Now, how does this logic work. It will get the first element on the list (with no filename), 0611 * following the previous example say "/home/foo" and set it as the top most url. 0612 * 0613 * After this, it will iterate over the rest of items and check if this URL (topmost url) 0614 * contains the url being iterated. 0615 * 0616 * As you might have guessed it will do "/home/foo" against "/boot/grub" (again stripping 0617 * filename), and a false will be returned. Then we upUrl the top most url, resulting in 0618 * "/home" against "/boot/grub", what will again return false, so we upUrl again. Now we 0619 * have "/" against "/boot/grub", what returns true for us, so we can say that the closest 0620 * common ancestor of both is "/". 0621 * 0622 * This example has been written for 2 urls, but this works for any number of urls. 0623 */ 0624 if (!d->m_differentHierarchyLevelItemsEntered) { // avoid infinite recursion. running this 0625 int start = 0; 0626 QUrl topMostUrl; 0627 KIO::StatJob *statJob = nullptr; 0628 bool res = false; 0629 0630 // we need to check for a valid first url, so in theory we only iterate one time over 0631 // this loop. However it can happen that the user did 0632 // "home/foo/nonexistantfile" "boot/grub/menu.lst", so we look for a good first 0633 // candidate. 0634 while (!res && start < locationEditCurrentTextList.count()) { 0635 topMostUrl = locationEditCurrentTextList.at(start); 0636 statJob = KIO::stat(topMostUrl, KIO::HideProgressInfo); 0637 KJobWidgets::setWindow(statJob, this); 0638 res = statJob->exec(); 0639 start++; 0640 } 0641 0642 Q_ASSERT(statJob); 0643 0644 // if this is not a dir, strip the filename. after this we have an existent and valid 0645 // dir (we stated correctly the file). 0646 if (!statJob->statResult().isDir()) { 0647 topMostUrl = topMostUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0648 } 0649 0650 // now the funny part. for the rest of filenames, go and look for the closest ancestor 0651 // of all them. 0652 for (int i = start; i < locationEditCurrentTextList.count(); ++i) { 0653 QUrl currUrl = locationEditCurrentTextList.at(i); 0654 KIO::StatJob *statJob = KIO::stat(currUrl, KIO::HideProgressInfo); 0655 KJobWidgets::setWindow(statJob, this); 0656 int res = statJob->exec(); 0657 if (res) { 0658 // again, we don't care about filenames 0659 if (!statJob->statResult().isDir()) { 0660 currUrl = currUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0661 } 0662 0663 // iterate while this item is contained on the top most url 0664 while (!topMostUrl.matches(currUrl, QUrl::StripTrailingSlash) && !topMostUrl.isParentOf(currUrl)) { 0665 topMostUrl = KIO::upUrl(topMostUrl); 0666 } 0667 } 0668 } 0669 0670 // now recalculate all paths for them being relative in base of the top most url 0671 QStringList stringList; 0672 stringList.reserve(locationEditCurrentTextList.count()); 0673 for (int i = 0; i < locationEditCurrentTextList.count(); ++i) { 0674 Q_ASSERT(topMostUrl.isParentOf(locationEditCurrentTextList[i])); 0675 QString relativePath = relativePathOrUrl(topMostUrl, locationEditCurrentTextList[i]); 0676 stringList << escapeDoubleQuotes(std::move(relativePath)); 0677 } 0678 0679 d->m_ops->setUrl(topMostUrl, true); 0680 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true); 0681 d->m_locationEdit->lineEdit()->setText(QStringLiteral("\"%1\"").arg(stringList.join(QStringLiteral("\" \"")))); 0682 d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked); 0683 0684 d->m_differentHierarchyLevelItemsEntered = true; 0685 slotOk(); 0686 return; 0687 } 0688 /** 0689 * end multi relative urls 0690 */ 0691 } else if (!locationEditCurrentTextList.isEmpty()) { 0692 // if we are on file or files mode, and we have an absolute url written by 0693 // the user: 0694 // * convert it to relative and call slotOk again if the protocol supports listing. 0695 // * use the full url if the protocol doesn't support listing 0696 // This is because when using a protocol that supports listing we want to show the directory 0697 // the user just opened/saved from the next time they open the dialog, it makes sense usability wise. 0698 // If the protocol doesn't support listing (i.e. http:// ) the user would end up with the dialog 0699 // showing an "empty directory" which is bad usability wise. 0700 if (!locationEditCurrentText.isEmpty() && !onlyDirectoryMode 0701 && (Utils::isAbsoluteLocalPath(locationEditCurrentText) || containsProtocolSection(locationEditCurrentText))) { 0702 QUrl url = urlFromString(locationEditCurrentText); 0703 if (KProtocolManager::supportsListing(url)) { 0704 QString fileName; 0705 if (d->m_operationMode == Opening) { 0706 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 0707 KJobWidgets::setWindow(statJob, this); 0708 int res = statJob->exec(); 0709 if (res) { 0710 if (!statJob->statResult().isDir()) { 0711 fileName = url.fileName(); 0712 url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash 0713 } else { 0714 Utils::appendSlashToPath(url); 0715 } 0716 } 0717 } else { 0718 const QUrl directory = url.adjusted(QUrl::RemoveFilename); 0719 // Check if the folder exists 0720 KIO::StatJob *statJob = KIO::stat(directory, KIO::HideProgressInfo); 0721 KJobWidgets::setWindow(statJob, this); 0722 int res = statJob->exec(); 0723 if (res) { 0724 if (statJob->statResult().isDir()) { 0725 url = url.adjusted(QUrl::StripTrailingSlash); 0726 fileName = url.fileName(); 0727 url = url.adjusted(QUrl::RemoveFilename); 0728 } 0729 } 0730 } 0731 d->m_ops->setUrl(url, true); 0732 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true); 0733 d->m_locationEdit->lineEdit()->setText(fileName); 0734 d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked); 0735 slotOk(); 0736 return; 0737 } else { 0738 locationEditCurrentTextList = {url}; 0739 } 0740 } 0741 } 0742 0743 // restore it 0744 d->m_differentHierarchyLevelItemsEntered = false; 0745 0746 // locationEditCurrentTextList contains absolute paths 0747 // this is the general loop for the File and Files mode. Obviously we know 0748 // that the File mode will iterate only one time here 0749 QList<QUrl>::ConstIterator it = locationEditCurrentTextList.constBegin(); 0750 bool filesInList = false; 0751 while (it != locationEditCurrentTextList.constEnd()) { 0752 QUrl url(*it); 0753 0754 if (d->m_operationMode == Saving && !directoryMode) { 0755 d->appendExtension(url); 0756 } 0757 0758 d->m_url = url; 0759 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 0760 KJobWidgets::setWindow(statJob, this); 0761 int res = statJob->exec(); 0762 0763 if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), url)) { 0764 QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_url.toDisplayString()); 0765 KMessageBox::error(this, msg); 0766 return; 0767 } 0768 0769 // if we are on local mode, make sure we haven't got a remote base url 0770 if ((mode & KFile::LocalOnly) && !d->mostLocalUrl(d->m_url).isLocalFile()) { 0771 KMessageBox::error(this, i18n("You can only select local files"), i18n("Remote files not accepted")); 0772 return; 0773 } 0774 0775 const auto &supportedSchemes = d->m_model->supportedSchemes(); 0776 if (!supportedSchemes.isEmpty() && !supportedSchemes.contains(d->m_url.scheme())) { 0777 KMessageBox::error(this, 0778 i18np("The selected URL uses an unsupported scheme. " 0779 "Please use the following scheme: %2", 0780 "The selected URL uses an unsupported scheme. " 0781 "Please use one of the following schemes: %2", 0782 supportedSchemes.size(), 0783 supportedSchemes.join(QLatin1String(", "))), 0784 i18n("Unsupported URL scheme")); 0785 return; 0786 } 0787 0788 // if we are given a folder when not on directory mode, let's get into it 0789 if (res && !directoryMode && statJob->statResult().isDir()) { 0790 // check if we were given more than one folder, in that case we don't know to which one 0791 // cd 0792 ++it; 0793 while (it != locationEditCurrentTextList.constEnd()) { 0794 QUrl checkUrl(*it); 0795 KIO::StatJob *checkStatJob = KIO::stat(checkUrl, KIO::HideProgressInfo); 0796 KJobWidgets::setWindow(checkStatJob, this); 0797 bool res = checkStatJob->exec(); 0798 if (res && checkStatJob->statResult().isDir()) { 0799 KMessageBox::error(this, 0800 i18n("More than one folder has been selected and this dialog does not accept folders, so it is not possible to decide " 0801 "which one to enter. Please select only one folder to list it."), 0802 i18n("More than one folder provided")); 0803 return; 0804 } else if (res) { 0805 filesInList = true; 0806 } 0807 ++it; 0808 } 0809 if (filesInList) { 0810 KMessageBox::information( 0811 this, 0812 i18n("At least one folder and one file has been selected. Selected files will be ignored and the selected folder will be listed"), 0813 i18n("Files and folders selected")); 0814 } 0815 d->m_ops->setUrl(url, true); 0816 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true); 0817 d->m_locationEdit->lineEdit()->setText(QString()); 0818 d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked); 0819 return; 0820 } else if (res && onlyDirectoryMode && !statJob->statResult().isDir()) { 0821 // if we are given a file when on directory only mode, reject it 0822 return; 0823 } else if (!(mode & KFile::ExistingOnly) || res) { 0824 // if we don't care about ExistingOnly flag, add the file even if 0825 // it doesn't exist. If we care about it, don't add it to the list 0826 if (!onlyDirectoryMode || (res && statJob->statResult().isDir())) { 0827 d->m_urlList << url; 0828 } 0829 filesInList = true; 0830 } else { 0831 KMessageBox::error(this, i18n("The file \"%1\" could not be found", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Cannot open file")); 0832 return; // do not emit accepted() if we had ExistingOnly flag and stat failed 0833 } 0834 0835 if ((d->m_operationMode == Saving) && d->m_confirmOverwrite && !d->toOverwrite(url)) { 0836 return; 0837 } 0838 0839 ++it; 0840 } 0841 0842 // if we have reached this point and we didn't return before, that is because 0843 // we want this dialog to be accepted 0844 Q_EMIT accepted(); 0845 } 0846 0847 void KFileWidget::accept() 0848 { 0849 d->m_inAccept = true; 0850 0851 *lastDirectory() = d->m_ops->url(); 0852 if (!d->m_fileClass.isEmpty()) { 0853 KRecentDirs::add(d->m_fileClass, d->m_ops->url().toString()); 0854 } 0855 0856 // clear the topmost item, we insert it as full path later on as item 1 0857 d->m_locationEdit->setItemText(0, QString()); 0858 0859 const QList<QUrl> list = selectedUrls(); 0860 int atmost = d->m_locationEdit->maxItems(); // don't add more items than necessary 0861 for (const auto &url : list) { 0862 if (atmost-- == 0) { 0863 break; 0864 } 0865 0866 // we strip the last slash (-1) because KUrlComboBox does that as well 0867 // when operating in file-mode. If we wouldn't , dupe-finding wouldn't 0868 // work. 0869 const QString file = url.toDisplayString(QUrl::StripTrailingSlash | QUrl::PreferLocalFile); 0870 0871 // remove dupes 0872 for (int i = 1; i < d->m_locationEdit->count(); ++i) { 0873 if (d->m_locationEdit->itemText(i) == file) { 0874 d->m_locationEdit->removeItem(i--); 0875 break; 0876 } 0877 } 0878 // FIXME I don't think this works correctly when the KUrlComboBox has some default urls. 0879 // KUrlComboBox should provide a function to add an url and rotate the existing ones, keeping 0880 // track of maxItems, and we shouldn't be able to insert items as we please. 0881 d->m_locationEdit->insertItem(1, file); 0882 } 0883 0884 d->writeViewConfig(); 0885 d->saveRecentFiles(); 0886 0887 d->addToRecentDocuments(); 0888 0889 if (!(mode() & KFile::Files)) { // single selection 0890 Q_EMIT fileSelected(d->m_url); 0891 } 0892 0893 d->m_ops->close(); 0894 } 0895 0896 void KFileWidgetPrivate::fileHighlighted(const KFileItem &i) 0897 { 0898 if ((m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty())) { // don't disturb 0899 return; 0900 } 0901 0902 if (!i.isNull() && i.isDir() && !(m_ops->mode() & KFile::Directory)) { 0903 return; 0904 } 0905 0906 const bool modified = m_locationEdit->lineEdit()->isModified(); 0907 0908 if (!(m_ops->mode() & KFile::Files)) { 0909 if (i.isNull()) { 0910 if (!modified) { 0911 setLocationText(QUrl()); 0912 } 0913 return; 0914 } 0915 0916 m_url = i.url(); 0917 0918 if (!m_locationEdit->hasFocus()) { // don't disturb while editing 0919 setLocationText(m_url); 0920 } 0921 0922 Q_EMIT q->fileHighlighted(m_url); 0923 } else { 0924 multiSelectionChanged(); 0925 Q_EMIT q->selectionChanged(); 0926 } 0927 0928 m_locationEdit->lineEdit()->setModified(false); 0929 0930 // When saving, and when double-click mode is being used, highlight the 0931 // filename after a file is single-clicked so the user has a chance to quickly 0932 // rename it if desired 0933 // Note that double-clicking will override this and overwrite regardless of 0934 // single/double click mouse setting (see slotViewDoubleClicked() ) 0935 if (m_operationMode == KFileWidget::Saving) { 0936 m_locationEdit->setFocus(); 0937 } 0938 } 0939 0940 void KFileWidgetPrivate::fileSelected(const KFileItem &i) 0941 { 0942 if (!i.isNull() && i.isDir()) { 0943 return; 0944 } 0945 0946 if (!(m_ops->mode() & KFile::Files)) { 0947 if (i.isNull()) { 0948 setLocationText(QUrl()); 0949 return; 0950 } 0951 setLocationText(i.targetUrl()); 0952 } else { 0953 multiSelectionChanged(); 0954 Q_EMIT q->selectionChanged(); 0955 } 0956 0957 // Same as above in fileHighlighted(), but for single-click mode 0958 if (m_operationMode == KFileWidget::Saving) { 0959 m_locationEdit->setFocus(); 0960 } else { 0961 q->slotOk(); 0962 } 0963 } 0964 0965 // I know it's slow to always iterate thru the whole filelist 0966 // (d->m_ops->selectedItems()), but what can we do? 0967 void KFileWidgetPrivate::multiSelectionChanged() 0968 { 0969 if (m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty()) { // don't disturb 0970 return; 0971 } 0972 0973 const KFileItemList list = m_ops->selectedItems(); 0974 0975 if (list.isEmpty()) { 0976 setLocationText(QUrl()); 0977 return; 0978 } 0979 0980 setLocationText(list.targetUrlList()); 0981 } 0982 0983 void KFileWidgetPrivate::setLocationText(const QUrl &url) 0984 { 0985 // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get 0986 // emitted, so slotLocationChanged() will be called. Make sure we don't clear the 0987 // KDirOperator's view-selection in there 0988 const QSignalBlocker blocker(m_locationEdit); 0989 0990 if (!url.isEmpty()) { 0991 if (!url.isRelative()) { 0992 const QUrl directory = url.adjusted(QUrl::RemoveFilename); 0993 if (!directory.path().isEmpty()) { 0994 q->setUrl(directory, false); 0995 } else { 0996 q->setUrl(url, false); 0997 } 0998 } 0999 m_locationEdit->setEditText(escapeDoubleQuotes(url.fileName())); 1000 } else if (!m_locationEdit->lineEdit()->text().isEmpty()) { 1001 m_locationEdit->clearEditText(); 1002 } 1003 1004 if (m_operationMode == KFileWidget::Saving) { 1005 setNonExtSelection(); 1006 } 1007 } 1008 1009 static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url) 1010 { 1011 if (baseUrl.isParentOf(url)) { 1012 const QString basePath(QDir::cleanPath(baseUrl.path())); 1013 QString relPath(QDir::cleanPath(url.path())); 1014 relPath.remove(0, basePath.length()); 1015 if (relPath.startsWith(QLatin1Char('/'))) { 1016 relPath.remove(0, 1); 1017 } 1018 return relPath; 1019 } else { 1020 return url.toDisplayString(); 1021 } 1022 } 1023 1024 static QString escapeDoubleQuotes(QString &&path) 1025 { 1026 // First escape the escape character that we are using 1027 path.replace(QStringLiteral("\\"), QStringLiteral("\\\\")); 1028 // Second, escape the quotes 1029 path.replace(QStringLiteral("\""), QStringLiteral("\\\"")); 1030 return path; 1031 } 1032 1033 void KFileWidgetPrivate::initDirOpWidgets() 1034 { 1035 m_opsWidget = new QWidget(q); 1036 m_opsWidgetLayout = new QVBoxLayout(m_opsWidget); 1037 m_opsWidgetLayout->setContentsMargins(0, 0, 0, 0); 1038 m_opsWidgetLayout->setSpacing(0); 1039 1040 m_model = new KFilePlacesModel(q); 1041 1042 // Don't pass "startDir" (KFileWidget constructor 1st arg) to the 1043 // KUrlNavigator at this stage: it may also contain a file name which 1044 // should not get inserted in that form into the old-style navigation 1045 // bar history. Wait until the KIO::stat has been done later. 1046 // 1047 // The stat cannot be done before this point, bug 172678. 1048 m_urlNavigator = new KUrlNavigator(m_model, QUrl(), m_opsWidget); // d->m_toolbar); 1049 m_urlNavigator->setPlacesSelectorVisible(false); 1050 1051 m_urlNavigator->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 1052 0, 1053 q->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 1054 q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin) - 2); 1055 1056 m_messageWidget = new KMessageWidget(q); 1057 m_messageWidget->setMessageType(KMessageWidget::Error); 1058 m_messageWidget->hide(); 1059 1060 auto topSeparator = new QFrame(q); 1061 topSeparator->setFrameStyle(QFrame::HLine); 1062 1063 m_ops = new KDirOperator(QUrl(), m_opsWidget); 1064 m_ops->installEventFilter(q); 1065 m_ops->setObjectName(QStringLiteral("KFileWidget::ops")); 1066 m_ops->setIsSaving(m_operationMode == KFileWidget::Saving); 1067 m_ops->setNewFileMenuSelectDirWhenAlreadyExist(true); 1068 m_ops->showOpenWithActions(true); 1069 m_ops->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 1070 1071 auto bottomSparator = new QFrame(q); 1072 bottomSparator->setFrameStyle(QFrame::HLine); 1073 1074 q->connect(m_ops, &KDirOperator::urlEntered, q, [this](const QUrl &url) { 1075 urlEntered(url); 1076 }); 1077 q->connect(m_ops, &KDirOperator::fileHighlighted, q, [this](const KFileItem &item) { 1078 fileHighlighted(item); 1079 }); 1080 q->connect(m_ops, &KDirOperator::fileSelected, q, [this](const KFileItem &item) { 1081 fileSelected(item); 1082 }); 1083 q->connect(m_ops, &KDirOperator::finishedLoading, q, [this]() { 1084 slotLoadingFinished(); 1085 }); 1086 q->connect(m_ops, &KDirOperator::keyEnterReturnPressed, q, [this]() { 1087 slotViewKeyEnterReturnPressed(); 1088 }); 1089 q->connect(m_ops, &KDirOperator::renamingFinished, q, [this](const QList<QUrl> &urls) { 1090 // Update file names in location text field after renaming selected files 1091 q->setSelectedUrls(urls); 1092 }); 1093 1094 q->connect(m_ops, &KDirOperator::viewChanged, q, [](QAbstractItemView *newView) { 1095 newView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge | Qt::BottomEdge})); 1096 }); 1097 1098 m_ops->dirLister()->setAutoErrorHandlingEnabled(false); 1099 q->connect(m_ops->dirLister(), &KDirLister::jobError, q, [this](KIO::Job *job) { 1100 m_messageWidget->setText(job->errorString()); 1101 m_messageWidget->animatedShow(); 1102 }); 1103 1104 m_ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions); 1105 1106 initToolbar(); 1107 1108 m_opsWidgetLayout->addWidget(m_toolbar); 1109 m_opsWidgetLayout->addWidget(m_urlNavigator); 1110 m_opsWidgetLayout->addWidget(m_messageWidget); 1111 m_opsWidgetLayout->addWidget(topSeparator); 1112 m_opsWidgetLayout->addWidget(m_ops); 1113 m_opsWidgetLayout->addWidget(bottomSparator); 1114 } 1115 1116 void KFileWidgetPrivate::initZoomWidget() 1117 { 1118 m_iconSizeSlider = new QSlider(q); 1119 m_iconSizeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); 1120 m_iconSizeSlider->setMinimumWidth(40); 1121 m_iconSizeSlider->setOrientation(Qt::Horizontal); 1122 m_iconSizeSlider->setMinimum(0); 1123 m_iconSizeSlider->setMaximum(m_stdIconSizes.size() - 1); 1124 m_iconSizeSlider->setSingleStep(1); 1125 m_iconSizeSlider->setPageStep(1); 1126 m_iconSizeSlider->setTickPosition(QSlider::TicksBelow); 1127 1128 q->connect(m_iconSizeSlider, &QAbstractSlider::valueChanged, q, [this](int step) { 1129 slotIconSizeChanged(m_stdIconSizes[step]); 1130 }); 1131 1132 q->connect(m_iconSizeSlider, &QAbstractSlider::sliderMoved, q, [this](int step) { 1133 slotIconSizeSliderMoved(m_stdIconSizes[step]); 1134 }); 1135 1136 q->connect(m_ops, &KDirOperator::currentIconSizeChanged, q, [this](int iconSize) { 1137 slotDirOpIconSizeChanged(iconSize); 1138 }); 1139 1140 m_zoomOutAction = KStandardAction::create( 1141 KStandardAction::ZoomOut, 1142 q, 1143 [this]() { 1144 changeIconsSize(ZoomOut); 1145 }, 1146 q); 1147 1148 q->addAction(m_zoomOutAction); 1149 1150 m_zoomInAction = KStandardAction::create( 1151 KStandardAction::ZoomIn, 1152 q, 1153 [this]() { 1154 changeIconsSize(ZoomIn); 1155 }, 1156 q); 1157 1158 q->addAction(m_zoomInAction); 1159 } 1160 1161 void KFileWidgetPrivate::initToolbar() 1162 { 1163 m_toolbar = new QToolBar(m_opsWidget); 1164 m_toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar")); 1165 m_toolbar->setMovable(false); 1166 1167 // add nav items to the toolbar 1168 // 1169 // NOTE: The order of the button icons here differs from that 1170 // found in the file manager and web browser, but has been discussed 1171 // and agreed upon on the kde-core-devel mailing list: 1172 // 1173 // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2 1174 1175 m_ops->action(KDirOperator::Up) 1176 ->setWhatsThis(i18n("<qt>Click this button to enter the parent folder.<br /><br />" 1177 "For instance, if the current location is file:/home/konqi clicking this " 1178 "button will take you to file:/home.</qt>")); 1179 1180 m_ops->action(KDirOperator::Back)->setWhatsThis(i18n("Click this button to move backwards one step in the browsing history.")); 1181 m_ops->action(KDirOperator::Forward)->setWhatsThis(i18n("Click this button to move forward one step in the browsing history.")); 1182 1183 m_ops->action(KDirOperator::Reload)->setWhatsThis(i18n("Click this button to reload the contents of the current location.")); 1184 m_ops->action(KDirOperator::NewFolder)->setShortcuts(KStandardShortcut::createFolder()); 1185 m_ops->action(KDirOperator::NewFolder)->setWhatsThis(i18n("Click this button to create a new folder.")); 1186 1187 m_togglePlacesPanelAction = new KToggleAction(i18n("Show Places Panel"), q); 1188 q->addAction(m_togglePlacesPanelAction); 1189 m_togglePlacesPanelAction->setShortcut(QKeySequence(Qt::Key_F9)); 1190 q->connect(m_togglePlacesPanelAction, &QAction::toggled, q, [this](bool show) { 1191 togglePlacesPanel(show); 1192 }); 1193 1194 m_toggleBookmarksAction = new KToggleAction(i18n("Show Bookmarks Button"), q); 1195 q->addAction(m_toggleBookmarksAction); 1196 q->connect(m_toggleBookmarksAction, &QAction::toggled, q, [this](bool show) { 1197 toggleBookmarks(show); 1198 }); 1199 1200 // Build the settings menu 1201 KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), q); 1202 q->addAction(menu); 1203 menu->setWhatsThis( 1204 i18n("<qt>This is the preferences menu for the file dialog. " 1205 "Various options can be accessed from this menu including: <ul>" 1206 "<li>how files are sorted in the list</li>" 1207 "<li>types of view, including icon and list</li>" 1208 "<li>showing of hidden files</li>" 1209 "<li>the Places panel</li>" 1210 "<li>file previews</li>" 1211 "<li>separating folders from files</li></ul></qt>")); 1212 1213 menu->addAction(m_ops->action(KDirOperator::AllowExpansionInDetailsView)); 1214 menu->addSeparator(); 1215 menu->addAction(m_ops->action(KDirOperator::ShowHiddenFiles)); 1216 menu->addAction(m_togglePlacesPanelAction); 1217 menu->addAction(m_toggleBookmarksAction); 1218 menu->addAction(m_ops->action(KDirOperator::ShowPreviewPanel)); 1219 1220 menu->setPopupMode(QToolButton::InstantPopup); 1221 q->connect(menu->menu(), &QMenu::aboutToShow, m_ops, &KDirOperator::updateSelectionDependentActions); 1222 1223 m_bookmarkButton = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), q); 1224 m_bookmarkButton->setPopupMode(QToolButton::InstantPopup); 1225 q->addAction(m_bookmarkButton); 1226 m_bookmarkButton->setWhatsThis( 1227 i18n("<qt>This button allows you to bookmark specific locations. " 1228 "Click on this button to open the bookmark menu where you may add, " 1229 "edit or select a bookmark.<br /><br />" 1230 "These bookmarks are specific to the file dialog, but otherwise operate " 1231 "like bookmarks elsewhere in KDE.</qt>")); 1232 1233 QWidget *midSpacer = new QWidget(q); 1234 midSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 1235 1236 m_toolbar->addAction(m_ops->action(KDirOperator::Back)); 1237 m_toolbar->addAction(m_ops->action(KDirOperator::Forward)); 1238 m_toolbar->addAction(m_ops->action(KDirOperator::Up)); 1239 m_toolbar->addAction(m_ops->action(KDirOperator::Reload)); 1240 m_toolbar->addSeparator(); 1241 m_toolbar->addAction(m_ops->action(KDirOperator::ViewIconsView)); 1242 m_toolbar->addAction(m_ops->action(KDirOperator::ViewCompactView)); 1243 m_toolbar->addAction(m_ops->action(KDirOperator::ViewDetailsView)); 1244 m_toolbar->addSeparator(); 1245 m_toolbar->addAction(m_ops->action(KDirOperator::ShowPreview)); 1246 m_toolbar->addAction(m_ops->action(KDirOperator::SortMenu)); 1247 m_toolbar->addAction(m_bookmarkButton); 1248 1249 m_toolbar->addWidget(midSpacer); 1250 1251 initZoomWidget(); 1252 m_toolbar->addAction(m_zoomOutAction); 1253 m_toolbar->addWidget(m_iconSizeSlider); 1254 m_toolbar->addAction(m_zoomInAction); 1255 m_toolbar->addSeparator(); 1256 1257 m_toolbar->addAction(m_ops->action(KDirOperator::NewFolder)); 1258 m_toolbar->addAction(menu); 1259 1260 m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); 1261 m_toolbar->setMovable(false); 1262 } 1263 1264 void KFileWidgetPrivate::initLocationWidget() 1265 { 1266 m_locationLabel = new QLabel(i18n("&Name:"), q); 1267 m_locationEdit = new KUrlComboBox(KUrlComboBox::Files, true, q); 1268 m_locationEdit->installEventFilter(q); 1269 // Properly let the dialog be resized (to smaller). Otherwise we could have 1270 // huge dialogs that can't be resized to smaller (it would be as big as the longest 1271 // item in this combo box). (ereslibre) 1272 m_locationEdit->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); 1273 q->connect(m_locationEdit, &KUrlComboBox::editTextChanged, q, [this](const QString &text) { 1274 slotLocationChanged(text); 1275 }); 1276 1277 updateLocationWhatsThis(); 1278 m_locationLabel->setBuddy(m_locationEdit); 1279 1280 KUrlCompletion *fileCompletionObj = new KUrlCompletion(KUrlCompletion::FileCompletion); 1281 m_locationEdit->setCompletionObject(fileCompletionObj); 1282 m_locationEdit->setAutoDeleteCompletionObject(true); 1283 1284 q->connect(m_locationEdit, &KUrlComboBox::returnPressed, q, [this](const QString &text) { 1285 locationAccepted(text); 1286 }); 1287 } 1288 1289 void KFileWidgetPrivate::initFilterWidget() 1290 { 1291 m_filterLabel = new QLabel(q); 1292 m_filterWidget = new KFileFilterCombo(q); 1293 m_filterWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 1294 updateFilterText(); 1295 // Properly let the dialog be resized (to smaller). Otherwise we could have 1296 // huge dialogs that can't be resized to smaller (it would be as big as the longest 1297 // item in this combo box). (ereslibre) 1298 m_filterWidget->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow); 1299 m_filterLabel->setBuddy(m_filterWidget); 1300 q->connect(m_filterWidget, &KFileFilterCombo::filterChanged, q, [this]() { 1301 slotFilterChanged(); 1302 }); 1303 1304 m_filterDelayTimer.setSingleShot(true); 1305 m_filterDelayTimer.setInterval(300); 1306 q->connect(m_filterWidget, &QComboBox::editTextChanged, &m_filterDelayTimer, qOverload<>(&QTimer::start)); 1307 q->connect(&m_filterDelayTimer, &QTimer::timeout, q, [this]() { 1308 slotFilterChanged(); 1309 }); 1310 } 1311 1312 void KFileWidgetPrivate::setLocationText(const QList<QUrl> &urlList) 1313 { 1314 // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get 1315 // emitted, so slotLocationChanged() will be called. Make sure we don't clear the 1316 // KDirOperator's view-selection in there 1317 const QSignalBlocker blocker(m_locationEdit); 1318 1319 const QUrl baseUrl = m_ops->url(); 1320 1321 if (urlList.count() > 1) { 1322 QString urls; 1323 for (const QUrl &url : urlList) { 1324 urls += QStringLiteral("\"%1\" ").arg(escapeDoubleQuotes(relativePathOrUrl(baseUrl, url))); 1325 } 1326 urls.chop(1); 1327 m_locationEdit->setEditText(urls); 1328 } else if (urlList.count() == 1) { 1329 const auto url = urlList[0]; 1330 m_locationEdit->setEditText(escapeDoubleQuotes(relativePathOrUrl(baseUrl, url))); 1331 } else if (!m_locationEdit->lineEdit()->text().isEmpty()) { 1332 m_locationEdit->clearEditText(); 1333 } 1334 1335 if (m_operationMode == KFileWidget::Saving) { 1336 setNonExtSelection(); 1337 } 1338 } 1339 1340 void KFileWidgetPrivate::updateLocationWhatsThis() 1341 { 1342 const QString autocompletionWhatsThisText = i18n( 1343 "<qt>While typing in the text area, you may be presented " 1344 "with possible matches. " 1345 "This feature can be controlled by clicking with the right mouse button " 1346 "and selecting a preferred mode from the <b>Text Completion</b> menu.</qt>"); 1347 1348 QString whatsThisText; 1349 if (m_operationMode == KFileWidget::Saving) { 1350 whatsThisText = QLatin1String("<qt>") + i18n("This is the name to save the file as.") + autocompletionWhatsThisText; 1351 } else if (m_ops->mode() & KFile::Files) { 1352 whatsThisText = QLatin1String("<qt>") 1353 + i18n("This is the list of files to open. More than " 1354 "one file can be specified by listing several " 1355 "files, separated by spaces.") 1356 + autocompletionWhatsThisText; 1357 } else { 1358 whatsThisText = QLatin1String("<qt>") + i18n("This is the name of the file to open.") + autocompletionWhatsThisText; 1359 } 1360 1361 m_locationLabel->setWhatsThis(whatsThisText); 1362 m_locationEdit->setWhatsThis(whatsThisText); 1363 } 1364 1365 void KFileWidgetPrivate::initPlacesPanel() 1366 { 1367 if (m_placesDock) { 1368 return; 1369 } 1370 1371 m_placesDock = new QDockWidget(i18nc("@title:window", "Places"), q); 1372 m_placesDock->setFeatures(QDockWidget::NoDockWidgetFeatures); 1373 m_placesDock->setTitleBarWidget(new KDEPrivate::KFileWidgetDockTitleBar(m_placesDock)); 1374 1375 m_placesView = new KFilePlacesView(m_placesDock); 1376 m_placesView->setModel(m_model); 1377 m_placesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 1378 1379 m_placesView->setObjectName(QStringLiteral("url bar")); 1380 QObject::connect(m_placesView, &KFilePlacesView::urlChanged, q, [this](const QUrl &url) { 1381 enterUrl(url); 1382 }); 1383 1384 QObject::connect(qobject_cast<KFilePlacesModel *>(m_placesView->model()), &KFilePlacesModel::errorMessage, q, [this](const QString &errorMessage) { 1385 m_messageWidget->setText(errorMessage); 1386 m_messageWidget->animatedShow(); 1387 }); 1388 1389 // need to set the current url of the urlbar manually (not via urlEntered() 1390 // here, because the initial url of KDirOperator might be the same as the 1391 // one that will be set later (and then urlEntered() won't be emitted). 1392 // TODO: KDE5 ### REMOVE THIS when KDirOperator's initial URL (in the c'tor) is gone. 1393 m_placesView->setUrl(m_url); 1394 1395 m_placesDock->setWidget(m_placesView); 1396 m_placesViewSplitter->insertWidget(0, m_placesDock); 1397 1398 // initialize the size of the splitter 1399 m_placesViewWidth = m_configGroup.readEntry(SpeedbarWidth, m_placesView->sizeHint().width()); 1400 1401 // Needed for when the dialog is shown with the places panel initially hidden 1402 setPlacesViewSplitterSizes(); 1403 1404 QObject::connect(m_placesDock, &QDockWidget::visibilityChanged, q, [this](bool visible) { 1405 togglePlacesPanel(visible, m_placesDock); 1406 }); 1407 } 1408 1409 void KFileWidgetPrivate::setPlacesViewSplitterSizes() 1410 { 1411 if (m_placesViewWidth > 0) { 1412 QList<int> sizes = m_placesViewSplitter->sizes(); 1413 sizes[0] = m_placesViewWidth; 1414 sizes[1] = q->width() - m_placesViewWidth - m_placesViewSplitter->handleWidth(); 1415 m_placesViewSplitter->setSizes(sizes); 1416 } 1417 } 1418 1419 void KFileWidgetPrivate::initGUI() 1420 { 1421 delete m_boxLayout; // deletes all sub layouts 1422 1423 m_boxLayout = new QVBoxLayout(q); 1424 m_boxLayout->setContentsMargins(0, 0, 0, 0); // no additional margin to the already existing 1425 1426 m_placesViewSplitter = new QSplitter(q); 1427 m_placesViewSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); 1428 m_placesViewSplitter->setChildrenCollapsible(false); 1429 m_boxLayout->addWidget(m_placesViewSplitter); 1430 1431 QObject::connect(m_placesViewSplitter, &QSplitter::splitterMoved, q, [this](int pos, int index) { 1432 placesViewSplitterMoved(pos, index); 1433 }); 1434 m_placesViewSplitter->insertWidget(0, m_opsWidget); 1435 1436 m_lafBox = new QFormLayout(); 1437 m_lafBox->setSpacing(q->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing)); 1438 m_lafBox->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 1439 q->style()->pixelMetric(QStyle::PM_LayoutTopMargin), 1440 q->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 1441 0); 1442 1443 m_lafBox->addRow(m_locationLabel, m_locationEdit); 1444 m_lafBox->addRow(m_filterLabel, m_filterWidget); 1445 // Add the "Automatically Select Extension" checkbox 1446 m_lafBox->addWidget(m_autoSelectExtCheckBox); 1447 1448 m_opsWidgetLayout->addLayout(m_lafBox); 1449 1450 auto hbox = new QHBoxLayout(); 1451 hbox->setSpacing(q->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing)); 1452 hbox->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutTopMargin), 1453 q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 1454 q->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 1455 q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); 1456 1457 hbox->addStretch(2); 1458 hbox->addWidget(m_okButton); 1459 hbox->addWidget(m_cancelButton); 1460 1461 m_opsWidgetLayout->addLayout(hbox); 1462 1463 q->setTabOrder(m_ops, m_autoSelectExtCheckBox); 1464 q->setTabOrder(m_autoSelectExtCheckBox, m_locationEdit); 1465 q->setTabOrder(m_locationEdit, m_filterWidget); 1466 q->setTabOrder(m_filterWidget, m_okButton); 1467 q->setTabOrder(m_okButton, m_cancelButton); 1468 q->setTabOrder(m_cancelButton, m_urlNavigator); 1469 q->setTabOrder(m_urlNavigator, m_ops); 1470 } 1471 1472 void KFileWidgetPrivate::slotFilterChanged() 1473 { 1474 m_filterDelayTimer.stop(); 1475 1476 KFileFilter filter = m_filterWidget->currentFilter(); 1477 1478 m_ops->clearFilter(); 1479 1480 if (!filter.mimePatterns().isEmpty()) { 1481 QStringList types = filter.mimePatterns(); 1482 types.prepend(QStringLiteral("inode/directory")); 1483 m_ops->setMimeFilter(types); 1484 } 1485 1486 const auto filePatterns = filter.filePatterns(); 1487 const bool hasRegExSyntax = std::any_of(filePatterns.constBegin(), filePatterns.constEnd(), [](const QString &filter) { 1488 return filter.contains(QLatin1Char('*')) || filter.contains(QLatin1Char('?')) || filter.contains(QLatin1Char('[')); 1489 }); 1490 1491 if (hasRegExSyntax) { 1492 m_ops->setNameFilter(filter.filePatterns().join(QLatin1Char(' '))); 1493 } else { 1494 m_ops->setNameFilter(QLatin1Char('*') + filePatterns.join(QLatin1Char('*')) + QLatin1Char('*')); 1495 } 1496 1497 updateAutoSelectExtension(); 1498 1499 m_ops->updateDir(); 1500 1501 Q_EMIT q->filterChanged(filter); 1502 } 1503 1504 void KFileWidget::setUrl(const QUrl &url, bool clearforward) 1505 { 1506 // qDebug(); 1507 1508 d->m_ops->setUrl(url, clearforward); 1509 } 1510 1511 // Protected 1512 void KFileWidgetPrivate::urlEntered(const QUrl &url) 1513 { 1514 // qDebug(); 1515 1516 KUrlComboBox *pathCombo = m_urlNavigator->editor(); 1517 if (pathCombo->count() != 0) { // little hack 1518 pathCombo->setUrl(url); 1519 } 1520 1521 bool blocked = m_locationEdit->blockSignals(true); 1522 if (m_keepLocation) { 1523 const QUrl currentUrl = urlFromString(locationEditCurrentText()); 1524 // iconNameForUrl will get the icon or fallback to a generic one 1525 m_locationEdit->setItemIcon(0, QIcon::fromTheme(KIO::iconNameForUrl(currentUrl))); 1526 // Preserve the text when clicking on the view (cf fileHighlighted) 1527 m_locationEdit->lineEdit()->setModified(true); 1528 } 1529 1530 m_locationEdit->blockSignals(blocked); 1531 1532 m_urlNavigator->setLocationUrl(url); 1533 1534 // is triggered in ctor before completion object is set 1535 KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject()); 1536 if (completion) { 1537 completion->setDir(url); 1538 } 1539 1540 if (m_placesView) { 1541 m_placesView->setUrl(url); 1542 } 1543 1544 m_messageWidget->hide(); 1545 } 1546 1547 void KFileWidgetPrivate::locationAccepted(const QString &url) 1548 { 1549 Q_UNUSED(url); 1550 // qDebug(); 1551 q->slotOk(); 1552 } 1553 1554 void KFileWidgetPrivate::enterUrl(const QUrl &url) 1555 { 1556 // qDebug(); 1557 1558 // append '/' if needed: url combo does not add it 1559 // tokenize() expects it because it uses QUrl::adjusted(QUrl::RemoveFilename) 1560 QUrl u(url); 1561 Utils::appendSlashToPath(u); 1562 q->setUrl(u); 1563 1564 // We need to check window()->focusWidget() instead of m_locationEdit->hasFocus 1565 // because when the window is showing up m_locationEdit 1566 // may still not have focus but it'll be the one that will have focus when the window 1567 // gets it and we don't want to steal its focus either 1568 if (q->window()->focusWidget() != m_locationEdit) { 1569 m_ops->setFocus(); 1570 } 1571 } 1572 1573 void KFileWidgetPrivate::enterUrl(const QString &url) 1574 { 1575 // qDebug(); 1576 1577 enterUrl(urlFromString(KUrlCompletion::replacedPath(url, true, true))); 1578 } 1579 1580 bool KFileWidgetPrivate::toOverwrite(const QUrl &url) 1581 { 1582 // qDebug(); 1583 1584 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 1585 KJobWidgets::setWindow(statJob, q); 1586 bool res = statJob->exec(); 1587 1588 if (res) { 1589 int ret = KMessageBox::warningContinueCancel(q, 1590 i18n("The file \"%1\" already exists. Do you wish to overwrite it?", url.fileName()), 1591 i18n("Overwrite File?"), 1592 KStandardGuiItem::overwrite(), 1593 KStandardGuiItem::cancel(), 1594 QString(), 1595 KMessageBox::Notify | KMessageBox::Dangerous); 1596 1597 if (ret != KMessageBox::Continue) { 1598 m_locationEdit->setFocus(); 1599 setNonExtSelection(); 1600 1601 return false; 1602 } 1603 return true; 1604 } 1605 1606 return true; 1607 } 1608 1609 void KFileWidget::setSelectedUrl(const QUrl &url) 1610 { 1611 // Honor protocols that do not support directory listing 1612 if (!url.isRelative() && !KProtocolManager::supportsListing(url)) { 1613 return; 1614 } 1615 d->setLocationText(url); 1616 } 1617 1618 void KFileWidget::setSelectedUrls(const QList<QUrl> &urls) 1619 { 1620 if (urls.isEmpty()) { 1621 return; 1622 } 1623 1624 // Honor protocols that do not support directory listing 1625 if (!urls[0].isRelative() && !KProtocolManager::supportsListing(urls[0])) { 1626 return; 1627 } 1628 d->setLocationText(urls); 1629 } 1630 1631 void KFileWidgetPrivate::slotLoadingFinished() 1632 { 1633 const QString currentText = m_locationEdit->currentText(); 1634 if (currentText.isEmpty()) { 1635 return; 1636 } 1637 1638 m_ops->blockSignals(true); 1639 QUrl u(m_ops->url()); 1640 if (currentText.startsWith(QLatin1Char('/'))) { 1641 u.setPath(currentText); 1642 } else { 1643 u.setPath(Utils::concatPaths(m_ops->url().path(), currentText)); 1644 } 1645 m_ops->setCurrentItem(u); 1646 m_ops->blockSignals(false); 1647 } 1648 1649 void KFileWidgetPrivate::slotLocationChanged(const QString &text) 1650 { 1651 // qDebug(); 1652 1653 m_locationEdit->lineEdit()->setModified(true); 1654 1655 if (text.isEmpty() && m_ops->view()) { 1656 m_ops->view()->clearSelection(); 1657 } 1658 1659 if (!m_locationEdit->lineEdit()->text().isEmpty()) { 1660 const QList<QUrl> urlList(tokenize(text)); 1661 m_ops->setCurrentItems(urlList); 1662 } 1663 1664 updateFilter(); 1665 } 1666 1667 QUrl KFileWidget::selectedUrl() const 1668 { 1669 // qDebug(); 1670 1671 if (d->m_inAccept) { 1672 return d->m_url; 1673 } else { 1674 return QUrl(); 1675 } 1676 } 1677 1678 QList<QUrl> KFileWidget::selectedUrls() const 1679 { 1680 // qDebug(); 1681 1682 QList<QUrl> list; 1683 if (d->m_inAccept) { 1684 if (d->m_ops->mode() & KFile::Files) { 1685 list = d->m_urlList; 1686 } else { 1687 list.append(d->m_url); 1688 } 1689 } 1690 return list; 1691 } 1692 1693 QList<QUrl> KFileWidgetPrivate::tokenize(const QString &line) const 1694 { 1695 qCDebug(KIO_KFILEWIDGETS_FW) << "Tokenizing:" << line; 1696 1697 QList<QUrl> urls; 1698 QUrl baseUrl(m_ops->url().adjusted(QUrl::RemoveFilename)); 1699 Utils::appendSlashToPath(baseUrl); 1700 1701 // A helper that creates, validates and appends a new url based 1702 // on the given filename. 1703 auto addUrl = [baseUrl, &urls](const QString &partial_name) { 1704 if (partial_name.trimmed().isEmpty()) { 1705 return; 1706 } 1707 1708 // url could be absolute 1709 QUrl partial_url(partial_name); 1710 if (!partial_url.isValid() 1711 || partial_url.isRelative() 1712 // the text might look like a url scheme but not be a real one 1713 || (!partial_url.scheme().isEmpty() && (!partial_name.contains(QStringLiteral("://")) || !KProtocolInfo::isKnownProtocol(partial_url.scheme())))) { 1714 // We have to use setPath here, so that something like "test#file" 1715 // isn't interpreted to have path "test" and fragment "file". 1716 partial_url.clear(); 1717 partial_url.setPath(partial_name); 1718 } 1719 1720 // This returns QUrl(partial_name) for absolute URLs. 1721 // Otherwise, returns the concatenated url. 1722 if (partial_url.isRelative() || baseUrl.isParentOf(partial_url)) { 1723 partial_url = baseUrl.resolved(partial_url); 1724 } 1725 1726 if (partial_url.isValid()) { 1727 urls.append(partial_url); 1728 } else { 1729 // This can happen in the first quote! (ex: ' "something here"') 1730 qCDebug(KIO_KFILEWIDGETS_FW) << "Discarding Invalid" << partial_url; 1731 } 1732 }; 1733 1734 // An iterative approach here where we toggle the "escape" flag 1735 // if we hit `\`. If we hit `"` and the escape flag is false, 1736 // we split 1737 QString partial_name; 1738 bool escape = false; 1739 for (int i = 0; i < line.length(); i++) { 1740 const QChar ch = line[i]; 1741 1742 // Handle any character previously escaped 1743 if (escape) { 1744 partial_name += ch; 1745 escape = false; 1746 continue; 1747 } 1748 1749 // Handle escape start 1750 if (ch.toLatin1() == '\\') { 1751 escape = true; 1752 continue; 1753 } 1754 1755 // Handle UNESCAPED quote (") since the above ifs are 1756 // dealing with the escaped ones 1757 if (ch.toLatin1() == '"') { 1758 addUrl(partial_name); 1759 partial_name.clear(); 1760 continue; 1761 } 1762 1763 // Any other character just append 1764 partial_name += ch; 1765 } 1766 1767 // Handle the last item which is buffered in partial_name. This is 1768 // required for single-file selection dialogs since the name will not 1769 // be wrapped in quotes 1770 if (!partial_name.isEmpty()) { 1771 addUrl(partial_name); 1772 partial_name.clear(); 1773 } 1774 1775 return urls; 1776 } 1777 1778 QString KFileWidget::selectedFile() const 1779 { 1780 // qDebug(); 1781 1782 if (d->m_inAccept) { 1783 const QUrl url = d->mostLocalUrl(d->m_url); 1784 if (url.isLocalFile()) { 1785 return url.toLocalFile(); 1786 } else { 1787 KMessageBox::error(const_cast<KFileWidget *>(this), i18n("You can only select local files."), i18n("Remote Files Not Accepted")); 1788 } 1789 } 1790 return QString(); 1791 } 1792 1793 QStringList KFileWidget::selectedFiles() const 1794 { 1795 // qDebug(); 1796 1797 QStringList list; 1798 1799 if (d->m_inAccept) { 1800 if (d->m_ops->mode() & KFile::Files) { 1801 const QList<QUrl> urls = d->m_urlList; 1802 for (const auto &u : urls) { 1803 const QUrl url = d->mostLocalUrl(u); 1804 if (url.isLocalFile()) { 1805 list.append(url.toLocalFile()); 1806 } 1807 } 1808 } 1809 1810 else { // single-selection mode 1811 if (d->m_url.isLocalFile()) { 1812 list.append(d->m_url.toLocalFile()); 1813 } 1814 } 1815 } 1816 1817 return list; 1818 } 1819 1820 QUrl KFileWidget::baseUrl() const 1821 { 1822 return d->m_ops->url(); 1823 } 1824 1825 void KFileWidget::resizeEvent(QResizeEvent *event) 1826 { 1827 QWidget::resizeEvent(event); 1828 1829 if (d->m_placesDock) { 1830 // we don't want our places dock actually changing size when we resize 1831 // and qt doesn't make it easy to enforce such a thing with QSplitter 1832 d->setPlacesViewSplitterSizes(); 1833 } 1834 } 1835 1836 void KFileWidget::showEvent(QShowEvent *event) 1837 { 1838 if (!d->m_hasView) { // delayed view-creation 1839 Q_ASSERT(d); 1840 Q_ASSERT(d->m_ops); 1841 d->m_ops->setViewMode(KFile::Default); 1842 d->m_hasView = true; 1843 1844 connect(d->m_ops->view(), &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &index) { 1845 d->slotViewDoubleClicked(index); 1846 }); 1847 } 1848 d->m_ops->clearHistory(); 1849 1850 QWidget::showEvent(event); 1851 } 1852 1853 bool KFileWidget::eventFilter(QObject *watched, QEvent *event) 1854 { 1855 const bool res = QWidget::eventFilter(watched, event); 1856 1857 QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>(event); 1858 if (!keyEvent) { 1859 return res; 1860 } 1861 1862 const auto type = event->type(); 1863 const auto key = keyEvent->key(); 1864 1865 if (watched == d->m_ops && type == QEvent::KeyPress && (key == Qt::Key_Return || key == Qt::Key_Enter)) { 1866 // ignore return events from the KDirOperator 1867 // they are not needed, activated is used to handle this case 1868 event->accept(); 1869 return true; 1870 } 1871 1872 return res; 1873 } 1874 1875 void KFileWidget::setMode(KFile::Modes m) 1876 { 1877 // qDebug(); 1878 1879 d->m_ops->setMode(m); 1880 if (d->m_ops->dirOnlyMode()) { 1881 d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Folders"), {QStringLiteral("*")}, {})); 1882 } else { 1883 d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Files"), {QStringLiteral("*")}, {})); 1884 } 1885 1886 d->updateAutoSelectExtension(); 1887 } 1888 1889 KFile::Modes KFileWidget::mode() const 1890 { 1891 return d->m_ops->mode(); 1892 } 1893 1894 void KFileWidgetPrivate::readViewConfig() 1895 { 1896 m_ops->setViewConfig(m_configGroup); 1897 m_ops->readConfig(m_configGroup); 1898 KUrlComboBox *combo = m_urlNavigator->editor(); 1899 1900 KCompletion::CompletionMode cm = 1901 (KCompletion::CompletionMode)m_configGroup.readEntry(PathComboCompletionMode, static_cast<int>(KCompletion::CompletionPopup)); 1902 if (cm != KCompletion::CompletionPopup) { 1903 combo->setCompletionMode(cm); 1904 } 1905 1906 cm = (KCompletion::CompletionMode)m_configGroup.readEntry(LocationComboCompletionMode, static_cast<int>(KCompletion::CompletionPopup)); 1907 if (cm != KCompletion::CompletionPopup) { 1908 m_locationEdit->setCompletionMode(cm); 1909 } 1910 1911 // Show or don't show the places panel 1912 togglePlacesPanel(m_configGroup.readEntry(ShowSpeedbar, true)); 1913 1914 // show or don't show the bookmarks 1915 toggleBookmarks(m_configGroup.readEntry(ShowBookmarks, false)); 1916 1917 // does the user want Automatically Select Extension? 1918 m_autoSelectExtChecked = m_configGroup.readEntry(AutoSelectExtChecked, DefaultAutoSelectExtChecked); 1919 updateAutoSelectExtension(); 1920 1921 // should the URL navigator use the breadcrumb navigation? 1922 m_urlNavigator->setUrlEditable(!m_configGroup.readEntry(BreadcrumbNavigation, true)); 1923 1924 // should the URL navigator show the full path? 1925 m_urlNavigator->setShowFullPath(m_configGroup.readEntry(ShowFullPath, false)); 1926 1927 int w1 = q->minimumSize().width(); 1928 int w2 = m_toolbar->sizeHint().width(); 1929 if (w1 < w2) { 1930 q->setMinimumWidth(w2); 1931 } 1932 } 1933 1934 void KFileWidgetPrivate::writeViewConfig() 1935 { 1936 // these settings are global settings; ALL instances of the file dialog 1937 // should reflect them. 1938 // There is no way to tell KFileOperator::writeConfig() to write to 1939 // kdeglobals so we write settings to a temporary config group then copy 1940 // them all to kdeglobals 1941 KConfig tmp(QString(), KConfig::SimpleConfig); 1942 KConfigGroup tmpGroup(&tmp, ConfigGroup); 1943 1944 KUrlComboBox *pathCombo = m_urlNavigator->editor(); 1945 // saveDialogSize( tmpGroup, KConfigGroup::Persistent | KConfigGroup::Global ); 1946 tmpGroup.writeEntry(PathComboCompletionMode, static_cast<int>(pathCombo->completionMode())); 1947 tmpGroup.writeEntry(LocationComboCompletionMode, static_cast<int>(m_locationEdit->completionMode())); 1948 1949 const bool showPlacesPanel = m_placesDock && !m_placesDock->isHidden(); 1950 tmpGroup.writeEntry(ShowSpeedbar, showPlacesPanel); 1951 if (m_placesViewWidth > 0) { 1952 tmpGroup.writeEntry(SpeedbarWidth, m_placesViewWidth); 1953 } 1954 1955 tmpGroup.writeEntry(ShowBookmarks, m_bookmarkHandler != nullptr); 1956 tmpGroup.writeEntry(AutoSelectExtChecked, m_autoSelectExtChecked); 1957 tmpGroup.writeEntry(BreadcrumbNavigation, !m_urlNavigator->isUrlEditable()); 1958 tmpGroup.writeEntry(ShowFullPath, m_urlNavigator->showFullPath()); 1959 1960 m_ops->writeConfig(tmpGroup); 1961 1962 // Copy saved settings to kdeglobals 1963 tmpGroup.copyTo(&m_configGroup, KConfigGroup::Persistent | KConfigGroup::Global); 1964 } 1965 1966 void KFileWidgetPrivate::readRecentFiles() 1967 { 1968 // qDebug(); 1969 1970 const bool oldState = m_locationEdit->blockSignals(true); 1971 m_locationEdit->setMaxItems(m_configGroup.readEntry(RecentFilesNumber, DefaultRecentURLsNumber)); 1972 m_locationEdit->setUrls(m_configGroup.readPathEntry(RecentFiles, QStringList()), KUrlComboBox::RemoveBottom); 1973 m_locationEdit->setCurrentIndex(-1); 1974 m_locationEdit->blockSignals(oldState); 1975 1976 KUrlComboBox *combo = m_urlNavigator->editor(); 1977 combo->setUrls(m_configGroup.readPathEntry(RecentURLs, QStringList()), KUrlComboBox::RemoveTop); 1978 combo->setMaxItems(m_configGroup.readEntry(RecentURLsNumber, DefaultRecentURLsNumber)); 1979 combo->setUrl(m_ops->url()); 1980 // since we delayed this moment, initialize the directory of the completion object to 1981 // our current directory (that was very probably set on the constructor) 1982 KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject()); 1983 if (completion) { 1984 completion->setDir(m_ops->url()); 1985 } 1986 } 1987 1988 void KFileWidgetPrivate::saveRecentFiles() 1989 { 1990 // qDebug(); 1991 m_configGroup.writePathEntry(RecentFiles, m_locationEdit->urls()); 1992 1993 KUrlComboBox *pathCombo = m_urlNavigator->editor(); 1994 m_configGroup.writePathEntry(RecentURLs, pathCombo->urls()); 1995 } 1996 1997 QPushButton *KFileWidget::okButton() const 1998 { 1999 return d->m_okButton; 2000 } 2001 2002 QPushButton *KFileWidget::cancelButton() const 2003 { 2004 return d->m_cancelButton; 2005 } 2006 2007 // Called by KFileDialog 2008 void KFileWidget::slotCancel() 2009 { 2010 d->writeViewConfig(); 2011 d->m_ops->close(); 2012 } 2013 2014 void KFileWidget::setKeepLocation(bool keep) 2015 { 2016 d->m_keepLocation = keep; 2017 } 2018 2019 bool KFileWidget::keepsLocation() const 2020 { 2021 return d->m_keepLocation; 2022 } 2023 2024 void KFileWidget::setOperationMode(OperationMode mode) 2025 { 2026 // qDebug(); 2027 2028 d->m_operationMode = mode; 2029 d->m_keepLocation = (mode == Saving); 2030 d->m_filterWidget->setEditable(!d->m_hasDefaultFilter || mode != Saving); 2031 if (mode == Opening) { 2032 // don't use KStandardGuiItem::open() here which has trailing ellipsis! 2033 d->m_okButton->setText(i18n("&Open")); 2034 d->m_okButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); 2035 // hide the new folder actions...usability team says they shouldn't be in open file dialog 2036 d->m_ops->action(KDirOperator::NewFolder)->setEnabled(false); 2037 d->m_toolbar->removeAction(d->m_ops->action(KDirOperator::NewFolder)); 2038 } else if (mode == Saving) { 2039 KGuiItem::assign(d->m_okButton, KStandardGuiItem::save()); 2040 d->setNonExtSelection(); 2041 } else { 2042 KGuiItem::assign(d->m_okButton, KStandardGuiItem::ok()); 2043 } 2044 d->updateLocationWhatsThis(); 2045 d->updateAutoSelectExtension(); 2046 2047 if (d->m_ops) { 2048 d->m_ops->setIsSaving(mode == Saving); 2049 } 2050 d->updateFilterText(); 2051 } 2052 2053 KFileWidget::OperationMode KFileWidget::operationMode() const 2054 { 2055 return d->m_operationMode; 2056 } 2057 2058 void KFileWidgetPrivate::slotAutoSelectExtClicked() 2059 { 2060 // qDebug() << "slotAutoSelectExtClicked(): " 2061 // << m_autoSelectExtCheckBox->isChecked() << endl; 2062 2063 // whether the _user_ wants it on/off 2064 m_autoSelectExtChecked = m_autoSelectExtCheckBox->isChecked(); 2065 2066 // update the current filename's extension 2067 updateLocationEditExtension(m_extension /* extension hasn't changed */); 2068 } 2069 2070 void KFileWidgetPrivate::placesViewSplitterMoved(int pos, int index) 2071 { 2072 // qDebug(); 2073 2074 // we need to record the size of the splitter when the splitter changes size 2075 // so we can keep the places box the right size! 2076 if (m_placesDock && index == 1) { 2077 m_placesViewWidth = pos; 2078 // qDebug() << "setting m_lafBox minwidth to" << m_placesViewWidth; 2079 } 2080 } 2081 2082 void KFileWidgetPrivate::activateUrlNavigator() 2083 { 2084 // qDebug(); 2085 2086 QLineEdit *lineEdit = m_urlNavigator->editor()->lineEdit(); 2087 2088 // If the text field currently has focus and everything is selected, 2089 // pressing the keyboard shortcut returns the whole thing to breadcrumb mode 2090 if (m_urlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) { 2091 m_urlNavigator->setUrlEditable(false); 2092 } else { 2093 m_urlNavigator->setUrlEditable(true); 2094 m_urlNavigator->setFocus(); 2095 lineEdit->selectAll(); 2096 } 2097 } 2098 2099 void KFileWidgetPrivate::slotDirOpIconSizeChanged(int size) 2100 { 2101 auto beginIt = m_stdIconSizes.cbegin(); 2102 auto endIt = m_stdIconSizes.cend(); 2103 auto it = std::lower_bound(beginIt, endIt, size); 2104 const int sliderStep = it != endIt ? it - beginIt : 0; 2105 m_iconSizeSlider->setValue(sliderStep); 2106 m_zoomOutAction->setDisabled(it == beginIt); 2107 m_zoomInAction->setDisabled(it == (endIt - 1)); 2108 } 2109 2110 void KFileWidgetPrivate::changeIconsSize(ZoomState zoom) 2111 { 2112 int step = m_iconSizeSlider->value(); 2113 2114 if (zoom == ZoomOut) { 2115 if (step == 0) { 2116 return; 2117 } 2118 --step; 2119 } else { // ZoomIn 2120 if (step == static_cast<int>(m_stdIconSizes.size() - 1)) { 2121 return; 2122 } 2123 ++step; 2124 } 2125 2126 m_iconSizeSlider->setValue(step); 2127 slotIconSizeSliderMoved(m_stdIconSizes[step]); 2128 } 2129 2130 void KFileWidgetPrivate::slotIconSizeChanged(int _value) 2131 { 2132 m_ops->setIconSize(_value); 2133 m_iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels", _value)); 2134 } 2135 2136 void KFileWidgetPrivate::slotIconSizeSliderMoved(int size) 2137 { 2138 // Force this to be called in case this slot is called first on the 2139 // slider move. 2140 slotIconSizeChanged(size); 2141 2142 QPoint global(m_iconSizeSlider->rect().topLeft()); 2143 global.ry() += m_iconSizeSlider->height() / 2; 2144 QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_iconSizeSlider->mapToGlobal(global)); 2145 QApplication::sendEvent(m_iconSizeSlider, &toolTipEvent); 2146 } 2147 2148 void KFileWidgetPrivate::slotViewDoubleClicked(const QModelIndex &index) 2149 { 2150 // double clicking to save should only work on files 2151 if (m_operationMode == KFileWidget::Saving && index.isValid() && m_ops->selectedItems().constFirst().isFile()) { 2152 q->slotOk(); 2153 } 2154 } 2155 2156 void KFileWidgetPrivate::slotViewKeyEnterReturnPressed() 2157 { 2158 // an enter/return event occurred in the view 2159 // when we are saving one file and there is no selection in the view (otherwise we get an activated event) 2160 if (m_operationMode == KFileWidget::Saving && (m_ops->mode() & KFile::File) && m_ops->selectedItems().isEmpty()) { 2161 q->slotOk(); 2162 } 2163 } 2164 2165 static QString getExtensionFromPatternList(const QStringList &patternList) 2166 { 2167 // qDebug(); 2168 2169 QString ret; 2170 // qDebug() << "\tgetExtension " << patternList; 2171 2172 QStringList::ConstIterator patternListEnd = patternList.end(); 2173 for (QStringList::ConstIterator it = patternList.begin(); it != patternListEnd; ++it) { 2174 // qDebug() << "\t\ttry: \'" << (*it) << "\'"; 2175 2176 // is this pattern like "*.BMP" rather than useless things like: 2177 // 2178 // README 2179 // *. 2180 // *.* 2181 // *.JP*G 2182 // *.JP? 2183 // *.[Jj][Pp][Gg] 2184 if ((*it).startsWith(QLatin1String("*.")) && (*it).length() > 2 && (*it).indexOf(QLatin1Char('*'), 2) < 0 && (*it).indexOf(QLatin1Char('?'), 2) < 0 2185 && (*it).indexOf(QLatin1Char('['), 2) < 0 && (*it).indexOf(QLatin1Char(']'), 2) < 0) { 2186 ret = (*it).mid(1); 2187 break; 2188 } 2189 } 2190 2191 return ret; 2192 } 2193 2194 static QString stripUndisplayable(const QString &string) 2195 { 2196 QString ret = string; 2197 2198 ret.remove(QLatin1Char(':')); 2199 ret = KLocalizedString::removeAcceleratorMarker(ret); 2200 2201 return ret; 2202 } 2203 2204 // QString KFileWidget::currentFilterExtension() 2205 //{ 2206 // return d->m_extension; 2207 //} 2208 2209 void KFileWidgetPrivate::updateAutoSelectExtension() 2210 { 2211 if (!m_autoSelectExtCheckBox) { 2212 return; 2213 } 2214 2215 QMimeDatabase db; 2216 // 2217 // Figure out an extension for the Automatically Select Extension thing 2218 // (some Windows users apparently don't know what to do when confronted 2219 // with a text file called "COPYING" but do know what to do with 2220 // COPYING.txt ...) 2221 // 2222 2223 // qDebug() << "Figure out an extension: "; 2224 QString lastExtension = m_extension; 2225 m_extension.clear(); 2226 2227 // Automatically Select Extension is only valid if the user is _saving_ a _file_ 2228 if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) { 2229 // 2230 // Get an extension from the filter 2231 // 2232 2233 KFileFilter fileFilter = m_filterWidget->currentFilter(); 2234 if (!fileFilter.isEmpty()) { 2235 // if the currently selected filename already has an extension which 2236 // is also included in the currently allowed extensions, keep it 2237 // otherwise use the default extension 2238 QString currentExtension = db.suffixForFileName(locationEditCurrentText()); 2239 if (currentExtension.isEmpty()) { 2240 currentExtension = locationEditCurrentText().section(QLatin1Char('.'), -1, -1); 2241 } 2242 // qDebug() << "filter:" << filter << "locationEdit:" << locationEditCurrentText() << "currentExtension:" << currentExtension; 2243 2244 QString defaultExtension; 2245 QStringList extensionList; 2246 2247 // e.g. "*.cpp" 2248 if (!fileFilter.filePatterns().isEmpty()) { 2249 extensionList = fileFilter.filePatterns(); 2250 defaultExtension = getExtensionFromPatternList(extensionList); 2251 } 2252 // e.g. "text/html" 2253 else if (!fileFilter.mimePatterns().isEmpty()) { 2254 QMimeType mime = db.mimeTypeForName(fileFilter.mimePatterns().first()); 2255 if (mime.isValid()) { 2256 extensionList = mime.globPatterns(); 2257 defaultExtension = mime.preferredSuffix(); 2258 if (!defaultExtension.isEmpty()) { 2259 defaultExtension.prepend(QLatin1Char('.')); 2260 } 2261 } 2262 } 2263 2264 if ((!currentExtension.isEmpty() && extensionList.contains(QLatin1String("*.") + currentExtension)) 2265 || (!fileFilter.mimePatterns().isEmpty() && fileFilter.mimePatterns().first() == QLatin1String("application/octet-stream"))) { 2266 m_extension = QLatin1Char('.') + currentExtension; 2267 } else { 2268 m_extension = defaultExtension; 2269 } 2270 2271 // qDebug() << "List:" << extensionList << "auto-selected extension:" << m_extension; 2272 } 2273 2274 // 2275 // GUI: checkbox 2276 // 2277 2278 QString whatsThisExtension; 2279 if (!m_extension.isEmpty()) { 2280 // remember: sync any changes to the string with below 2281 m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension (%1)", m_extension)); 2282 whatsThisExtension = i18n("the extension <b>%1</b>", m_extension); 2283 2284 m_autoSelectExtCheckBox->setEnabled(true); 2285 m_autoSelectExtCheckBox->setChecked(m_autoSelectExtChecked); 2286 } else { 2287 // remember: sync any changes to the string with above 2288 m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension")); 2289 whatsThisExtension = i18n("a suitable extension"); 2290 2291 m_autoSelectExtCheckBox->setChecked(false); 2292 m_autoSelectExtCheckBox->setEnabled(false); 2293 } 2294 2295 const QString locationLabelText = stripUndisplayable(m_locationLabel->text()); 2296 m_autoSelectExtCheckBox->setWhatsThis(QLatin1String("<qt>") 2297 + i18n("This option enables some convenient features for " 2298 "saving files with extensions:<br />" 2299 "<ol>" 2300 "<li>Any extension specified in the <b>%1</b> text " 2301 "area will be updated if you change the file type " 2302 "to save in.<br />" 2303 "<br /></li>" 2304 "<li>If no extension is specified in the <b>%2</b> " 2305 "text area when you click " 2306 "<b>Save</b>, %3 will be added to the end of the " 2307 "filename (if the filename does not already exist). " 2308 "This extension is based on the file type that you " 2309 "have chosen to save in.<br />" 2310 "<br />" 2311 "If you do not want KDE to supply an extension for the " 2312 "filename, you can either turn this option off or you " 2313 "can suppress it by adding a period (.) to the end of " 2314 "the filename (the period will be automatically " 2315 "removed)." 2316 "</li>" 2317 "</ol>" 2318 "If unsure, keep this option enabled as it makes your " 2319 "files more manageable.", 2320 locationLabelText, 2321 locationLabelText, 2322 whatsThisExtension) 2323 + QLatin1String("</qt>")); 2324 2325 m_autoSelectExtCheckBox->show(); 2326 2327 // update the current filename's extension 2328 updateLocationEditExtension(lastExtension); 2329 } 2330 // Automatically Select Extension not valid 2331 else { 2332 m_autoSelectExtCheckBox->setChecked(false); 2333 m_autoSelectExtCheckBox->hide(); 2334 } 2335 } 2336 2337 // Updates the extension of the filename specified in d->m_locationEdit if the 2338 // Automatically Select Extension feature is enabled. 2339 // (this prevents you from accidentally saving "file.kwd" as RTF, for example) 2340 void KFileWidgetPrivate::updateLocationEditExtension(const QString &lastExtension) 2341 { 2342 if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) { 2343 return; 2344 } 2345 2346 const QString urlStr = locationEditCurrentText(); 2347 if (urlStr.isEmpty()) { 2348 return; 2349 } 2350 2351 const int fileNameOffset = urlStr.lastIndexOf(QLatin1Char('/')) + 1; 2352 QStringView fileName = QStringView(urlStr).mid(fileNameOffset); 2353 2354 const int dot = fileName.lastIndexOf(QLatin1Char('.')); 2355 const int len = fileName.length(); 2356 if (dot > 0 && // has an extension already and it's not a hidden file 2357 // like ".hidden" (but we do accept ".hidden.ext") 2358 dot != len - 1 // and not deliberately suppressing extension 2359 ) { 2360 const QUrl url = getCompleteUrl(urlStr); 2361 // qDebug() << "updateLocationEditExtension (" << url << ")"; 2362 // exists? 2363 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 2364 KJobWidgets::setWindow(statJob, q); 2365 bool result = statJob->exec(); 2366 if (result) { 2367 // qDebug() << "\tfile exists"; 2368 2369 if (statJob->statResult().isDir()) { 2370 // qDebug() << "\tisDir - won't alter extension"; 2371 return; 2372 } 2373 2374 // --- fall through --- 2375 } 2376 2377 // 2378 // try to get rid of the current extension 2379 // 2380 2381 // catch "double extensions" like ".tar.gz" 2382 if (!lastExtension.isEmpty() && fileName.endsWith(lastExtension)) { 2383 fileName.chop(lastExtension.length()); 2384 } else if (!m_extension.isEmpty() && fileName.endsWith(m_extension)) { 2385 fileName.chop(m_extension.length()); 2386 } else { // can only handle "single extensions" 2387 fileName.truncate(dot); 2388 } 2389 2390 // add extension 2391 const QString newText = QStringView(urlStr).left(fileNameOffset) + fileName + m_extension; 2392 if (newText != locationEditCurrentText()) { 2393 const int idx = m_locationEdit->currentIndex(); 2394 if (idx == -1) { 2395 m_locationEdit->setEditText(newText); 2396 } else { 2397 m_locationEdit->setItemText(idx, newText); 2398 } 2399 m_locationEdit->lineEdit()->setModified(true); 2400 } 2401 } 2402 } 2403 2404 QString KFileWidgetPrivate::findMatchingFilter(const QString &filter, const QString &filename) const 2405 { 2406 // e.g.: '*.foo *.bar|Foo type' -> '*.foo', '*.bar' 2407 const QStringList patterns = filter.left(filter.indexOf(QLatin1Char('|'))).split(QLatin1Char(' '), Qt::SkipEmptyParts); 2408 2409 QRegularExpression rx; 2410 for (const QString &p : patterns) { 2411 rx.setPattern(QRegularExpression::wildcardToRegularExpression(p)); 2412 if (rx.match(filename).hasMatch()) { 2413 return p; 2414 } 2415 } 2416 return QString(); 2417 } 2418 2419 // Updates the filter if the extension of the filename specified in d->m_locationEdit is changed 2420 // (this prevents you from accidentally saving "file.kwd" as RTF, for example) 2421 void KFileWidgetPrivate::updateFilter() 2422 { 2423 if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) { 2424 QString urlStr = locationEditCurrentText(); 2425 if (urlStr.isEmpty()) { 2426 return; 2427 } 2428 2429 QMimeDatabase db; 2430 QMimeType urlMimeType = db.mimeTypeForFile(urlStr, QMimeDatabase::MatchExtension); 2431 2432 bool matchesCurrentFilter = [this, urlMimeType, urlStr] { 2433 const KFileFilter filter = m_filterWidget->currentFilter(); 2434 if (filter.mimePatterns().contains(urlMimeType.name())) { 2435 return true; 2436 } 2437 2438 QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename 2439 2440 const auto filePatterns = filter.filePatterns(); 2441 const bool hasMatch = std::any_of(filePatterns.cbegin(), filePatterns.cend(), [filename](const QString &pattern) { 2442 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(pattern)); 2443 2444 return rx.match(filename).hasMatch(); 2445 }); 2446 return hasMatch; 2447 }(); 2448 2449 if (matchesCurrentFilter) { 2450 return; 2451 } 2452 2453 const auto filters = m_filterWidget->filters(); 2454 2455 auto filterIt = std::find_if(filters.cbegin(), filters.cend(), [urlStr, urlMimeType](const KFileFilter &filter) { 2456 if (filter.mimePatterns().contains(urlMimeType.name())) { 2457 return true; 2458 } 2459 2460 QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename 2461 // accept any match to honor the user's selection; see later code handling the "*" match 2462 2463 const auto filePatterns = filter.filePatterns(); 2464 const bool hasMatch = std::any_of(filePatterns.cbegin(), filePatterns.cend(), [filename](const QString &pattern) { 2465 // never match the catch-all filter 2466 if (pattern == QLatin1String("*")) { 2467 return false; 2468 } 2469 2470 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(pattern)); 2471 2472 return rx.match(filename).hasMatch(); 2473 }); 2474 2475 return hasMatch; 2476 }); 2477 2478 if (filterIt != filters.cend()) { 2479 m_filterWidget->setCurrentFilter(*filterIt); 2480 } 2481 } 2482 } 2483 2484 // applies only to a file that doesn't already exist 2485 void KFileWidgetPrivate::appendExtension(QUrl &url) 2486 { 2487 // qDebug(); 2488 2489 if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) { 2490 return; 2491 } 2492 2493 QString fileName = url.fileName(); 2494 if (fileName.isEmpty()) { 2495 return; 2496 } 2497 2498 // qDebug() << "appendExtension(" << url << ")"; 2499 2500 const int len = fileName.length(); 2501 const int dot = fileName.lastIndexOf(QLatin1Char('.')); 2502 2503 const bool suppressExtension = (dot == len - 1); 2504 const bool unspecifiedExtension = !fileName.endsWith(m_extension); 2505 2506 // don't KIO::Stat if unnecessary 2507 if (!(suppressExtension || unspecifiedExtension)) { 2508 return; 2509 } 2510 2511 // exists? 2512 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 2513 KJobWidgets::setWindow(statJob, q); 2514 bool res = statJob->exec(); 2515 if (res) { 2516 // qDebug() << "\tfile exists - won't append extension"; 2517 return; 2518 } 2519 2520 // suppress automatically append extension? 2521 if (suppressExtension) { 2522 // 2523 // Strip trailing dot 2524 // This allows lazy people to have m_autoSelectExtCheckBox->isChecked 2525 // but don't want a file extension to be appended 2526 // e.g. "README." will make a file called "README" 2527 // 2528 // If you really want a name like "README.", then type "README.." 2529 // and the trailing dot will be removed (or just stop being lazy and 2530 // turn off this feature so that you can type "README.") 2531 // 2532 // qDebug() << "\tstrip trailing dot"; 2533 QString path = url.path(); 2534 path.chop(1); 2535 url.setPath(path); 2536 } 2537 // evilmatically append extension :) if the user hasn't specified one 2538 else if (unspecifiedExtension) { 2539 // qDebug() << "\tappending extension \'" << m_extension << "\'..."; 2540 url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash 2541 url.setPath(url.path() + fileName + m_extension); 2542 // qDebug() << "\tsaving as \'" << url << "\'"; 2543 } 2544 } 2545 2546 // adds the selected files/urls to 'recent documents' 2547 void KFileWidgetPrivate::addToRecentDocuments() 2548 { 2549 int m = m_ops->mode(); 2550 int atmost = KRecentDocument::maximumItems(); 2551 // don't add more than we need. KRecentDocument::add() is pretty slow 2552 2553 if (m & KFile::LocalOnly) { 2554 const QStringList files = q->selectedFiles(); 2555 QStringList::ConstIterator it = files.begin(); 2556 for (; it != files.end() && atmost > 0; ++it) { 2557 KRecentDocument::add(QUrl::fromLocalFile(*it)); 2558 atmost--; 2559 } 2560 } 2561 2562 else { // urls 2563 const QList<QUrl> urls = q->selectedUrls(); 2564 QList<QUrl>::ConstIterator it = urls.begin(); 2565 for (; it != urls.end() && atmost > 0; ++it) { 2566 if ((*it).isValid()) { 2567 KRecentDocument::add(*it); 2568 atmost--; 2569 } 2570 } 2571 } 2572 } 2573 2574 KUrlComboBox *KFileWidget::locationEdit() const 2575 { 2576 return d->m_locationEdit; 2577 } 2578 2579 KFileFilterCombo *KFileWidget::filterWidget() const 2580 { 2581 return d->m_filterWidget; 2582 } 2583 2584 void KFileWidgetPrivate::togglePlacesPanel(bool show, QObject *sender) 2585 { 2586 if (show) { 2587 initPlacesPanel(); 2588 m_placesDock->show(); 2589 2590 // check to see if they have a home item defined, if not show the home button 2591 QUrl homeURL; 2592 homeURL.setPath(QDir::homePath()); 2593 KFilePlacesModel *model = static_cast<KFilePlacesModel *>(m_placesView->model()); 2594 for (int rowIndex = 0; rowIndex < model->rowCount(); rowIndex++) { 2595 QModelIndex index = model->index(rowIndex, 0); 2596 QUrl url = model->url(index); 2597 2598 if (homeURL.matches(url, QUrl::StripTrailingSlash)) { 2599 m_toolbar->removeAction(m_ops->action(KDirOperator::Home)); 2600 break; 2601 } 2602 } 2603 } else { 2604 if (sender == m_placesDock && m_placesDock && m_placesDock->isVisibleTo(q)) { 2605 // we didn't *really* go away! the dialog was simply hidden or 2606 // we changed virtual desktops or ... 2607 return; 2608 } 2609 2610 if (m_placesDock) { 2611 m_placesDock->hide(); 2612 } 2613 2614 QAction *homeAction = m_ops->action(KDirOperator::Home); 2615 QAction *reloadAction = m_ops->action(KDirOperator::Reload); 2616 if (!m_toolbar->actions().contains(homeAction)) { 2617 m_toolbar->insertAction(reloadAction, homeAction); 2618 } 2619 } 2620 2621 m_togglePlacesPanelAction->setChecked(show); 2622 2623 // if we don't show the places panel, at least show the places menu 2624 m_urlNavigator->setPlacesSelectorVisible(!show); 2625 } 2626 2627 void KFileWidgetPrivate::toggleBookmarks(bool show) 2628 { 2629 if (show) { 2630 if (m_bookmarkHandler) { 2631 return; 2632 } 2633 m_bookmarkHandler = new KFileBookmarkHandler(q); 2634 q->connect(m_bookmarkHandler, &KFileBookmarkHandler::openUrl, q, [this](const QString &path) { 2635 enterUrl(path); 2636 }); 2637 m_bookmarkButton->setMenu(m_bookmarkHandler->menu()); 2638 } else if (m_bookmarkHandler) { 2639 m_bookmarkButton->setMenu(nullptr); 2640 delete m_bookmarkHandler; 2641 m_bookmarkHandler = nullptr; 2642 } 2643 2644 if (m_bookmarkButton) { 2645 m_bookmarkButton->setVisible(show); 2646 } 2647 2648 m_toggleBookmarksAction->setChecked(show); 2649 } 2650 2651 // static, overloaded 2652 QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass) 2653 { 2654 QString fileName; // result discarded 2655 return getStartUrl(startDir, recentDirClass, fileName); 2656 } 2657 2658 // static, overloaded 2659 QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName) 2660 { 2661 recentDirClass.clear(); 2662 fileName.clear(); 2663 QUrl ret; 2664 2665 bool useDefaultStartDir = startDir.isEmpty(); 2666 if (!useDefaultStartDir) { 2667 if (startDir.scheme() == QLatin1String("kfiledialog")) { 2668 // The startDir URL with this protocol may be in the format: 2669 // directory() fileName() 2670 // 1. kfiledialog:///keyword "/" keyword 2671 // 2. kfiledialog:///keyword?global "/" keyword 2672 // 3. kfiledialog:///keyword/ "/" keyword 2673 // 4. kfiledialog:///keyword/?global "/" keyword 2674 // 5. kfiledialog:///keyword/filename /keyword filename 2675 // 6. kfiledialog:///keyword/filename?global /keyword filename 2676 2677 QString keyword; 2678 QString urlDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); 2679 QString urlFile = startDir.fileName(); 2680 if (urlDir == QLatin1String("/")) { // '1'..'4' above 2681 keyword = urlFile; 2682 fileName.clear(); 2683 } else { // '5' or '6' above 2684 keyword = urlDir.mid(1); 2685 fileName = urlFile; 2686 } 2687 2688 const QLatin1String query(":%1"); 2689 recentDirClass = query.arg(keyword); 2690 2691 ret = QUrl::fromLocalFile(KRecentDirs::dir(recentDirClass)); 2692 } else { // not special "kfiledialog" URL 2693 // "foo.png" only gives us a file name, the default start dir will be used. 2694 // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same 2695 // (and is the reason why we don't just use QUrl::isRelative()). 2696 2697 // In all other cases (startDir contains a directory path, or has no 2698 // fileName for us anyway, such as smb://), startDir is indeed a dir url. 2699 2700 if (!startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) { 2701 // can use start directory 2702 ret = startDir; // will be checked by stat later 2703 // If we won't be able to list it (e.g. http), then use default 2704 if (!KProtocolManager::supportsListing(ret)) { 2705 useDefaultStartDir = true; 2706 fileName = startDir.fileName(); 2707 } 2708 } else { // file name only 2709 fileName = startDir.fileName(); 2710 useDefaultStartDir = true; 2711 } 2712 } 2713 } 2714 2715 if (useDefaultStartDir) { 2716 if (lastDirectory()->isEmpty()) { 2717 *lastDirectory() = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); 2718 const QUrl home(QUrl::fromLocalFile(QDir::homePath())); 2719 // if there is no docpath set (== home dir), we prefer the current 2720 // directory over it. We also prefer the homedir when our CWD is 2721 // different from our homedirectory or when the document dir 2722 // does not exist 2723 if (lastDirectory()->adjusted(QUrl::StripTrailingSlash) == home.adjusted(QUrl::StripTrailingSlash) // 2724 || QDir::currentPath() != QDir::homePath() // 2725 || !QDir(lastDirectory()->toLocalFile()).exists()) { 2726 *lastDirectory() = QUrl::fromLocalFile(QDir::currentPath()); 2727 } 2728 } 2729 ret = *lastDirectory(); 2730 } 2731 2732 // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName; 2733 return ret; 2734 } 2735 2736 void KFileWidget::setStartDir(const QUrl &directory) 2737 { 2738 if (directory.isValid()) { 2739 *lastDirectory() = directory; 2740 } 2741 } 2742 2743 void KFileWidgetPrivate::setNonExtSelection() 2744 { 2745 // Enhanced rename: Don't highlight the file extension. 2746 QString filename = locationEditCurrentText(); 2747 QMimeDatabase db; 2748 QString extension = db.suffixForFileName(filename); 2749 2750 if (!extension.isEmpty()) { 2751 m_locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1); 2752 } else { 2753 int lastDot = filename.lastIndexOf(QLatin1Char('.')); 2754 if (lastDot > 0) { 2755 m_locationEdit->lineEdit()->setSelection(0, lastDot); 2756 } else { 2757 m_locationEdit->lineEdit()->selectAll(); 2758 } 2759 } 2760 } 2761 2762 // Sets the filter text to "File type" if the dialog is saving and a MIME type 2763 // filter has been set; otherwise, the text is "Filter:" 2764 void KFileWidgetPrivate::updateFilterText() 2765 { 2766 QString label; 2767 QString whatsThisText; 2768 2769 if (m_operationMode == KFileWidget::Saving && !m_filterWidget->currentFilter().mimePatterns().isEmpty()) { 2770 label = i18n("&File type:"); 2771 whatsThisText = i18n("<qt>This is the file type selector. It is used to select the format that the file will be saved as.</qt>"); 2772 } else { 2773 label = i18n("&Filter:"); 2774 whatsThisText = i18n( 2775 "<qt>This is the filter to apply to the file list. " 2776 "File names that do not match the filter will not be shown.<p>" 2777 "You may select from one of the preset filters in the " 2778 "drop down menu, or you may enter a custom filter " 2779 "directly into the text area.</p><p>" 2780 "Wildcards such as * and ? are allowed.</p></qt>"); 2781 } 2782 2783 if (m_filterLabel) { 2784 m_filterLabel->setText(label); 2785 m_filterLabel->setWhatsThis(whatsThisText); 2786 } 2787 if (m_filterWidget) { 2788 m_filterWidget->setWhatsThis(whatsThisText); 2789 } 2790 } 2791 2792 void KFileWidget::setCustomWidget(QWidget *widget) 2793 { 2794 delete d->m_bottomCustomWidget; 2795 d->m_bottomCustomWidget = widget; 2796 2797 // add it to the dialog, below the filter list box. 2798 2799 // Change the parent so that this widget is a child of the main widget 2800 d->m_bottomCustomWidget->setParent(this); 2801 2802 d->m_opsWidgetLayout->addWidget(d->m_bottomCustomWidget); 2803 2804 // FIXME: This should adjust the tab orders so that the custom widget 2805 // comes after the Cancel button. The code appears to do this, but the result 2806 // somehow screws up the tab order of the file path combo box. Not a major 2807 // problem, but ideally the tab order with a custom widget should be 2808 // the same as the order without one. 2809 setTabOrder(d->m_cancelButton, d->m_bottomCustomWidget); 2810 setTabOrder(d->m_bottomCustomWidget, d->m_urlNavigator); 2811 } 2812 2813 void KFileWidget::setCustomWidget(const QString &text, QWidget *widget) 2814 { 2815 delete d->m_labeledCustomWidget; 2816 d->m_labeledCustomWidget = widget; 2817 2818 QLabel *label = new QLabel(text, this); 2819 label->setAlignment(Qt::AlignRight); 2820 d->m_lafBox->addRow(label, widget); 2821 } 2822 2823 KDirOperator *KFileWidget::dirOperator() 2824 { 2825 return d->m_ops; 2826 } 2827 2828 void KFileWidget::readConfig(KConfigGroup &group) 2829 { 2830 d->m_configGroup = group; 2831 d->readViewConfig(); 2832 d->readRecentFiles(); 2833 } 2834 2835 QString KFileWidgetPrivate::locationEditCurrentText() const 2836 { 2837 return QDir::fromNativeSeparators(m_locationEdit->currentText()); 2838 } 2839 2840 QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url) 2841 { 2842 if (url.isLocalFile()) { 2843 return url; 2844 } 2845 2846 KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); 2847 KJobWidgets::setWindow(statJob, q); 2848 bool res = statJob->exec(); 2849 2850 if (!res) { 2851 return url; 2852 } 2853 2854 const QString path = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); 2855 if (!path.isEmpty()) { 2856 QUrl newUrl; 2857 newUrl.setPath(path); 2858 return newUrl; 2859 } 2860 2861 return url; 2862 } 2863 2864 void KFileWidgetPrivate::setInlinePreviewShown(bool show) 2865 { 2866 m_ops->setInlinePreviewShown(show); 2867 } 2868 2869 void KFileWidget::setConfirmOverwrite(bool enable) 2870 { 2871 d->m_confirmOverwrite = enable; 2872 } 2873 2874 void KFileWidget::setInlinePreviewShown(bool show) 2875 { 2876 d->setInlinePreviewShown(show); 2877 } 2878 2879 QSize KFileWidget::dialogSizeHint() const 2880 { 2881 int fontSize = fontMetrics().height(); 2882 QSize goodSize(48 * fontSize, 30 * fontSize); 2883 const QSize scrnSize = d->screenSize(); 2884 QSize minSize(scrnSize / 2); 2885 QSize maxSize(scrnSize * qreal(0.9)); 2886 return (goodSize.expandedTo(minSize).boundedTo(maxSize)); 2887 } 2888 2889 void KFileWidget::setViewMode(KFile::FileView mode) 2890 { 2891 d->m_ops->setViewMode(mode); 2892 d->m_hasView = true; 2893 } 2894 2895 void KFileWidget::setSupportedSchemes(const QStringList &schemes) 2896 { 2897 d->m_model->setSupportedSchemes(schemes); 2898 d->m_ops->setSupportedSchemes(schemes); 2899 d->m_urlNavigator->setSupportedSchemes(schemes); 2900 } 2901 2902 QStringList KFileWidget::supportedSchemes() const 2903 { 2904 return d->m_model->supportedSchemes(); 2905 } 2906 2907 #include "moc_kfilewidget.cpp"