1// -*- c++ -*-
2/*
3 This file is part of the KDE libraries
4 SPDX-FileCopyrightText: 1997, 1998 Richard Moore <rich@kde.org>
5 SPDX-FileCopyrightText: 1998 Stephan Kulow <coolo@kde.org>
6 SPDX-FileCopyrightText: 1998 Daniel Grana <grana@ie.iwi.unibe.ch>
7 SPDX-FileCopyrightText: 1999, 2000, 2001, 2002, 2003 Carsten Pfeiffer <pfeiffer@kde.org>
8 SPDX-FileCopyrightText: 2003 Clarence Dang <dang@kde.org>
9 SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
10 SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
11
12 SPDX-License-Identifier: LGPL-2.0-or-later
13*/
14
15#include "kfilewidget.h"
16
17#include "../utils_p.h"
18#include "kfilebookmarkhandler_p.h"
19#include "kfileplacesmodel.h"
20#include "kfileplacesview.h"
21#include "kfilepreviewgenerator.h"
22#include "kfilewidgetdocktitlebar_p.h"
23#include "kurlcombobox.h"
24#include "kurlnavigator.h"
25
26#include <config-kiofilewidgets.h>
27
28#include <defaults-kfile.h>
29#include <kdiroperator.h>
30#include <kfilefiltercombo.h>
31#include <kfileitemdelegate.h>
32#include <kio/job.h>
33#include <kio/jobuidelegate.h>
34#include <kio/statjob.h>
35#include <kprotocolmanager.h>
36#include <krecentdirs.h>
37#include <krecentdocument.h>
38#include <kurlauthorized.h>
39#include <kurlcompletion.h>
40
41#include <KActionMenu>
42#include <KConfigGroup>
43#include <KDirLister>
44#include <KFileItem>
45#include <KFilePlacesModel>
46#include <KIconLoader>
47#include <KJobWidgets>
48#include <KLocalizedString>
49#include <KMessageBox>
50#include <KMessageWidget>
51#include <KSharedConfig>
52#include <KShell>
53#include <KStandardAction>
54
55#include <QAbstractProxyModel>
56#include <QApplication>
57#include <QCheckBox>
58#include <QDebug>
59#include <QDockWidget>
60#include <QFormLayout>
61#include <QHelpEvent>
62#include <QIcon>
63#include <QLabel>
64#include <QLayout>
65#include <QLineEdit>
66#include <QLoggingCategory>
67#include <QMenu>
68#include <QMimeDatabase>
69#include <QPushButton>
70#include <QScreen>
71#include <QSplitter>
72#include <QStandardPaths>
73#include <QTimer>
74#include <QToolBar>
75
76#include <algorithm>
77#include <array>
78
79Q_DECLARE_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW)
80Q_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW, "kf.kio.kfilewidgets.kfilewidget", QtInfoMsg)
81
82class KFileWidgetPrivate
83{
84public:
85 explicit KFileWidgetPrivate(KFileWidget *qq)
86 : q(qq)
87 {
88 }
89
90 ~KFileWidgetPrivate()
91 {
92 delete m_bookmarkHandler; // Should be deleted before m_ops!
93 // Must be deleted before m_ops, otherwise the unit test crashes due to the
94 // connection to the QDockWidget::visibilityChanged signal, which may get
95 // emitted after this object is destroyed
96 delete m_placesDock;
97 delete m_ops;
98 }
99
100 QSize screenSize() const
101 {
102 return q->parentWidget() ? q->parentWidget()->screen()->availableGeometry().size() //
103 : QGuiApplication::primaryScreen()->availableGeometry().size();
104 }
105
106 void initDirOpWidgets();
107 void initToolbar();
108 void initZoomWidget();
109 void initLocationWidget();
110 void initFilterWidget();
111 void updateLocationWhatsThis();
112 void updateAutoSelectExtension();
113 void initPlacesPanel();
114 void setPlacesViewSplitterSizes();
115 void initGUI();
116 void readViewConfig();
117 void writeViewConfig();
118 void setNonExtSelection();
119 void setLocationText(const QUrl &);
120 void setLocationText(const QList<QUrl> &);
121 void appendExtension(QUrl &url);
122 void updateLocationEditExtension(const QString &);
123 QString findMatchingFilter(const QString &filter, const QString &filename) const;
124 void updateFilter();
125 void updateFilterText();
126 /**
127 * Parses the string "line" for files. If line doesn't contain any ", the
128 * whole line will be interpreted as one file. If the number of " is odd,
129 * an empty list will be returned. Otherwise, all items enclosed in " "
130 * will be returned as correct urls.
131 */
132 QList<QUrl> tokenize(const QString &line) const;
133 /**
134 * Reads the recent used files and inserts them into the location combobox
135 */
136 void readRecentFiles();
137 /**
138 * Saves the entries from the location combobox.
139 */
140 void saveRecentFiles();
141 /**
142 * called when an item is highlighted/selected in multiselection mode.
143 * handles setting the m_locationEdit.
144 */
145 void multiSelectionChanged();
146
147 /**
148 * Returns the absolute version of the URL specified in m_locationEdit.
149 */
150 QUrl getCompleteUrl(const QString &) const;
151
152 /**
153 * Asks for overwrite confirmation using a KMessageBox and returns
154 * true if the user accepts.
155 *
156 */
157 bool toOverwrite(const QUrl &);
158
159 // private slots
160 void slotLocationChanged(const QString &);
161 void urlEntered(const QUrl &);
162 void enterUrl(const QUrl &);
163 void enterUrl(const QString &);
164 void locationAccepted(const QString &);
165 void slotFilterChanged();
166 void fileHighlighted(const KFileItem &);
167 void fileSelected(const KFileItem &);
168 void slotLoadingFinished();
169 void togglePlacesPanel(bool show, QObject *sender = nullptr);
170 void toggleBookmarks(bool);
171 void slotAutoSelectExtClicked();
172 void placesViewSplitterMoved(int, int);
173 void activateUrlNavigator();
174
175 enum ZoomState {
176 ZoomOut,
177 ZoomIn,
178 };
179 void changeIconsSize(ZoomState zoom);
180 void slotDirOpIconSizeChanged(int size);
181 void slotIconSizeSliderMoved(int);
182 void slotIconSizeChanged(int);
183 void slotViewDoubleClicked(const QModelIndex &);
184 void slotViewKeyEnterReturnPressed();
185
186 void addToRecentDocuments();
187
188 QString locationEditCurrentText() const;
189
190 /**
191 * KIO::NetAccess::mostLocalUrl local replacement.
192 * This method won't show any progress dialogs for stating, since
193 * they are very annoying when stating.
194 */
195 QUrl mostLocalUrl(const QUrl &url);
196
197 void setInlinePreviewShown(bool show);
198
199 KFileWidget *const q;
200
201 // the last selected url
202 QUrl m_url;
203
204 // now following all kind of widgets, that I need to rebuild
205 // the geometry management
206 QBoxLayout *m_boxLayout = nullptr;
207 QFormLayout *m_lafBox = nullptr;
208
209 QLabel *m_locationLabel = nullptr;
210 QWidget *m_opsWidget = nullptr;
211 QVBoxLayout *m_opsWidgetLayout = nullptr;
212
213 QLabel *m_filterLabel = nullptr;
214 KUrlNavigator *m_urlNavigator = nullptr;
215 KMessageWidget *m_messageWidget = nullptr;
216 QPushButton *m_okButton = nullptr;
217 QPushButton *m_cancelButton = nullptr;
218 QDockWidget *m_placesDock = nullptr;
219 KFilePlacesView *m_placesView = nullptr;
220 QSplitter *m_placesViewSplitter = nullptr;
221 // caches the places view width. This value will be updated when the splitter
222 // is moved. This allows us to properly set a value when the dialog itself
223 // is resized
224 int m_placesViewWidth = -1;
225
226 QWidget *m_labeledCustomWidget = nullptr;
227 QWidget *m_bottomCustomWidget = nullptr;
228
229 // Automatically Select Extension stuff
230 QCheckBox *m_autoSelectExtCheckBox = nullptr;
231 QString m_extension; // current extension for this filter
232
233 QList<QUrl> m_urlList; // the list of selected urls
234
235 KFileWidget::OperationMode m_operationMode = KFileWidget::Opening;
236
237 // The file class used for KRecentDirs
238 QString m_fileClass;
239
240 KFileBookmarkHandler *m_bookmarkHandler = nullptr;
241
242 KActionMenu *m_bookmarkButton = nullptr;
243
244 QToolBar *m_toolbar = nullptr;
245 KUrlComboBox *m_locationEdit = nullptr;
246 KDirOperator *m_ops = nullptr;
247 KFileFilterCombo *m_filterWidget = nullptr;
248 QTimer m_filterDelayTimer;
249
250 KFilePlacesModel *m_model = nullptr;
251
252 // whether or not the _user_ has checked the above box
253 bool m_autoSelectExtChecked = false;
254
255 // indicates if the location edit should be kept or cleared when changing
256 // directories
257 bool m_keepLocation = false;
258
259 // the KDirOperators view is set in KFileWidget::show(), so to avoid
260 // setting it again and again, we have this nice little boolean :)
261 bool m_hasView = false;
262
263 bool m_hasDefaultFilter = false; // necessary for the m_operationMode
264 bool m_inAccept = false; // true between beginning and end of accept()
265 bool m_confirmOverwrite = false;
266 bool m_differentHierarchyLevelItemsEntered = false;
267
268 const std::array<short, 8> m_stdIconSizes = {
269 KIconLoader::SizeSmall,
270 KIconLoader::SizeSmallMedium,
271 KIconLoader::SizeMedium,
272 KIconLoader::SizeLarge,
273 KIconLoader::SizeHuge,
274 KIconLoader::SizeEnormous,
275 256,
276 512,
277 };
278
279 QSlider *m_iconSizeSlider = nullptr;
280 QAction *m_zoomOutAction = nullptr;
281 QAction *m_zoomInAction = nullptr;
282
283 // The group which stores app-specific settings. These settings are recent
284 // files and urls. Visual settings (view mode, sorting criteria...) are not
285 // app-specific and are stored in kdeglobals
286 KConfigGroup m_configGroup;
287 KConfigGroup m_stateConfigGroup;
288
289 KToggleAction *m_toggleBookmarksAction = nullptr;
290 KToggleAction *m_togglePlacesPanelAction = nullptr;
291};
292
293Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path
294
295// returns true if the string contains "<a>:/" sequence, where <a> is at least 2 alpha chars
296static bool containsProtocolSection(const QString &string)
297{
298 int len = string.length();
299 static const char prot[] = ":/";
300 for (int i = 0; i < len;) {
301 i = string.indexOf(s: QLatin1String(prot), from: i);
302 if (i == -1) {
303 return false;
304 }
305 int j = i - 1;
306 for (; j >= 0; j--) {
307 const QChar &ch(string[j]);
308 if (ch.toLatin1() == 0 || !ch.isLetter()) {
309 break;
310 }
311 if (ch.isSpace() && (i - j - 1) >= 2) {
312 return true;
313 }
314 }
315 if (j < 0 && i >= 2) {
316 return true; // at least two letters before ":/"
317 }
318 i += 3; // skip : and / and one char
319 }
320 return false;
321}
322
323// this string-to-url conversion function handles relative paths, full paths and URLs
324// without the http-prepending that QUrl::fromUserInput does.
325static QUrl urlFromString(const QString &str)
326{
327 if (Utils::isAbsoluteLocalPath(path: str)) {
328 return QUrl::fromLocalFile(localfile: str);
329 }
330 QUrl url(str);
331 if (url.isRelative()) {
332 url.clear();
333 url.setPath(path: str);
334 }
335 return url;
336}
337
338KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent)
339 : QWidget(parent)
340 , d(new KFileWidgetPrivate(this))
341{
342 QUrl startDir(_startDir);
343 // qDebug() << "startDir" << startDir;
344 QString filename;
345
346 d->m_okButton = new QPushButton(this);
347 KGuiItem::assign(button: d->m_okButton, item: KStandardGuiItem::ok());
348 d->m_okButton->setDefault(true);
349 d->m_cancelButton = new QPushButton(this);
350 KGuiItem::assign(button: d->m_cancelButton, item: KStandardGuiItem::cancel());
351 // The dialog shows them
352 d->m_okButton->hide();
353 d->m_cancelButton->hide();
354
355 d->initDirOpWidgets();
356
357 // Resolve this now so that a 'kfiledialog:' URL, if specified,
358 // does not get inserted into the urlNavigator history.
359 d->m_url = getStartUrl(startDir, recentDirClass&: d->m_fileClass, fileName&: filename);
360 startDir = d->m_url;
361
362 const auto operatorActions = d->m_ops->allActions();
363 for (QAction *action : operatorActions) {
364 addAction(action);
365 }
366
367 QAction *goToNavigatorAction = new QAction(this);
368
369 connect(sender: goToNavigatorAction, signal: &QAction::triggered, context: this, slot: [this]() {
370 d->activateUrlNavigator();
371 });
372
373 goToNavigatorAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L));
374
375 addAction(action: goToNavigatorAction);
376
377 KUrlComboBox *pathCombo = d->m_urlNavigator->editor();
378 KUrlCompletion *pathCompletionObj = new KUrlCompletion(KUrlCompletion::DirCompletion);
379 pathCombo->setCompletionObject(compObj: pathCompletionObj);
380 pathCombo->setAutoDeleteCompletionObject(true);
381
382 connect(sender: d->m_urlNavigator, signal: &KUrlNavigator::urlChanged, context: this, slot: [this](const QUrl &url) {
383 d->enterUrl(url);
384 });
385 connect(sender: d->m_urlNavigator, signal: &KUrlNavigator::returnPressed, context: d->m_ops, slot: qOverload<>(&QWidget::setFocus));
386
387 // Location, "Name:", line-edit and label
388 d->initLocationWidget();
389
390 // "Filter:" line-edit and label
391 d->initFilterWidget();
392
393 // the Automatically Select Extension checkbox
394 // (the text, visibility etc. is set in updateAutoSelectExtension(), which is called by readConfig())
395 d->m_autoSelectExtCheckBox = new QCheckBox(this);
396 connect(sender: d->m_autoSelectExtCheckBox, signal: &QCheckBox::clicked, context: this, slot: [this]() {
397 d->slotAutoSelectExtClicked();
398 });
399
400 d->initGUI(); // activate GM
401
402 // read our configuration
403 KSharedConfig::Ptr config = KSharedConfig::openConfig();
404 config->reparseConfiguration(); // grab newly added dirs by other processes (#403524)
405 KConfigGroup group(config, ConfigGroup);
406
407 d->m_stateConfigGroup = KSharedConfig::openStateConfig()->group(group: ConfigGroup);
408
409 // migrate existing recent files/urls from main config to state config
410 if (group.hasKey(key: RecentURLs)) {
411 d->m_stateConfigGroup.writeEntry(key: RecentURLs, value: group.readEntry(key: RecentURLs));
412 group.revertToDefault(key: RecentURLs);
413 }
414
415 if (group.hasKey(key: RecentFiles)) {
416 d->m_stateConfigGroup.writeEntry(key: RecentFiles, value: group.readEntry(key: RecentFiles));
417 group.revertToDefault(key: RecentFiles);
418 }
419
420 readConfig(group);
421
422 d->m_ops->action(action: KDirOperator::ShowPreview)->setChecked(d->m_ops->isInlinePreviewShown());
423 d->slotDirOpIconSizeChanged(size: d->m_ops->iconSize());
424
425 KFilePreviewGenerator *pg = d->m_ops->previewGenerator();
426 if (pg) {
427 d->m_ops->action(action: KDirOperator::ShowPreview)->setChecked(pg->isPreviewShown());
428 }
429
430 // getStartUrl() above will have resolved the startDir parameter into
431 // a directory and file name in the two cases: (a) where it is a
432 // special "kfiledialog:" URL, or (b) where it is a plain file name
433 // only without directory or protocol. For any other startDir
434 // specified, it is not possible to resolve whether there is a file name
435 // present just by looking at the URL; the only way to be sure is
436 // to stat it.
437 bool statRes = false;
438 if (filename.isEmpty()) {
439 KIO::StatJob *statJob = KIO::stat(url: startDir, flags: KIO::HideProgressInfo);
440 KJobWidgets::setWindow(job: statJob, widget: this);
441 statRes = statJob->exec();
442 // qDebug() << "stat of" << startDir << "-> statRes" << statRes << "isDir" << statJob->statResult().isDir();
443 if (!statRes || !statJob->statResult().isDir()) {
444 filename = startDir.fileName();
445 startDir = startDir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
446 // qDebug() << "statJob -> startDir" << startDir << "filename" << filename;
447 }
448 }
449
450 d->m_ops->setUrl(url: startDir, clearforward: true);
451 d->m_urlNavigator->setLocationUrl(startDir);
452 if (d->m_placesView) {
453 d->m_placesView->setUrl(startDir);
454 }
455
456 // We have a file name either explicitly specified, or have checked that
457 // we could stat it and it is not a directory. Set it.
458 if (!filename.isEmpty()) {
459 QLineEdit *lineEdit = d->m_locationEdit->lineEdit();
460 // qDebug() << "selecting filename" << filename;
461 if (statRes) {
462 d->setLocationText(QUrl(filename));
463 } else {
464 lineEdit->setText(filename);
465 // Preserve this filename when clicking on the view (cf fileHighlighted)
466 lineEdit->setModified(true);
467 }
468 lineEdit->selectAll();
469 }
470
471 d->m_locationEdit->setFocus();
472
473 const QAction *showHiddenAction = d->m_ops->action(action: KDirOperator::ShowHiddenFiles);
474 Q_ASSERT(showHiddenAction);
475 d->m_urlNavigator->setShowHiddenFolders(showHiddenAction->isChecked());
476 connect(sender: showHiddenAction, signal: &QAction::toggled, context: this, slot: [this](bool checked) {
477 d->m_urlNavigator->setShowHiddenFolders(checked);
478 });
479
480 const QAction *hiddenFilesLastAction = d->m_ops->action(action: KDirOperator::SortHiddenFilesLast);
481 Q_ASSERT(hiddenFilesLastAction);
482 d->m_urlNavigator->setSortHiddenFoldersLast(hiddenFilesLastAction->isChecked());
483 connect(sender: hiddenFilesLastAction, signal: &QAction::toggled, context: this, slot: [this](bool checked) {
484 d->m_urlNavigator->setSortHiddenFoldersLast(checked);
485 });
486}
487
488KFileWidget::~KFileWidget()
489{
490 KSharedConfig::Ptr config = KSharedConfig::openConfig();
491 config->sync();
492 d->m_ops->removeEventFilter(obj: this);
493 d->m_locationEdit->removeEventFilter(obj: this);
494}
495
496void KFileWidget::setLocationLabel(const QString &text)
497{
498 d->m_locationLabel->setText(text);
499}
500
501void KFileWidget::setFilters(const QList<KFileFilter> &filters, const KFileFilter &activeFilter)
502{
503 d->m_ops->clearFilter();
504 d->m_filterWidget->setFilters(filters, defaultFilter: activeFilter);
505 d->m_ops->updateDir();
506 d->m_hasDefaultFilter = false;
507 d->m_filterWidget->setEditable(true);
508 d->updateFilterText();
509
510 d->updateAutoSelectExtension();
511}
512
513KFileFilter KFileWidget::currentFilter() const
514{
515 return d->m_filterWidget->currentFilter();
516}
517
518void KFileWidget::clearFilter()
519{
520 d->m_filterWidget->setFilters(filters: {}, defaultFilter: KFileFilter());
521 d->m_ops->clearFilter();
522 d->m_hasDefaultFilter = false;
523 d->m_filterWidget->setEditable(true);
524
525 d->updateAutoSelectExtension();
526}
527
528void KFileWidget::setPreviewWidget(KPreviewWidgetBase *w)
529{
530 d->m_ops->setPreviewWidget(w);
531 d->m_ops->clearHistory();
532 d->m_hasView = true;
533}
534
535QUrl KFileWidgetPrivate::getCompleteUrl(const QString &_url) const
536{
537 // qDebug() << "got url " << _url;
538
539 const QString url = KShell::tildeExpand(path: _url);
540 QUrl u;
541
542 if (Utils::isAbsoluteLocalPath(path: url)) {
543 u = QUrl::fromLocalFile(localfile: url);
544 } else {
545 QUrl relativeUrlTest(m_ops->url());
546 relativeUrlTest.setPath(path: Utils::concatPaths(path1: relativeUrlTest.path(), path2: url));
547 if (!m_ops->dirLister()->findByUrl(url: relativeUrlTest).isNull() || !KProtocolInfo::isKnownProtocol(url: relativeUrlTest)) {
548 u = relativeUrlTest;
549 } else {
550 // Try to preserve URLs if they have a scheme (for example,
551 // "https://example.com/foo.txt") and otherwise resolve relative
552 // paths to absolute ones (e.g. "foo.txt" -> "file:///tmp/foo.txt").
553 u = QUrl(url);
554 if (u.isRelative()) {
555 u = relativeUrlTest;
556 }
557 }
558 }
559
560 return u;
561}
562
563QSize KFileWidget::sizeHint() const
564{
565 int fontSize = fontMetrics().height();
566 const QSize goodSize(48 * fontSize, 30 * fontSize);
567 const QSize scrnSize = d->screenSize();
568 const QSize minSize(scrnSize / 2);
569 const QSize maxSize(scrnSize * qreal(0.9));
570 return (goodSize.expandedTo(otherSize: minSize).boundedTo(otherSize: maxSize));
571}
572
573static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url);
574
575/**
576 * Escape the given Url so that is fit for use in the selected list of file. This
577 * mainly handles double quote (") characters. These are used to separate entries
578 * in the list, however, if `"` appears in the filename (or path), this will be
579 * escaped as `\"`. Later, the tokenizer is able to understand the difference
580 * and do the right thing
581 */
582static QString escapeDoubleQuotes(QString &&path);
583
584// Called by KFileDialog
585void KFileWidget::slotOk()
586{
587 // qDebug() << "slotOk\n";
588
589 const QString locationEditCurrentText(KShell::tildeExpand(path: d->locationEditCurrentText()));
590
591 QList<QUrl> locationEditCurrentTextList(d->tokenize(line: locationEditCurrentText));
592 KFile::Modes mode = d->m_ops->mode();
593
594 // if there is nothing to do, just return from here
595 if (locationEditCurrentTextList.isEmpty()) {
596 return;
597 }
598
599 // Make sure that one of the modes was provided
600 if (!((mode & KFile::File) || (mode & KFile::Directory) || (mode & KFile::Files))) {
601 mode |= KFile::File;
602 // qDebug() << "No mode() provided";
603 }
604
605 // Clear the list as we are going to refill it
606 d->m_urlList.clear();
607
608 const bool directoryMode = (mode & KFile::Directory);
609 const bool onlyDirectoryMode = directoryMode && !(mode & KFile::File) && !(mode & KFile::Files);
610
611 // if we are on file mode, and the list of provided files/folder is greater than one, inform
612 // the user about it
613 if (locationEditCurrentTextList.count() > 1) {
614 if (mode & KFile::File) {
615 KMessageBox::error(parent: this, i18n("You can only select one file"), i18n("More than one file provided"));
616 return;
617 }
618
619 /**
620 * Logic of the next part of code (ends at "end multi relative urls").
621 *
622 * We allow for instance to be at "/" and insert '"home/foo/bar.txt" "boot/grub/menu.lst"'.
623 * Why we need to support this ? Because we provide tree views, which aren't plain.
624 *
625 * Now, how does this logic work. It will get the first element on the list (with no filename),
626 * following the previous example say "/home/foo" and set it as the top most url.
627 *
628 * After this, it will iterate over the rest of items and check if this URL (topmost url)
629 * contains the url being iterated.
630 *
631 * As you might have guessed it will do "/home/foo" against "/boot/grub" (again stripping
632 * filename), and a false will be returned. Then we upUrl the top most url, resulting in
633 * "/home" against "/boot/grub", what will again return false, so we upUrl again. Now we
634 * have "/" against "/boot/grub", what returns true for us, so we can say that the closest
635 * common ancestor of both is "/".
636 *
637 * This example has been written for 2 urls, but this works for any number of urls.
638 */
639 if (!d->m_differentHierarchyLevelItemsEntered) { // avoid infinite recursion. running this
640 int start = 0;
641 QUrl topMostUrl;
642 KIO::StatJob *statJob = nullptr;
643 bool res = false;
644
645 // we need to check for a valid first url, so in theory we only iterate one time over
646 // this loop. However it can happen that the user did
647 // "home/foo/nonexistantfile" "boot/grub/menu.lst", so we look for a good first
648 // candidate.
649 while (!res && start < locationEditCurrentTextList.count()) {
650 topMostUrl = locationEditCurrentTextList.at(i: start);
651 statJob = KIO::stat(url: topMostUrl, flags: KIO::HideProgressInfo);
652 KJobWidgets::setWindow(job: statJob, widget: this);
653 res = statJob->exec();
654 start++;
655 }
656
657 Q_ASSERT(statJob);
658
659 // if this is not a dir, strip the filename. after this we have an existent and valid
660 // dir (we stated correctly the file).
661 if (!statJob->statResult().isDir()) {
662 topMostUrl = topMostUrl.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
663 }
664
665 // now the funny part. for the rest of filenames, go and look for the closest ancestor
666 // of all them.
667 for (int i = start; i < locationEditCurrentTextList.count(); ++i) {
668 QUrl currUrl = locationEditCurrentTextList.at(i);
669 KIO::StatJob *statJob = KIO::stat(url: currUrl, flags: KIO::HideProgressInfo);
670 KJobWidgets::setWindow(job: statJob, widget: this);
671 int res = statJob->exec();
672 if (res) {
673 // again, we don't care about filenames
674 if (!statJob->statResult().isDir()) {
675 currUrl = currUrl.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
676 }
677
678 // iterate while this item is contained on the top most url
679 while (!topMostUrl.matches(url: currUrl, options: QUrl::StripTrailingSlash) && !topMostUrl.isParentOf(url: currUrl)) {
680 topMostUrl = KIO::upUrl(url: topMostUrl);
681 }
682 }
683 }
684
685 // now recalculate all paths for them being relative in base of the top most url
686 QStringList stringList;
687 stringList.reserve(asize: locationEditCurrentTextList.count());
688 for (int i = 0; i < locationEditCurrentTextList.count(); ++i) {
689 Q_ASSERT(topMostUrl.isParentOf(locationEditCurrentTextList[i]));
690 QString relativePath = relativePathOrUrl(baseUrl: topMostUrl, url: locationEditCurrentTextList[i]);
691 stringList << escapeDoubleQuotes(path: std::move(relativePath));
692 }
693
694 d->m_ops->setUrl(url: topMostUrl, clearforward: true);
695 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(b: true);
696 d->m_locationEdit->lineEdit()->setText(QStringLiteral("\"%1\"").arg(a: stringList.join(QStringLiteral("\" \""))));
697 d->m_locationEdit->lineEdit()->blockSignals(b: signalsBlocked);
698
699 d->m_differentHierarchyLevelItemsEntered = true;
700 slotOk();
701 return;
702 }
703 /**
704 * end multi relative urls
705 */
706 } else if (!locationEditCurrentTextList.isEmpty()) {
707 // if we are on file or files mode, and we have an absolute url written by
708 // the user:
709 // * convert it to relative and call slotOk again if the protocol supports listing.
710 // * use the full url if the protocol doesn't support listing
711 // This is because when using a protocol that supports listing we want to show the directory
712 // the user just opened/saved from the next time they open the dialog, it makes sense usability wise.
713 // If the protocol doesn't support listing (i.e. http:// ) the user would end up with the dialog
714 // showing an "empty directory" which is bad usability wise.
715 if (!locationEditCurrentText.isEmpty() && !onlyDirectoryMode
716 && (Utils::isAbsoluteLocalPath(path: locationEditCurrentText) || containsProtocolSection(string: locationEditCurrentText))) {
717 QUrl url = urlFromString(str: locationEditCurrentText);
718 if (KProtocolManager::supportsListing(url)) {
719 QString fileName;
720 if (d->m_operationMode == Opening) {
721 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
722 KJobWidgets::setWindow(job: statJob, widget: this);
723 int res = statJob->exec();
724 if (res) {
725 if (!statJob->statResult().isDir()) {
726 fileName = url.fileName();
727 url = url.adjusted(options: QUrl::RemoveFilename); // keeps trailing slash
728 } else {
729 Utils::appendSlashToPath(url);
730 }
731 }
732 } else {
733 const QUrl directory = url.adjusted(options: QUrl::RemoveFilename);
734 // Check if the folder exists
735 KIO::StatJob *statJob = KIO::stat(url: directory, flags: KIO::HideProgressInfo);
736 KJobWidgets::setWindow(job: statJob, widget: this);
737 int res = statJob->exec();
738 if (res) {
739 if (statJob->statResult().isDir()) {
740 url = url.adjusted(options: QUrl::StripTrailingSlash);
741 fileName = url.fileName();
742 url = url.adjusted(options: QUrl::RemoveFilename);
743 }
744 }
745 }
746 d->m_ops->setUrl(url, clearforward: true);
747 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(b: true);
748 d->m_locationEdit->lineEdit()->setText(fileName);
749 d->m_locationEdit->lineEdit()->blockSignals(b: signalsBlocked);
750 slotOk();
751 return;
752 } else {
753 locationEditCurrentTextList = {url};
754 }
755 }
756 }
757
758 // restore it
759 d->m_differentHierarchyLevelItemsEntered = false;
760
761 // locationEditCurrentTextList contains absolute paths
762 // this is the general loop for the File and Files mode. Obviously we know
763 // that the File mode will iterate only one time here
764 QList<QUrl>::ConstIterator it = locationEditCurrentTextList.constBegin();
765 bool filesInList = false;
766 while (it != locationEditCurrentTextList.constEnd()) {
767 QUrl url(*it);
768
769 if (d->m_operationMode == Saving && !directoryMode) {
770 d->appendExtension(url);
771 }
772
773 d->m_url = url;
774 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
775 KJobWidgets::setWindow(job: statJob, widget: this);
776 int res = statJob->exec();
777
778 if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), baseUrl: QUrl(), destUrl: url)) {
779 QString msg = KIO::buildErrorString(errorCode: KIO::ERR_ACCESS_DENIED, errorText: d->m_url.toDisplayString());
780 KMessageBox::error(parent: this, text: msg);
781 return;
782 }
783
784 // if we are on local mode, make sure we haven't got a remote base url
785 if ((mode & KFile::LocalOnly) && !d->mostLocalUrl(url: d->m_url).isLocalFile()) {
786 KMessageBox::error(parent: this, i18n("You can only select local files"), i18n("Remote files not accepted"));
787 return;
788 }
789
790 const auto &supportedSchemes = d->m_model->supportedSchemes();
791 if (!supportedSchemes.isEmpty() && !supportedSchemes.contains(str: d->m_url.scheme())) {
792 KMessageBox::error(parent: this,
793 i18np("The selected URL uses an unsupported scheme. "
794 "Please use the following scheme: %2",
795 "The selected URL uses an unsupported scheme. "
796 "Please use one of the following schemes: %2",
797 supportedSchemes.size(),
798 supportedSchemes.join(QLatin1String(", "))),
799 i18n("Unsupported URL scheme"));
800 return;
801 }
802
803 // if we are given a folder when not on directory mode, let's get into it
804 if (res && !directoryMode && statJob->statResult().isDir()) {
805 // check if we were given more than one folder, in that case we don't know to which one
806 // cd
807 ++it;
808 while (it != locationEditCurrentTextList.constEnd()) {
809 QUrl checkUrl(*it);
810 KIO::StatJob *checkStatJob = KIO::stat(url: checkUrl, flags: KIO::HideProgressInfo);
811 KJobWidgets::setWindow(job: checkStatJob, widget: this);
812 bool res = checkStatJob->exec();
813 if (res && checkStatJob->statResult().isDir()) {
814 KMessageBox::error(parent: this,
815 i18n("More than one folder has been selected and this dialog does not accept folders, so it is not possible to decide "
816 "which one to enter. Please select only one folder to list it."),
817 i18n("More than one folder provided"));
818 return;
819 } else if (res) {
820 filesInList = true;
821 }
822 ++it;
823 }
824 if (filesInList) {
825 KMessageBox::information(
826 parent: this,
827 i18n("At least one folder and one file has been selected. Selected files will be ignored and the selected folder will be listed"),
828 i18n("Files and folders selected"));
829 }
830 d->m_ops->setUrl(url, clearforward: true);
831 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(b: true);
832 d->m_locationEdit->lineEdit()->setText(QString());
833 d->m_locationEdit->lineEdit()->blockSignals(b: signalsBlocked);
834 return;
835 } else if (res && onlyDirectoryMode && !statJob->statResult().isDir()) {
836 // if we are given a file when on directory only mode, reject it
837 return;
838 } else if (!(mode & KFile::ExistingOnly) || res) {
839 // if we don't care about ExistingOnly flag, add the file even if
840 // it doesn't exist. If we care about it, don't add it to the list
841 if (!onlyDirectoryMode || (res && statJob->statResult().isDir())) {
842 d->m_urlList << url;
843 }
844 filesInList = true;
845 } else {
846 KMessageBox::error(parent: this, i18n("The file \"%1\" could not be found", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Cannot open file"));
847 return; // do not emit accepted() if we had ExistingOnly flag and stat failed
848 }
849
850 if ((d->m_operationMode == Saving) && d->m_confirmOverwrite && !d->toOverwrite(url)) {
851 return;
852 }
853
854 ++it;
855 }
856
857 // if we have reached this point and we didn't return before, that is because
858 // we want this dialog to be accepted
859 Q_EMIT accepted();
860}
861
862void KFileWidget::accept()
863{
864 d->m_inAccept = true;
865
866 *lastDirectory() = d->m_ops->url();
867 if (!d->m_fileClass.isEmpty()) {
868 KRecentDirs::add(fileClass: d->m_fileClass, directory: d->m_ops->url().toString());
869 }
870
871 // clear the topmost item, we insert it as full path later on as item 1
872 d->m_locationEdit->setItemText(index: 0, text: QString());
873
874 const QList<QUrl> list = selectedUrls();
875 int atmost = d->m_locationEdit->maxItems(); // don't add more items than necessary
876 for (const auto &url : list) {
877 if (atmost-- == 0) {
878 break;
879 }
880
881 // we strip the last slash (-1) because KUrlComboBox does that as well
882 // when operating in file-mode. If we wouldn't , dupe-finding wouldn't
883 // work.
884 const QString file = url.toDisplayString(options: QUrl::StripTrailingSlash | QUrl::PreferLocalFile);
885
886 // remove dupes
887 for (int i = 1; i < d->m_locationEdit->count(); ++i) {
888 if (d->m_locationEdit->itemText(index: i) == file) {
889 d->m_locationEdit->removeItem(index: i--);
890 break;
891 }
892 }
893 // FIXME I don't think this works correctly when the KUrlComboBox has some default urls.
894 // KUrlComboBox should provide a function to add an url and rotate the existing ones, keeping
895 // track of maxItems, and we shouldn't be able to insert items as we please.
896 d->m_locationEdit->insertItem(aindex: 1, atext: file);
897 }
898
899 d->writeViewConfig();
900 d->saveRecentFiles();
901
902 d->addToRecentDocuments();
903
904 if (!(mode() & KFile::Files)) { // single selection
905 Q_EMIT fileSelected(d->m_url);
906 }
907
908 d->m_ops->close();
909}
910
911void KFileWidgetPrivate::fileHighlighted(const KFileItem &i)
912{
913 if ((m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty())) { // don't disturb
914 return;
915 }
916
917 if (!i.isNull() && i.isDir() && !(m_ops->mode() & KFile::Directory)) {
918 return;
919 }
920
921 const bool modified = m_locationEdit->lineEdit()->isModified();
922
923 if (!(m_ops->mode() & KFile::Files)) {
924 if (i.isNull()) {
925 if (!modified) {
926 setLocationText(QUrl());
927 }
928 return;
929 }
930
931 m_url = i.url();
932
933 if (!m_locationEdit->hasFocus()) { // don't disturb while editing
934 setLocationText(m_url);
935 }
936
937 Q_EMIT q->fileHighlighted(m_url);
938 } else {
939 multiSelectionChanged();
940 Q_EMIT q->selectionChanged();
941 }
942
943 m_locationEdit->lineEdit()->setModified(false);
944
945 // When saving, and when double-click mode is being used, highlight the
946 // filename after a file is single-clicked so the user has a chance to quickly
947 // rename it if desired
948 // Note that double-clicking will override this and overwrite regardless of
949 // single/double click mouse setting (see slotViewDoubleClicked() )
950 if (m_operationMode == KFileWidget::Saving) {
951 m_locationEdit->setFocus();
952 }
953}
954
955void KFileWidgetPrivate::fileSelected(const KFileItem &i)
956{
957 if (!i.isNull() && i.isDir()) {
958 return;
959 }
960
961 if (!(m_ops->mode() & KFile::Files)) {
962 if (i.isNull()) {
963 setLocationText(QUrl());
964 return;
965 }
966 setLocationText(i.targetUrl());
967 } else {
968 multiSelectionChanged();
969 Q_EMIT q->selectionChanged();
970 }
971
972 // Same as above in fileHighlighted(), but for single-click mode
973 if (m_operationMode == KFileWidget::Saving) {
974 m_locationEdit->setFocus();
975 } else {
976 q->slotOk();
977 }
978}
979
980// I know it's slow to always iterate thru the whole filelist
981// (d->m_ops->selectedItems()), but what can we do?
982void KFileWidgetPrivate::multiSelectionChanged()
983{
984 if (m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty()) { // don't disturb
985 return;
986 }
987
988 const KFileItemList list = m_ops->selectedItems();
989
990 if (list.isEmpty()) {
991 setLocationText(QUrl());
992 return;
993 }
994
995 setLocationText(list.targetUrlList());
996}
997
998void KFileWidgetPrivate::setLocationText(const QUrl &url)
999{
1000 // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get
1001 // emitted, so slotLocationChanged() will be called. Make sure we don't clear the
1002 // KDirOperator's view-selection in there
1003 const QSignalBlocker blocker(m_locationEdit);
1004
1005 if (!url.isEmpty()) {
1006 if (!url.isRelative()) {
1007 const QUrl directory = url.adjusted(options: QUrl::RemoveFilename);
1008 if (!directory.path().isEmpty()) {
1009 q->setUrl(url: directory, clearforward: false);
1010 } else {
1011 q->setUrl(url, clearforward: false);
1012 }
1013 }
1014 m_locationEdit->setEditText(escapeDoubleQuotes(path: url.fileName()));
1015 } else if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1016 m_locationEdit->clearEditText();
1017 }
1018
1019 if (m_operationMode == KFileWidget::Saving) {
1020 setNonExtSelection();
1021 }
1022}
1023
1024static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url)
1025{
1026 if (baseUrl.isParentOf(url)) {
1027 const QString basePath(QDir::cleanPath(path: baseUrl.path()));
1028 QString relPath(QDir::cleanPath(path: url.path()));
1029 relPath.remove(i: 0, len: basePath.length());
1030 if (relPath.startsWith(c: QLatin1Char('/'))) {
1031 relPath.remove(i: 0, len: 1);
1032 }
1033 return relPath;
1034 } else {
1035 return url.toDisplayString();
1036 }
1037}
1038
1039static QString escapeDoubleQuotes(QString &&path)
1040{
1041 // First escape the escape character that we are using
1042 path.replace(QStringLiteral("\\"), QStringLiteral("\\\\"));
1043 // Second, escape the quotes
1044 path.replace(QStringLiteral("\""), QStringLiteral("\\\""));
1045 return path;
1046}
1047
1048void KFileWidgetPrivate::initDirOpWidgets()
1049{
1050 m_opsWidget = new QWidget(q);
1051 m_opsWidgetLayout = new QVBoxLayout(m_opsWidget);
1052 m_opsWidgetLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
1053 m_opsWidgetLayout->setSpacing(0);
1054
1055 m_model = new KFilePlacesModel(q);
1056
1057 // Don't pass "startDir" (KFileWidget constructor 1st arg) to the
1058 // KUrlNavigator at this stage: it may also contain a file name which
1059 // should not get inserted in that form into the old-style navigation
1060 // bar history. Wait until the KIO::stat has been done later.
1061 //
1062 // The stat cannot be done before this point, bug 172678.
1063 m_urlNavigator = new KUrlNavigator(m_model, QUrl(), m_opsWidget); // d->m_toolbar);
1064 m_urlNavigator->setPlacesSelectorVisible(false);
1065
1066 m_urlNavigator->setContentsMargins(left: q->style()->pixelMetric(metric: QStyle::PM_LayoutLeftMargin),
1067 top: 0,
1068 right: q->style()->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
1069 bottom: q->style()->pixelMetric(metric: QStyle::PM_LayoutBottomMargin) - 2);
1070
1071 m_messageWidget = new KMessageWidget(q);
1072 m_messageWidget->setMessageType(KMessageWidget::Error);
1073 m_messageWidget->hide();
1074
1075 auto topSeparator = new QFrame(q);
1076 topSeparator->setFrameStyle(QFrame::HLine);
1077
1078 m_ops = new KDirOperator(QUrl(), m_opsWidget);
1079 m_ops->installEventFilter(filterObj: q);
1080 m_ops->setObjectName(QStringLiteral("KFileWidget::ops"));
1081 m_ops->setIsSaving(m_operationMode == KFileWidget::Saving);
1082 m_ops->setNewFileMenuSelectDirWhenAlreadyExist(true);
1083 m_ops->showOpenWithActions(enable: true);
1084 m_ops->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding);
1085
1086 auto bottomSparator = new QFrame(q);
1087 bottomSparator->setFrameStyle(QFrame::HLine);
1088
1089 q->connect(sender: m_ops, signal: &KDirOperator::urlEntered, context: q, slot: [this](const QUrl &url) {
1090 urlEntered(url);
1091 });
1092 q->connect(sender: m_ops, signal: &KDirOperator::fileHighlighted, context: q, slot: [this](const KFileItem &item) {
1093 fileHighlighted(i: item);
1094 });
1095 q->connect(sender: m_ops, signal: &KDirOperator::fileSelected, context: q, slot: [this](const KFileItem &item) {
1096 fileSelected(i: item);
1097 });
1098 q->connect(sender: m_ops, signal: &KDirOperator::finishedLoading, context: q, slot: [this]() {
1099 slotLoadingFinished();
1100 });
1101 q->connect(sender: m_ops, signal: &KDirOperator::keyEnterReturnPressed, context: q, slot: [this]() {
1102 slotViewKeyEnterReturnPressed();
1103 });
1104 q->connect(sender: m_ops, signal: &KDirOperator::renamingFinished, context: q, slot: [this](const QList<QUrl> &urls) {
1105 // Update file names in location text field after renaming selected files
1106 q->setSelectedUrls(urls);
1107 });
1108
1109 q->connect(sender: m_ops, signal: &KDirOperator::viewChanged, context: q, slot: [](QAbstractItemView *newView) {
1110 newView->setProperty(name: "_breeze_borders_sides", value: QVariant::fromValue(value: QFlags{Qt::TopEdge | Qt::BottomEdge}));
1111 });
1112
1113 m_ops->dirLister()->setAutoErrorHandlingEnabled(false);
1114 q->connect(sender: m_ops->dirLister(), signal: &KDirLister::jobError, context: q, slot: [this](KIO::Job *job) {
1115 m_messageWidget->setText(job->errorString());
1116 m_messageWidget->animatedShow();
1117 });
1118
1119 m_ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions);
1120
1121 initToolbar();
1122
1123 m_opsWidgetLayout->addWidget(m_toolbar);
1124 m_opsWidgetLayout->addWidget(m_urlNavigator);
1125 m_opsWidgetLayout->addWidget(m_messageWidget);
1126 m_opsWidgetLayout->addWidget(topSeparator);
1127 m_opsWidgetLayout->addWidget(m_ops);
1128 m_opsWidgetLayout->addWidget(bottomSparator);
1129}
1130
1131void KFileWidgetPrivate::initZoomWidget()
1132{
1133 m_iconSizeSlider = new QSlider(q);
1134 m_iconSizeSlider->setSizePolicy(hor: QSizePolicy::Maximum, ver: QSizePolicy::Fixed);
1135 m_iconSizeSlider->setMinimumWidth(40);
1136 m_iconSizeSlider->setOrientation(Qt::Horizontal);
1137 m_iconSizeSlider->setMinimum(0);
1138 m_iconSizeSlider->setMaximum(m_stdIconSizes.size() - 1);
1139 m_iconSizeSlider->setSingleStep(1);
1140 m_iconSizeSlider->setPageStep(1);
1141 m_iconSizeSlider->setTickPosition(QSlider::TicksBelow);
1142
1143 q->connect(sender: m_iconSizeSlider, signal: &QAbstractSlider::valueChanged, context: q, slot: [this](int step) {
1144 slotIconSizeChanged(m_stdIconSizes[step]);
1145 });
1146
1147 q->connect(sender: m_iconSizeSlider, signal: &QAbstractSlider::sliderMoved, context: q, slot: [this](int step) {
1148 slotIconSizeSliderMoved(m_stdIconSizes[step]);
1149 });
1150
1151 q->connect(sender: m_ops, signal: &KDirOperator::currentIconSizeChanged, context: q, slot: [this](int iconSize) {
1152 slotDirOpIconSizeChanged(size: iconSize);
1153 });
1154
1155 m_zoomOutAction = KStandardAction::create(
1156 id: KStandardAction::ZoomOut,
1157 recvr: q,
1158 slot: [this]() {
1159 changeIconsSize(zoom: ZoomOut);
1160 },
1161 parent: q);
1162
1163 q->addAction(action: m_zoomOutAction);
1164
1165 m_zoomInAction = KStandardAction::create(
1166 id: KStandardAction::ZoomIn,
1167 recvr: q,
1168 slot: [this]() {
1169 changeIconsSize(zoom: ZoomIn);
1170 },
1171 parent: q);
1172
1173 q->addAction(action: m_zoomInAction);
1174}
1175
1176void KFileWidgetPrivate::initToolbar()
1177{
1178 m_toolbar = new QToolBar(m_opsWidget);
1179 m_toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar"));
1180 m_toolbar->setMovable(false);
1181
1182 // add nav items to the toolbar
1183 //
1184 // NOTE: The order of the button icons here differs from that
1185 // found in the file manager and web browser, but has been discussed
1186 // and agreed upon on the kde-core-devel mailing list:
1187 //
1188 // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2
1189
1190 m_ops->action(action: KDirOperator::Up)
1191 ->setWhatsThis(i18n("<qt>Click this button to enter the parent folder.<br /><br />"
1192 "For instance, if the current location is file:/home/konqi clicking this "
1193 "button will take you to file:/home.</qt>"));
1194
1195 m_ops->action(action: KDirOperator::Back)->setWhatsThis(i18n("Click this button to move backwards one step in the browsing history."));
1196 m_ops->action(action: KDirOperator::Forward)->setWhatsThis(i18n("Click this button to move forward one step in the browsing history."));
1197
1198 m_ops->action(action: KDirOperator::Reload)->setWhatsThis(i18n("Click this button to reload the contents of the current location."));
1199 m_ops->action(action: KDirOperator::NewFolder)->setShortcuts(KStandardShortcut::createFolder());
1200 m_ops->action(action: KDirOperator::NewFolder)->setWhatsThis(i18n("Click this button to create a new folder."));
1201
1202 m_togglePlacesPanelAction = new KToggleAction(i18n("Show Places Panel"), q);
1203 q->addAction(action: m_togglePlacesPanelAction);
1204 m_togglePlacesPanelAction->setShortcut(QKeySequence(Qt::Key_F9));
1205 q->connect(sender: m_togglePlacesPanelAction, signal: &QAction::toggled, context: q, slot: [this](bool show) {
1206 togglePlacesPanel(show);
1207 });
1208
1209 m_toggleBookmarksAction = new KToggleAction(i18n("Show Bookmarks Button"), q);
1210 q->addAction(action: m_toggleBookmarksAction);
1211 q->connect(sender: m_toggleBookmarksAction, signal: &QAction::toggled, context: q, slot: [this](bool show) {
1212 toggleBookmarks(show);
1213 });
1214
1215 // Build the settings menu
1216 KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), q);
1217 q->addAction(action: menu);
1218 menu->setWhatsThis(
1219 i18n("<qt>This is the preferences menu for the file dialog. "
1220 "Various options can be accessed from this menu including: <ul>"
1221 "<li>how files are sorted in the list</li>"
1222 "<li>types of view, including icon and list</li>"
1223 "<li>showing of hidden files</li>"
1224 "<li>the Places panel</li>"
1225 "<li>file previews</li>"
1226 "<li>separating folders from files</li></ul></qt>"));
1227
1228 menu->addAction(action: m_ops->action(action: KDirOperator::AllowExpansionInDetailsView));
1229 menu->addSeparator();
1230 menu->addAction(action: m_ops->action(action: KDirOperator::ShowHiddenFiles));
1231 menu->addAction(action: m_togglePlacesPanelAction);
1232 menu->addAction(action: m_toggleBookmarksAction);
1233 menu->addAction(action: m_ops->action(action: KDirOperator::ShowPreviewPanel));
1234
1235 menu->setPopupMode(QToolButton::InstantPopup);
1236 q->connect(sender: menu->menu(), signal: &QMenu::aboutToShow, context: m_ops, slot: &KDirOperator::updateSelectionDependentActions);
1237
1238 m_bookmarkButton = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), q);
1239 m_bookmarkButton->setPopupMode(QToolButton::InstantPopup);
1240 q->addAction(action: m_bookmarkButton);
1241 m_bookmarkButton->setWhatsThis(
1242 i18n("<qt>This button allows you to bookmark specific locations. "
1243 "Click on this button to open the bookmark menu where you may add, "
1244 "edit or select a bookmark.<br /><br />"
1245 "These bookmarks are specific to the file dialog, but otherwise operate "
1246 "like bookmarks elsewhere in KDE.</qt>"));
1247
1248 QWidget *midSpacer = new QWidget(q);
1249 midSpacer->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding);
1250
1251 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::Back));
1252 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::Forward));
1253 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::Up));
1254 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::Reload));
1255 m_toolbar->addSeparator();
1256 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::ViewIconsView));
1257 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::ViewCompactView));
1258 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::ViewDetailsView));
1259 m_toolbar->addSeparator();
1260 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::ShowPreview));
1261 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::SortMenu));
1262 m_toolbar->addAction(action: m_bookmarkButton);
1263
1264 m_toolbar->addWidget(widget: midSpacer);
1265
1266 initZoomWidget();
1267 m_toolbar->addAction(action: m_zoomOutAction);
1268 m_toolbar->addWidget(widget: m_iconSizeSlider);
1269 m_toolbar->addAction(action: m_zoomInAction);
1270 m_toolbar->addSeparator();
1271
1272 m_toolbar->addAction(action: m_ops->action(action: KDirOperator::NewFolder));
1273 m_toolbar->addAction(action: menu);
1274
1275 m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
1276 m_toolbar->setMovable(false);
1277}
1278
1279void KFileWidgetPrivate::initLocationWidget()
1280{
1281 m_locationLabel = new QLabel(i18n("&Name:"), q);
1282 m_locationEdit = new KUrlComboBox(KUrlComboBox::Files, true, q);
1283 m_locationEdit->installEventFilter(filterObj: q);
1284 // Properly let the dialog be resized (to smaller). Otherwise we could have
1285 // huge dialogs that can't be resized to smaller (it would be as big as the longest
1286 // item in this combo box). (ereslibre)
1287 m_locationEdit->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
1288 q->connect(sender: m_locationEdit, signal: &KUrlComboBox::editTextChanged, context: q, slot: [this](const QString &text) {
1289 slotLocationChanged(text);
1290 });
1291
1292 updateLocationWhatsThis();
1293 m_locationLabel->setBuddy(m_locationEdit);
1294
1295 KUrlCompletion *fileCompletionObj = new KUrlCompletion(KUrlCompletion::FileCompletion);
1296 m_locationEdit->setCompletionObject(compObj: fileCompletionObj);
1297 m_locationEdit->setAutoDeleteCompletionObject(true);
1298
1299 q->connect(sender: m_locationEdit, signal: &KUrlComboBox::returnPressed, context: q, slot: [this](const QString &text) {
1300 locationAccepted(text);
1301 });
1302}
1303
1304void KFileWidgetPrivate::initFilterWidget()
1305{
1306 m_filterLabel = new QLabel(q);
1307 m_filterWidget = new KFileFilterCombo(q);
1308 m_filterWidget->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Fixed);
1309 updateFilterText();
1310 // Properly let the dialog be resized (to smaller). Otherwise we could have
1311 // huge dialogs that can't be resized to smaller (it would be as big as the longest
1312 // item in this combo box). (ereslibre)
1313 m_filterWidget->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
1314 m_filterLabel->setBuddy(m_filterWidget);
1315 q->connect(sender: m_filterWidget, signal: &KFileFilterCombo::filterChanged, context: q, slot: [this]() {
1316 slotFilterChanged();
1317 });
1318
1319 m_filterDelayTimer.setSingleShot(true);
1320 m_filterDelayTimer.setInterval(300);
1321 q->connect(sender: m_filterWidget, signal: &QComboBox::editTextChanged, context: &m_filterDelayTimer, slot: qOverload<>(&QTimer::start));
1322 q->connect(sender: &m_filterDelayTimer, signal: &QTimer::timeout, context: q, slot: [this]() {
1323 slotFilterChanged();
1324 });
1325}
1326
1327void KFileWidgetPrivate::setLocationText(const QList<QUrl> &urlList)
1328{
1329 // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get
1330 // emitted, so slotLocationChanged() will be called. Make sure we don't clear the
1331 // KDirOperator's view-selection in there
1332 const QSignalBlocker blocker(m_locationEdit);
1333
1334 const QUrl baseUrl = m_ops->url();
1335
1336 if (urlList.count() > 1) {
1337 QString urls;
1338 for (const QUrl &url : urlList) {
1339 urls += QStringLiteral("\"%1\" ").arg(a: escapeDoubleQuotes(path: relativePathOrUrl(baseUrl, url)));
1340 }
1341 urls.chop(n: 1);
1342 m_locationEdit->setEditText(urls);
1343 } else if (urlList.count() == 1) {
1344 const auto url = urlList[0];
1345 m_locationEdit->setEditText(escapeDoubleQuotes(path: relativePathOrUrl(baseUrl, url)));
1346 } else if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1347 m_locationEdit->clearEditText();
1348 }
1349
1350 if (m_operationMode == KFileWidget::Saving) {
1351 setNonExtSelection();
1352 }
1353}
1354
1355void KFileWidgetPrivate::updateLocationWhatsThis()
1356{
1357 const QString autocompletionWhatsThisText = i18n(
1358 "<qt>While typing in the text area, you may be presented "
1359 "with possible matches. "
1360 "This feature can be controlled by clicking with the right mouse button "
1361 "and selecting a preferred mode from the <b>Text Completion</b> menu.</qt>");
1362
1363 QString whatsThisText;
1364 if (m_operationMode == KFileWidget::Saving) {
1365 whatsThisText = QLatin1String("<qt>") + i18n("This is the name to save the file as.") + autocompletionWhatsThisText;
1366 } else if (m_ops->mode() & KFile::Files) {
1367 whatsThisText = QLatin1String("<qt>")
1368 + i18n("This is the list of files to open. More than "
1369 "one file can be specified by listing several "
1370 "files, separated by spaces.")
1371 + autocompletionWhatsThisText;
1372 } else {
1373 whatsThisText = QLatin1String("<qt>") + i18n("This is the name of the file to open.") + autocompletionWhatsThisText;
1374 }
1375
1376 m_locationLabel->setWhatsThis(whatsThisText);
1377 m_locationEdit->setWhatsThis(whatsThisText);
1378}
1379
1380void KFileWidgetPrivate::initPlacesPanel()
1381{
1382 if (m_placesDock) {
1383 return;
1384 }
1385
1386 m_placesDock = new QDockWidget(i18nc("@title:window", "Places"), q);
1387 m_placesDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
1388 m_placesDock->setTitleBarWidget(new KDEPrivate::KFileWidgetDockTitleBar(m_placesDock));
1389
1390 m_placesView = new KFilePlacesView(m_placesDock);
1391 m_placesView->setModel(m_model);
1392 m_placesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1393
1394 m_placesView->setObjectName(QStringLiteral("url bar"));
1395 QObject::connect(sender: m_placesView, signal: &KFilePlacesView::urlChanged, context: q, slot: [this](const QUrl &url) {
1396 enterUrl(url);
1397 });
1398
1399 QObject::connect(sender: qobject_cast<KFilePlacesModel *>(object: m_placesView->model()), signal: &KFilePlacesModel::errorMessage, context: q, slot: [this](const QString &errorMessage) {
1400 m_messageWidget->setText(errorMessage);
1401 m_messageWidget->animatedShow();
1402 });
1403
1404 // need to set the current url of the urlbar manually (not via urlEntered()
1405 // here, because the initial url of KDirOperator might be the same as the
1406 // one that will be set later (and then urlEntered() won't be emitted).
1407 // TODO: KDE5 ### REMOVE THIS when KDirOperator's initial URL (in the c'tor) is gone.
1408 m_placesView->setUrl(m_url);
1409
1410 m_placesDock->setWidget(m_placesView);
1411 m_placesViewSplitter->insertWidget(index: 0, widget: m_placesDock);
1412
1413 // initialize the size of the splitter
1414 m_placesViewWidth = m_configGroup.readEntry(key: SpeedbarWidth, aDefault: m_placesView->sizeHint().width());
1415
1416 // Needed for when the dialog is shown with the places panel initially hidden
1417 setPlacesViewSplitterSizes();
1418
1419 QObject::connect(sender: m_placesDock, signal: &QDockWidget::visibilityChanged, context: q, slot: [this](bool visible) {
1420 togglePlacesPanel(show: visible, sender: m_placesDock);
1421 });
1422}
1423
1424void KFileWidgetPrivate::setPlacesViewSplitterSizes()
1425{
1426 if (m_placesViewWidth > 0) {
1427 QList<int> sizes = m_placesViewSplitter->sizes();
1428 sizes[0] = m_placesViewWidth;
1429 sizes[1] = q->width() - m_placesViewWidth - m_placesViewSplitter->handleWidth();
1430 m_placesViewSplitter->setSizes(sizes);
1431 }
1432}
1433
1434void KFileWidgetPrivate::initGUI()
1435{
1436 delete m_boxLayout; // deletes all sub layouts
1437
1438 m_boxLayout = new QVBoxLayout(q);
1439 m_boxLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0); // no additional margin to the already existing
1440
1441 m_placesViewSplitter = new QSplitter(q);
1442 m_placesViewSplitter->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Expanding);
1443 m_placesViewSplitter->setChildrenCollapsible(false);
1444 m_boxLayout->addWidget(m_placesViewSplitter);
1445
1446 QObject::connect(sender: m_placesViewSplitter, signal: &QSplitter::splitterMoved, context: q, slot: [this](int pos, int index) {
1447 placesViewSplitterMoved(pos, index);
1448 });
1449 m_placesViewSplitter->insertWidget(index: 0, widget: m_opsWidget);
1450
1451 m_lafBox = new QFormLayout();
1452 m_lafBox->setSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutVerticalSpacing));
1453 m_lafBox->setContentsMargins(left: q->style()->pixelMetric(metric: QStyle::PM_LayoutLeftMargin),
1454 top: q->style()->pixelMetric(metric: QStyle::PM_LayoutTopMargin),
1455 right: q->style()->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
1456 bottom: 0);
1457
1458 m_lafBox->addRow(label: m_locationLabel, field: m_locationEdit);
1459 m_lafBox->addRow(label: m_filterLabel, field: m_filterWidget);
1460 // Add the "Automatically Select Extension" checkbox
1461 m_lafBox->addWidget(w: m_autoSelectExtCheckBox);
1462
1463 m_opsWidgetLayout->addLayout(layout: m_lafBox);
1464
1465 auto hbox = new QHBoxLayout();
1466 hbox->setSpacing(q->style()->pixelMetric(metric: QStyle::PM_LayoutHorizontalSpacing));
1467 hbox->setContentsMargins(left: q->style()->pixelMetric(metric: QStyle::PM_LayoutTopMargin),
1468 top: q->style()->pixelMetric(metric: QStyle::PM_LayoutLeftMargin),
1469 right: q->style()->pixelMetric(metric: QStyle::PM_LayoutRightMargin),
1470 bottom: q->style()->pixelMetric(metric: QStyle::PM_LayoutBottomMargin));
1471
1472 hbox->addStretch(stretch: 2);
1473 hbox->addWidget(m_okButton);
1474 hbox->addWidget(m_cancelButton);
1475
1476 m_opsWidgetLayout->addLayout(layout: hbox);
1477
1478 q->setTabOrder(m_ops, m_autoSelectExtCheckBox);
1479 q->setTabOrder(m_autoSelectExtCheckBox, m_locationEdit);
1480 q->setTabOrder(m_locationEdit, m_filterWidget);
1481 q->setTabOrder(m_filterWidget, m_okButton);
1482 q->setTabOrder(m_okButton, m_cancelButton);
1483 q->setTabOrder(m_cancelButton, m_urlNavigator);
1484 q->setTabOrder(m_urlNavigator, m_ops);
1485}
1486
1487void KFileWidgetPrivate::slotFilterChanged()
1488{
1489 m_filterDelayTimer.stop();
1490
1491 KFileFilter filter = m_filterWidget->currentFilter();
1492
1493 m_ops->clearFilter();
1494
1495 if (!filter.mimePatterns().isEmpty()) {
1496 QStringList types = filter.mimePatterns();
1497 types.prepend(QStringLiteral("inode/directory"));
1498 m_ops->setMimeFilter(types);
1499 }
1500
1501 const auto filePatterns = filter.filePatterns();
1502 const bool hasRegExSyntax = std::any_of(first: filePatterns.constBegin(), last: filePatterns.constEnd(), pred: [](const QString &filter) {
1503 return filter.contains(c: QLatin1Char('*')) || filter.contains(c: QLatin1Char('?')) || filter.contains(c: QLatin1Char('['));
1504 });
1505
1506 if (hasRegExSyntax) {
1507 m_ops->setNameFilter(filter.filePatterns().join(sep: QLatin1Char(' ')));
1508 } else {
1509 m_ops->setNameFilter(QLatin1Char('*') + filePatterns.join(sep: QLatin1Char('*')) + QLatin1Char('*'));
1510 }
1511
1512 updateAutoSelectExtension();
1513
1514 m_ops->updateDir();
1515
1516 Q_EMIT q->filterChanged(filter);
1517}
1518
1519void KFileWidget::setUrl(const QUrl &url, bool clearforward)
1520{
1521 // qDebug();
1522
1523 d->m_ops->setUrl(url, clearforward);
1524}
1525
1526// Protected
1527void KFileWidgetPrivate::urlEntered(const QUrl &url)
1528{
1529 // qDebug();
1530
1531 KUrlComboBox *pathCombo = m_urlNavigator->editor();
1532 if (pathCombo->count() != 0) { // little hack
1533 pathCombo->setUrl(url);
1534 }
1535
1536 bool blocked = m_locationEdit->blockSignals(b: true);
1537 if (m_keepLocation) {
1538 const QUrl currentUrl = urlFromString(str: locationEditCurrentText());
1539 // iconNameForUrl will get the icon or fallback to a generic one
1540 m_locationEdit->setItemIcon(index: 0, icon: QIcon::fromTheme(name: KIO::iconNameForUrl(url: currentUrl)));
1541 // Preserve the text when clicking on the view (cf fileHighlighted)
1542 m_locationEdit->lineEdit()->setModified(true);
1543 }
1544
1545 m_locationEdit->blockSignals(b: blocked);
1546
1547 m_urlNavigator->setLocationUrl(url);
1548
1549 // is triggered in ctor before completion object is set
1550 KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject());
1551 if (completion) {
1552 completion->setDir(url);
1553 }
1554
1555 if (m_placesView) {
1556 m_placesView->setUrl(url);
1557 }
1558
1559 m_messageWidget->hide();
1560}
1561
1562void KFileWidgetPrivate::locationAccepted(const QString &url)
1563{
1564 Q_UNUSED(url);
1565 // qDebug();
1566 q->slotOk();
1567}
1568
1569void KFileWidgetPrivate::enterUrl(const QUrl &url)
1570{
1571 // qDebug();
1572
1573 // append '/' if needed: url combo does not add it
1574 // tokenize() expects it because it uses QUrl::adjusted(QUrl::RemoveFilename)
1575 QUrl u(url);
1576 Utils::appendSlashToPath(url&: u);
1577 q->setUrl(url: u);
1578
1579 // We need to check window()->focusWidget() instead of m_locationEdit->hasFocus
1580 // because when the window is showing up m_locationEdit
1581 // may still not have focus but it'll be the one that will have focus when the window
1582 // gets it and we don't want to steal its focus either
1583 if (q->window()->focusWidget() != m_locationEdit) {
1584 m_ops->setFocus();
1585 }
1586}
1587
1588void KFileWidgetPrivate::enterUrl(const QString &url)
1589{
1590 // qDebug();
1591
1592 enterUrl(url: urlFromString(str: KUrlCompletion::replacedPath(text: url, replaceHome: true, replaceEnv: true)));
1593}
1594
1595bool KFileWidgetPrivate::toOverwrite(const QUrl &url)
1596{
1597 // qDebug();
1598
1599 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
1600 KJobWidgets::setWindow(job: statJob, widget: q);
1601 bool res = statJob->exec();
1602
1603 if (res) {
1604 int ret = KMessageBox::warningContinueCancel(parent: q,
1605 i18n("The file \"%1\" already exists. Do you wish to overwrite it?", url.fileName()),
1606 i18n("Overwrite File?"),
1607 buttonContinue: KStandardGuiItem::overwrite(),
1608 buttonCancel: KStandardGuiItem::cancel(),
1609 dontAskAgainName: QString(),
1610 options: KMessageBox::Notify | KMessageBox::Dangerous);
1611
1612 if (ret != KMessageBox::Continue) {
1613 m_locationEdit->setFocus();
1614 setNonExtSelection();
1615
1616 return false;
1617 }
1618 return true;
1619 }
1620
1621 return true;
1622}
1623
1624void KFileWidget::setSelectedUrl(const QUrl &url)
1625{
1626 // Honor protocols that do not support directory listing
1627 if (!url.isRelative() && !KProtocolManager::supportsListing(url)) {
1628 return;
1629 }
1630 d->setLocationText(url);
1631}
1632
1633void KFileWidget::setSelectedUrls(const QList<QUrl> &urls)
1634{
1635 if (urls.isEmpty()) {
1636 return;
1637 }
1638
1639 // Honor protocols that do not support directory listing
1640 if (!urls[0].isRelative() && !KProtocolManager::supportsListing(url: urls[0])) {
1641 return;
1642 }
1643 d->setLocationText(urls);
1644}
1645
1646void KFileWidgetPrivate::slotLoadingFinished()
1647{
1648 const QString currentText = m_locationEdit->currentText();
1649 if (currentText.isEmpty()) {
1650 return;
1651 }
1652
1653 m_ops->blockSignals(b: true);
1654 QUrl u(m_ops->url());
1655 if (currentText.startsWith(c: QLatin1Char('/'))) {
1656 u.setPath(path: currentText);
1657 } else {
1658 u.setPath(path: Utils::concatPaths(path1: m_ops->url().path(), path2: currentText));
1659 }
1660 m_ops->setCurrentItem(u);
1661 m_ops->blockSignals(b: false);
1662}
1663
1664void KFileWidgetPrivate::slotLocationChanged(const QString &text)
1665{
1666 // qDebug();
1667
1668 m_locationEdit->lineEdit()->setModified(true);
1669
1670 if (text.isEmpty() && m_ops->view()) {
1671 m_ops->view()->clearSelection();
1672 }
1673
1674 if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1675 const QList<QUrl> urlList(tokenize(line: text));
1676 m_ops->setCurrentItems(urlList);
1677 }
1678
1679 updateFilter();
1680}
1681
1682QUrl KFileWidget::selectedUrl() const
1683{
1684 // qDebug();
1685
1686 if (d->m_inAccept) {
1687 return d->m_url;
1688 } else {
1689 return QUrl();
1690 }
1691}
1692
1693QList<QUrl> KFileWidget::selectedUrls() const
1694{
1695 // qDebug();
1696
1697 QList<QUrl> list;
1698 if (d->m_inAccept) {
1699 if (d->m_ops->mode() & KFile::Files) {
1700 list = d->m_urlList;
1701 } else {
1702 list.append(t: d->m_url);
1703 }
1704 }
1705 return list;
1706}
1707
1708QList<QUrl> KFileWidgetPrivate::tokenize(const QString &line) const
1709{
1710 qCDebug(KIO_KFILEWIDGETS_FW) << "Tokenizing:" << line;
1711
1712 QList<QUrl> urls;
1713 QUrl baseUrl(m_ops->url().adjusted(options: QUrl::RemoveFilename));
1714 Utils::appendSlashToPath(url&: baseUrl);
1715
1716 // A helper that creates, validates and appends a new url based
1717 // on the given filename.
1718 auto addUrl = [baseUrl, &urls](const QString &partial_name) {
1719 if (partial_name.trimmed().isEmpty()) {
1720 return;
1721 }
1722
1723 // url could be absolute
1724 QUrl partial_url(partial_name);
1725 if (!partial_url.isValid()
1726 || partial_url.isRelative()
1727 // the text might look like a url scheme but not be a real one
1728 || (!partial_url.scheme().isEmpty() && (!partial_name.contains(QStringLiteral("://")) || !KProtocolInfo::isKnownProtocol(protocol: partial_url.scheme())))) {
1729 // We have to use setPath here, so that something like "test#file"
1730 // isn't interpreted to have path "test" and fragment "file".
1731 partial_url.clear();
1732 partial_url.setPath(path: partial_name);
1733 }
1734
1735 // This returns QUrl(partial_name) for absolute URLs.
1736 // Otherwise, returns the concatenated url.
1737 if (partial_url.isRelative() || baseUrl.isParentOf(url: partial_url)) {
1738 partial_url = baseUrl.resolved(relative: partial_url);
1739 }
1740
1741 if (partial_url.isValid()) {
1742 urls.append(t: partial_url);
1743 } else {
1744 // This can happen in the first quote! (ex: ' "something here"')
1745 qCDebug(KIO_KFILEWIDGETS_FW) << "Discarding Invalid" << partial_url;
1746 }
1747 };
1748
1749 // An iterative approach here where we toggle the "escape" flag
1750 // if we hit `\`. If we hit `"` and the escape flag is false,
1751 // we split
1752 QString partial_name;
1753 bool escape = false;
1754 for (int i = 0; i < line.length(); i++) {
1755 const QChar ch = line[i];
1756
1757 // Handle any character previously escaped
1758 if (escape) {
1759 partial_name += ch;
1760 escape = false;
1761 continue;
1762 }
1763
1764 // Handle escape start
1765 if (ch.toLatin1() == '\\') {
1766 escape = true;
1767 continue;
1768 }
1769
1770 // Handle UNESCAPED quote (") since the above ifs are
1771 // dealing with the escaped ones
1772 if (ch.toLatin1() == '"') {
1773 addUrl(partial_name);
1774 partial_name.clear();
1775 continue;
1776 }
1777
1778 // Any other character just append
1779 partial_name += ch;
1780 }
1781
1782 // Handle the last item which is buffered in partial_name. This is
1783 // required for single-file selection dialogs since the name will not
1784 // be wrapped in quotes
1785 if (!partial_name.isEmpty()) {
1786 addUrl(partial_name);
1787 partial_name.clear();
1788 }
1789
1790 return urls;
1791}
1792
1793QString KFileWidget::selectedFile() const
1794{
1795 // qDebug();
1796
1797 if (d->m_inAccept) {
1798 const QUrl url = d->mostLocalUrl(url: d->m_url);
1799 if (url.isLocalFile()) {
1800 return url.toLocalFile();
1801 } else {
1802 KMessageBox::error(parent: const_cast<KFileWidget *>(this), i18n("You can only select local files."), i18n("Remote Files Not Accepted"));
1803 }
1804 }
1805 return QString();
1806}
1807
1808QStringList KFileWidget::selectedFiles() const
1809{
1810 // qDebug();
1811
1812 QStringList list;
1813
1814 if (d->m_inAccept) {
1815 if (d->m_ops->mode() & KFile::Files) {
1816 const QList<QUrl> urls = d->m_urlList;
1817 for (const auto &u : urls) {
1818 const QUrl url = d->mostLocalUrl(url: u);
1819 if (url.isLocalFile()) {
1820 list.append(t: url.toLocalFile());
1821 }
1822 }
1823 }
1824
1825 else { // single-selection mode
1826 if (d->m_url.isLocalFile()) {
1827 list.append(t: d->m_url.toLocalFile());
1828 }
1829 }
1830 }
1831
1832 return list;
1833}
1834
1835QUrl KFileWidget::baseUrl() const
1836{
1837 return d->m_ops->url();
1838}
1839
1840void KFileWidget::resizeEvent(QResizeEvent *event)
1841{
1842 QWidget::resizeEvent(event);
1843
1844 if (d->m_placesDock) {
1845 // we don't want our places dock actually changing size when we resize
1846 // and qt doesn't make it easy to enforce such a thing with QSplitter
1847 d->setPlacesViewSplitterSizes();
1848 }
1849}
1850
1851void KFileWidget::showEvent(QShowEvent *event)
1852{
1853 if (!d->m_hasView) { // delayed view-creation
1854 Q_ASSERT(d);
1855 Q_ASSERT(d->m_ops);
1856 d->m_ops->setViewMode(KFile::Default);
1857 d->m_hasView = true;
1858
1859 connect(sender: d->m_ops->view(), signal: &QAbstractItemView::doubleClicked, context: this, slot: [this](const QModelIndex &index) {
1860 d->slotViewDoubleClicked(index);
1861 });
1862 }
1863 d->m_ops->clearHistory();
1864
1865 QWidget::showEvent(event);
1866}
1867
1868bool KFileWidget::eventFilter(QObject *watched, QEvent *event)
1869{
1870 const bool res = QWidget::eventFilter(watched, event);
1871
1872 QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>(event);
1873 if (!keyEvent) {
1874 return res;
1875 }
1876
1877 const auto type = event->type();
1878 const auto key = keyEvent->key();
1879
1880 if (watched == d->m_ops && type == QEvent::KeyPress && (key == Qt::Key_Return || key == Qt::Key_Enter)) {
1881 // ignore return events from the KDirOperator
1882 // they are not needed, activated is used to handle this case
1883 event->accept();
1884 return true;
1885 }
1886
1887 return res;
1888}
1889
1890void KFileWidget::setMode(KFile::Modes m)
1891{
1892 // qDebug();
1893
1894 d->m_ops->setMode(m);
1895 if (d->m_ops->dirOnlyMode()) {
1896 d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Folders"), {QStringLiteral("*")}, {}));
1897 } else {
1898 d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Files"), {QStringLiteral("*")}, {}));
1899 }
1900
1901 d->updateAutoSelectExtension();
1902}
1903
1904KFile::Modes KFileWidget::mode() const
1905{
1906 return d->m_ops->mode();
1907}
1908
1909void KFileWidgetPrivate::readViewConfig()
1910{
1911 m_ops->setViewConfig(m_configGroup);
1912 m_ops->readConfig(configGroup: m_configGroup);
1913 KUrlComboBox *combo = m_urlNavigator->editor();
1914
1915 KCompletion::CompletionMode cm =
1916 (KCompletion::CompletionMode)m_configGroup.readEntry(key: PathComboCompletionMode, aDefault: static_cast<int>(KCompletion::CompletionPopup));
1917 if (cm != KCompletion::CompletionPopup) {
1918 combo->setCompletionMode(cm);
1919 }
1920
1921 cm = (KCompletion::CompletionMode)m_configGroup.readEntry(key: LocationComboCompletionMode, aDefault: static_cast<int>(KCompletion::CompletionPopup));
1922 if (cm != KCompletion::CompletionPopup) {
1923 m_locationEdit->setCompletionMode(cm);
1924 }
1925
1926 // Show or don't show the places panel
1927 togglePlacesPanel(show: m_configGroup.readEntry(key: ShowSpeedbar, aDefault: true));
1928
1929 // show or don't show the bookmarks
1930 toggleBookmarks(m_configGroup.readEntry(key: ShowBookmarks, aDefault: false));
1931
1932 // does the user want Automatically Select Extension?
1933 m_autoSelectExtChecked = m_configGroup.readEntry(key: AutoSelectExtChecked, aDefault: DefaultAutoSelectExtChecked);
1934 updateAutoSelectExtension();
1935
1936 // should the URL navigator use the breadcrumb navigation?
1937 m_urlNavigator->setUrlEditable(!m_configGroup.readEntry(key: BreadcrumbNavigation, aDefault: true));
1938
1939 // should the URL navigator show the full path?
1940 m_urlNavigator->setShowFullPath(m_configGroup.readEntry(key: ShowFullPath, aDefault: false));
1941
1942 int w1 = q->minimumSize().width();
1943 int w2 = m_toolbar->sizeHint().width();
1944 if (w1 < w2) {
1945 q->setMinimumWidth(w2);
1946 }
1947}
1948
1949void KFileWidgetPrivate::writeViewConfig()
1950{
1951 // these settings are global settings; ALL instances of the file dialog
1952 // should reflect them.
1953 // There is no way to tell KFileOperator::writeConfig() to write to
1954 // kdeglobals so we write settings to a temporary config group then copy
1955 // them all to kdeglobals
1956 KConfig tmp(QString(), KConfig::SimpleConfig);
1957 KConfigGroup tmpGroup(&tmp, ConfigGroup);
1958
1959 KUrlComboBox *pathCombo = m_urlNavigator->editor();
1960 // saveDialogSize( tmpGroup, KConfigGroup::Persistent | KConfigGroup::Global );
1961 tmpGroup.writeEntry(key: PathComboCompletionMode, value: static_cast<int>(pathCombo->completionMode()));
1962 tmpGroup.writeEntry(key: LocationComboCompletionMode, value: static_cast<int>(m_locationEdit->completionMode()));
1963
1964 const bool showPlacesPanel = m_placesDock && !m_placesDock->isHidden();
1965 tmpGroup.writeEntry(key: ShowSpeedbar, value: showPlacesPanel);
1966 if (m_placesViewWidth > 0) {
1967 tmpGroup.writeEntry(key: SpeedbarWidth, value: m_placesViewWidth);
1968 }
1969
1970 tmpGroup.writeEntry(key: ShowBookmarks, value: m_bookmarkHandler != nullptr);
1971 tmpGroup.writeEntry(key: AutoSelectExtChecked, value: m_autoSelectExtChecked);
1972 tmpGroup.writeEntry(key: BreadcrumbNavigation, value: !m_urlNavigator->isUrlEditable());
1973 tmpGroup.writeEntry(key: ShowFullPath, value: m_urlNavigator->showFullPath());
1974
1975 m_ops->writeConfig(configGroup&: tmpGroup);
1976
1977 // Copy saved settings to kdeglobals
1978 tmpGroup.copyTo(other: &m_configGroup, pFlags: KConfigGroup::Persistent | KConfigGroup::Global);
1979}
1980
1981void KFileWidgetPrivate::readRecentFiles()
1982{
1983 // qDebug();
1984
1985 const bool oldState = m_locationEdit->blockSignals(b: true);
1986 m_locationEdit->setMaxItems(m_configGroup.readEntry(key: RecentFilesNumber, aDefault: DefaultRecentURLsNumber));
1987 m_locationEdit->setUrls(urls: m_stateConfigGroup.readPathEntry(pKey: RecentFiles, aDefault: QStringList()), remove: KUrlComboBox::RemoveBottom);
1988 m_locationEdit->setCurrentIndex(-1);
1989 m_locationEdit->blockSignals(b: oldState);
1990
1991 KUrlComboBox *combo = m_urlNavigator->editor();
1992 combo->setUrls(urls: m_stateConfigGroup.readPathEntry(pKey: RecentURLs, aDefault: QStringList()), remove: KUrlComboBox::RemoveTop);
1993 combo->setMaxItems(m_configGroup.readEntry(key: RecentURLsNumber, aDefault: DefaultRecentURLsNumber));
1994 combo->setUrl(m_ops->url());
1995 // since we delayed this moment, initialize the directory of the completion object to
1996 // our current directory (that was very probably set on the constructor)
1997 KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject());
1998 if (completion) {
1999 completion->setDir(m_ops->url());
2000 }
2001}
2002
2003void KFileWidgetPrivate::saveRecentFiles()
2004{
2005 // qDebug();
2006 m_stateConfigGroup.writePathEntry(pKey: RecentFiles, value: m_locationEdit->urls());
2007
2008 KUrlComboBox *pathCombo = m_urlNavigator->editor();
2009 m_stateConfigGroup.writePathEntry(pKey: RecentURLs, value: pathCombo->urls());
2010}
2011
2012QPushButton *KFileWidget::okButton() const
2013{
2014 return d->m_okButton;
2015}
2016
2017QPushButton *KFileWidget::cancelButton() const
2018{
2019 return d->m_cancelButton;
2020}
2021
2022// Called by KFileDialog
2023void KFileWidget::slotCancel()
2024{
2025 d->writeViewConfig();
2026 d->m_ops->close();
2027}
2028
2029void KFileWidget::setKeepLocation(bool keep)
2030{
2031 d->m_keepLocation = keep;
2032}
2033
2034bool KFileWidget::keepsLocation() const
2035{
2036 return d->m_keepLocation;
2037}
2038
2039void KFileWidget::setOperationMode(OperationMode mode)
2040{
2041 // qDebug();
2042
2043 d->m_operationMode = mode;
2044 d->m_keepLocation = (mode == Saving);
2045 d->m_filterWidget->setEditable(!d->m_hasDefaultFilter || mode != Saving);
2046 if (mode == Opening) {
2047 // don't use KStandardGuiItem::open() here which has trailing ellipsis!
2048 d->m_okButton->setText(i18n("&Open"));
2049 d->m_okButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
2050 // hide the new folder actions...usability team says they shouldn't be in open file dialog
2051 d->m_ops->action(action: KDirOperator::NewFolder)->setEnabled(false);
2052 d->m_toolbar->removeAction(action: d->m_ops->action(action: KDirOperator::NewFolder));
2053 } else if (mode == Saving) {
2054 KGuiItem::assign(button: d->m_okButton, item: KStandardGuiItem::save());
2055 d->setNonExtSelection();
2056 } else {
2057 KGuiItem::assign(button: d->m_okButton, item: KStandardGuiItem::ok());
2058 }
2059 d->updateLocationWhatsThis();
2060 d->updateAutoSelectExtension();
2061
2062 if (d->m_ops) {
2063 d->m_ops->setIsSaving(mode == Saving);
2064 }
2065 d->updateFilterText();
2066}
2067
2068KFileWidget::OperationMode KFileWidget::operationMode() const
2069{
2070 return d->m_operationMode;
2071}
2072
2073void KFileWidgetPrivate::slotAutoSelectExtClicked()
2074{
2075 // qDebug() << "slotAutoSelectExtClicked(): "
2076 // << m_autoSelectExtCheckBox->isChecked() << endl;
2077
2078 // whether the _user_ wants it on/off
2079 m_autoSelectExtChecked = m_autoSelectExtCheckBox->isChecked();
2080
2081 // update the current filename's extension
2082 updateLocationEditExtension(m_extension /* extension hasn't changed */);
2083}
2084
2085void KFileWidgetPrivate::placesViewSplitterMoved(int pos, int index)
2086{
2087 // qDebug();
2088
2089 // we need to record the size of the splitter when the splitter changes size
2090 // so we can keep the places box the right size!
2091 if (m_placesDock && index == 1) {
2092 m_placesViewWidth = pos;
2093 // qDebug() << "setting m_lafBox minwidth to" << m_placesViewWidth;
2094 }
2095}
2096
2097void KFileWidgetPrivate::activateUrlNavigator()
2098{
2099 // qDebug();
2100
2101 QLineEdit *lineEdit = m_urlNavigator->editor()->lineEdit();
2102
2103 // If the text field currently has focus and everything is selected,
2104 // pressing the keyboard shortcut returns the whole thing to breadcrumb mode
2105 if (m_urlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) {
2106 m_urlNavigator->setUrlEditable(false);
2107 } else {
2108 m_urlNavigator->setUrlEditable(true);
2109 m_urlNavigator->setFocus();
2110 lineEdit->selectAll();
2111 }
2112}
2113
2114void KFileWidgetPrivate::slotDirOpIconSizeChanged(int size)
2115{
2116 auto beginIt = m_stdIconSizes.cbegin();
2117 auto endIt = m_stdIconSizes.cend();
2118 auto it = std::lower_bound(first: beginIt, last: endIt, val: size);
2119 const int sliderStep = it != endIt ? it - beginIt : 0;
2120 m_iconSizeSlider->setValue(sliderStep);
2121 m_zoomOutAction->setDisabled(it == beginIt);
2122 m_zoomInAction->setDisabled(it == (endIt - 1));
2123}
2124
2125void KFileWidgetPrivate::changeIconsSize(ZoomState zoom)
2126{
2127 int step = m_iconSizeSlider->value();
2128
2129 if (zoom == ZoomOut) {
2130 if (step == 0) {
2131 return;
2132 }
2133 --step;
2134 } else { // ZoomIn
2135 if (step == static_cast<int>(m_stdIconSizes.size() - 1)) {
2136 return;
2137 }
2138 ++step;
2139 }
2140
2141 m_iconSizeSlider->setValue(step);
2142 slotIconSizeSliderMoved(m_stdIconSizes[step]);
2143}
2144
2145void KFileWidgetPrivate::slotIconSizeChanged(int _value)
2146{
2147 m_ops->setIconSize(_value);
2148 m_iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels", _value));
2149}
2150
2151void KFileWidgetPrivate::slotIconSizeSliderMoved(int size)
2152{
2153 // Force this to be called in case this slot is called first on the
2154 // slider move.
2155 slotIconSizeChanged(value: size);
2156
2157 QPoint global(m_iconSizeSlider->rect().topLeft());
2158 global.ry() += m_iconSizeSlider->height() / 2;
2159 QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_iconSizeSlider->mapToGlobal(global));
2160 QApplication::sendEvent(receiver: m_iconSizeSlider, event: &toolTipEvent);
2161}
2162
2163void KFileWidgetPrivate::slotViewDoubleClicked(const QModelIndex &index)
2164{
2165 // double clicking to save should only work on files
2166 if (m_operationMode == KFileWidget::Saving && index.isValid() && m_ops->selectedItems().constFirst().isFile()) {
2167 q->slotOk();
2168 }
2169}
2170
2171void KFileWidgetPrivate::slotViewKeyEnterReturnPressed()
2172{
2173 // an enter/return event occurred in the view
2174 // when we are saving one file and there is no selection in the view (otherwise we get an activated event)
2175 if (m_operationMode == KFileWidget::Saving && (m_ops->mode() & KFile::File) && m_ops->selectedItems().isEmpty()) {
2176 q->slotOk();
2177 }
2178}
2179
2180static QString getExtensionFromPatternList(const QStringList &patternList)
2181{
2182 // qDebug();
2183
2184 QString ret;
2185 // qDebug() << "\tgetExtension " << patternList;
2186
2187 QStringList::ConstIterator patternListEnd = patternList.end();
2188 for (QStringList::ConstIterator it = patternList.begin(); it != patternListEnd; ++it) {
2189 // qDebug() << "\t\ttry: \'" << (*it) << "\'";
2190
2191 // is this pattern like "*.BMP" rather than useless things like:
2192 //
2193 // README
2194 // *.
2195 // *.*
2196 // *.JP*G
2197 // *.JP?
2198 // *.[Jj][Pp][Gg]
2199 if ((*it).startsWith(s: QLatin1String("*.")) && (*it).length() > 2 && (*it).indexOf(c: QLatin1Char('*'), from: 2) < 0 && (*it).indexOf(c: QLatin1Char('?'), from: 2) < 0
2200 && (*it).indexOf(c: QLatin1Char('['), from: 2) < 0 && (*it).indexOf(c: QLatin1Char(']'), from: 2) < 0) {
2201 ret = (*it).mid(position: 1);
2202 break;
2203 }
2204 }
2205
2206 return ret;
2207}
2208
2209static QString stripUndisplayable(const QString &string)
2210{
2211 QString ret = string;
2212
2213 ret.remove(c: QLatin1Char(':'));
2214 ret = KLocalizedString::removeAcceleratorMarker(label: ret);
2215
2216 return ret;
2217}
2218
2219// QString KFileWidget::currentFilterExtension()
2220//{
2221// return d->m_extension;
2222//}
2223
2224void KFileWidgetPrivate::updateAutoSelectExtension()
2225{
2226 if (!m_autoSelectExtCheckBox) {
2227 return;
2228 }
2229
2230 QMimeDatabase db;
2231 //
2232 // Figure out an extension for the Automatically Select Extension thing
2233 // (some Windows users apparently don't know what to do when confronted
2234 // with a text file called "COPYING" but do know what to do with
2235 // COPYING.txt ...)
2236 //
2237
2238 // qDebug() << "Figure out an extension: ";
2239 QString lastExtension = m_extension;
2240 m_extension.clear();
2241
2242 // Automatically Select Extension is only valid if the user is _saving_ a _file_
2243 if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) {
2244 //
2245 // Get an extension from the filter
2246 //
2247
2248 KFileFilter fileFilter = m_filterWidget->currentFilter();
2249 if (!fileFilter.isEmpty()) {
2250 // if the currently selected filename already has an extension which
2251 // is also included in the currently allowed extensions, keep it
2252 // otherwise use the default extension
2253 QString currentExtension = db.suffixForFileName(fileName: locationEditCurrentText());
2254 if (currentExtension.isEmpty()) {
2255 currentExtension = locationEditCurrentText().section(asep: QLatin1Char('.'), astart: -1, aend: -1);
2256 }
2257 // qDebug() << "filter:" << filter << "locationEdit:" << locationEditCurrentText() << "currentExtension:" << currentExtension;
2258
2259 QString defaultExtension;
2260 QStringList extensionList;
2261
2262 // e.g. "*.cpp"
2263 if (!fileFilter.filePatterns().isEmpty()) {
2264 extensionList = fileFilter.filePatterns();
2265 defaultExtension = getExtensionFromPatternList(patternList: extensionList);
2266 }
2267 // e.g. "text/html"
2268 else if (!fileFilter.mimePatterns().isEmpty()) {
2269 QMimeType mime = db.mimeTypeForName(nameOrAlias: fileFilter.mimePatterns().first());
2270 if (mime.isValid()) {
2271 extensionList = mime.globPatterns();
2272 defaultExtension = mime.preferredSuffix();
2273 if (!defaultExtension.isEmpty()) {
2274 defaultExtension.prepend(c: QLatin1Char('.'));
2275 }
2276 }
2277 }
2278
2279 if ((!currentExtension.isEmpty() && extensionList.contains(t: QLatin1String("*.") + currentExtension))
2280 || (!fileFilter.mimePatterns().isEmpty() && fileFilter.mimePatterns().first() == QLatin1String("application/octet-stream"))) {
2281 m_extension = QLatin1Char('.') + currentExtension;
2282 } else {
2283 m_extension = defaultExtension;
2284 }
2285
2286 // qDebug() << "List:" << extensionList << "auto-selected extension:" << m_extension;
2287 }
2288
2289 //
2290 // GUI: checkbox
2291 //
2292
2293 QString whatsThisExtension;
2294 if (!m_extension.isEmpty()) {
2295 // remember: sync any changes to the string with below
2296 m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension (%1)", m_extension));
2297 whatsThisExtension = i18n("the extension <b>%1</b>", m_extension);
2298
2299 m_autoSelectExtCheckBox->setEnabled(true);
2300 m_autoSelectExtCheckBox->setChecked(m_autoSelectExtChecked);
2301 } else {
2302 // remember: sync any changes to the string with above
2303 m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension"));
2304 whatsThisExtension = i18n("a suitable extension");
2305
2306 m_autoSelectExtCheckBox->setChecked(false);
2307 m_autoSelectExtCheckBox->setEnabled(false);
2308 }
2309
2310 const QString locationLabelText = stripUndisplayable(string: m_locationLabel->text());
2311 m_autoSelectExtCheckBox->setWhatsThis(QLatin1String("<qt>")
2312 + i18n("This option enables some convenient features for "
2313 "saving files with extensions:<br />"
2314 "<ol>"
2315 "<li>Any extension specified in the <b>%1</b> text "
2316 "area will be updated if you change the file type "
2317 "to save in.<br />"
2318 "<br /></li>"
2319 "<li>If no extension is specified in the <b>%2</b> "
2320 "text area when you click "
2321 "<b>Save</b>, %3 will be added to the end of the "
2322 "filename (if the filename does not already exist). "
2323 "This extension is based on the file type that you "
2324 "have chosen to save in.<br />"
2325 "<br />"
2326 "If you do not want KDE to supply an extension for the "
2327 "filename, you can either turn this option off or you "
2328 "can suppress it by adding a period (.) to the end of "
2329 "the filename (the period will be automatically "
2330 "removed)."
2331 "</li>"
2332 "</ol>"
2333 "If unsure, keep this option enabled as it makes your "
2334 "files more manageable.",
2335 locationLabelText,
2336 locationLabelText,
2337 whatsThisExtension)
2338 + QLatin1String("</qt>"));
2339
2340 m_autoSelectExtCheckBox->show();
2341
2342 // update the current filename's extension
2343 updateLocationEditExtension(lastExtension);
2344 }
2345 // Automatically Select Extension not valid
2346 else {
2347 m_autoSelectExtCheckBox->setChecked(false);
2348 m_autoSelectExtCheckBox->hide();
2349 }
2350}
2351
2352// Updates the extension of the filename specified in d->m_locationEdit if the
2353// Automatically Select Extension feature is enabled.
2354// (this prevents you from accidentally saving "file.kwd" as RTF, for example)
2355void KFileWidgetPrivate::updateLocationEditExtension(const QString &lastExtension)
2356{
2357 if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) {
2358 return;
2359 }
2360
2361 const QString urlStr = locationEditCurrentText();
2362 if (urlStr.isEmpty()) {
2363 return;
2364 }
2365
2366 const int fileNameOffset = urlStr.lastIndexOf(c: QLatin1Char('/')) + 1;
2367 QStringView fileName = QStringView(urlStr).mid(pos: fileNameOffset);
2368
2369 const int dot = fileName.lastIndexOf(c: QLatin1Char('.'));
2370 const int len = fileName.length();
2371 if (dot > 0 && // has an extension already and it's not a hidden file
2372 // like ".hidden" (but we do accept ".hidden.ext")
2373 dot != len - 1 // and not deliberately suppressing extension
2374 ) {
2375 const QUrl url = getCompleteUrl(url: urlStr);
2376 // qDebug() << "updateLocationEditExtension (" << url << ")";
2377 // exists?
2378 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
2379 KJobWidgets::setWindow(job: statJob, widget: q);
2380 bool result = statJob->exec();
2381 if (result) {
2382 // qDebug() << "\tfile exists";
2383
2384 if (statJob->statResult().isDir()) {
2385 // qDebug() << "\tisDir - won't alter extension";
2386 return;
2387 }
2388
2389 // --- fall through ---
2390 }
2391
2392 //
2393 // try to get rid of the current extension
2394 //
2395
2396 // catch "double extensions" like ".tar.gz"
2397 if (!lastExtension.isEmpty() && fileName.endsWith(s: lastExtension)) {
2398 fileName.chop(n: lastExtension.length());
2399 } else if (!m_extension.isEmpty() && fileName.endsWith(s: m_extension)) {
2400 fileName.chop(n: m_extension.length());
2401 } else { // can only handle "single extensions"
2402 fileName.truncate(n: dot);
2403 }
2404
2405 // add extension
2406 const QString newText = QStringView(urlStr).left(n: fileNameOffset) + fileName + m_extension;
2407 if (newText != locationEditCurrentText()) {
2408 const int idx = m_locationEdit->currentIndex();
2409 if (idx == -1) {
2410 m_locationEdit->setEditText(newText);
2411 } else {
2412 m_locationEdit->setItemText(index: idx, text: newText);
2413 }
2414 m_locationEdit->lineEdit()->setModified(true);
2415 }
2416 }
2417}
2418
2419QString KFileWidgetPrivate::findMatchingFilter(const QString &filter, const QString &filename) const
2420{
2421 // e.g.: '*.foo *.bar|Foo type' -> '*.foo', '*.bar'
2422 const QStringList patterns = filter.left(n: filter.indexOf(c: QLatin1Char('|'))).split(sep: QLatin1Char(' '), behavior: Qt::SkipEmptyParts);
2423
2424 QRegularExpression rx;
2425 for (const QString &p : patterns) {
2426 rx.setPattern(QRegularExpression::wildcardToRegularExpression(str: p));
2427 if (rx.match(subject: filename).hasMatch()) {
2428 return p;
2429 }
2430 }
2431 return QString();
2432}
2433
2434// Updates the filter if the extension of the filename specified in d->m_locationEdit is changed
2435// (this prevents you from accidentally saving "file.kwd" as RTF, for example)
2436void KFileWidgetPrivate::updateFilter()
2437{
2438 if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) {
2439 QString urlStr = locationEditCurrentText();
2440 if (urlStr.isEmpty()) {
2441 return;
2442 }
2443
2444 QMimeDatabase db;
2445 QMimeType urlMimeType = db.mimeTypeForFile(fileName: urlStr, mode: QMimeDatabase::MatchExtension);
2446
2447 bool matchesCurrentFilter = [this, urlMimeType, urlStr] {
2448 const KFileFilter filter = m_filterWidget->currentFilter();
2449 if (filter.mimePatterns().contains(str: urlMimeType.name())) {
2450 return true;
2451 }
2452
2453 QString filename = urlStr.mid(position: urlStr.lastIndexOf(c: QLatin1Char('/')) + 1); // only filename
2454
2455 const auto filePatterns = filter.filePatterns();
2456 const bool hasMatch = std::any_of(first: filePatterns.cbegin(), last: filePatterns.cend(), pred: [filename](const QString &pattern) {
2457 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: pattern));
2458
2459 return rx.match(subject: filename).hasMatch();
2460 });
2461 return hasMatch;
2462 }();
2463
2464 if (matchesCurrentFilter) {
2465 return;
2466 }
2467
2468 const auto filters = m_filterWidget->filters();
2469
2470 auto filterIt = std::find_if(first: filters.cbegin(), last: filters.cend(), pred: [urlStr, urlMimeType](const KFileFilter &filter) {
2471 if (filter.mimePatterns().contains(str: urlMimeType.name())) {
2472 return true;
2473 }
2474
2475 QString filename = urlStr.mid(position: urlStr.lastIndexOf(c: QLatin1Char('/')) + 1); // only filename
2476 // accept any match to honor the user's selection; see later code handling the "*" match
2477
2478 const auto filePatterns = filter.filePatterns();
2479 const bool hasMatch = std::any_of(first: filePatterns.cbegin(), last: filePatterns.cend(), pred: [filename](const QString &pattern) {
2480 // never match the catch-all filter
2481 if (pattern == QLatin1String("*")) {
2482 return false;
2483 }
2484
2485 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: pattern));
2486
2487 return rx.match(subject: filename).hasMatch();
2488 });
2489
2490 return hasMatch;
2491 });
2492
2493 if (filterIt != filters.cend()) {
2494 m_filterWidget->setCurrentFilter(*filterIt);
2495 }
2496 }
2497}
2498
2499// applies only to a file that doesn't already exist
2500void KFileWidgetPrivate::appendExtension(QUrl &url)
2501{
2502 // qDebug();
2503
2504 if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) {
2505 return;
2506 }
2507
2508 QString fileName = url.fileName();
2509 if (fileName.isEmpty()) {
2510 return;
2511 }
2512
2513 // qDebug() << "appendExtension(" << url << ")";
2514
2515 const int len = fileName.length();
2516 const int dot = fileName.lastIndexOf(c: QLatin1Char('.'));
2517
2518 const bool suppressExtension = (dot == len - 1);
2519 const bool unspecifiedExtension = !fileName.endsWith(s: m_extension);
2520
2521 // don't KIO::Stat if unnecessary
2522 if (!(suppressExtension || unspecifiedExtension)) {
2523 return;
2524 }
2525
2526 // exists?
2527 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
2528 KJobWidgets::setWindow(job: statJob, widget: q);
2529 bool res = statJob->exec();
2530 if (res) {
2531 // qDebug() << "\tfile exists - won't append extension";
2532 return;
2533 }
2534
2535 // suppress automatically append extension?
2536 if (suppressExtension) {
2537 //
2538 // Strip trailing dot
2539 // This allows lazy people to have m_autoSelectExtCheckBox->isChecked
2540 // but don't want a file extension to be appended
2541 // e.g. "README." will make a file called "README"
2542 //
2543 // If you really want a name like "README.", then type "README.."
2544 // and the trailing dot will be removed (or just stop being lazy and
2545 // turn off this feature so that you can type "README.")
2546 //
2547 // qDebug() << "\tstrip trailing dot";
2548 QString path = url.path();
2549 path.chop(n: 1);
2550 url.setPath(path);
2551 }
2552 // evilmatically append extension :) if the user hasn't specified one
2553 else if (unspecifiedExtension) {
2554 // qDebug() << "\tappending extension \'" << m_extension << "\'...";
2555 url = url.adjusted(options: QUrl::RemoveFilename); // keeps trailing slash
2556 url.setPath(path: url.path() + fileName + m_extension);
2557 // qDebug() << "\tsaving as \'" << url << "\'";
2558 }
2559}
2560
2561// adds the selected files/urls to 'recent documents'
2562void KFileWidgetPrivate::addToRecentDocuments()
2563{
2564 int m = m_ops->mode();
2565 int atmost = KRecentDocument::maximumItems();
2566 // don't add more than we need. KRecentDocument::add() is pretty slow
2567
2568 if (m & KFile::LocalOnly) {
2569 const QStringList files = q->selectedFiles();
2570 QStringList::ConstIterator it = files.begin();
2571 for (; it != files.end() && atmost > 0; ++it) {
2572 KRecentDocument::add(url: QUrl::fromLocalFile(localfile: *it));
2573 atmost--;
2574 }
2575 }
2576
2577 else { // urls
2578 const QList<QUrl> urls = q->selectedUrls();
2579 QList<QUrl>::ConstIterator it = urls.begin();
2580 for (; it != urls.end() && atmost > 0; ++it) {
2581 if ((*it).isValid()) {
2582 KRecentDocument::add(url: *it);
2583 atmost--;
2584 }
2585 }
2586 }
2587}
2588
2589KUrlComboBox *KFileWidget::locationEdit() const
2590{
2591 return d->m_locationEdit;
2592}
2593
2594KFileFilterCombo *KFileWidget::filterWidget() const
2595{
2596 return d->m_filterWidget;
2597}
2598
2599void KFileWidgetPrivate::togglePlacesPanel(bool show, QObject *sender)
2600{
2601 if (show) {
2602 initPlacesPanel();
2603 m_placesDock->show();
2604
2605 // check to see if they have a home item defined, if not show the home button
2606 QUrl homeURL;
2607 homeURL.setPath(path: QDir::homePath());
2608 KFilePlacesModel *model = static_cast<KFilePlacesModel *>(m_placesView->model());
2609 for (int rowIndex = 0; rowIndex < model->rowCount(); rowIndex++) {
2610 QModelIndex index = model->index(row: rowIndex, column: 0);
2611 QUrl url = model->url(index);
2612
2613 if (homeURL.matches(url, options: QUrl::StripTrailingSlash)) {
2614 m_toolbar->removeAction(action: m_ops->action(action: KDirOperator::Home));
2615 break;
2616 }
2617 }
2618 } else {
2619 if (sender == m_placesDock && m_placesDock && m_placesDock->isVisibleTo(q)) {
2620 // we didn't *really* go away! the dialog was simply hidden or
2621 // we changed virtual desktops or ...
2622 return;
2623 }
2624
2625 if (m_placesDock) {
2626 m_placesDock->hide();
2627 }
2628
2629 QAction *homeAction = m_ops->action(action: KDirOperator::Home);
2630 QAction *reloadAction = m_ops->action(action: KDirOperator::Reload);
2631 if (!m_toolbar->actions().contains(t: homeAction)) {
2632 m_toolbar->insertAction(before: reloadAction, action: homeAction);
2633 }
2634 }
2635
2636 m_togglePlacesPanelAction->setChecked(show);
2637
2638 // if we don't show the places panel, at least show the places menu
2639 m_urlNavigator->setPlacesSelectorVisible(!show);
2640}
2641
2642void KFileWidgetPrivate::toggleBookmarks(bool show)
2643{
2644 if (show) {
2645 if (m_bookmarkHandler) {
2646 return;
2647 }
2648 m_bookmarkHandler = new KFileBookmarkHandler(q);
2649 q->connect(sender: m_bookmarkHandler, signal: &KFileBookmarkHandler::openUrl, context: q, slot: [this](const QString &path) {
2650 enterUrl(url: path);
2651 });
2652 m_bookmarkButton->setMenu(m_bookmarkHandler->menu());
2653 } else if (m_bookmarkHandler) {
2654 m_bookmarkButton->setMenu(nullptr);
2655 delete m_bookmarkHandler;
2656 m_bookmarkHandler = nullptr;
2657 }
2658
2659 if (m_bookmarkButton) {
2660 m_bookmarkButton->setVisible(show);
2661 }
2662
2663 m_toggleBookmarksAction->setChecked(show);
2664}
2665
2666// static, overloaded
2667QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass)
2668{
2669 QString fileName; // result discarded
2670 return getStartUrl(startDir, recentDirClass, fileName);
2671}
2672
2673// static, overloaded
2674QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName)
2675{
2676 recentDirClass.clear();
2677 fileName.clear();
2678 QUrl ret;
2679
2680 bool useDefaultStartDir = startDir.isEmpty();
2681 if (!useDefaultStartDir) {
2682 if (startDir.scheme() == QLatin1String("kfiledialog")) {
2683 // The startDir URL with this protocol may be in the format:
2684 // directory() fileName()
2685 // 1. kfiledialog:///keyword "/" keyword
2686 // 2. kfiledialog:///keyword?global "/" keyword
2687 // 3. kfiledialog:///keyword/ "/" keyword
2688 // 4. kfiledialog:///keyword/?global "/" keyword
2689 // 5. kfiledialog:///keyword/filename /keyword filename
2690 // 6. kfiledialog:///keyword/filename?global /keyword filename
2691
2692 QString keyword;
2693 QString urlDir = startDir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
2694 QString urlFile = startDir.fileName();
2695 if (urlDir == QLatin1String("/")) { // '1'..'4' above
2696 keyword = urlFile;
2697 fileName.clear();
2698 } else { // '5' or '6' above
2699 keyword = urlDir.mid(position: 1);
2700 fileName = urlFile;
2701 }
2702
2703 const QLatin1String query(":%1");
2704 recentDirClass = query.arg(args&: keyword);
2705
2706 ret = QUrl::fromLocalFile(localfile: KRecentDirs::dir(fileClass: recentDirClass));
2707 } else { // not special "kfiledialog" URL
2708 // "foo.png" only gives us a file name, the default start dir will be used.
2709 // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same
2710 // (and is the reason why we don't just use QUrl::isRelative()).
2711
2712 // In all other cases (startDir contains a directory path, or has no
2713 // fileName for us anyway, such as smb://), startDir is indeed a dir url.
2714
2715 if (!startDir.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) {
2716 // can use start directory
2717 ret = startDir; // will be checked by stat later
2718 // If we won't be able to list it (e.g. http), then use default
2719 if (!KProtocolManager::supportsListing(url: ret)) {
2720 useDefaultStartDir = true;
2721 fileName = startDir.fileName();
2722 }
2723 } else { // file name only
2724 fileName = startDir.fileName();
2725 useDefaultStartDir = true;
2726 }
2727 }
2728 }
2729
2730 if (useDefaultStartDir) {
2731 if (lastDirectory()->isEmpty()) {
2732 *lastDirectory() = QUrl::fromLocalFile(localfile: QStandardPaths::writableLocation(type: QStandardPaths::DocumentsLocation));
2733 const QUrl home(QUrl::fromLocalFile(localfile: QDir::homePath()));
2734 // if there is no docpath set (== home dir), we prefer the current
2735 // directory over it. We also prefer the homedir when our CWD is
2736 // different from our homedirectory or when the document dir
2737 // does not exist
2738 if (lastDirectory()->adjusted(options: QUrl::StripTrailingSlash) == home.adjusted(options: QUrl::StripTrailingSlash) //
2739 || QDir::currentPath() != QDir::homePath() //
2740 || !QDir(lastDirectory()->toLocalFile()).exists()) {
2741 *lastDirectory() = QUrl::fromLocalFile(localfile: QDir::currentPath());
2742 }
2743 }
2744 ret = *lastDirectory();
2745 }
2746
2747 // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName;
2748 return ret;
2749}
2750
2751void KFileWidget::setStartDir(const QUrl &directory)
2752{
2753 if (directory.isValid()) {
2754 *lastDirectory() = directory;
2755 }
2756}
2757
2758void KFileWidgetPrivate::setNonExtSelection()
2759{
2760 // Enhanced rename: Don't highlight the file extension.
2761 QString filename = locationEditCurrentText();
2762 QMimeDatabase db;
2763 QString extension = db.suffixForFileName(fileName: filename);
2764
2765 if (!extension.isEmpty()) {
2766 m_locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1);
2767 } else {
2768 int lastDot = filename.lastIndexOf(c: QLatin1Char('.'));
2769 if (lastDot > 0) {
2770 m_locationEdit->lineEdit()->setSelection(0, lastDot);
2771 } else {
2772 m_locationEdit->lineEdit()->selectAll();
2773 }
2774 }
2775}
2776
2777// Sets the filter text to "File type" if the dialog is saving and a MIME type
2778// filter has been set; otherwise, the text is "Filter:"
2779void KFileWidgetPrivate::updateFilterText()
2780{
2781 QString label;
2782 QString whatsThisText;
2783
2784 if (m_operationMode == KFileWidget::Saving && !m_filterWidget->currentFilter().mimePatterns().isEmpty()) {
2785 label = i18n("&File type:");
2786 whatsThisText = i18n("<qt>This is the file type selector. It is used to select the format that the file will be saved as.</qt>");
2787 } else {
2788 label = i18n("&Filter:");
2789 whatsThisText = i18n(
2790 "<qt>This is the filter to apply to the file list. "
2791 "File names that do not match the filter will not be shown.<p>"
2792 "You may select from one of the preset filters in the "
2793 "drop down menu, or you may enter a custom filter "
2794 "directly into the text area.</p><p>"
2795 "Wildcards such as * and ? are allowed.</p></qt>");
2796 }
2797
2798 if (m_filterLabel) {
2799 m_filterLabel->setText(label);
2800 m_filterLabel->setWhatsThis(whatsThisText);
2801 }
2802 if (m_filterWidget) {
2803 m_filterWidget->setWhatsThis(whatsThisText);
2804 }
2805}
2806
2807void KFileWidget::setCustomWidget(QWidget *widget)
2808{
2809 delete d->m_bottomCustomWidget;
2810 d->m_bottomCustomWidget = widget;
2811
2812 // add it to the dialog, below the filter list box.
2813
2814 // Change the parent so that this widget is a child of the main widget
2815 d->m_bottomCustomWidget->setParent(this);
2816
2817 d->m_opsWidgetLayout->addWidget(d->m_bottomCustomWidget);
2818
2819 // FIXME: This should adjust the tab orders so that the custom widget
2820 // comes after the Cancel button. The code appears to do this, but the result
2821 // somehow screws up the tab order of the file path combo box. Not a major
2822 // problem, but ideally the tab order with a custom widget should be
2823 // the same as the order without one.
2824 setTabOrder(d->m_cancelButton, d->m_bottomCustomWidget);
2825 setTabOrder(d->m_bottomCustomWidget, d->m_urlNavigator);
2826}
2827
2828void KFileWidget::setCustomWidget(const QString &text, QWidget *widget)
2829{
2830 delete d->m_labeledCustomWidget;
2831 d->m_labeledCustomWidget = widget;
2832
2833 QLabel *label = new QLabel(text, this);
2834 label->setAlignment(Qt::AlignRight);
2835 d->m_lafBox->addRow(label, field: widget);
2836}
2837
2838KDirOperator *KFileWidget::dirOperator()
2839{
2840 return d->m_ops;
2841}
2842
2843void KFileWidget::readConfig(KConfigGroup &group)
2844{
2845 d->m_configGroup = group;
2846 d->readViewConfig();
2847 d->readRecentFiles();
2848}
2849
2850QString KFileWidgetPrivate::locationEditCurrentText() const
2851{
2852 return QDir::fromNativeSeparators(pathName: m_locationEdit->currentText());
2853}
2854
2855QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url)
2856{
2857 if (url.isLocalFile()) {
2858 return url;
2859 }
2860
2861 KIO::StatJob *statJob = KIO::stat(url, flags: KIO::HideProgressInfo);
2862 KJobWidgets::setWindow(job: statJob, widget: q);
2863 bool res = statJob->exec();
2864
2865 if (!res) {
2866 return url;
2867 }
2868
2869 const QString path = statJob->statResult().stringValue(field: KIO::UDSEntry::UDS_LOCAL_PATH);
2870 if (!path.isEmpty()) {
2871 QUrl newUrl;
2872 newUrl.setPath(path);
2873 return newUrl;
2874 }
2875
2876 return url;
2877}
2878
2879void KFileWidgetPrivate::setInlinePreviewShown(bool show)
2880{
2881 m_ops->setInlinePreviewShown(show);
2882}
2883
2884void KFileWidget::setConfirmOverwrite(bool enable)
2885{
2886 d->m_confirmOverwrite = enable;
2887}
2888
2889void KFileWidget::setInlinePreviewShown(bool show)
2890{
2891 d->setInlinePreviewShown(show);
2892}
2893
2894QSize KFileWidget::dialogSizeHint() const
2895{
2896 int fontSize = fontMetrics().height();
2897 QSize goodSize(48 * fontSize, 30 * fontSize);
2898 const QSize scrnSize = d->screenSize();
2899 QSize minSize(scrnSize / 2);
2900 QSize maxSize(scrnSize * qreal(0.9));
2901 return (goodSize.expandedTo(otherSize: minSize).boundedTo(otherSize: maxSize));
2902}
2903
2904void KFileWidget::setViewMode(KFile::FileView mode)
2905{
2906 d->m_ops->setViewMode(mode);
2907 d->m_hasView = true;
2908}
2909
2910void KFileWidget::setSupportedSchemes(const QStringList &schemes)
2911{
2912 d->m_model->setSupportedSchemes(schemes);
2913 d->m_ops->setSupportedSchemes(schemes);
2914 d->m_urlNavigator->setSupportedSchemes(schemes);
2915}
2916
2917QStringList KFileWidget::supportedSchemes() const
2918{
2919 return d->m_model->supportedSchemes();
2920}
2921
2922#include "moc_kfilewidget.cpp"
2923

source code of kio/src/filewidgets/kfilewidget.cpp