1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1999, 2000, 2001 Carsten Pfeiffer <pfeiffer@kde.org>
4 SPDX-FileCopyrightText: 2013 Teo Mrnjavac <teo@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7*/
8
9#include "kurlrequester.h"
10#include "../utils_p.h"
11#include "kio_widgets_debug.h"
12
13#include <KComboBox>
14#include <KDragWidgetDecorator>
15#include <KLineEdit>
16#include <KLocalizedString>
17#include <kprotocolmanager.h>
18#include <kurlcompletion.h>
19
20#include <QAction>
21#include <QApplication>
22#include <QDrag>
23#include <QEvent>
24#include <QHBoxLayout>
25#include <QKeySequence>
26#include <QMenu>
27#include <QMimeData>
28
29class KUrlDragPushButton : public QPushButton
30{
31 Q_OBJECT
32public:
33 explicit KUrlDragPushButton(QWidget *parent)
34 : QPushButton(parent)
35 {
36 new DragDecorator(this);
37 }
38 ~KUrlDragPushButton() override
39 {
40 }
41
42 void setURL(const QUrl &url)
43 {
44 m_urls.clear();
45 m_urls.append(t: url);
46 }
47
48private:
49 class DragDecorator : public KDragWidgetDecoratorBase
50 {
51 public:
52 explicit DragDecorator(KUrlDragPushButton *button)
53 : KDragWidgetDecoratorBase(button)
54 , m_button(button)
55 {
56 }
57
58 protected:
59 QDrag *dragObject() override
60 {
61 if (m_button->m_urls.isEmpty()) {
62 return nullptr;
63 }
64
65 QDrag *drag = new QDrag(m_button);
66 QMimeData *mimeData = new QMimeData;
67 mimeData->setUrls(m_button->m_urls);
68 drag->setMimeData(mimeData);
69 return drag;
70 }
71
72 private:
73 KUrlDragPushButton *m_button;
74 };
75
76 QList<QUrl> m_urls;
77};
78
79class Q_DECL_HIDDEN KUrlRequester::KUrlRequesterPrivate
80{
81public:
82 explicit KUrlRequesterPrivate(KUrlRequester *parent)
83 : m_fileDialogModeWasDirAndFile(false)
84 , m_parent(parent)
85 , edit(nullptr)
86 , combo(nullptr)
87 , fileDialogMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly)
88 , fileDialogAcceptMode(QFileDialog::AcceptOpen)
89 {
90 }
91
92 ~KUrlRequesterPrivate()
93 {
94 delete myCompletion;
95 delete myFileDialog;
96 }
97
98 void init();
99
100 void setText(const QString &text)
101 {
102 if (combo) {
103 if (combo->isEditable()) {
104 combo->setEditText(text);
105 } else {
106 int i = combo->findText(text);
107 if (i == -1) {
108 combo->addItem(atext: text);
109 combo->setCurrentIndex(combo->count() - 1);
110 } else {
111 combo->setCurrentIndex(i);
112 }
113 }
114 } else {
115 edit->setText(text);
116 }
117 }
118
119 void connectSignals(KUrlRequester *receiver)
120 {
121 if (combo) {
122 connect(sender: combo, signal: &QComboBox::currentTextChanged, context: receiver, slot: &KUrlRequester::textChanged);
123 connect(sender: combo, signal: &QComboBox::editTextChanged, context: receiver, slot: &KUrlRequester::textEdited);
124
125 connect(sender: combo, signal: &KComboBox::returnPressed, context: receiver, slot: &KUrlRequester::returnPressed);
126 } else if (edit) {
127 connect(sender: edit, signal: &QLineEdit::textChanged, context: receiver, slot: &KUrlRequester::textChanged);
128 connect(sender: edit, signal: &QLineEdit::textEdited, context: receiver, slot: &KUrlRequester::textEdited);
129
130 connect(sender: edit, signal: qOverload<>(&QLineEdit::returnPressed), context: receiver, slot: [this]() {
131 m_parent->Q_EMIT returnPressed(text: QString{});
132 });
133
134 if (auto kline = qobject_cast<KLineEdit *>(object: edit)) {
135 connect(sender: kline, signal: &KLineEdit::returnKeyPressed, context: receiver, slot: &KUrlRequester::returnPressed);
136 }
137 }
138 }
139
140 void setCompletionObject(KCompletion *comp)
141 {
142 if (combo) {
143 combo->setCompletionObject(completionObject: comp);
144 } else {
145 edit->setCompletionObject(comp);
146 }
147 }
148
149 void updateCompletionStartDir(const QUrl &newStartDir)
150 {
151 myCompletion->setDir(newStartDir);
152 }
153
154 QString text() const
155 {
156 return combo ? combo->currentText() : edit->text();
157 }
158
159 /**
160 * replaces ~user or $FOO, if necessary
161 * if text() is a relative path, make it absolute using startDir()
162 */
163 QUrl url() const
164 {
165 const QString txt = text();
166 KUrlCompletion *comp;
167 if (combo) {
168 comp = qobject_cast<KUrlCompletion *>(object: combo->completionObject());
169 } else {
170 comp = qobject_cast<KUrlCompletion *>(object: edit->completionObject());
171 }
172
173 QString enteredPath;
174 if (comp) {
175 enteredPath = comp->replacedPath(text: txt);
176 } else {
177 enteredPath = txt;
178 }
179
180 if (Utils::isAbsoluteLocalPath(path: enteredPath)) {
181 return QUrl::fromLocalFile(localfile: enteredPath);
182 }
183
184 const QUrl enteredUrl = QUrl(enteredPath); // absolute or relative
185 if (enteredUrl.isRelative() && !txt.isEmpty()) {
186 QUrl finalUrl(m_startDir);
187 finalUrl.setPath(path: Utils::concatPaths(path1: finalUrl.path(), path2: enteredPath));
188 return finalUrl;
189 } else {
190 return enteredUrl;
191 }
192 }
193
194 static void applyFileMode(QFileDialog *dlg, KFile::Modes m, QFileDialog::AcceptMode acceptMode)
195 {
196 QFileDialog::FileMode fileMode;
197 bool dirsOnly = false;
198 if (m & KFile::Directory) {
199 fileMode = QFileDialog::Directory;
200 if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) {
201 dirsOnly = true;
202 }
203 } else if (m & KFile::Files && m & KFile::ExistingOnly) {
204 fileMode = QFileDialog::ExistingFiles;
205 } else if (m & KFile::File && m & KFile::ExistingOnly) {
206 fileMode = QFileDialog::ExistingFile;
207 } else {
208 fileMode = QFileDialog::AnyFile;
209 }
210
211 dlg->setFileMode(fileMode);
212 dlg->setAcceptMode(acceptMode);
213 dlg->setOption(option: QFileDialog::ShowDirsOnly, on: dirsOnly);
214 }
215
216 QUrl getDirFromFileDialog(const QUrl &openUrl) const
217 {
218 return QFileDialog::getExistingDirectoryUrl(parent: m_parent, caption: QString(), dir: openUrl, options: QFileDialog::ShowDirsOnly);
219 }
220
221 void createFileDialog()
222 {
223 // Creates the fileDialog if it doesn't exist yet
224 QFileDialog *dlg = m_parent->fileDialog();
225
226 if (!url().isEmpty() && !url().isRelative()) {
227 QUrl u(url());
228 // If we won't be able to list it (e.g. http), then don't try :)
229 if (KProtocolManager::supportsListing(url: u)) {
230 dlg->selectUrl(url: u);
231 }
232 } else {
233 dlg->setDirectoryUrl(m_startDir);
234 }
235
236 dlg->setAcceptMode(fileDialogAcceptMode);
237
238 // Update the file dialog window modality
239 if (dlg->windowModality() != fileDialogModality) {
240 dlg->setWindowModality(fileDialogModality);
241 }
242
243 if (fileDialogModality == Qt::NonModal) {
244 dlg->show();
245 } else {
246 dlg->exec();
247 }
248 }
249
250 // slots
251 void slotUpdateUrl();
252 void slotOpenDialog();
253 void slotFileDialogAccepted();
254
255 QUrl m_startDir;
256 bool m_startDirCustomized;
257 bool m_fileDialogModeWasDirAndFile;
258 KUrlRequester *const m_parent; // TODO: rename to 'q'
259 KLineEdit *edit;
260 KComboBox *combo;
261 KFile::Modes fileDialogMode;
262 QFileDialog::AcceptMode fileDialogAcceptMode;
263 QStringList nameFilters;
264 QStringList mimeTypeFilters;
265 KEditListWidget::CustomEditor editor;
266 KUrlDragPushButton *myButton;
267 QFileDialog *myFileDialog;
268 KUrlCompletion *myCompletion;
269 Qt::WindowModality fileDialogModality;
270};
271
272KUrlRequester::KUrlRequester(QWidget *editWidget, QWidget *parent)
273 : QWidget(parent)
274 , d(new KUrlRequesterPrivate(this))
275{
276 // must have this as parent
277 editWidget->setParent(this);
278 d->combo = qobject_cast<KComboBox *>(object: editWidget);
279 d->edit = qobject_cast<KLineEdit *>(object: editWidget);
280 if (d->edit) {
281 d->edit->setClearButtonEnabled(true);
282 }
283
284 d->init();
285}
286
287KUrlRequester::KUrlRequester(QWidget *parent)
288 : QWidget(parent)
289 , d(new KUrlRequesterPrivate(this))
290{
291 d->init();
292}
293
294KUrlRequester::KUrlRequester(const QUrl &url, QWidget *parent)
295 : QWidget(parent)
296 , d(new KUrlRequesterPrivate(this))
297{
298 d->init();
299 setUrl(url);
300}
301
302KUrlRequester::~KUrlRequester()
303{
304 QWidget *widget = d->combo ? static_cast<QWidget *>(d->combo) : static_cast<QWidget *>(d->edit);
305 widget->removeEventFilter(obj: this);
306}
307
308void KUrlRequester::KUrlRequesterPrivate::init()
309{
310 myFileDialog = nullptr;
311 fileDialogModality = Qt::ApplicationModal;
312
313 if (!combo && !edit) {
314 edit = new KLineEdit(m_parent);
315 edit->setClearButtonEnabled(true);
316 }
317
318 QWidget *widget = combo ? static_cast<QWidget *>(combo) : static_cast<QWidget *>(edit);
319
320 QHBoxLayout *topLayout = new QHBoxLayout(m_parent);
321 topLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
322 topLayout->setSpacing(-1); // use default spacing
323 topLayout->addWidget(widget);
324
325 myButton = new KUrlDragPushButton(m_parent);
326 myButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
327 int buttonSize = myButton->sizeHint().expandedTo(otherSize: widget->sizeHint()).height();
328 myButton->setFixedSize(w: buttonSize, h: buttonSize);
329 myButton->setToolTip(i18n("Open file dialog"));
330
331 connect(sender: myButton, signal: &KUrlDragPushButton::pressed, context: m_parent, slot: [this]() {
332 slotUpdateUrl();
333 });
334
335 widget->installEventFilter(filterObj: m_parent);
336 m_parent->setFocusProxy(widget);
337 m_parent->setFocusPolicy(Qt::StrongFocus);
338 topLayout->addWidget(myButton);
339
340 connectSignals(receiver: m_parent);
341 connect(sender: myButton, signal: &KUrlDragPushButton::clicked, context: m_parent, slot: [this]() {
342 slotOpenDialog();
343 });
344
345 m_startDir = QUrl::fromLocalFile(localfile: QDir::currentPath());
346 m_startDirCustomized = false;
347
348 myCompletion = new KUrlCompletion();
349 updateCompletionStartDir(newStartDir: m_startDir);
350
351 setCompletionObject(myCompletion);
352
353 QAction *openAction = new QAction(m_parent);
354 openAction->setShortcut(QKeySequence::Open);
355 m_parent->connect(sender: openAction, signal: &QAction::triggered, context: m_parent, slot: [this]() {
356 slotOpenDialog();
357 });
358}
359
360void KUrlRequester::setUrl(const QUrl &url)
361{
362 d->setText(url.toDisplayString(options: QUrl::PreferLocalFile));
363}
364
365void KUrlRequester::setText(const QString &text)
366{
367 d->setText(text);
368}
369
370void KUrlRequester::setStartDir(const QUrl &startDir)
371{
372 d->m_startDir = startDir;
373 d->m_startDirCustomized = true;
374 d->updateCompletionStartDir(newStartDir: startDir);
375}
376
377void KUrlRequester::changeEvent(QEvent *e)
378{
379 if (e->type() == QEvent::WindowTitleChange) {
380 if (d->myFileDialog) {
381 d->myFileDialog->setWindowTitle(windowTitle());
382 }
383 }
384 QWidget::changeEvent(e);
385}
386
387QUrl KUrlRequester::url() const
388{
389 return d->url();
390}
391
392QUrl KUrlRequester::startDir() const
393{
394 return d->m_startDir;
395}
396
397QString KUrlRequester::text() const
398{
399 return d->text();
400}
401
402void KUrlRequester::KUrlRequesterPrivate::slotOpenDialog()
403{
404 if (myFileDialog) {
405 if (myFileDialog->isVisible()) {
406 // The file dialog is already being shown, raise it and exit
407 myFileDialog->raise();
408 myFileDialog->activateWindow();
409 return;
410 }
411 }
412
413 if (!m_fileDialogModeWasDirAndFile
414 && (((fileDialogMode & KFile::Directory) && !(fileDialogMode & KFile::File)) ||
415 /* catch possible fileDialog()->setMode( KFile::Directory ) changes */
416 (myFileDialog && (myFileDialog->fileMode() == QFileDialog::Directory && myFileDialog->testOption(option: QFileDialog::ShowDirsOnly))))) {
417 const QUrl openUrl = (!m_parent->url().isEmpty() && !m_parent->url().isRelative()) ? m_parent->url() : m_startDir;
418
419 /* FIXME We need a new abstract interface for using KDirSelectDialog in a non-modal way */
420
421 QUrl newUrl;
422 if (fileDialogMode & KFile::LocalOnly) {
423 newUrl = QFileDialog::getExistingDirectoryUrl(parent: m_parent, caption: QString(), dir: openUrl, options: QFileDialog::ShowDirsOnly, supportedSchemes: QStringList() << QStringLiteral("file"));
424 } else {
425 newUrl = getDirFromFileDialog(openUrl);
426 }
427
428 if (newUrl.isValid()) {
429 m_parent->setUrl(newUrl);
430 Q_EMIT m_parent->urlSelected(url());
431 }
432 } else {
433 Q_EMIT m_parent->openFileDialog(m_parent);
434
435 if (((fileDialogMode & KFile::Directory) && (fileDialogMode & KFile::File)) || m_fileDialogModeWasDirAndFile) {
436 QMenu *dirOrFileMenu = new QMenu();
437 QAction *fileAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("File"));
438 QAction *dirAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Directory"));
439 dirOrFileMenu->addAction(action: fileAction);
440 dirOrFileMenu->addAction(action: dirAction);
441
442 connect(sender: fileAction, signal: &QAction::triggered, slot: [this]() {
443 fileDialogMode = KFile::File;
444 applyFileMode(dlg: m_parent->fileDialog(), m: fileDialogMode, acceptMode: fileDialogAcceptMode);
445 m_fileDialogModeWasDirAndFile = true;
446 createFileDialog();
447 });
448
449 connect(sender: dirAction, signal: &QAction::triggered, slot: [this]() {
450 fileDialogMode = KFile::Directory;
451 applyFileMode(dlg: m_parent->fileDialog(), m: fileDialogMode, acceptMode: fileDialogAcceptMode);
452 m_fileDialogModeWasDirAndFile = true;
453 createFileDialog();
454 });
455
456 dirOrFileMenu->exec(pos: m_parent->mapToGlobal(QPoint(m_parent->width(), m_parent->height())));
457
458 return;
459 }
460
461 createFileDialog();
462 }
463}
464
465void KUrlRequester::KUrlRequesterPrivate::slotFileDialogAccepted()
466{
467 if (!myFileDialog) {
468 return;
469 }
470
471 const QUrl newUrl = myFileDialog->selectedUrls().constFirst();
472 if (newUrl.isValid()) {
473 m_parent->setUrl(newUrl);
474 Q_EMIT m_parent->urlSelected(url());
475 // remember url as defaultStartDir and update startdir for autocompletion
476 if (newUrl.isLocalFile() && !m_startDirCustomized) {
477 m_startDir = newUrl.adjusted(options: QUrl::RemoveFilename);
478 updateCompletionStartDir(newStartDir: m_startDir);
479 }
480 }
481}
482
483void KUrlRequester::setMode(KFile::Modes mode)
484{
485 Q_ASSERT((mode & KFile::Files) == 0);
486 d->fileDialogMode = mode;
487 if ((mode & KFile::Directory) && !(mode & KFile::File)) {
488 d->myCompletion->setMode(KUrlCompletion::DirCompletion);
489 }
490
491 if (d->myFileDialog) {
492 d->applyFileMode(dlg: d->myFileDialog, m: mode, acceptMode: d->fileDialogAcceptMode);
493 }
494}
495
496KFile::Modes KUrlRequester::mode() const
497{
498 return d->fileDialogMode;
499}
500
501void KUrlRequester::setAcceptMode(QFileDialog::AcceptMode mode)
502{
503 d->fileDialogAcceptMode = mode;
504
505 if (d->myFileDialog) {
506 d->applyFileMode(dlg: d->myFileDialog, m: d->fileDialogMode, acceptMode: mode);
507 }
508}
509
510QFileDialog::AcceptMode KUrlRequester::acceptMode() const
511{
512 return d->fileDialogAcceptMode;
513}
514
515QStringList KUrlRequester::nameFilters() const
516{
517 return d->nameFilters;
518}
519
520void KUrlRequester::setNameFilters(const QStringList &filters)
521{
522 d->nameFilters = filters;
523
524 if (d->myFileDialog) {
525 d->myFileDialog->setNameFilters(d->nameFilters);
526 }
527}
528
529void KUrlRequester::setNameFilter(const QString &filter)
530{
531 if (filter.isEmpty()) {
532 setNameFilters(QStringList());
533 return;
534 }
535
536 // by default use ";;" as separator
537 // if not present, support alternatively "\n" (matching QFileDialog behaviour)
538 // if also not present split() will simply return the string passed in
539 QString separator = QStringLiteral(";;");
540 if (!filter.contains(s: separator)) {
541 separator = QStringLiteral("\n");
542 }
543 setNameFilters(filter.split(sep: separator));
544}
545
546void KUrlRequester::setMimeTypeFilters(const QStringList &mimeTypes)
547{
548 d->mimeTypeFilters = mimeTypes;
549
550 if (d->myFileDialog) {
551 d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters);
552 }
553 d->myCompletion->setMimeTypeFilters(d->mimeTypeFilters);
554}
555
556QStringList KUrlRequester::mimeTypeFilters() const
557{
558 return d->mimeTypeFilters;
559}
560
561QFileDialog *KUrlRequester::fileDialog() const
562{
563 if (d->myFileDialog
564 && ((d->myFileDialog->fileMode() == QFileDialog::Directory && !(d->fileDialogMode & KFile::Directory))
565 || (d->myFileDialog->fileMode() != QFileDialog::Directory && (d->fileDialogMode & KFile::Directory)))) {
566 delete d->myFileDialog;
567 d->myFileDialog = nullptr;
568 }
569
570 if (!d->myFileDialog) {
571 d->myFileDialog = new QFileDialog(window(), windowTitle());
572 if (!d->mimeTypeFilters.isEmpty()) {
573 d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters);
574 } else {
575 d->myFileDialog->setNameFilters(d->nameFilters);
576 }
577
578 d->applyFileMode(dlg: d->myFileDialog, m: d->fileDialogMode, acceptMode: d->fileDialogAcceptMode);
579
580 d->myFileDialog->setWindowModality(d->fileDialogModality);
581 connect(sender: d->myFileDialog, signal: &QFileDialog::accepted, context: this, slot: [this]() {
582 d->slotFileDialogAccepted();
583 });
584 }
585
586 return d->myFileDialog;
587}
588
589void KUrlRequester::clear()
590{
591 d->setText(QString());
592}
593
594KLineEdit *KUrlRequester::lineEdit() const
595{
596 return d->edit;
597}
598
599KComboBox *KUrlRequester::comboBox() const
600{
601 return d->combo;
602}
603
604void KUrlRequester::KUrlRequesterPrivate::slotUpdateUrl()
605{
606 const QUrl visibleUrl = url();
607 QUrl u = visibleUrl;
608 if (visibleUrl.isRelative()) {
609 u = QUrl::fromLocalFile(localfile: QDir::currentPath() + QLatin1Char('/')).resolved(relative: visibleUrl);
610 }
611 myButton->setURL(u);
612}
613
614bool KUrlRequester::eventFilter(QObject *obj, QEvent *ev)
615{
616 if ((d->edit == obj) || (d->combo == obj)) {
617 if ((ev->type() == QEvent::FocusIn) || (ev->type() == QEvent::FocusOut))
618 // Forward focusin/focusout events to the urlrequester; needed by file form element in khtml
619 {
620 QApplication::sendEvent(receiver: this, event: ev);
621 }
622 }
623 return QWidget::eventFilter(watched: obj, event: ev);
624}
625
626QPushButton *KUrlRequester::button() const
627{
628 return d->myButton;
629}
630
631KUrlCompletion *KUrlRequester::completionObject() const
632{
633 return d->myCompletion;
634}
635
636void KUrlRequester::setPlaceholderText(const QString &msg)
637{
638 if (d->edit) {
639 d->edit->setPlaceholderText(msg);
640 }
641}
642
643QString KUrlRequester::placeholderText() const
644{
645 if (d->edit) {
646 return d->edit->placeholderText();
647 } else {
648 return QString();
649 }
650}
651
652Qt::WindowModality KUrlRequester::fileDialogModality() const
653{
654 return d->fileDialogModality;
655}
656
657void KUrlRequester::setFileDialogModality(Qt::WindowModality modality)
658{
659 d->fileDialogModality = modality;
660}
661
662const KEditListWidget::CustomEditor &KUrlRequester::customEditor()
663{
664 setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed));
665
666 KLineEdit *edit = d->edit;
667 if (!edit && d->combo) {
668 edit = qobject_cast<KLineEdit *>(object: d->combo->lineEdit());
669 }
670
671#ifndef NDEBUG
672 if (!edit) {
673 qCWarning(KIO_WIDGETS) << "KUrlRequester's lineedit is not a KLineEdit!??\n";
674 }
675#endif
676
677 d->editor.setRepresentationWidget(this);
678 d->editor.setLineEdit(edit);
679 return d->editor;
680}
681
682KUrlComboRequester::KUrlComboRequester(QWidget *parent)
683 : KUrlRequester(new KComboBox(false), parent)
684 , d(nullptr)
685{
686}
687
688#include "kurlrequester.moc"
689#include "moc_kurlrequester.cpp"
690

source code of kio/src/widgets/kurlrequester.cpp