diff --git a/autotests/kfilewidgettest.cpp b/autotests/kfilewidgettest.cpp index c0e4581f..e91f28b8 100644 --- a/autotests/kfilewidgettest.cpp +++ b/autotests/kfilewidgettest.cpp @@ -1,538 +1,537 @@ /* This file is part of the KIO framework tests Copyright (c) 2016 Albert Astals Cid This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfilewidget.h" #include #include #include #include #include #include #include #include #include #include -#include #include "kiotesthelper.h" // createTestFile #include #include #include /** * Unit test for KFileWidget */ class KFileWidgetTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); QStandardPaths::setTestModeEnabled(true); QVERIFY(QDir::homePath() != QDir::tempPath()); } void cleanupTestCase() { } QWidget *findLocationLabel(QWidget *parent) { const QList labels = parent->findChildren(); for (QLabel *label : labels) { if (label->text() == i18n("&Name:")) return label->buddy(); } Q_ASSERT(false); return nullptr; } void testFilterCombo() { KFileWidget fw(QUrl(QStringLiteral("kfiledialog:///SaveDialog")), nullptr); fw.setOperationMode(KFileWidget::Saving); fw.setMode(KFile::File); fw.setFilter(QStringLiteral( "*.xml *.a|Word 2003 XML (.xml)\n" "*.odt|ODF Text Document (.odt)\n" "*.xml *.b|DocBook (.xml)\n" "*|Raw (*)")); // default filter is selected QCOMPARE(fw.currentFilter(), QStringLiteral("*.xml *.a")); // setUrl runs with blocked signals, so use setUrls. // auto-select ODT filter via filename fw.locationEdit()->setUrls(QStringList(QStringLiteral("test.odt"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*.odt")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.odt")); // select 2nd duplicate XML filter (see bug 407642) fw.filterWidget()->setCurrentFilter("*.xml *.b|DocBook (.xml)"); QCOMPARE(fw.currentFilter(), QStringLiteral("*.xml *.b")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.xml")); // keep filter after file change with same extension fw.locationEdit()->setUrls(QStringList(QStringLiteral("test2.xml"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*.xml *.b")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test2.xml")); // back to the non-xml / ODT filter fw.locationEdit()->setUrls(QStringList(QStringLiteral("test.odt"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*.odt")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.odt")); // auto-select 1st XML filter fw.locationEdit()->setUrls(QStringList(QStringLiteral("test.xml"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*.xml *.a")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.xml")); // select Raw '*' filter fw.filterWidget()->setCurrentFilter("*|Raw (*)"); QCOMPARE(fw.currentFilter(), QStringLiteral("*")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.xml")); // keep Raw '*' filter with matching file extension fw.locationEdit()->setUrls(QStringList(QStringLiteral("test.odt"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.odt")); // keep Raw '*' filter with non-matching file extension fw.locationEdit()->setUrls(QStringList(QStringLiteral("test.core"))); QCOMPARE(fw.currentFilter(), QStringLiteral("*")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.core")); // select 2nd XML filter fw.filterWidget()->setCurrentFilter("*.xml *.b|DocBook (.xml)"); QCOMPARE(fw.currentFilter(), QStringLiteral("*.xml *.b")); QCOMPARE(fw.locationEdit()->urls()[0], QStringLiteral("test.xml")); } void testFocusOnLocationEdit() { KFileWidget fw(QUrl::fromLocalFile(QDir::homePath())); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); QVERIFY(findLocationLabel(&fw)->hasFocus()); } void testFocusOnLocationEditChangeDir() { KFileWidget fw(QUrl::fromLocalFile(QDir::homePath())); fw.setUrl(QUrl::fromLocalFile(QDir::tempPath())); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); QVERIFY(findLocationLabel(&fw)->hasFocus()); } void testFocusOnLocationEditChangeDir2() { KFileWidget fw(QUrl::fromLocalFile(QDir::homePath())); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); fw.setUrl(QUrl::fromLocalFile(QDir::tempPath())); QVERIFY(findLocationLabel(&fw)->hasFocus()); } void testFocusOnDirOps() { KFileWidget fw(QUrl::fromLocalFile(QDir::homePath())); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); const QList nav = fw.findChildren(); QCOMPARE(nav.count(), 1); nav[0]->setFocus(); fw.setUrl(QUrl::fromLocalFile(QDir::tempPath())); const QList ops = fw.findChildren(); QCOMPARE(ops.count(), 1); QVERIFY(ops[0]->hasFocus()); } void testGetStartUrl() { QString recentDirClass; QString outFileName; QUrl localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass, outFileName); QCOMPARE(recentDirClass, QStringLiteral(":attachmentDir")); QCOMPARE(localUrl.path(), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); QVERIFY(outFileName.isEmpty()); localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachments/foo.txt?global")), recentDirClass, outFileName); QCOMPARE(recentDirClass, QStringLiteral("::attachments")); QCOMPARE(localUrl.path(), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); QCOMPARE(outFileName, QStringLiteral("foo.txt")); } void testSetSelection_data() { QTest::addColumn("baseDir"); QTest::addColumn("selection"); QTest::addColumn("expectedBaseDir"); QTest::addColumn("expectedCurrentText"); const QString baseDir = QDir::homePath(); // A nice filename to detect URL encoding issues const QString fileName = QStringLiteral("some:fi#le"); // Bug 369216, kdialog calls setSelection(path) QTest::newRow("path") << baseDir << baseDir + QLatin1Char('/') + fileName << baseDir << fileName; QTest::newRow("differentPath") << QDir::rootPath() << baseDir + QLatin1Char('/') + fileName << baseDir << fileName; // kdeplatformfiledialoghelper.cpp calls setSelection(URL as string) QTest::newRow("url") << baseDir << QUrl::fromLocalFile(baseDir + QLatin1Char('/') + fileName).toString() << baseDir << fileName; // What if someone calls setSelection(fileName)? That breaks, hence e70f8134a2b in plasma-integration.git QTest::newRow("filename") << baseDir << fileName << baseDir << fileName; } void testSetSelection() { // GIVEN QFETCH(QString, baseDir); QFETCH(QString, selection); QFETCH(QString, expectedBaseDir); QFETCH(QString, expectedCurrentText); const QUrl baseUrl = QUrl::fromLocalFile(baseDir).adjusted(QUrl::StripTrailingSlash); const QUrl expectedBaseUrl = QUrl::fromLocalFile(expectedBaseDir); KFileWidget fw(baseUrl); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); // WHEN fw.setSelection(selection); // now deprecated, this test shows why ;) // THEN QCOMPARE(fw.baseUrl().adjusted(QUrl::StripTrailingSlash), expectedBaseUrl); //if (QByteArray(QTest::currentDataTag()) == "filename") { QEXPECT_FAIL("filename", "setSelection cannot work with filenames, bad API", Continue); //} QCOMPARE(fw.locationEdit()->currentText(), expectedCurrentText); } void testSetSelectedUrl_data() { QTest::addColumn("baseDir"); QTest::addColumn("selectionUrl"); QTest::addColumn("expectedBaseDir"); QTest::addColumn("expectedCurrentText"); const QString baseDir = QDir::homePath(); // A nice filename to detect URL encoding issues const QString fileName = QStringLiteral("some:fi#le"); const QUrl fileUrl = QUrl::fromLocalFile(baseDir + QLatin1Char('/') + fileName); QTest::newRow("path") << baseDir << fileUrl << baseDir << fileName; QTest::newRow("differentPath") << QDir::rootPath() << fileUrl << baseDir << fileName; QTest::newRow("url") << baseDir << QUrl::fromLocalFile(baseDir + QLatin1Char('/') + fileName) << baseDir << fileName; QUrl relativeUrl; relativeUrl.setPath(fileName); QTest::newRow("filename") << baseDir << relativeUrl << baseDir << fileName; } void testSetSelectedUrl() { // GIVEN QFETCH(QString, baseDir); QFETCH(QUrl, selectionUrl); QFETCH(QString, expectedBaseDir); QFETCH(QString, expectedCurrentText); const QUrl baseUrl = QUrl::fromLocalFile(baseDir).adjusted(QUrl::StripTrailingSlash); const QUrl expectedBaseUrl = QUrl::fromLocalFile(expectedBaseDir); KFileWidget fw(baseUrl); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); // WHEN fw.setSelectedUrl(selectionUrl); // THEN QCOMPARE(fw.baseUrl().adjusted(QUrl::StripTrailingSlash), expectedBaseUrl); QCOMPARE(fw.locationEdit()->currentText(), expectedCurrentText); } void testEnterUrl_data() { QTest::addColumn("expectedUrl"); // Check if the root urls are well transformed into themself, otherwise // when going up from file:///home/ it will become file:///home/user QTest::newRow("file") << QUrl::fromLocalFile("/"); QTest::newRow("trash") << QUrl("trash:/"); QTest::newRow("sftp") << QUrl("sftp://127.0.0.1/"); } void testEnterUrl() { // GIVEN QFETCH(QUrl, expectedUrl); // WHEN // These lines are copied from src/filewidgets/kfilewidget.cpp // void KFileWidgetPrivate::_k_enterUrl(const QUrl &url) QUrl u(expectedUrl); if (!u.path().isEmpty() && !u.path().endsWith(QLatin1Char('/'))) { u.setPath(u.path() + QLatin1Char('/')); } // THEN QVERIFY(u.isValid()); QCOMPARE(u, expectedUrl); } void testSetFilterForSave_data() { QTest::addColumn("fileName"); QTest::addColumn("filter"); QTest::addColumn("expectedCurrentText"); QTest::addColumn("expectedSelectedFileName"); const QString filter = QStringLiteral("*.txt|Text files\n*.HTML|HTML files"); QTest::newRow("some.txt") << "some.txt" << filter << QStringLiteral("some.txt") << QStringLiteral("some.txt"); // If an application provides a name without extension, then the // displayed name will not receive an extension. It will however be // appended when the dialog is closed. QTest::newRow("extensionless name") << "some" << filter << QStringLiteral("some") << QStringLiteral("some.txt"); // If the file literally exists, then no new extension will be appended. QTest::newRow("existing file") << "README" << filter << QStringLiteral("README") << QStringLiteral("README"); // XXX perhaps the "extension" should not be modified when it does not // match any of the existing types? Should "some.2019.txt" be expected? QTest::newRow("some.2019") << "some.2019" << filter << QStringLiteral("some.txt") << QStringLiteral("some.txt"); // XXX be smarter and do not change the extension if one of the other // filters match. Should "some.html" be expected? QTest::newRow("some.html") << "some.html" << filter << QStringLiteral("some.txt") << QStringLiteral("some.txt"); } void testSetFilterForSave() { QFETCH(QString, fileName); QFETCH(QString, filter); QFETCH(QString, expectedCurrentText); QFETCH(QString, expectedSelectedFileName); // Use a temporary directory since the presence of existing files // influences whether an extension is automatically appended. QTemporaryDir tempDir; const QUrl dirUrl = QUrl::fromLocalFile(tempDir.path()); const QUrl fileUrl = QUrl::fromLocalFile(tempDir.filePath(fileName)); const QUrl expectedFileUrl = QUrl::fromLocalFile(tempDir.filePath(expectedSelectedFileName)); createTestFile(tempDir.filePath("README")); KFileWidget fw(dirUrl); fw.setOperationMode(KFileWidget::Saving); fw.setSelectedUrl(fileUrl); // Calling setFilter has side-effects and changes the file name. fw.setFilter(filter); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); // Verify the expected populated name. QCOMPARE(fw.baseUrl().adjusted(QUrl::StripTrailingSlash), dirUrl); QCOMPARE(fw.locationEdit()->currentText(), expectedCurrentText); // QFileDialog ends up calling KDEPlatformFileDialog::selectedFiles() // which calls KFileWidget::selectedUrls(). // Accept the filename to ensure that a filename is selected. connect(&fw, &KFileWidget::accepted, &fw, &KFileWidget::accept); QTest::keyClick(fw.locationEdit(), Qt::Key_Return); QList urls = fw.selectedUrls(); QCOMPARE(urls.size(), 1); QCOMPARE(urls[0], expectedFileUrl); } void testFilterChange() { QTemporaryDir tempDir; createTestFile(tempDir.filePath("some.c")); bool created = QDir(tempDir.path()).mkdir("directory"); Q_ASSERT(created); KFileWidget fw(QUrl::fromLocalFile(tempDir.path())); fw.setOperationMode(KFileWidget::Saving); fw.setSelectedUrl(QUrl::fromLocalFile(tempDir.filePath("some.txt"))); fw.setFilter("*.txt|Txt\n*.c|C"); fw.show(); fw.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fw)); // Initial filename. QCOMPARE(fw.locationEdit()->currentText(), QStringLiteral("some.txt")); QCOMPARE(fw.filterWidget()->currentFilter(), QStringLiteral("*.txt")); // Select type with an existing file. fw.filterWidget()->setCurrentFilter("*.c|C"); QCOMPARE(fw.locationEdit()->currentText(), QStringLiteral("some.c")); QCOMPARE(fw.filterWidget()->currentFilter(), QStringLiteral("*.c")); // Do not update extension if the current selection is a directory. fw.setSelectedUrl(QUrl::fromLocalFile(tempDir.filePath("directory"))); fw.filterWidget()->setCurrentFilter("*.txt|Txt"); QCOMPARE(fw.locationEdit()->currentText(), QStringLiteral("directory")); QCOMPARE(fw.filterWidget()->currentFilter(), QStringLiteral("*.txt")); } void testDropFile_data() { QTest::addColumn("dir"); QTest::addColumn("fileName"); QTest::addColumn("expectedCurrentText"); QTest::newRow("some.txt") << "" << "some.txt" << "some.txt"; QTest::newRow("subdir/some.txt") << "subdir" << "subdir/some.txt" << "some.txt"; } void testDropFile() { QFETCH(QString, dir); QFETCH(QString, fileName); QFETCH(QString, expectedCurrentText); // Use a temporary directory since the presence of existing files // influences whether an extension is automatically appended. QTemporaryDir tempDir; QUrl dirUrl = QUrl::fromLocalFile(tempDir.path()); const QUrl fileUrl = QUrl::fromLocalFile(tempDir.filePath(fileName)); if (!dir.isEmpty()) { createTestDirectory(tempDir.filePath(dir)); dirUrl = QUrl::fromLocalFile(tempDir.filePath(dir)); } createTestFile(tempDir.filePath(fileName)); KFileWidget fileWidget(QUrl::fromLocalFile(tempDir.path())); fileWidget.setOperationMode(KFileWidget::Saving); fileWidget.setMode(KFile::File); fileWidget.show(); fileWidget.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&fileWidget)); QMimeData *mimeData = new QMimeData(); mimeData->setUrls(QList() << fileUrl); KDirLister *dirLister = fileWidget.dirOperator()->dirLister(); QSignalSpy spy(dirLister, SIGNAL(completed(const QUrl &_url))); QDragEnterEvent event1(QPoint(), Qt::DropAction::MoveAction, mimeData, Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier); QVERIFY(qApp->sendEvent(fileWidget.dirOperator()->view()->viewport(), &event1)); // Fake drop QDropEvent event(QPoint(), Qt::DropAction::MoveAction, mimeData, Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier); QVERIFY(qApp->sendEvent(fileWidget.dirOperator()->view()->viewport(), &event)); // QVERIFY(QTest::qWaitForWindowActive(&fileWidget)); // once we drop a file the dirlister scans the dir // wait for the completed signal from the dirlister spy.wait(); // Verify the expected populated name. QCOMPARE(fileWidget.baseUrl().adjusted(QUrl::StripTrailingSlash), dirUrl); QCOMPARE(fileWidget.locationEdit()->currentText(), expectedCurrentText); // QFileDialog ends up calling KDEPlatformFileDialog::selectedFiles() // which calls KFileWidget::selectedUrls(). // Accept the filename to ensure that a filename is selected. connect(&fileWidget, &KFileWidget::accepted, &fileWidget, &KFileWidget::accept); QTest::keyClick(fileWidget.locationEdit(), Qt::Key_Return); QList urls = fileWidget.selectedUrls(); QCOMPARE(urls.size(), 1); QCOMPARE(urls[0], fileUrl); } void testCreateNestedNewFolders() { // when creating multiple nested new folders in the "save as" dialog, where folders are //created and entered, kdirlister would hit an assert (in reinsert()), bug 408801 QTemporaryDir tempDir; QVERIFY(tempDir.isValid()); const QString dir = tempDir.path(); const QUrl url = QUrl::fromLocalFile(dir); KFileWidget fw(url); fw.setOperationMode(KFileWidget::Saving); fw.setMode(KFile::File); // create the nested folders for (int i = 1; i < 6; ++i) { fw.dirOperator()->mkdir(QStringLiteral("folder%1").arg(i), true); // simulate the time the user will take to type the new folder name QTest::qWait(1000); } QVERIFY(QFile::exists(dir + QStringLiteral("/folder1/folder2/folder3/folder4/folder5"))); } }; QTEST_MAIN(KFileWidgetTest) #include "kfilewidgettest.moc" diff --git a/src/core/ksambasharedata.h b/src/core/ksambasharedata.h index 0fa13941..48b6f237 100644 --- a/src/core/ksambasharedata.h +++ b/src/core/ksambasharedata.h @@ -1,191 +1,191 @@ /* * Copyright 2010 Rodrigo Belem * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see */ #ifndef ksambasharedata_h #define ksambasharedata_h #include #include "kiocore_export.h" class QString; class KSambaShare; class KSambaSharePrivate; class KSambaShareDataPrivate; /** * @class KSambaShareData ksambasharedata.h * * This class represents a Samba user share. It is possible to share a directory with one or more * different names, update the share details or remove. * * @author Rodrigo Belem * @since 4.7 */ class KIOCORE_EXPORT KSambaShareData { public: enum GuestPermission { GuestsNotAllowed, GuestsAllowed }; enum UserShareError { UserShareOk, UserShareExceedMaxShares, UserShareNameOk, UserShareNameInvalid, UserShareNameInUse, UserSharePathOk, UserSharePathInvalid, UserSharePathNotExists, UserSharePathNotDirectory, UserSharePathNotAbsolute, UserSharePathNotAllowed, UserShareAclOk, UserShareAclInvalid, UserShareAclUserNotValid, UserShareCommentOk, UserShareGuestsOk, UserShareGuestsInvalid, UserShareGuestsNotAllowed, UserShareSystemError }; KSambaShareData(); KSambaShareData(const KSambaShareData &other); ~KSambaShareData(); /** * @return @c the share name. */ QString name() const; /** * @return @c the share path. */ QString path() const; /** * @return @c the share comment. */ QString comment() const; /** * Returns a @c containing a string describing the permission added to the users, such as * "[DOMAIN\]username1:X,[DOMAIN\]username2:X,...". X stands for "F" (full control), "R" - * (read-only) and "D" (deny). By dafault the acl is Everyone:R. + * (read-only) and "D" (deny). By default the acl is Everyone:R. * * @return @c the share acl. */ QString acl() const; /** * @return @c whether guest access to the share is allowed or not. */ KSambaShareData::GuestPermission guestPermission() const; /** * Sets the share name. If the share name is changed and valid it will remove the existing * share and will create a new share. * The share name cannot use a name of a system user or containing the forbidden characters * '%, <, >, *, ?, |, /, \, +, =, ;, :, ",,. To check if the name is available or valid use * the method KSambaShare::isShareNameAvailable(). * * @param name the name that will be given to the share. * * @return @c UserShareNameOk if the name is valid. * @return @c UserShareNameInvalid if the name contains invalid characters. * @return @c UserShareNameInUse if the name is already in use by another shared folder or a * by a system user. */ KSambaShareData::UserShareError setName(const QString &name); /** * Set the path for the share. * * @param path the path that will be given to the share. * * @return @c UserSharePathOk if valid. * @return @c UserSharePathInvalid if the path is in invalid format. * @return @c UserSharePathNotExists if the path does not exists. * @return @c UserSharePathNotDirectory if the path points to file instead of a directory. * @return @c UserSharePathNotAbsolute if the path is not is absolute form. * @return @c UserSharePathNotAllowed if the path is not owner by the user. */ KSambaShareData::UserShareError setPath(const QString &path); /** * Sets the comment for the share. * * @param comment the comment that will be given to the share. * * @return @c UserShareCommentOk always. */ KSambaShareData::UserShareError setComment(const QString &comment); /** * Sets the acl to the share. * * @param acl the acl that will be given to the share. * * @return @c UserShareAclOk if the acl is valid. * @return @c UserShareAclInvalid if the acl has invalid format. * @return @c UserShareAclUserNotValid if one of the users in the acl is invalid. */ KSambaShareData::UserShareError setAcl(const QString &acl); /** * Flags if guest is allowed or not to access the share. * * @param permission the permission that will be given to the share. * * @return @c UserShareGuestsOk if the permission was set. * @return @c UserShareGuestsNotAllowed if the system does not allow guest access to the * shares. */ KSambaShareData::UserShareError setGuestPermission(const GuestPermission &permission = KSambaShareData::GuestsNotAllowed); /** * Share the folder with the information that has been set. * * @return @c UserShareOk if the share was added. */ KSambaShareData::UserShareError save(); /** * Unshare the folder held by the object. * * @return @c UserShareOk if the share was removed. */ KSambaShareData::UserShareError remove(); KSambaShareData &operator=(const KSambaShareData &other); bool operator==(const KSambaShareData &other) const; bool operator!=(const KSambaShareData &other) const; private: QExplicitlySharedDataPointer dd; friend class KSambaSharePrivate; }; #endif diff --git a/src/filewidgets/kfilepreviewgenerator.cpp b/src/filewidgets/kfilepreviewgenerator.cpp index c7ebb868..14ab6fff 100644 --- a/src/filewidgets/kfilepreviewgenerator.cpp +++ b/src/filewidgets/kfilepreviewgenerator.cpp @@ -1,1282 +1,1282 @@ /******************************************************************************* * Copyright (C) 2008-2009 by Peter Penz * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public License * * along with this library; see the file COPYING.LIB. If not, write to * * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301, USA. * *******************************************************************************/ #include "kfilepreviewgenerator.h" #include "defaultviewadapter_p.h" #include // from kiowidgets #include // for HAVE_XRENDER #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 && HAVE_XRENDER # include # include # include #endif /** * If the passed item view is an instance of QListView, expensive * layout operations are blocked in the constructor and are unblocked * again in the destructor. * * This helper class is a workaround for the following huge performance * problem when having directories with several 1000 items: * - each change of an icon emits a dataChanged() signal from the model * - QListView iterates through all items on each dataChanged() signal * and invokes QItemDelegate::sizeHint() * - the sizeHint() implementation of KFileItemDelegate is quite complex, * invoking it 1000 times for each icon change might block the UI * * QListView does not invoke QItemDelegate::sizeHint() when the * uniformItemSize property has been set to true, so this property is * set before exchanging a block of icons. */ class KFilePreviewGenerator::LayoutBlocker { public: LayoutBlocker(QAbstractItemView *view) : m_uniformSizes(false), m_view(qobject_cast(view)) { if (m_view != nullptr) { m_uniformSizes = m_view->uniformItemSizes(); m_view->setUniformItemSizes(true); } } ~LayoutBlocker() { if (m_view != nullptr) { m_view->setUniformItemSizes(m_uniformSizes); /* The QListView did the layout with uniform item * sizes, so trigger a relayout with the expected sizes. */ if (!m_uniformSizes) { m_view->setGridSize(m_view->gridSize()); } } } private: bool m_uniformSizes; QListView *m_view; }; /** Helper class for drawing frames for image previews. */ class KFilePreviewGenerator::TileSet { public: enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 }; enum Tile { TopLeftCorner = 0, TopSide, TopRightCorner, LeftSide, RightSide, BottomLeftCorner, BottomSide, BottomRightCorner, NumTiles }; TileSet() { QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied); QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(image.rect(), Qt::transparent); p.fillRect(image.rect().adjusted(3, 3, -3, -3), Qt::black); p.end(); KIO::ImageFilter::shadowBlur(image, 3, Qt::black); QPixmap pixmap = QPixmap::fromImage(image); m_tiles[TopLeftCorner] = pixmap.copy(0, 0, 8, 8); m_tiles[TopSide] = pixmap.copy(8, 0, 8, 8); m_tiles[TopRightCorner] = pixmap.copy(16, 0, 8, 8); m_tiles[LeftSide] = pixmap.copy(0, 8, 8, 8); m_tiles[RightSide] = pixmap.copy(16, 8, 8, 8); m_tiles[BottomLeftCorner] = pixmap.copy(0, 16, 8, 8); m_tiles[BottomSide] = pixmap.copy(8, 16, 8, 8); m_tiles[BottomRightCorner] = pixmap.copy(16, 16, 8, 8); } void paint(QPainter *p, const QRect &r) { p->drawPixmap(r.topLeft(), m_tiles[TopLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.y(), r.width() - 16, 8, m_tiles[TopSide]); } p->drawPixmap(r.right() - 8 + 1, r.y(), m_tiles[TopRightCorner]); if (r.height() - 16 > 0) { p->drawTiledPixmap(r.x(), r.y() + 8, 8, r.height() - 16, m_tiles[LeftSide]); p->drawTiledPixmap(r.right() - 8 + 1, r.y() + 8, 8, r.height() - 16, m_tiles[RightSide]); } p->drawPixmap(r.x(), r.bottom() - 8 + 1, m_tiles[BottomLeftCorner]); if (r.width() - 16 > 0) { p->drawTiledPixmap(r.x() + 8, r.bottom() - 8 + 1, r.width() - 16, 8, m_tiles[BottomSide]); } p->drawPixmap(r.right() - 8 + 1, r.bottom() - 8 + 1, m_tiles[BottomRightCorner]); const QRect contentRect = r.adjusted(LeftMargin + 1, TopMargin + 1, -(RightMargin + 1), -(BottomMargin + 1)); p->fillRect(contentRect, Qt::transparent); } private: QPixmap m_tiles[NumTiles]; }; class Q_DECL_HIDDEN KFilePreviewGenerator::Private { public: Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model); ~Private(); /** * Requests a new icon for the item \a index. * @param sequenceIndex If this is zero, the standard icon is requested, else another one. */ void requestSequenceIcon(const QModelIndex &index, int sequenceIndex); /** * Generates previews for the items \a items asynchronously. */ void updateIcons(const KFileItemList &items); /** * Generates previews for the indices within \a topLeft * and \a bottomRight asynchronously. */ void updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight); /** * Adds the preview \a pixmap for the item \a item to the preview * queue and starts a timer which will dispatch the preview queue * later. */ void addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap); /** * Is invoked when the preview job has been finished and * removes the job from the m_previewJobs list. */ void slotPreviewJobFinished(KJob *job); /** Synchronizes the icon of all items with the clipboard of cut items. */ void updateCutItems(); /** * Reset all icons of the items from m_cutItemsCache and clear * the cache. */ void clearCutItemsCache(); /** * Dispatches the preview queue block by block within * time slices. */ void dispatchIconUpdateQueue(); /** * Pauses all icon updates and invokes KFilePreviewGenerator::resumeIconUpdates() * after a short delay. Is invoked as soon as the user has moved * a scrollbar. */ void pauseIconUpdates(); /** * Resumes the icons updates that have been paused after moving the * scrollbar. The previews for the current visible area are * generated first. */ void resumeIconUpdates(); /** * Starts the resolving of the MIME types from * the m_pendingItems queue. */ void startMimeTypeResolving(); /** * Resolves the MIME type for exactly one item of the * m_pendingItems queue. */ void resolveMimeType(); /** * Returns true, if the item \a item has been cut into * the clipboard. */ bool isCutItem(const KFileItem &item) const; /** * Applies a cut-item effect to all given \a items, if they * are marked as cut in the clipboard. */ void applyCutItemEffect(const KFileItemList &items); /** * Applies a frame around the icon. False is returned if * no frame has been added because the icon is too small. */ bool applyImageFrame(QPixmap &icon); /** * Resizes the icon to \a maxSize if the icon size does not * fit into the maximum size. The aspect ratio of the icon * is kept. */ void limitToSize(QPixmap &icon, const QSize &maxSize); /** * Creates previews by starting new preview jobs for the items * and triggers the preview timer. */ void createPreviews(const KFileItemList &items); /** * Helper method for createPreviews(): Starts a preview job for the given * items. For each returned preview addToPreviewQueue() will get invoked. */ void startPreviewJob(const KFileItemList &items, int width, int height); /** Kills all ongoing preview jobs. */ void killPreviewJobs(); /** * Orders the items \a items in a way that the visible items * are moved to the front of the list. When passing this * list to a preview job, the visible items will get generated * first. */ void orderItems(KFileItemList &items); /** * Helper method for KFilePreviewGenerator::updateIcons(). Adds * recursively all items from the model to the list \a list. */ void addItemsToList(const QModelIndex &index, KFileItemList &list); /** * Updates the icons of files that are constantly changed due to a copy * operation. See m_changedItems and m_changedItemsTimer for details. */ void delayedIconUpdate(); /** * Any items that are removed from the model are also removed from m_changedItems. */ void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); /** Remembers the pixmap for an item specified by an URL. */ struct ItemInfo { QUrl url; QPixmap pixmap; }; /** * During the lifetime of a DataChangeObtainer instance changing * the data of the model won't trigger generating a preview. */ class DataChangeObtainer { public: DataChangeObtainer(KFilePreviewGenerator::Private *generator) : m_gen(generator) { ++m_gen->m_internalDataChange; } ~DataChangeObtainer() { --m_gen->m_internalDataChange; } private: KFilePreviewGenerator::Private *m_gen; }; bool m_previewShown; /** * True, if m_pendingItems and m_dispatchedItems should be * cleared when the preview jobs have been finished. */ bool m_clearItemQueues; /** * True if a selection has been done which should cut items. */ bool m_hasCutSelection; /** * True if the updates of icons has been paused by pauseIconUpdates(). * The value is reset by resumeIconUpdates(). */ bool m_iconUpdatesPaused; /** * If the value is 0, the slot * updateIcons(const QModelIndex&, const QModelIndex&) has * been triggered by an external data change. */ int m_internalDataChange; int m_pendingVisibleIconUpdates; KAbstractViewAdapter *m_viewAdapter; QAbstractItemView *m_itemView; QTimer *m_iconUpdateTimer; QTimer *m_scrollAreaTimer; QList m_previewJobs; QPointer m_dirModel; QAbstractProxyModel *m_proxyModel; /** * Set of all items that already have the 'cut' effect applied, together with the pixmap it was applied to * This is used to make sure that the 'cut' effect is applied max. once for each pixmap * * Referencing the pixmaps here imposes no overhead, as they were also given to KDirModel::setData(), * and thus are held anyway. */ QHash m_cutItemsCache; QList m_previews; QMap m_sequenceIndices; /** * When huge items are copied, it must be prevented that a preview gets generated * for each item size change. m_changedItems keeps track of the changed items and it * is assured that a final preview is only done if an item does not change within * at least 5 seconds. */ QHash m_changedItems; QTimer *m_changedItemsTimer; /** * Contains all items where a preview must be generated, but * where the preview job has not dispatched the items yet. */ KFileItemList m_pendingItems; /** * Contains all items, where a preview has already been * generated by the preview jobs. */ KFileItemList m_dispatchedItems; KFileItemList m_resolvedMimeTypes; QStringList m_enabledPlugins; TileSet *m_tileSet; private: KFilePreviewGenerator *const q; }; KFilePreviewGenerator::Private::Private(KFilePreviewGenerator *parent, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model) : m_previewShown(true), m_clearItemQueues(true), m_hasCutSelection(false), m_iconUpdatesPaused(false), m_internalDataChange(0), m_pendingVisibleIconUpdates(0), m_viewAdapter(viewAdapter), m_itemView(nullptr), m_iconUpdateTimer(nullptr), m_scrollAreaTimer(nullptr), m_previewJobs(), m_proxyModel(nullptr), m_cutItemsCache(), m_previews(), m_sequenceIndices(), m_changedItems(), m_changedItemsTimer(nullptr), m_pendingItems(), m_dispatchedItems(), m_resolvedMimeTypes(), m_enabledPlugins(), m_tileSet(nullptr), q(parent) { if (!m_viewAdapter->iconSize().isValid()) { m_previewShown = false; } m_proxyModel = qobject_cast(model); m_dirModel = (m_proxyModel == nullptr) ? qobject_cast(model) : qobject_cast(m_proxyModel->sourceModel()); if (!m_dirModel) { // previews can only get generated for directory models m_previewShown = false; } else { KDirModel *dirModel = m_dirModel.data(); connect(dirModel->dirLister(), SIGNAL(newItems(KFileItemList)), q, SLOT(updateIcons(KFileItemList))); connect(dirModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), q, SLOT(updateIcons(QModelIndex,QModelIndex))); connect(dirModel, SIGNAL(needSequenceIcon(QModelIndex,int)), q, SLOT(requestSequenceIcon(QModelIndex,int))); connect(dirModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), q, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int))); } QClipboard *clipboard = QApplication::clipboard(); connect(clipboard, SIGNAL(dataChanged()), q, SLOT(updateCutItems())); m_iconUpdateTimer = new QTimer(q); m_iconUpdateTimer->setSingleShot(true); m_iconUpdateTimer->setInterval(200); connect(m_iconUpdateTimer, SIGNAL(timeout()), q, SLOT(dispatchIconUpdateQueue())); // Whenever the scrollbar values have been changed, the pending previews should // be reordered in a way that the previews for the visible items are generated // first. The reordering is done with a small delay, so that during moving the // scrollbars the CPU load is kept low. m_scrollAreaTimer = new QTimer(q); m_scrollAreaTimer->setSingleShot(true); m_scrollAreaTimer->setInterval(200); connect(m_scrollAreaTimer, SIGNAL(timeout()), q, SLOT(resumeIconUpdates())); m_viewAdapter->connect(KAbstractViewAdapter::IconSizeChanged, q, SLOT(updateIcons())); m_viewAdapter->connect(KAbstractViewAdapter::ScrollBarValueChanged, q, SLOT(pauseIconUpdates())); m_changedItemsTimer = new QTimer(q); m_changedItemsTimer->setSingleShot(true); m_changedItemsTimer->setInterval(5000); connect(m_changedItemsTimer, SIGNAL(timeout()), q, SLOT(delayedIconUpdate())); KConfigGroup globalConfig(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), "PreviewSettings"); m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList{ QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")}); // Compatibility update: in 4.7, jpegrotatedthumbnail was merged into (or // replaced with?) jpegthumbnail if (m_enabledPlugins.contains(QLatin1String("jpegrotatedthumbnail"))) { m_enabledPlugins.removeAll(QStringLiteral("jpegrotatedthumbnail")); m_enabledPlugins.append(QStringLiteral("jpegthumbnail")); globalConfig.writeEntry("Plugins", m_enabledPlugins); globalConfig.sync(); } } KFilePreviewGenerator::Private::~Private() { killPreviewJobs(); m_pendingItems.clear(); m_dispatchedItems.clear(); delete m_tileSet; } void KFilePreviewGenerator::Private::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) { if (m_pendingItems.isEmpty() || (sequenceIndex == 0)) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItem item = dirModel->itemForIndex(index); if (sequenceIndex == 0) { m_sequenceIndices.remove(item.url()); } else { m_sequenceIndices.insert(item.url(), sequenceIndex); } ///@todo Update directly, without using m_sequenceIndices updateIcons(KFileItemList() << item); } } void KFilePreviewGenerator::Private::updateIcons(const KFileItemList &items) { if (items.isEmpty()) { return; } applyCutItemEffect(items); KFileItemList orderedItems = items; orderItems(orderedItems); m_pendingItems.reserve(m_pendingItems.size() + orderedItems.size()); for (const KFileItem &item : qAsConst(orderedItems)) { m_pendingItems.append(item); } if (m_previewShown) { createPreviews(orderedItems); } else { startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (m_internalDataChange > 0) { // QAbstractItemModel::setData() has been invoked internally by the KFilePreviewGenerator. // The signal dataChanged() is connected with this method, but previews only need // to be generated when an external data change has occurred. return; } // dataChanged emitted for the root dir (e.g. permission changes) if (!topLeft.isValid() || !bottomRight.isValid()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } KFileItemList itemList; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { const QModelIndex index = dirModel->index(row, 0); if (!index.isValid()) { continue; } const KFileItem item = dirModel->itemForIndex(index); Q_ASSERT(!item.isNull()); if (m_previewShown) { const QUrl url = item.url(); const bool hasChanged = m_changedItems.contains(url); // O(1) m_changedItems.insert(url, hasChanged); if (!hasChanged) { // only update the icon if it has not been already updated within // the last 5 seconds (the other icons will be updated later with // the help of m_changedItemsTimer) itemList.append(item); } } else { itemList.append(item); } } updateIcons(itemList); m_changedItemsTimer->start(); } void KFilePreviewGenerator::Private::addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap) { KIO::PreviewJob *senderJob = qobject_cast(q->sender()); Q_ASSERT(senderJob != nullptr); if (senderJob != nullptr) { QMap::iterator it = m_sequenceIndices.find(item.url()); if (senderJob->sequenceIndex() && (it == m_sequenceIndices.end() || *it != senderJob->sequenceIndex())) { return; // the sequence index does not match the one we want } if (!senderJob->sequenceIndex() && it != m_sequenceIndices.end()) { return; // the sequence index does not match the one we want } m_sequenceIndices.erase(it); } if (!m_previewShown) { // the preview has been canceled in the meantime return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // check whether the item is part of the directory lister (it is possible // that a preview from an old directory lister is received) bool isOldPreview = true; const QUrl itemParentDir = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); const QList dirs = dirModel->dirLister()->directories(); for (const QUrl &dir : dirs) { if (dir == itemParentDir || dir.path().isEmpty()) { isOldPreview = false; break; } } if (isOldPreview) { return; } QPixmap icon = pixmap; const QString mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if ((mimeTypeGroup != QLatin1String("image")) || !applyImageFrame(icon)) { limitToSize(icon, m_viewAdapter->iconSize()); } if (m_hasCutSelection && isCutItem(item)) { // apply the disabled effect to the icon for marking it as "cut item" // and apply the icon to the item KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); icon = iconEffect->apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); } KIconLoader::global()->drawOverlays(item.overlays(), icon, KIconLoader::Desktop); // remember the preview and URL, so that it can be applied to the model // in KFilePreviewGenerator::dispatchIconUpdateQueue() ItemInfo preview; preview.url = item.url(); preview.pixmap = icon; m_previews.append(preview); m_pendingItems.removeOne(item); m_dispatchedItems.append(item); } void KFilePreviewGenerator::Private::slotPreviewJobFinished(KJob *job) { const int index = m_previewJobs.indexOf(job); m_previewJobs.removeAt(index); if (m_previewJobs.isEmpty()) { for (const KFileItem &item : qAsConst(m_pendingItems)) { if (item.isMimeTypeKnown()) { m_resolvedMimeTypes.append(item); } } if (m_clearItemQueues) { m_pendingItems.clear(); m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; QMetaObject::invokeMethod(q, "dispatchIconUpdateQueue", Qt::QueuedConnection); } m_sequenceIndices.clear(); // just to be sure that we don't leak anything } } void KFilePreviewGenerator::Private::updateCutItems() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); clearCutItemsCache(); KFileItemList items; KDirLister *dirLister = dirModel->dirLister(); const QList dirs = dirLister->directories(); items.reserve(dirs.size()); for (const QUrl &url : dirs) { items << dirLister->itemsForDir(url); } applyCutItemEffect(items); } void KFilePreviewGenerator::Private::clearCutItemsCache() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } DataChangeObtainer obt(this); KFileItemList previews; // Reset the icons of all items that are stored in the cache // to use their default MIME type icon. for (auto it = m_cutItemsCache.cbegin(); it != m_cutItemsCache.cend(); ++it) { const QModelIndex index = dirModel->indexForUrl(it.key()); if (index.isValid()) { dirModel->setData(index, QIcon(), Qt::DecorationRole); if (m_previewShown) { previews.append(dirModel->itemForIndex(index)); } } } m_cutItemsCache.clear(); if (!previews.isEmpty()) { // assure that the previews gets restored Q_ASSERT(m_previewShown); orderItems(previews); updateIcons(previews); } } void KFilePreviewGenerator::Private::dispatchIconUpdateQueue() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int count = m_previews.count() + m_resolvedMimeTypes.count(); if (count > 0) { LayoutBlocker blocker(m_itemView); DataChangeObtainer obt(this); if (m_previewShown) { // dispatch preview queue for (const ItemInfo &preview : qAsConst(m_previews)) { const QModelIndex idx = dirModel->indexForUrl(preview.url); if (idx.isValid() && (idx.column() == 0)) { dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); } } m_previews.clear(); } // dispatch mime type queue for (const KFileItem &item : qAsConst(m_resolvedMimeTypes)) { const QModelIndex idx = dirModel->indexForItem(item); dirModel->itemChanged(idx); } m_resolvedMimeTypes.clear(); m_pendingVisibleIconUpdates -= count; if (m_pendingVisibleIconUpdates < 0) { m_pendingVisibleIconUpdates = 0; } } if (m_pendingVisibleIconUpdates > 0) { // As long as there are pending previews for visible items, poll // the preview queue periodically. If there are no pending previews, // the queue is dispatched in slotPreviewJobFinished(). m_iconUpdateTimer->start(); } } void KFilePreviewGenerator::Private::pauseIconUpdates() { m_iconUpdatesPaused = true; for (KJob *job : qAsConst(m_previewJobs)) { Q_ASSERT(job != nullptr); job->suspend(); } m_scrollAreaTimer->start(); } void KFilePreviewGenerator::Private::resumeIconUpdates() { m_iconUpdatesPaused = false; // Before creating new preview jobs the m_pendingItems queue must be // cleaned up by removing the already dispatched items. Implementation // note: The order of the m_dispatchedItems queue and the m_pendingItems // queue is usually equal. So even when having a lot of elements the // nested loop is no performance bottle neck, as the inner loop is only // entered once in most cases. for (const KFileItem &item : qAsConst(m_dispatchedItems)) { KFileItemList::iterator begin = m_pendingItems.begin(); KFileItemList::iterator end = m_pendingItems.end(); for (KFileItemList::iterator it = begin; it != end; ++it) { if ((*it).url() == item.url()) { m_pendingItems.erase(it); break; } } } m_dispatchedItems.clear(); m_pendingVisibleIconUpdates = 0; dispatchIconUpdateQueue(); if (m_previewShown) { KFileItemList orderedItems = m_pendingItems; orderItems(orderedItems); // Kill all suspended preview jobs. Usually when a preview job // has been finished, slotPreviewJobFinished() clears all item queues. // This is not wanted in this case, as a new job is created afterwards // for m_pendingItems. m_clearItemQueues = false; killPreviewJobs(); m_clearItemQueues = true; createPreviews(orderedItems); } else { orderItems(m_pendingItems); startMimeTypeResolving(); } } void KFilePreviewGenerator::Private::startMimeTypeResolving() { resolveMimeType(); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::resolveMimeType() { if (m_pendingItems.isEmpty()) { return; } // resolve at least one MIME type bool resolved = false; do { KFileItem item = m_pendingItems.takeFirst(); if (item.isMimeTypeKnown()) { if (m_pendingVisibleIconUpdates > 0) { // The item is visible and the MIME type already known. // Decrease the update counter for dispatchIconUpdateQueue(): --m_pendingVisibleIconUpdates; } } else { // The MIME type is unknown and must get resolved. The // directory model is not informed yet, as a single update // would be very expensive. Instead the item is remembered in // m_resolvedMimeTypes and will be dispatched later // by dispatchIconUpdateQueue(). item.determineMimeType(); m_resolvedMimeTypes.append(item); resolved = true; } } while (!resolved && !m_pendingItems.isEmpty()); if (m_pendingItems.isEmpty()) { // All MIME types have been resolved now. Assure // that the directory model gets informed about // this, so that an update of the icons is done. dispatchIconUpdateQueue(); } else if (!m_iconUpdatesPaused) { // assure that the MIME type of the next // item will be resolved asynchronously QMetaObject::invokeMethod(q, "resolveMimeType", Qt::QueuedConnection); } } bool KFilePreviewGenerator::Private::isCutItem(const KFileItem &item) const { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); const QList cutUrls = KUrlMimeData::urlsFromMimeData(mimeData); return cutUrls.contains(item.url()); } void KFilePreviewGenerator::Private::applyCutItemEffect(const KFileItemList &items) { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); if (!m_hasCutSelection) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const QSet cutUrls = KUrlMimeData::urlsFromMimeData(mimeData).toSet(); DataChangeObtainer obt(this); KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); for (const KFileItem &item : items) { if (cutUrls.contains(item.url())) { const QModelIndex index = dirModel->indexForItem(item); const QVariant value = dirModel->data(index, Qt::DecorationRole); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); const QSize actualSize = icon.actualSize(m_viewAdapter->iconSize()); QPixmap pixmap = icon.pixmap(actualSize); const QHash::const_iterator cacheIt = m_cutItemsCache.constFind(item.url()); if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) { pixmap = iconEffect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); m_cutItemsCache.insert(item.url(), pixmap); } } } } } bool KFilePreviewGenerator::Private::applyImageFrame(QPixmap &icon) { const QSize maxSize = m_viewAdapter->iconSize(); const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha(); if (!applyFrame) { // the maximum size or the image itself is too small for a frame return false; } // resize the icon to the maximum size minus the space required for the frame const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin); limitToSize(icon, size); if (m_tileSet == nullptr) { m_tileSet = new TileSet(); } QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); framedIcon.fill(Qt::transparent); QPainter painter; painter.begin(&framedIcon); painter.setCompositionMode(QPainter::CompositionMode_Source); m_tileSet->paint(&painter, framedIcon.rect()); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); painter.end(); icon = framedIcon; return true; } void KFilePreviewGenerator::Private::limitToSize(QPixmap &icon, const QSize &maxSize) { if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { #pragma message("Cannot use XRender with QPixmap anymore. Find equivalent with Qt API.") #if 0 // HAVE_X11 && HAVE_XRENDER // Assume that the texture size limit is 2048x2048 if ((icon.width() <= 2048) && (icon.height() <= 2048) && icon.x11PictureHandle()) { QSize size = icon.size(); size.scale(maxSize, Qt::KeepAspectRatio); const qreal factor = size.width() / qreal(icon.width()); XTransform xform = {{ { XDoubleToFixed(1 / factor), 0, 0 }, { 0, XDoubleToFixed(1 / factor), 0 }, { 0, 0, XDoubleToFixed(1) } } }; QPixmap pixmap(size); pixmap.fill(Qt::transparent); Display *dpy = QX11Info::display(); XRenderPictureAttributes attr; attr.repeat = RepeatPad; XRenderChangePicture(dpy, icon.x11PictureHandle(), CPRepeat, &attr); XRenderSetPictureFilter(dpy, icon.x11PictureHandle(), FilterBilinear, 0, 0); XRenderSetPictureTransform(dpy, icon.x11PictureHandle(), &xform); XRenderComposite(dpy, PictOpOver, icon.x11PictureHandle(), None, pixmap.x11PictureHandle(), 0, 0, 0, 0, 0, 0, pixmap.width(), pixmap.height()); icon = pixmap; } else { icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::FastTransformation); } #else icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); #endif } } void KFilePreviewGenerator::Private::createPreviews(const KFileItemList &items) { if (items.isEmpty()) { return; } const QMimeData *mimeData = QApplication::clipboard()->mimeData(); m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); // PreviewJob internally caches items always with the size of // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must // do a downscaling anyhow because of the frame, so in this case only the provided // cache sizes are requested. KFileItemList imageItems; KFileItemList otherItems; QString mimeType; for (const KFileItem &item : items) { mimeType = item.mimetype(); const int slashIndex = mimeType.indexOf(QLatin1Char('/')); const QStringRef mimeTypeGroup = mimeType.leftRef(slashIndex); if (mimeTypeGroup == QLatin1String("image")) { imageItems.append(item); } else { otherItems.append(item); } } const QSize size = m_viewAdapter->iconSize(); startPreviewJob(otherItems, size.width(), size.height()); const int cacheSize = (size.width() > 128) || (size.height() > 128) ? 256 : 128; startPreviewJob(imageItems, cacheSize, cacheSize); m_iconUpdateTimer->start(); } void KFilePreviewGenerator::Private::startPreviewJob(const KFileItemList &items, int width, int height) { if (!items.isEmpty()) { KIO::PreviewJob *job = KIO::filePreview(items, QSize(width, height), &m_enabledPlugins); // Set the sequence index to the target. We only need to check if items.count() == 1, // because requestSequenceIcon(..) creates exactly such a request. if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) { QMap::iterator it = m_sequenceIndices.find(items[0].url()); if (it != m_sequenceIndices.end()) { job->setSequenceIndex(*it); } } connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), q, SLOT(addToPreviewQueue(KFileItem,QPixmap))); connect(job, SIGNAL(finished(KJob*)), q, SLOT(slotPreviewJobFinished(KJob*))); m_previewJobs.append(job); } } void KFilePreviewGenerator::Private::killPreviewJobs() { for (KJob *job : qAsConst(m_previewJobs)) { Q_ASSERT(job != nullptr); job->kill(); } m_previewJobs.clear(); m_sequenceIndices.clear(); m_iconUpdateTimer->stop(); m_scrollAreaTimer->stop(); m_changedItemsTimer->stop(); } void KFilePreviewGenerator::Private::orderItems(KFileItemList &items) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Order the items in a way that the preview for the visible items - // is generated first, as this improves the feeled performance a lot. + // is generated first, as this improves the felt performance a lot. const bool hasProxy = (m_proxyModel != nullptr); const int itemCount = items.count(); const QRect visibleArea = m_viewAdapter->visibleArea(); QModelIndex dirIndex; QRect itemRect; int insertPos = 0; for (int i = 0; i < itemCount; ++i) { dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows) if (hasProxy) { const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); itemRect = m_viewAdapter->visualRect(proxyIndex); } else { itemRect = m_viewAdapter->visualRect(dirIndex); } if (itemRect.intersects(visibleArea)) { // The current item is (at least partly) visible. Move it // to the front of the list, so that the preview is // generated earlier. items.insert(insertPos, items.at(i)); items.removeAt(i + 1); ++insertPos; ++m_pendingVisibleIconUpdates; } } } void KFilePreviewGenerator::Private::addItemsToList(const QModelIndex &index, KFileItemList &list) { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } const int rowCount = dirModel->rowCount(index); for (int row = 0; row < rowCount; ++row) { const QModelIndex subIndex = dirModel->index(row, 0, index); KFileItem item = dirModel->itemForIndex(subIndex); list.append(item); if (dirModel->rowCount(subIndex) > 0) { // the model is hierarchical (treeview) addItemsToList(subIndex, list); } } } void KFilePreviewGenerator::Private::delayedIconUpdate() { KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } // Precondition: No items have been changed within the last // 5 seconds. This means that items that have been changed constantly // due to a copy operation should be updated now. KFileItemList itemList; QHash::const_iterator it = m_changedItems.constBegin(); while (it != m_changedItems.constEnd()) { const bool hasChanged = it.value(); if (hasChanged) { const QModelIndex index = dirModel->indexForUrl(it.key()); const KFileItem item = dirModel->itemForIndex(index); itemList.append(item); } ++it; } m_changedItems.clear(); updateIcons(itemList); } void KFilePreviewGenerator::Private::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { if (m_changedItems.isEmpty()) { return; } KDirModel *dirModel = m_dirModel.data(); if (!dirModel) { return; } for (int row = start; row <= end; row++) { const QModelIndex index = dirModel->index(row, 0, parent); const KFileItem item = dirModel->itemForIndex(index); if (!item.isNull()) { m_changedItems.remove(item.url()); } if (dirModel->hasChildren(index)) { rowsAboutToBeRemoved(index, 0, dirModel->rowCount(index) - 1); } } } KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent) : QObject(parent), d(new Private(this, new KIO::DefaultViewAdapter(parent, this), parent->model())) { d->m_itemView = parent; } KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model) : QObject(parent), d(new Private(this, parent, model)) { } KFilePreviewGenerator::~KFilePreviewGenerator() { delete d; } void KFilePreviewGenerator::setPreviewShown(bool show) { if (d->m_previewShown == show) { return; } KDirModel *dirModel = d->m_dirModel.data(); if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) { // The view must provide an icon size and a directory model, // otherwise the showing the previews will get ignored return; } d->m_previewShown = show; if (!show) { dirModel->clearAllPreviews(); } updateIcons(); } bool KFilePreviewGenerator::isPreviewShown() const { return d->m_previewShown; } // deprecated (use updateIcons() instead) void KFilePreviewGenerator::updatePreviews() { updateIcons(); } void KFilePreviewGenerator::updateIcons() { d->killPreviewJobs(); d->clearCutItemsCache(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); KFileItemList itemList; d->addItemsToList(QModelIndex(), itemList); d->updateIcons(itemList); } void KFilePreviewGenerator::cancelPreviews() { d->killPreviewJobs(); d->m_pendingItems.clear(); d->m_dispatchedItems.clear(); updateIcons(); } void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins) { d->m_enabledPlugins = plugins; } QStringList KFilePreviewGenerator::enabledPlugins() const { return d->m_enabledPlugins; } #include "moc_kfilepreviewgenerator.cpp" diff --git a/src/filewidgets/kfilewidget.cpp b/src/filewidgets/kfilewidget.cpp index 58edb3fb..8324cee0 100644 --- a/src/filewidgets/kfilewidget.cpp +++ b/src/filewidgets/kfilewidget.cpp @@ -1,2965 +1,2965 @@ // -*- c++ -*- /* This file is part of the KDE libraries Copyright (C) 1997, 1998 Richard Moore 1998 Stephan Kulow 1998 Daniel Grana 1999,2000,2001,2002,2003 Carsten Pfeiffer 2003 Clarence Dang 2007 David Faure 2008 Rafael Fernández López This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfilewidget.h" #include "../pathhelpers_p.h" #include "kfileplacesview.h" #include "kfileplacesmodel.h" #include "kfilebookmarkhandler_p.h" #include "kurlcombobox.h" #include "kurlnavigator.h" #include "kfilepreviewgenerator.h" #include "kfilewidgetdocktitlebar_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KFileWidgetPrivate { public: explicit KFileWidgetPrivate(KFileWidget *widget) : q(widget), boxLayout(nullptr), placesDock(nullptr), placesView(nullptr), placesViewSplitter(nullptr), placesViewWidth(-1), labeledCustomWidget(nullptr), bottomCustomWidget(nullptr), autoSelectExtCheckBox(nullptr), operationMode(KFileWidget::Opening), bookmarkHandler(nullptr), toolbar(nullptr), locationEdit(nullptr), ops(nullptr), filterWidget(nullptr), autoSelectExtChecked(false), keepLocation(false), hasView(false), hasDefaultFilter(false), inAccept(false), dummyAdded(false), confirmOverwrite(false), differentHierarchyLevelItemsEntered(false), iconSizeSlider(nullptr), zoomOutAction(nullptr), zoomInAction(nullptr) { } ~KFileWidgetPrivate() { delete bookmarkHandler; // Should be deleted before ops! delete ops; } void updateLocationWhatsThis(); void updateAutoSelectExtension(); void initSpeedbar(); void setPlacesViewSplitterSizes(); void setLafBoxColumnWidth(); void initGUI(); void readViewConfig(); void writeViewConfig(); void setNonExtSelection(); void setLocationText(const QUrl &); void setLocationText(const QList &); void appendExtension(QUrl &url); void updateLocationEditExtension(const QString &); QString findMatchingFilter(const QString &filter, const QString &filename) const; void updateFilter(); void updateFilterText(); QList &parseSelectedUrls(); /** * Parses the string "line" for files. If line doesn't contain any ", the * whole line will be interpreted as one file. If the number of " is odd, * an empty list will be returned. Otherwise, all items enclosed in " " * will be returned as correct urls. */ QList tokenize(const QString &line) const; /** * Reads the recent used files and inserts them into the location combobox */ void readRecentFiles(); /** * Saves the entries from the location combobox. */ void saveRecentFiles(); /** * called when an item is highlighted/selected in multiselection mode. * handles setting the locationEdit. */ void multiSelectionChanged(); /** * Returns the absolute version of the URL specified in locationEdit. */ QUrl getCompleteUrl(const QString &) const; /** * Sets the dummy entry on the history combo box. If the dummy entry * already exists, it is overwritten with this information. */ void setDummyHistoryEntry(const QString &text, const QPixmap &icon = QPixmap(), bool usePreviousPixmapIfNull = true); /** * Removes the dummy entry of the history combo box. */ void removeDummyHistoryEntry(); /** * Asks for overwrite confirmation using a KMessageBox and returns * true if the user accepts. * * @since 4.2 */ bool toOverwrite(const QUrl &); // private slots void _k_slotLocationChanged(const QString &); void _k_urlEntered(const QUrl &); void _k_enterUrl(const QUrl &); void _k_enterUrl(const QString &); void _k_locationAccepted(const QString &); void _k_slotFilterChanged(); void _k_fileHighlighted(const KFileItem &); void _k_fileSelected(const KFileItem &); void _k_slotLoadingFinished(); void _k_fileCompletion(const QString &); void _k_toggleSpeedbar(bool); void _k_toggleBookmarks(bool); void _k_slotAutoSelectExtClicked(); void _k_placesViewSplitterMoved(int, int); void _k_activateUrlNavigator(); void _k_zoomOutIconsSize(); void _k_zoomInIconsSize(); void _k_slotIconSizeSliderMoved(int); void _k_slotIconSizeChanged(int); void _k_slotViewDoubleClicked(const QModelIndex&); void _k_slotViewKeyEnterReturnPressed(); void addToRecentDocuments(); QString locationEditCurrentText() const; /** * KIO::NetAccess::mostLocalUrl local replacement. * This method won't show any progress dialogs for stating, since * they are very annoying when stating. */ QUrl mostLocalUrl(const QUrl &url); void setInlinePreviewShown(bool show); KFileWidget * const q; // the last selected url QUrl url; // the selected filenames in multiselection mode -- FIXME QString filenames; // now following all kind of widgets, that I need to rebuild // the geometry management QBoxLayout *boxLayout; QGridLayout *lafBox; QVBoxLayout *vbox; QLabel *locationLabel; QWidget *opsWidget; QWidget *pathSpacer; QLabel *filterLabel; KUrlNavigator *urlNavigator; QPushButton *okButton, *cancelButton; QDockWidget *placesDock; KFilePlacesView *placesView; QSplitter *placesViewSplitter; // caches the places view width. This value will be updated when the splitter // is moved. This allows us to properly set a value when the dialog itself // is resized int placesViewWidth; QWidget *labeledCustomWidget; QWidget *bottomCustomWidget; // Automatically Select Extension stuff QCheckBox *autoSelectExtCheckBox; QString extension; // current extension for this filter QList statJobs; QList urlList; //the list of selected urls KFileWidget::OperationMode operationMode; // The file class used for KRecentDirs QString fileClass; KFileBookmarkHandler *bookmarkHandler; KActionMenu *bookmarkButton; KToolBar *toolbar; KUrlComboBox *locationEdit; KDirOperator *ops; KFileFilterCombo *filterWidget; QTimer filterDelayTimer; KFilePlacesModel *model; // whether or not the _user_ has checked the above box bool autoSelectExtChecked : 1; // indicates if the location edit should be kept or cleared when changing // directories bool keepLocation : 1; // the KDirOperators view is set in KFileWidget::show(), so to avoid // setting it again and again, we have this nice little boolean :) bool hasView : 1; bool hasDefaultFilter : 1; // necessary for the operationMode bool autoDirectoryFollowing : 1; bool inAccept : 1; // true between beginning and end of accept() bool dummyAdded : 1; // if the dummy item has been added. This prevents the combo from having a // blank item added when loaded bool confirmOverwrite : 1; bool differentHierarchyLevelItemsEntered; QSlider *iconSizeSlider; QAction *zoomOutAction; QAction *zoomInAction; // The group which stores app-specific settings. These settings are recent // files and urls. Visual settings (view mode, sorting criteria...) are not // app-specific and are stored in kdeglobals KConfigGroup configGroup; }; Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path static const char autocompletionWhatsThisText[] = I18N_NOOP("While typing in the text area, you may be presented " "with possible matches. " "This feature can be controlled by clicking with the right mouse button " "and selecting a preferred mode from the Text Completion menu."); // returns true if the string contains ":/" sequence, where is at least 2 alpha chars static bool containsProtocolSection(const QString &string) { int len = string.length(); static const char prot[] = ":/"; for (int i = 0; i < len;) { i = string.indexOf(QLatin1String(prot), i); if (i == -1) { return false; } int j = i - 1; for (; j >= 0; j--) { const QChar &ch(string[j]); if (ch.toLatin1() == 0 || !ch.isLetter()) { break; } if (ch.isSpace() && (i - j - 1) >= 2) { return true; } } if (j < 0 && i >= 2) { return true; // at least two letters before ":/" } i += 3; // skip : and / and one char } return false; } // this string-to-url conversion function handles relative paths, full paths and URLs // without the http-prepending that QUrl::fromUserInput does. static QUrl urlFromString(const QString& str) { if (QDir::isAbsolutePath(str)) { return QUrl::fromLocalFile(str); } QUrl url(str); if (url.isRelative()) { url.clear(); url.setPath(str); } return url; } KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent) : QWidget(parent), d(new KFileWidgetPrivate(this)) { QUrl startDir(_startDir); // qDebug() << "startDir" << startDir; QString filename; d->okButton = new QPushButton(this); KGuiItem::assign(d->okButton, KStandardGuiItem::ok()); d->okButton->setDefault(true); d->cancelButton = new QPushButton(this); KGuiItem::assign(d->cancelButton, KStandardGuiItem::cancel()); // The dialog shows them d->okButton->hide(); d->cancelButton->hide(); d->opsWidget = new QWidget(this); QVBoxLayout *opsWidgetLayout = new QVBoxLayout(d->opsWidget); opsWidgetLayout->setContentsMargins(0, 0, 0, 0); opsWidgetLayout->setSpacing(0); //d->toolbar = new KToolBar(this, true); d->toolbar = new KToolBar(d->opsWidget, true); d->toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar")); d->toolbar->setMovable(false); opsWidgetLayout->addWidget(d->toolbar); d->model = new KFilePlacesModel(this); // Resolve this now so that a 'kfiledialog:' URL, if specified, // does not get inserted into the urlNavigator history. d->url = getStartUrl(startDir, d->fileClass, filename); startDir = d->url; // Don't pass startDir to the KUrlNavigator at this stage: as well as // the above, it may also contain a file name which should not get // inserted in that form into the old-style navigation bar history. // Wait until the KIO::stat has been done later. // // The stat cannot be done before this point, bug 172678. d->urlNavigator = new KUrlNavigator(d->model, QUrl(), d->opsWidget); //d->toolbar); d->urlNavigator->setPlacesSelectorVisible(false); opsWidgetLayout->addWidget(d->urlNavigator); QUrl u; KUrlComboBox *pathCombo = d->urlNavigator->editor(); #ifdef Q_OS_WIN #if 0 foreach (const QFileInfo &drive, QFSFileEngine::drives()) { u = QUrl::fromLocalFile(drive.filePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), i18n("Drive: %1", u.toLocalFile())); } #else #pragma message("QT5 PORT") #endif #else u = QUrl::fromLocalFile(QDir::rootPath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); #endif u = QUrl::fromLocalFile(QDir::homePath()); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); QUrl docPath = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); if (u.adjusted(QUrl::StripTrailingSlash) != docPath.adjusted(QUrl::StripTrailingSlash) && QDir(docPath.toLocalFile()).exists()) { pathCombo->addDefaultUrl(docPath, KIO::pixmapForUrl(docPath, 0, KIconLoader::Small), docPath.toLocalFile()); } u = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); pathCombo->addDefaultUrl(u, KIO::pixmapForUrl(u, 0, KIconLoader::Small), u.toLocalFile()); d->ops = new KDirOperator(QUrl(), d->opsWidget); d->ops->setObjectName(QStringLiteral("KFileWidget::ops")); d->ops->setIsSaving(d->operationMode == Saving); opsWidgetLayout->addWidget(d->ops); connect(d->ops, SIGNAL(urlEntered(QUrl)), SLOT(_k_urlEntered(QUrl))); connect(d->ops, SIGNAL(fileHighlighted(KFileItem)), SLOT(_k_fileHighlighted(KFileItem))); connect(d->ops, SIGNAL(fileSelected(KFileItem)), SLOT(_k_fileSelected(KFileItem))); connect(d->ops, SIGNAL(finishedLoading()), SLOT(_k_slotLoadingFinished())); connect(d->ops, SIGNAL(keyEnterReturnPressed()), SLOT(_k_slotViewKeyEnterReturnPressed())); d->ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions); KActionCollection *coll = d->ops->actionCollection(); coll->addAssociatedWidget(this); // add nav items to the toolbar // // NOTE: The order of the button icons here differs from that // found in the file manager and web browser, but has been discussed // and agreed upon on the kde-core-devel mailing list: // // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2 coll->action(QStringLiteral("up"))->setWhatsThis(i18n("Click this button to enter the parent folder.

" "For instance, if the current location is file:/home/konqi clicking this " "button will take you to file:/home.
")); coll->action(QStringLiteral("back"))->setWhatsThis(i18n("Click this button to move backwards one step in the browsing history.")); coll->action(QStringLiteral("forward"))->setWhatsThis(i18n("Click this button to move forward one step in the browsing history.")); coll->action(QStringLiteral("reload"))->setWhatsThis(i18n("Click this button to reload the contents of the current location.")); coll->action(QStringLiteral("mkdir"))->setShortcut(QKeySequence(Qt::Key_F10)); coll->action(QStringLiteral("mkdir"))->setWhatsThis(i18n("Click this button to create a new folder.")); QAction *goToNavigatorAction = coll->addAction(QStringLiteral("gotonavigator"), this, SLOT(_k_activateUrlNavigator())); goToNavigatorAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_L)); KToggleAction *showSidebarAction = new KToggleAction(i18n("Show Places Panel"), this); coll->addAction(QStringLiteral("toggleSpeedbar"), showSidebarAction); showSidebarAction->setShortcut(QKeySequence(Qt::Key_F9)); connect(showSidebarAction, SIGNAL(toggled(bool)), SLOT(_k_toggleSpeedbar(bool))); KToggleAction *showBookmarksAction = new KToggleAction(i18n("Show Bookmarks Button"), this); coll->addAction(QStringLiteral("toggleBookmarks"), showBookmarksAction); connect(showBookmarksAction, SIGNAL(toggled(bool)), SLOT(_k_toggleBookmarks(bool))); // Build the settings menu KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), this); coll->addAction(QStringLiteral("extra menu"), menu); menu->setWhatsThis(i18n("This is the preferences menu for the file dialog. " "Various options can be accessed from this menu including:
    " "
  • how files are sorted in the list
  • " "
  • types of view, including icon and list
  • " "
  • showing of hidden files
  • " "
  • the Places panel
  • " "
  • file previews
  • " "
  • separating folders from files
")); menu->addAction(coll->action(QStringLiteral("allow expansion"))); menu->addSeparator(); menu->addAction(coll->action(QStringLiteral("show hidden"))); menu->addAction(showSidebarAction); menu->addAction(showBookmarksAction); menu->addAction(coll->action(QStringLiteral("preview"))); menu->setDelayed(false); connect(menu->menu(), &QMenu::aboutToShow, d->ops, &KDirOperator::updateSelectionDependentActions); d->iconSizeSlider = new QSlider(this); d->iconSizeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); d->iconSizeSlider->setMinimumWidth(40); d->iconSizeSlider->setOrientation(Qt::Horizontal); d->iconSizeSlider->setMinimum(0); d->iconSizeSlider->setMaximum(100); d->iconSizeSlider->installEventFilter(this); connect(d->iconSizeSlider, &QAbstractSlider::valueChanged, d->ops, &KDirOperator::setIconsZoom); connect(d->iconSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(_k_slotIconSizeChanged(int))); connect(d->iconSizeSlider, SIGNAL(sliderMoved(int)), this, SLOT(_k_slotIconSizeSliderMoved(int))); connect(d->ops, &KDirOperator::currentIconSizeChanged, [this](int value) { d->iconSizeSlider->setValue(value); d->zoomOutAction->setDisabled(value <= d->iconSizeSlider->minimum()); d->zoomInAction->setDisabled(value >= d->iconSizeSlider->maximum()); }); d->zoomOutAction = new QAction(QIcon::fromTheme(QStringLiteral("file-zoom-out")), i18n("Zoom out"), this); connect(d->zoomOutAction, SIGNAL(triggered()), SLOT(_k_zoomOutIconsSize())); d->zoomInAction = new QAction(QIcon::fromTheme(QStringLiteral("file-zoom-in")), i18n("Zoom in"), this); connect(d->zoomInAction, SIGNAL(triggered()), SLOT(_k_zoomInIconsSize())); QWidget *midSpacer = new QWidget(this); midSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->toolbar->addAction(coll->action(QStringLiteral("back"))); d->toolbar->addAction(coll->action(QStringLiteral("forward"))); d->toolbar->addAction(coll->action(QStringLiteral("up"))); d->toolbar->addAction(coll->action(QStringLiteral("reload"))); d->toolbar->addSeparator(); d->toolbar->addAction(coll->action(QStringLiteral("icons view"))); d->toolbar->addAction(coll->action(QStringLiteral("compact view"))); d->toolbar->addAction(coll->action(QStringLiteral("details view"))); d->toolbar->addSeparator(); d->toolbar->addAction(coll->action(QStringLiteral("inline preview"))); d->toolbar->addAction(coll->action(QStringLiteral("sorting menu"))); d->toolbar->addWidget(midSpacer); d->toolbar->addAction(d->zoomOutAction); d->toolbar->addWidget(d->iconSizeSlider); d->toolbar->addAction(d->zoomInAction); d->toolbar->addSeparator(); d->toolbar->addAction(coll->action(QStringLiteral("mkdir"))); d->toolbar->addAction(menu); d->toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); d->toolbar->setMovable(false); KUrlCompletion *pathCompletionObj = new KUrlCompletion(KUrlCompletion::DirCompletion); pathCombo->setCompletionObject(pathCompletionObj); pathCombo->setAutoDeleteCompletionObject(true); connect(d->urlNavigator, SIGNAL(urlChanged(QUrl)), this, SLOT(_k_enterUrl(QUrl))); connect(d->urlNavigator, &KUrlNavigator::returnPressed, d->ops, QOverload<>::of(&QWidget::setFocus)); QString whatsThisText; // the Location label/edit d->locationLabel = new QLabel(i18n("&Name:"), this); d->locationEdit = new KUrlComboBox(KUrlComboBox::Files, true, this); d->locationEdit->installEventFilter(this); // Properly let the dialog be resized (to smaller). Otherwise we could have // huge dialogs that can't be resized to smaller (it would be as big as the longest // item in this combo box). (ereslibre) d->locationEdit->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); connect(d->locationEdit, SIGNAL(editTextChanged(QString)), SLOT(_k_slotLocationChanged(QString))); d->updateLocationWhatsThis(); d->locationLabel->setBuddy(d->locationEdit); KUrlCompletion *fileCompletionObj = new KUrlCompletion(KUrlCompletion::FileCompletion); d->locationEdit->setCompletionObject(fileCompletionObj); d->locationEdit->setAutoDeleteCompletionObject(true); connect(fileCompletionObj, SIGNAL(match(QString)), SLOT(_k_fileCompletion(QString))); connect(d->locationEdit, SIGNAL(returnPressed(QString)), this, SLOT(_k_locationAccepted(QString))); // the Filter label/edit d->filterLabel = new QLabel(this); d->filterWidget = new KFileFilterCombo(this); d->updateFilterText(); // Properly let the dialog be resized (to smaller). Otherwise we could have // huge dialogs that can't be resized to smaller (it would be as big as the longest // item in this combo box). (ereslibre) d->filterWidget->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); d->filterLabel->setBuddy(d->filterWidget); connect(d->filterWidget, SIGNAL(filterChanged()), SLOT(_k_slotFilterChanged())); d->filterDelayTimer.setSingleShot(true); d->filterDelayTimer.setInterval(300); connect(d->filterWidget, &QComboBox::editTextChanged, &d->filterDelayTimer, QOverload<>::of(&QTimer::start)); connect(&d->filterDelayTimer, SIGNAL(timeout()), SLOT(_k_slotFilterChanged())); // the Automatically Select Extension checkbox // (the text, visibility etc. is set in updateAutoSelectExtension(), which is called by readConfig()) d->autoSelectExtCheckBox = new QCheckBox(this); const int spacingHint = style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing); d->autoSelectExtCheckBox->setStyleSheet(QStringLiteral("QCheckBox { padding-top: %1px; }").arg(spacingHint)); connect(d->autoSelectExtCheckBox, SIGNAL(clicked()), SLOT(_k_slotAutoSelectExtClicked())); d->initGUI(); // activate GM // read our configuration KSharedConfig::Ptr config = KSharedConfig::openConfig(); KConfigGroup group(config, ConfigGroup); readConfig(group); coll->action(QStringLiteral("inline preview"))->setChecked(d->ops->isInlinePreviewShown()); d->iconSizeSlider->setValue(d->ops->iconsZoom()); KFilePreviewGenerator *pg = d->ops->previewGenerator(); if (pg) { coll->action(QStringLiteral("inline preview"))->setChecked(pg->isPreviewShown()); } // getStartUrl() above will have resolved the startDir parameter into // a directory and file name in the two cases: (a) where it is a // special "kfiledialog:" URL, or (b) where it is a plain file name // only without directory or protocol. For any other startDir // specified, it is not possible to resolve whether there is a file name // present just by looking at the URL; the only way to be sure is // to stat it. bool statRes = false; if (filename.isEmpty()) { KIO::StatJob *statJob = KIO::stat(startDir, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); statRes = statJob->exec(); // qDebug() << "stat of" << startDir << "-> statRes" << statRes << "isDir" << statJob->statResult().isDir(); if (!statRes || !statJob->statResult().isDir()) { filename = startDir.fileName(); startDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); // qDebug() << "statJob -> startDir" << startDir << "filename" << filename; } } d->ops->setUrl(startDir, true); d->urlNavigator->setLocationUrl(startDir); if (d->placesView) { d->placesView->setUrl(startDir); } // We have a file name either explicitly specified, or have checked that // we could stat it and it is not a directory. Set it. if (!filename.isEmpty()) { QLineEdit *lineEdit = d->locationEdit->lineEdit(); // qDebug() << "selecting filename" << filename; if (statRes) { d->setLocationText(QUrl(filename)); } else { lineEdit->setText(filename); // Preserve this filename when clicking on the view (cf _k_fileHighlighted) lineEdit->setModified(true); } lineEdit->selectAll(); } d->locationEdit->setFocus(); } KFileWidget::~KFileWidget() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->sync(); delete d; } void KFileWidget::setLocationLabel(const QString &text) { d->locationLabel->setText(text); } void KFileWidget::setFilter(const QString &filter) { int pos = filter.indexOf(QLatin1Char('/')); // Check for an un-escaped '/', if found // interpret as a MIME filter. if (pos > 0 && filter[pos - 1] != QLatin1Char('\\')) { QStringList filters = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); setMimeFilter(filters); return; } // Strip the escape characters from // escaped '/' characters. QString copy(filter); for (pos = 0; (pos = copy.indexOf(QLatin1String("\\/"), pos)) != -1; ++pos) { copy.remove(pos, 1); } d->ops->clearFilter(); d->filterWidget->setFilter(copy); d->ops->setNameFilter(d->filterWidget->currentFilter()); d->ops->updateDir(); d->hasDefaultFilter = false; d->filterWidget->setEditable(true); d->updateAutoSelectExtension(); } QString KFileWidget::currentFilter() const { return d->filterWidget->currentFilter(); } void KFileWidget::setMimeFilter(const QStringList &mimeTypes, const QString &defaultType) { d->filterWidget->setMimeFilter(mimeTypes, defaultType); QStringList types = d->filterWidget->currentFilter().split(QLatin1Char(' '), QString::SkipEmptyParts); //QStringList::split(" ", d->filterWidget->currentFilter()); types.append(QStringLiteral("inode/directory")); d->ops->clearFilter(); d->ops->setMimeFilter(types); d->hasDefaultFilter = !defaultType.isEmpty(); d->filterWidget->setEditable(!d->hasDefaultFilter || d->operationMode != Saving); d->updateAutoSelectExtension(); d->updateFilterText(); } void KFileWidget::clearFilter() { d->filterWidget->setFilter(QString()); d->ops->clearFilter(); d->hasDefaultFilter = false; d->filterWidget->setEditable(true); d->updateAutoSelectExtension(); } QString KFileWidget::currentMimeFilter() const { int i = d->filterWidget->currentIndex(); if (d->filterWidget->showsAllTypes() && i == 0) { return QString(); // The "all types" item has no mimetype } return d->filterWidget->filters().at(i); } QMimeType KFileWidget::currentFilterMimeType() { QMimeDatabase db; return db.mimeTypeForName(currentMimeFilter()); } void KFileWidget::setPreviewWidget(KPreviewWidgetBase *w) { d->ops->setPreviewWidget(w); d->ops->clearHistory(); d->hasView = true; } QUrl KFileWidgetPrivate::getCompleteUrl(const QString &_url) const { // qDebug() << "got url " << _url; const QString url = KShell::tildeExpand(_url); QUrl u; if (QDir::isAbsolutePath(url)) { u = QUrl::fromLocalFile(url); } else { QUrl relativeUrlTest(ops->url()); relativeUrlTest.setPath(concatPaths(relativeUrlTest.path(), url)); if (!ops->dirLister()->findByUrl(relativeUrlTest).isNull() || !KProtocolInfo::isKnownProtocol(relativeUrlTest)) { u = relativeUrlTest; } else { // Try to preserve URLs if they have a scheme (for example, // "https://example.com/foo.txt") and otherwise resolve relative // paths to absolute ones (e.g. "foo.txt" -> "file:///tmp/foo.txt"). u = QUrl(url); if (u.isRelative()) { u = relativeUrlTest; } } } return u; } QSize KFileWidget::sizeHint() const { int fontSize = fontMetrics().height(); const QSize goodSize(48 * fontSize, 30 * fontSize); const QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); const QSize minSize(screenSize / 2); const QSize maxSize(screenSize * qreal(0.9)); return (goodSize.expandedTo(minSize).boundedTo(maxSize)); } static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url); // Called by KFileDialog void KFileWidget::slotOk() { // qDebug() << "slotOk\n"; const QString locationEditCurrentText(KShell::tildeExpand(d->locationEditCurrentText())); QList locationEditCurrentTextList(d->tokenize(locationEditCurrentText)); KFile::Modes mode = d->ops->mode(); // if there is nothing to do, just return from here if (locationEditCurrentTextList.isEmpty()) { return; } // Make sure that one of the modes was provided if (!((mode & KFile::File) || (mode & KFile::Directory) || (mode & KFile::Files))) { mode |= KFile::File; // qDebug() << "No mode() provided"; } // if we are on file mode, and the list of provided files/folder is greater than one, inform // the user about it if (locationEditCurrentTextList.count() > 1) { if (mode & KFile::File) { KMessageBox::sorry(this, i18n("You can only select one file"), i18n("More than one file provided")); return; } /** * Logic of the next part of code (ends at "end multi relative urls"). * * We allow for instance to be at "/" and insert '"home/foo/bar.txt" "boot/grub/menu.lst"'. * Why we need to support this ? Because we provide tree views, which aren't plain. * * Now, how does this logic work. It will get the first element on the list (with no filename), * following the previous example say "/home/foo" and set it as the top most url. * * After this, it will iterate over the rest of items and check if this URL (topmost url) * contains the url being iterated. * * As you might have guessed it will do "/home/foo" against "/boot/grub" (again stripping * filename), and a false will be returned. Then we upUrl the top most url, resulting in * "/home" against "/boot/grub", what will again return false, so we upUrl again. Now we * have "/" against "/boot/grub", what returns true for us, so we can say that the closest * common ancestor of both is "/". * * This example has been written for 2 urls, but this works for any number of urls. */ if (!d->differentHierarchyLevelItemsEntered) { // avoid infinite recursion. running this int start = 0; QUrl topMostUrl; KIO::StatJob *statJob = nullptr; bool res = false; // we need to check for a valid first url, so in theory we only iterate one time over // this loop. However it can happen that the user did // "home/foo/nonexistantfile" "boot/grub/menu.lst", so we look for a good first // candidate. while (!res && start < locationEditCurrentTextList.count()) { topMostUrl = locationEditCurrentTextList.at(start); statJob = KIO::stat(topMostUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); res = statJob->exec(); start++; } Q_ASSERT(statJob); // if this is not a dir, strip the filename. after this we have an existent and valid // dir (we stated correctly the file). if (!statJob->statResult().isDir()) { topMostUrl = topMostUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } // now the funny part. for the rest of filenames, go and look for the closest ancestor // of all them. for (int i = start; i < locationEditCurrentTextList.count(); ++i) { QUrl currUrl = locationEditCurrentTextList.at(i); KIO::StatJob *statJob = KIO::stat(currUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { // again, we don't care about filenames if (!statJob->statResult().isDir()) { currUrl = currUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); } // iterate while this item is contained on the top most url while (!topMostUrl.matches(currUrl, QUrl::StripTrailingSlash) && !topMostUrl.isParentOf(currUrl)) { topMostUrl = KIO::upUrl(topMostUrl); } } } // now recalculate all paths for them being relative in base of the top most url QStringList stringList; stringList.reserve(locationEditCurrentTextList.count()); for (int i = 0; i < locationEditCurrentTextList.count(); ++i) { Q_ASSERT(topMostUrl.isParentOf(locationEditCurrentTextList[i])); stringList << relativePathOrUrl(topMostUrl, locationEditCurrentTextList[i]); } d->ops->setUrl(topMostUrl, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(QStringLiteral("\"%1\"").arg(stringList.join(QStringLiteral("\" \"")))); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); d->differentHierarchyLevelItemsEntered = true; slotOk(); return; } /** * end multi relative urls */ } else if (!locationEditCurrentTextList.isEmpty()) { // if we are on file or files mode, and we have an absolute url written by // the user, convert it to relative if (!locationEditCurrentText.isEmpty() && !(mode & KFile::Directory) && (QDir::isAbsolutePath(locationEditCurrentText) || containsProtocolSection(locationEditCurrentText))) { QString fileName; QUrl url = urlFromString(locationEditCurrentText); if (d->operationMode == Opening) { KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { if (!statJob->statResult().isDir()) { fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash } else { if (!url.path().endsWith(QLatin1Char('/'))) { url.setPath(url.path() + QLatin1Char('/')); } } } } else { const QUrl directory = url.adjusted(QUrl::RemoveFilename); //Check if the folder exists KIO::StatJob *statJob = KIO::stat(directory, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (res) { if (statJob->statResult().isDir()) { url = url.adjusted(QUrl::StripTrailingSlash); fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); } } } d->ops->setUrl(url, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(fileName); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); slotOk(); return; } } // restore it d->differentHierarchyLevelItemsEntered = false; // locationEditCurrentTextList contains absolute paths // this is the general loop for the File and Files mode. Obviously we know // that the File mode will iterate only one time here bool directoryMode = (mode & KFile::Directory); bool onlyDirectoryMode = directoryMode && !(mode & KFile::File) && !(mode & KFile::Files); QList::ConstIterator it = locationEditCurrentTextList.constBegin(); bool filesInList = false; while (it != locationEditCurrentTextList.constEnd()) { QUrl url(*it); if (d->operationMode == Saving && !directoryMode) { d->appendExtension(url); } d->url = url; KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, this); int res = statJob->exec(); if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), url)) { QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->url.toDisplayString()); KMessageBox::error(this, msg); return; } // if we are on local mode, make sure we haven't got a remote base url if ((mode & KFile::LocalOnly) && !d->mostLocalUrl(d->url).isLocalFile()) { KMessageBox::sorry(this, i18n("You can only select local files"), i18n("Remote files not accepted")); return; } const auto &supportedSchemes = d->model->supportedSchemes(); if (!supportedSchemes.isEmpty() && !supportedSchemes.contains(d->url.scheme())) { KMessageBox::sorry(this, i18np("The selected URL uses an unsupported scheme. " "Please use the following scheme: %2", "The selected URL uses an unsupported scheme. " "Please use one of the following schemes: %2", supportedSchemes.size(), supportedSchemes.join(QLatin1String(", "))), i18n("Unsupported URL scheme")); return; } // if we are given a folder when not on directory mode, let's get into it if (res && !directoryMode && statJob->statResult().isDir()) { // check if we were given more than one folder, in that case we don't know to which one // cd ++it; while (it != locationEditCurrentTextList.constEnd()) { QUrl checkUrl(*it); KIO::StatJob *checkStatJob = KIO::stat(checkUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(checkStatJob, this); bool res = checkStatJob->exec(); if (res && checkStatJob->statResult().isDir()) { KMessageBox::sorry(this, i18n("More than one folder has been selected and this dialog does not accept folders, so it is not possible to decide which one to enter. Please select only one folder to list it."), i18n("More than one folder provided")); return; } else if (res) { filesInList = true; } ++it; } if (filesInList) { KMessageBox::information(this, i18n("At least one folder and one file has been selected. Selected files will be ignored and the selected folder will be listed"), i18n("Files and folders selected")); } d->ops->setUrl(url, true); const bool signalsBlocked = d->locationEdit->lineEdit()->blockSignals(true); d->locationEdit->lineEdit()->setText(QString()); d->locationEdit->lineEdit()->blockSignals(signalsBlocked); return; } else if (res && onlyDirectoryMode && !statJob->statResult().isDir()) { // if we are given a file when on directory only mode, reject it return; } else if (!(mode & KFile::ExistingOnly) || res) { // if we don't care about ExistingOnly flag, add the file even if // it doesn't exist. If we care about it, don't add it to the list if (!onlyDirectoryMode || (res && statJob->statResult().isDir())) { d->urlList << url; } filesInList = true; } else { KMessageBox::sorry(this, i18n("The file \"%1\" could not be found", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Cannot open file")); return; // do not emit accepted() if we had ExistingOnly flag and stat failed } if ((d->operationMode == Saving) && d->confirmOverwrite && !d->toOverwrite(url)) { return; } ++it; } // if we have reached this point and we didn't return before, that is because // we want this dialog to be accepted emit accepted(); } void KFileWidget::accept() { d->inAccept = true; // parseSelectedUrls() checks that *lastDirectory() = d->ops->url(); if (!d->fileClass.isEmpty()) { KRecentDirs::add(d->fileClass, d->ops->url().toString()); } // clear the topmost item, we insert it as full path later on as item 1 d->locationEdit->setItemText(0, QString()); const QList list = selectedUrls(); QList::const_iterator it = list.begin(); int atmost = d->locationEdit->maxItems(); //don't add more items than necessary for (; it != list.end() && atmost > 0; ++it) { const QUrl &url = *it; // we strip the last slash (-1) because KUrlComboBox does that as well // when operating in file-mode. If we wouldn't , dupe-finding wouldn't // work. QString file = url.isLocalFile() ? url.toLocalFile() : url.toDisplayString(); // remove dupes for (int i = 1; i < d->locationEdit->count(); i++) { if (d->locationEdit->itemText(i) == file) { d->locationEdit->removeItem(i--); break; } } //FIXME I don't think this works correctly when the KUrlComboBox has some default urls. //KUrlComboBox should provide a function to add an url and rotate the existing ones, keeping //track of maxItems, and we shouldn't be able to insert items as we please. d->locationEdit->insertItem(1, file); atmost--; } d->writeViewConfig(); d->saveRecentFiles(); d->addToRecentDocuments(); if (!(mode() & KFile::Files)) { // single selection emit fileSelected(d->url); } d->ops->close(); } void KFileWidgetPrivate::_k_fileHighlighted(const KFileItem &i) { if ((!i.isNull() && i.isDir()) || (locationEdit->hasFocus() && !locationEdit->currentText().isEmpty())) { // don't disturb return; } const bool modified = locationEdit->lineEdit()->isModified(); if (!(ops->mode() & KFile::Files)) { if (i.isNull()) { if (!modified) { setLocationText(QUrl()); } return; } url = i.url(); if (!locationEdit->hasFocus()) { // don't disturb while editing setLocationText(url); } emit q->fileHighlighted(url); } else { multiSelectionChanged(); emit q->selectionChanged(); } locationEdit->lineEdit()->setModified(false); // When saving, and when double-click mode is being used, highlight the // filename after a file is single-clicked so the user has a chance to quickly // rename it if desired // Note that double-clicking will override this and overwrite regardless of // single/double click mouse setting (see _k_slotViewDoubleClicked() ) if (operationMode == KFileWidget::Saving) { locationEdit->setFocus(); } } void KFileWidgetPrivate::_k_fileSelected(const KFileItem &i) { if (!i.isNull() && i.isDir()) { return; } if (!(ops->mode() & KFile::Files)) { if (i.isNull()) { setLocationText(QUrl()); return; } setLocationText(i.url()); } else { multiSelectionChanged(); emit q->selectionChanged(); } // Same as above in _k_fileHighlighted(), but for single-click mode if (operationMode == KFileWidget::Saving) { locationEdit->setFocus(); } else { q->slotOk(); } } // I know it's slow to always iterate thru the whole filelist // (d->ops->selectedItems()), but what can we do? void KFileWidgetPrivate::multiSelectionChanged() { if (locationEdit->hasFocus() && !locationEdit->currentText().isEmpty()) { // don't disturb return; } const KFileItemList list = ops->selectedItems(); if (list.isEmpty()) { setLocationText(QUrl()); return; } setLocationText(list.urlList()); } void KFileWidgetPrivate::setDummyHistoryEntry(const QString &text, const QPixmap &icon, bool usePreviousPixmapIfNull) { // setCurrentItem() will cause textChanged() being emitted, // so slotLocationChanged() will be called. Make sure we don't clear // the KDirOperator's view-selection in there QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); bool dummyExists = dummyAdded; int cursorPosition = locationEdit->lineEdit()->cursorPosition(); if (dummyAdded) { if (!icon.isNull()) { locationEdit->setItemIcon(0, icon); locationEdit->setItemText(0, text); } else { if (!usePreviousPixmapIfNull) { locationEdit->setItemIcon(0, QPixmap()); } locationEdit->setItemText(0, text); } } else { if (!text.isEmpty()) { if (!icon.isNull()) { locationEdit->insertItem(0, icon, text); } else { if (!usePreviousPixmapIfNull) { locationEdit->insertItem(0, QPixmap(), text); } else { locationEdit->insertItem(0, text); } } dummyAdded = true; dummyExists = true; } } if (dummyExists && !text.isEmpty()) { locationEdit->setCurrentIndex(0); } locationEdit->lineEdit()->setCursorPosition(cursorPosition); QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); } void KFileWidgetPrivate::removeDummyHistoryEntry() { if (!dummyAdded) { return; } // setCurrentItem() will cause textChanged() being emitted, // so slotLocationChanged() will be called. Make sure we don't clear // the KDirOperator's view-selection in there QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); if (locationEdit->count()) { locationEdit->removeItem(0); } locationEdit->setCurrentIndex(-1); dummyAdded = false; QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); } void KFileWidgetPrivate::setLocationText(const QUrl &url) { if (!url.isEmpty()) { QPixmap mimeTypeIcon = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(url), KIconLoader::Small); if (!url.isRelative()) { const QUrl directory = url.adjusted(QUrl::RemoveFilename); if (!directory.path().isEmpty()) { q->setUrl(directory, false); } else { q->setUrl(url, false); } } setDummyHistoryEntry(url.fileName(), mimeTypeIcon); } else { removeDummyHistoryEntry(); } if (operationMode == KFileWidget::Saving) { setNonExtSelection(); } } static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url) { if (baseUrl.isParentOf(url)) { const QString basePath(QDir::cleanPath(baseUrl.path())); QString relPath(QDir::cleanPath(url.path())); relPath.remove(0, basePath.length()); if (relPath.startsWith(QLatin1Char('/'))) { relPath.remove(0, 1); } return relPath; } else { return url.toDisplayString(); } } void KFileWidgetPrivate::setLocationText(const QList &urlList) { const QUrl currUrl = ops->url(); if (urlList.count() > 1) { QString urls; for (const QUrl &url : urlList) { urls += QStringLiteral("\"%1\"").arg(relativePathOrUrl(currUrl, url)) + QLatin1Char(' '); } urls.chop(1); setDummyHistoryEntry(urls, QPixmap(), false); } else if (urlList.count() == 1) { const QPixmap mimeTypeIcon = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(urlList[0]), KIconLoader::Small); setDummyHistoryEntry(relativePathOrUrl(currUrl, urlList[0]), mimeTypeIcon); } else { removeDummyHistoryEntry(); } if (operationMode == KFileWidget::Saving) { setNonExtSelection(); } } void KFileWidgetPrivate::updateLocationWhatsThis() { QString whatsThisText; if (operationMode == KFileWidget::Saving) { whatsThisText = QLatin1String("") + i18n("This is the name to save the file as.") + i18n(autocompletionWhatsThisText); } else if (ops->mode() & KFile::Files) { whatsThisText = QLatin1String("") + i18n("This is the list of files to open. More than " "one file can be specified by listing several " "files, separated by spaces.") + i18n(autocompletionWhatsThisText); } else { whatsThisText = QLatin1String("") + i18n("This is the name of the file to open.") + i18n(autocompletionWhatsThisText); } locationLabel->setWhatsThis(whatsThisText); locationEdit->setWhatsThis(whatsThisText); } void KFileWidgetPrivate::initSpeedbar() { if (placesDock) { return; } placesDock = new QDockWidget(i18nc("@title:window", "Places"), q); placesDock->setFeatures(QDockWidget::NoDockWidgetFeatures); placesDock->setTitleBarWidget(new KDEPrivate::KFileWidgetDockTitleBar(placesDock)); placesView = new KFilePlacesView(placesDock); placesView->setModel(model); placesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); placesView->setObjectName(QStringLiteral("url bar")); QObject::connect(placesView, SIGNAL(urlChanged(QUrl)), q, SLOT(_k_enterUrl(QUrl))); // need to set the current url of the urlbar manually (not via urlEntered() // here, because the initial url of KDirOperator might be the same as the // one that will be set later (and then urlEntered() won't be emitted). // TODO: KDE5 ### REMOVE THIS when KDirOperator's initial URL (in the c'tor) is gone. placesView->setUrl(url); placesDock->setWidget(placesView); placesViewSplitter->insertWidget(0, placesDock); // initialize the size of the splitter placesViewWidth = configGroup.readEntry(SpeedbarWidth, placesView->sizeHint().width()); // Needed for when the dialog is shown with the places panel initially hidden setPlacesViewSplitterSizes(); QObject::connect(placesDock, SIGNAL(visibilityChanged(bool)), q, SLOT(_k_toggleSpeedbar(bool))); } void KFileWidgetPrivate::setPlacesViewSplitterSizes() { if (placesViewWidth > 0) { QList sizes = placesViewSplitter->sizes(); sizes[0] = placesViewWidth; sizes[1] = q->width() - placesViewWidth - placesViewSplitter->handleWidth(); placesViewSplitter->setSizes(sizes); } } void KFileWidgetPrivate::setLafBoxColumnWidth() { // In order to perfectly align the filename widget with KDirOperator's icon view // - placesViewWidth needs to account for the size of the splitter handle // - the lafBox grid layout spacing should only affect the label, but not the line edit const int adjustment = placesViewSplitter->handleWidth() - lafBox->horizontalSpacing(); lafBox->setColumnMinimumWidth(0, placesViewWidth + adjustment); } void KFileWidgetPrivate::initGUI() { delete boxLayout; // deletes all sub layouts boxLayout = new QVBoxLayout(q); boxLayout->setContentsMargins(0, 0, 0, 0); // no additional margin to the already existing placesViewSplitter = new QSplitter(q); placesViewSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); placesViewSplitter->setChildrenCollapsible(false); boxLayout->addWidget(placesViewSplitter); QObject::connect(placesViewSplitter, SIGNAL(splitterMoved(int,int)), q, SLOT(_k_placesViewSplitterMoved(int,int))); placesViewSplitter->insertWidget(0, opsWidget); vbox = new QVBoxLayout(); vbox->setContentsMargins(0, 0, 0, 0); boxLayout->addLayout(vbox); lafBox = new QGridLayout(); lafBox->addWidget(locationLabel, 0, 0, Qt::AlignVCenter | Qt::AlignRight); lafBox->addWidget(locationEdit, 0, 1, Qt::AlignVCenter); lafBox->addWidget(okButton, 0, 2, Qt::AlignVCenter); lafBox->addWidget(filterLabel, 1, 0, Qt::AlignVCenter | Qt::AlignRight); lafBox->addWidget(filterWidget, 1, 1, Qt::AlignVCenter); lafBox->addWidget(cancelButton, 1, 2, Qt::AlignVCenter); lafBox->setColumnStretch(1, 4); vbox->addLayout(lafBox); // add the Automatically Select Extension checkbox vbox->addWidget(autoSelectExtCheckBox); q->setTabOrder(ops, autoSelectExtCheckBox); q->setTabOrder(autoSelectExtCheckBox, locationEdit); q->setTabOrder(locationEdit, filterWidget); q->setTabOrder(filterWidget, okButton); q->setTabOrder(okButton, cancelButton); q->setTabOrder(cancelButton, urlNavigator); q->setTabOrder(urlNavigator, ops); } void KFileWidgetPrivate::_k_slotFilterChanged() { // qDebug(); filterDelayTimer.stop(); QString filter = filterWidget->currentFilter(); ops->clearFilter(); if (filter.contains(QLatin1Char('/'))) { QStringList types = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); types.prepend(QStringLiteral("inode/directory")); ops->setMimeFilter(types); } else if (filter.contains(QLatin1Char('*')) || filter.contains(QLatin1Char('?')) || filter.contains(QLatin1Char('['))) { ops->setNameFilter(filter); } else { ops->setNameFilter(QLatin1Char('*') + filter.replace(QLatin1Char(' '), QLatin1Char('*')) + QLatin1Char('*')); } updateAutoSelectExtension(); ops->updateDir(); emit q->filterChanged(filter); } void KFileWidget::setUrl(const QUrl &url, bool clearforward) { // qDebug(); d->ops->setUrl(url, clearforward); } // Protected void KFileWidgetPrivate::_k_urlEntered(const QUrl &url) { // qDebug(); QString filename = locationEditCurrentText(); KUrlComboBox *pathCombo = urlNavigator->editor(); if (pathCombo->count() != 0) { // little hack pathCombo->setUrl(url); } bool blocked = locationEdit->blockSignals(true); if (keepLocation) { QUrl currentUrl = urlFromString(filename); locationEdit->changeUrl(0, QIcon::fromTheme(KIO::iconNameForUrl(currentUrl)), currentUrl); locationEdit->lineEdit()->setModified(true); } locationEdit->blockSignals(blocked); urlNavigator->setLocationUrl(url); // is trigged in ctor before completion object is set KUrlCompletion *completion = dynamic_cast(locationEdit->completionObject()); if (completion) { completion->setDir(url); } if (placesView) { placesView->setUrl(url); } } void KFileWidgetPrivate::_k_locationAccepted(const QString &url) { Q_UNUSED(url); // qDebug(); q->slotOk(); } void KFileWidgetPrivate::_k_enterUrl(const QUrl &url) { // qDebug(); // append '/' if needed: url combo does not add it // tokenize() expects it because it uses QUrl::adjusted(QUrl::RemoveFilename) QUrl u(url); if (!u.path().isEmpty() && !u.path().endsWith(QLatin1Char('/'))) { u.setPath(u.path() + QLatin1Char('/')); } q->setUrl(u); // We need to check window()->focusWidget() instead of locationEdit->hasFocus // because when the window is showing up locationEdit // may still not have focus but it'll be the one that will have focus when the window // gets it and we don't want to steal its focus either if (q->window()->focusWidget() != locationEdit) { ops->setFocus(); } } void KFileWidgetPrivate::_k_enterUrl(const QString &url) { // qDebug(); _k_enterUrl(urlFromString(KUrlCompletion::replacedPath(url, true, true))); } bool KFileWidgetPrivate::toOverwrite(const QUrl &url) { // qDebug(); KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (res) { int ret = KMessageBox::warningContinueCancel(q, i18n("The file \"%1\" already exists. Do you wish to overwrite it?", url.fileName()), i18n("Overwrite File?"), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (ret != KMessageBox::Continue) { return false; } return true; } return true; } void KFileWidget::setSelection(const QString &url) { // qDebug() << "setSelection " << url; if (url.isEmpty()) { return; } QUrl u = d->getCompleteUrl(url); if (!u.isValid()) { // Relative path was treated as URL, but it was found to be invalid. qWarning() << url << " is not a correct argument for setSelection!"; return; } setSelectedUrl(urlFromString(url)); } void KFileWidget::setSelectedUrl(const QUrl &url) { // Honor protocols that do not support directory listing if (!url.isRelative() && !KProtocolManager::supportsListing(url)) { return; } d->setLocationText(url); } void KFileWidgetPrivate::_k_slotLoadingFinished() { const QString currentText = locationEdit->currentText(); if (currentText.isEmpty()) { return; } ops->blockSignals(true); QUrl u(ops->url()); if (currentText.startsWith(QLatin1Char('/'))) u.setPath(currentText); else u.setPath(concatPaths(ops->url().path(), currentText)); ops->setCurrentItem(u); ops->blockSignals(false); } void KFileWidgetPrivate::_k_fileCompletion(const QString &match) { // qDebug(); if (match.isEmpty() || locationEdit->currentText().contains(QLatin1Char('"'))) { return; } const QUrl url = urlFromString(match); const QPixmap pix = KIconLoader::global()->loadMimeTypeIcon(KIO::iconNameForUrl(url), KIconLoader::Small); setDummyHistoryEntry(locationEdit->currentText(), pix, !locationEdit->currentText().isEmpty()); } void KFileWidgetPrivate::_k_slotLocationChanged(const QString &text) { // qDebug(); locationEdit->lineEdit()->setModified(true); if (text.isEmpty() && ops->view()) { ops->view()->clearSelection(); } if (text.isEmpty()) { removeDummyHistoryEntry(); } else { setDummyHistoryEntry(text); } if (!locationEdit->lineEdit()->text().isEmpty()) { const QList urlList(tokenize(text)); ops->setCurrentItems(urlList); } updateFilter(); } QUrl KFileWidget::selectedUrl() const { // qDebug(); if (d->inAccept) { return d->url; } else { return QUrl(); } } QList KFileWidget::selectedUrls() const { // qDebug(); QList list; if (d->inAccept) { if (d->ops->mode() & KFile::Files) { list = d->parseSelectedUrls(); } else { list.append(d->url); } } return list; } QList &KFileWidgetPrivate::parseSelectedUrls() { // qDebug(); if (filenames.isEmpty()) { return urlList; } urlList.clear(); if (filenames.contains(QLatin1Char('/'))) { // assume _one_ absolute filename QUrl u; if (containsProtocolSection(filenames)) { u = QUrl(filenames); } else { u.setPath(filenames); } if (u.isValid()) { urlList.append(u); } else KMessageBox::error(q, i18n("The chosen filenames do not\n" "appear to be valid."), i18n("Invalid Filenames")); } else { urlList = tokenize(filenames); } filenames.clear(); // indicate that we parsed that one return urlList; } // FIXME: current implementation drawback: a filename can't contain quotes QList KFileWidgetPrivate::tokenize(const QString &line) const { // qDebug(); QList urls; QUrl u(ops->url()); if (!u.path().endsWith(QLatin1Char('/'))) { u.setPath(u.path() + QLatin1Char('/')); } QString name; const int count = line.count(QLatin1Char('"')); if (count == 0) { // no " " -> assume one single file if (!QDir::isAbsolutePath(line)) { u = u.adjusted(QUrl::RemoveFilename); u.setPath(u.path() + line); if (u.isValid()) { urls.append(u); } } else { urls << QUrl::fromLocalFile(line); } return urls; } int start = 0; int index1 = -1, index2 = -1; while (true) { index1 = line.indexOf(QLatin1Char('"'), start); index2 = line.indexOf(QLatin1Char('"'), index1 + 1); if (index1 < 0 || index2 < 0) { break; } // get everything between the " " name = line.mid(index1 + 1, index2 - index1 - 1); // since we use setPath we need to do this under a temporary url QUrl _u(u); QUrl currUrl(name); if (!QDir::isAbsolutePath(currUrl.url())) { _u = _u.adjusted(QUrl::RemoveFilename); _u.setPath(_u.path() + name); } else { // we allow to insert various absolute paths like: // "/home/foo/bar.txt" "/boot/grub/menu.lst" _u = currUrl; } if (_u.isValid()) { urls.append(_u); } start = index2 + 1; } return urls; } QString KFileWidget::selectedFile() const { // qDebug(); if (d->inAccept) { const QUrl url = d->mostLocalUrl(d->url); if (url.isLocalFile()) { return url.toLocalFile(); } else { KMessageBox::sorry(const_cast(this), i18n("You can only select local files."), i18n("Remote Files Not Accepted")); } } return QString(); } QStringList KFileWidget::selectedFiles() const { // qDebug(); QStringList list; if (d->inAccept) { if (d->ops->mode() & KFile::Files) { const QList urls = d->parseSelectedUrls(); QList::const_iterator it = urls.begin(); while (it != urls.end()) { QUrl url = d->mostLocalUrl(*it); if (url.isLocalFile()) { list.append(url.toLocalFile()); } ++it; } } else { // single-selection mode if (d->url.isLocalFile()) { list.append(d->url.toLocalFile()); } } } return list; } QUrl KFileWidget::baseUrl() const { return d->ops->url(); } void KFileWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); if (d->placesDock) { // we don't want our places dock actually changing size when we resize // and qt doesn't make it easy to enforce such a thing with QSplitter d->setPlacesViewSplitterSizes(); } } void KFileWidget::showEvent(QShowEvent *event) { if (!d->hasView) { // delayed view-creation Q_ASSERT(d); Q_ASSERT(d->ops); d->ops->setView(KFile::Default); d->ops->view()->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum)); d->hasView = true; connect(d->ops->view(), SIGNAL(doubleClicked(QModelIndex)), this, SLOT(_k_slotViewDoubleClicked(QModelIndex))); } d->ops->clearHistory(); QWidget::showEvent(event); } bool KFileWidget::eventFilter(QObject *watched, QEvent *event) { const bool res = QWidget::eventFilter(watched, event); QKeyEvent *keyEvent = dynamic_cast(event); if (watched == d->iconSizeSlider && keyEvent) { if (keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Right || keyEvent->key() == Qt::Key_Down) { d->_k_slotIconSizeSliderMoved(d->iconSizeSlider->value()); } } else if (watched == d->locationEdit && event->type() == QEvent::KeyPress) { if (keyEvent->modifiers() & Qt::AltModifier) { switch (keyEvent->key()) { case Qt::Key_Up: d->ops->actionCollection()->action(QStringLiteral("up"))->trigger(); break; case Qt::Key_Left: d->ops->actionCollection()->action(QStringLiteral("back"))->trigger(); break; case Qt::Key_Right: d->ops->actionCollection()->action(QStringLiteral("forward"))->trigger(); break; default: break; } } } return res; } void KFileWidget::setMode(KFile::Modes m) { // qDebug(); d->ops->setMode(m); if (d->ops->dirOnlyMode()) { d->filterWidget->setDefaultFilter(i18n("*|All Folders")); } else { d->filterWidget->setDefaultFilter(i18n("*|All Files")); } d->updateAutoSelectExtension(); } KFile::Modes KFileWidget::mode() const { return d->ops->mode(); } void KFileWidgetPrivate::readViewConfig() { ops->setViewConfig(configGroup); ops->readConfig(configGroup); KUrlComboBox *combo = urlNavigator->editor(); autoDirectoryFollowing = configGroup.readEntry(AutoDirectoryFollowing, DefaultDirectoryFollowing); KCompletion::CompletionMode cm = (KCompletion::CompletionMode) configGroup.readEntry(PathComboCompletionMode, static_cast(KCompletion::CompletionPopup)); if (cm != KCompletion::CompletionPopup) { combo->setCompletionMode(cm); } cm = (KCompletion::CompletionMode) configGroup.readEntry(LocationComboCompletionMode, static_cast(KCompletion::CompletionPopup)); if (cm != KCompletion::CompletionPopup) { locationEdit->setCompletionMode(cm); } // show or don't show the speedbar _k_toggleSpeedbar(configGroup.readEntry(ShowSpeedbar, true)); // show or don't show the bookmarks _k_toggleBookmarks(configGroup.readEntry(ShowBookmarks, false)); // does the user want Automatically Select Extension? autoSelectExtChecked = configGroup.readEntry(AutoSelectExtChecked, DefaultAutoSelectExtChecked); updateAutoSelectExtension(); // should the URL navigator use the breadcrumb navigation? urlNavigator->setUrlEditable(!configGroup.readEntry(BreadcrumbNavigation, true)); // should the URL navigator show the full path? urlNavigator->setShowFullPath(configGroup.readEntry(ShowFullPath, false)); int w1 = q->minimumSize().width(); int w2 = toolbar->sizeHint().width(); if (w1 < w2) { q->setMinimumWidth(w2); } } void KFileWidgetPrivate::writeViewConfig() { // these settings are global settings; ALL instances of the file dialog // should reflect them. // There is no way to tell KFileOperator::writeConfig() to write to // kdeglobals so we write settings to a temporary config group then copy // them all to kdeglobals KConfig tmp(QString(), KConfig::SimpleConfig); KConfigGroup tmpGroup(&tmp, ConfigGroup); KUrlComboBox *pathCombo = urlNavigator->editor(); //saveDialogSize( tmpGroup, KConfigGroup::Persistent | KConfigGroup::Global ); tmpGroup.writeEntry(PathComboCompletionMode, static_cast(pathCombo->completionMode())); tmpGroup.writeEntry(LocationComboCompletionMode, static_cast(locationEdit->completionMode())); const bool showSpeedbar = placesDock && !placesDock->isHidden(); tmpGroup.writeEntry(ShowSpeedbar, showSpeedbar); if (placesViewWidth > 0) { tmpGroup.writeEntry(SpeedbarWidth, placesViewWidth); } tmpGroup.writeEntry(ShowBookmarks, bookmarkHandler != nullptr); tmpGroup.writeEntry(AutoSelectExtChecked, autoSelectExtChecked); tmpGroup.writeEntry(BreadcrumbNavigation, !urlNavigator->isUrlEditable()); tmpGroup.writeEntry(ShowFullPath, urlNavigator->showFullPath()); ops->writeConfig(tmpGroup); // Copy saved settings to kdeglobals tmpGroup.copyTo(&configGroup, KConfigGroup::Persistent | KConfigGroup::Global); } void KFileWidgetPrivate::readRecentFiles() { // qDebug(); QObject::disconnect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); locationEdit->setMaxItems(configGroup.readEntry(RecentFilesNumber, DefaultRecentURLsNumber)); locationEdit->setUrls(configGroup.readPathEntry(RecentFiles, QStringList()), KUrlComboBox::RemoveBottom); locationEdit->setCurrentIndex(-1); QObject::connect(locationEdit, SIGNAL(editTextChanged(QString)), q, SLOT(_k_slotLocationChanged(QString))); KUrlComboBox *combo = urlNavigator->editor(); combo->setUrls(configGroup.readPathEntry(RecentURLs, QStringList()), KUrlComboBox::RemoveTop); combo->setMaxItems(configGroup.readEntry(RecentURLsNumber, DefaultRecentURLsNumber)); combo->setUrl(ops->url()); // since we delayed this moment, initialize the directory of the completion object to // our current directory (that was very probably set on the constructor) KUrlCompletion *completion = dynamic_cast(locationEdit->completionObject()); if (completion) { completion->setDir(ops->url()); } } void KFileWidgetPrivate::saveRecentFiles() { // qDebug(); configGroup.writePathEntry(RecentFiles, locationEdit->urls()); KUrlComboBox *pathCombo = urlNavigator->editor(); configGroup.writePathEntry(RecentURLs, pathCombo->urls()); } QPushButton *KFileWidget::okButton() const { return d->okButton; } QPushButton *KFileWidget::cancelButton() const { return d->cancelButton; } // Called by KFileDialog void KFileWidget::slotCancel() { d->writeViewConfig(); d->ops->close(); } void KFileWidget::setKeepLocation(bool keep) { d->keepLocation = keep; } bool KFileWidget::keepsLocation() const { return d->keepLocation; } void KFileWidget::setOperationMode(OperationMode mode) { // qDebug(); d->operationMode = mode; d->keepLocation = (mode == Saving); d->filterWidget->setEditable(!d->hasDefaultFilter || mode != Saving); if (mode == Opening) { // don't use KStandardGuiItem::open() here which has trailing ellipsis! d->okButton->setText(i18n("&Open")); d->okButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); // hide the new folder actions...usability team says they shouldn't be in open file dialog actionCollection()->removeAction(actionCollection()->action(QStringLiteral("mkdir"))); } else if (mode == Saving) { KGuiItem::assign(d->okButton, KStandardGuiItem::save()); d->setNonExtSelection(); } else { KGuiItem::assign(d->okButton, KStandardGuiItem::ok()); } d->updateLocationWhatsThis(); d->updateAutoSelectExtension(); if (d->ops) { d->ops->setIsSaving(mode == Saving); } d->updateFilterText(); } KFileWidget::OperationMode KFileWidget::operationMode() const { return d->operationMode; } void KFileWidgetPrivate::_k_slotAutoSelectExtClicked() { // qDebug() << "slotAutoSelectExtClicked(): " // << autoSelectExtCheckBox->isChecked() << endl; // whether the _user_ wants it on/off autoSelectExtChecked = autoSelectExtCheckBox->isChecked(); // update the current filename's extension updateLocationEditExtension(extension /* extension hasn't changed */); } void KFileWidgetPrivate::_k_placesViewSplitterMoved(int pos, int index) { // qDebug(); // we need to record the size of the splitter when the splitter changes size // so we can keep the places box the right size! if (placesDock && index == 1) { placesViewWidth = pos; // qDebug() << "setting lafBox minwidth to" << placesViewWidth; setLafBoxColumnWidth(); } } void KFileWidgetPrivate::_k_activateUrlNavigator() { // qDebug(); QLineEdit* lineEdit = urlNavigator->editor()->lineEdit(); // If the text field currently has focus and everything is selected, // pressing the keyboard shortcut returns the whole thing to breadcrumb mode if (urlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text() ) { urlNavigator->setUrlEditable(false); } else { urlNavigator->setUrlEditable(true); urlNavigator->setFocus(); lineEdit->selectAll(); } } void KFileWidgetPrivate::_k_zoomOutIconsSize() { const int currValue = ops->iconsZoom(); const int futValue = qMax(0, currValue - 10); iconSizeSlider->setValue(futValue); _k_slotIconSizeSliderMoved(futValue); } void KFileWidgetPrivate::_k_zoomInIconsSize() { const int currValue = ops->iconsZoom(); const int futValue = qMin(100, currValue + 10); iconSizeSlider->setValue(futValue); _k_slotIconSizeSliderMoved(futValue); } void KFileWidgetPrivate::_k_slotIconSizeChanged(int _value) { int maxSize = KIconLoader::SizeEnormous - KIconLoader::SizeSmall; int value = (maxSize * _value / 100) + KIconLoader::SizeSmall; switch (value) { case KIconLoader::SizeSmall: case KIconLoader::SizeSmallMedium: case KIconLoader::SizeMedium: case KIconLoader::SizeLarge: case KIconLoader::SizeHuge: case KIconLoader::SizeEnormous: iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels (standard size)", value)); break; default: iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels", value)); break; } } void KFileWidgetPrivate::_k_slotIconSizeSliderMoved(int _value) { // Force this to be called in case this slot is called first on the // slider move. _k_slotIconSizeChanged(_value); QPoint global(iconSizeSlider->rect().topLeft()); global.ry() += iconSizeSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), iconSizeSlider->mapToGlobal(global)); QApplication::sendEvent(iconSizeSlider, &toolTipEvent); } void KFileWidgetPrivate::_k_slotViewDoubleClicked(const QModelIndex &index) { // double clicking to save should only work on files if (operationMode == KFileWidget::Saving && index.isValid() && ops->selectedItems().constFirst().isFile()) { q->slotOk(); } } void KFileWidgetPrivate::_k_slotViewKeyEnterReturnPressed() { - // an enter/return event occured in the view + // an enter/return event occurred in the view // when we are saving one file and there is no selection in the view (otherwise we get an activated event) if (operationMode == KFileWidget::Saving && (ops->mode() & KFile::File) && ops->selectedItems().isEmpty()) { q->slotOk(); } } static QString getExtensionFromPatternList(const QStringList &patternList) { // qDebug(); QString ret; // qDebug() << "\tgetExtension " << patternList; QStringList::ConstIterator patternListEnd = patternList.end(); for (QStringList::ConstIterator it = patternList.begin(); it != patternListEnd; ++it) { // qDebug() << "\t\ttry: \'" << (*it) << "\'"; // is this pattern like "*.BMP" rather than useless things like: // // README // *. // *.* // *.JP*G // *.JP? // *.[Jj][Pp][Gg] if ((*it).startsWith(QLatin1String("*.")) && (*it).length() > 2 && (*it).indexOf(QLatin1Char('*'), 2) < 0 && (*it).indexOf(QLatin1Char('?'), 2) < 0 && (*it).indexOf(QLatin1Char('['), 2) < 0 && (*it).indexOf(QLatin1Char(']'), 2) < 0) { ret = (*it).mid(1); break; } } return ret; } static QString stripUndisplayable(const QString &string) { QString ret = string; ret.remove(QLatin1Char(':')); ret = KLocalizedString::removeAcceleratorMarker(ret); return ret; } //QString KFileWidget::currentFilterExtension() //{ // return d->extension; //} void KFileWidgetPrivate::updateAutoSelectExtension() { if (!autoSelectExtCheckBox) { return; } QMimeDatabase db; // // Figure out an extension for the Automatically Select Extension thing // (some Windows users apparently don't know what to do when confronted // with a text file called "COPYING" but do know what to do with // COPYING.txt ...) // // qDebug() << "Figure out an extension: "; QString lastExtension = extension; extension.clear(); // Automatically Select Extension is only valid if the user is _saving_ a _file_ if ((operationMode == KFileWidget::Saving) && (ops->mode() & KFile::File)) { // // Get an extension from the filter // QString filter = filterWidget->currentFilter(); if (!filter.isEmpty()) { // if the currently selected filename already has an extension which // is also included in the currently allowed extensions, keep it // otherwise use the default extension QString currentExtension = db.suffixForFileName(locationEditCurrentText()); if (currentExtension.isEmpty()) { currentExtension = locationEditCurrentText().section(QLatin1Char('.'), -1, -1); } // qDebug() << "filter:" << filter << "locationEdit:" << locationEditCurrentText() << "currentExtension:" << currentExtension; QString defaultExtension; QStringList extensionList; // e.g. "*.cpp" if (filter.indexOf(QLatin1Char('/')) < 0) { extensionList = filter.split(QLatin1Char(' '), QString::SkipEmptyParts); defaultExtension = getExtensionFromPatternList(extensionList); } // e.g. "text/html" else { QMimeType mime = db.mimeTypeForName(filter); if (mime.isValid()) { extensionList = mime.globPatterns(); defaultExtension = mime.preferredSuffix(); if (!defaultExtension.isEmpty()) { defaultExtension.prepend(QLatin1Char('.')); } } } if ((!currentExtension.isEmpty() && extensionList.contains(QLatin1String("*.") + currentExtension)) || filter == QLatin1String("application/octet-stream")) { extension = QLatin1Char('.') + currentExtension; } else { extension = defaultExtension; } // qDebug() << "List:" << extensionList << "auto-selected extension:" << extension; } // // GUI: checkbox // QString whatsThisExtension; if (!extension.isEmpty()) { // remember: sync any changes to the string with below autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension (%1)", extension)); whatsThisExtension = i18n("the extension %1", extension); autoSelectExtCheckBox->setEnabled(true); autoSelectExtCheckBox->setChecked(autoSelectExtChecked); } else { // remember: sync any changes to the string with above autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension")); whatsThisExtension = i18n("a suitable extension"); autoSelectExtCheckBox->setChecked(false); autoSelectExtCheckBox->setEnabled(false); } const QString locationLabelText = stripUndisplayable(locationLabel->text()); autoSelectExtCheckBox->setWhatsThis(QLatin1String("") + i18n( "This option enables some convenient features for " "saving files with extensions:
" "
    " "
  1. Any extension specified in the %1 text " "area will be updated if you change the file type " "to save in.
    " "
  2. " "
  3. If no extension is specified in the %2 " "text area when you click " "Save, %3 will be added to the end of the " "filename (if the filename does not already exist). " "This extension is based on the file type that you " "have chosen to save in.
    " "
    " "If you do not want KDE to supply an extension for the " "filename, you can either turn this option off or you " "can suppress it by adding a period (.) to the end of " "the filename (the period will be automatically " "removed)." "
  4. " "
" "If unsure, keep this option enabled as it makes your " "files more manageable." , locationLabelText, locationLabelText, whatsThisExtension) + QLatin1String("
") ); autoSelectExtCheckBox->show(); // update the current filename's extension updateLocationEditExtension(lastExtension); } // Automatically Select Extension not valid else { autoSelectExtCheckBox->setChecked(false); autoSelectExtCheckBox->hide(); } } // Updates the extension of the filename specified in d->locationEdit if the // Automatically Select Extension feature is enabled. // (this prevents you from accidentally saving "file.kwd" as RTF, for example) void KFileWidgetPrivate::updateLocationEditExtension(const QString &lastExtension) { if (!autoSelectExtCheckBox->isChecked() || extension.isEmpty()) { return; } QString urlStr = locationEditCurrentText(); if (urlStr.isEmpty()) { return; } QUrl url = getCompleteUrl(urlStr); // qDebug() << "updateLocationEditExtension (" << url << ")"; const int fileNameOffset = urlStr.lastIndexOf(QLatin1Char('/')) + 1; QString fileName = urlStr.mid(fileNameOffset); const int dot = fileName.lastIndexOf(QLatin1Char('.')); const int len = fileName.length(); if (dot > 0 && // has an extension already and it's not a hidden file // like ".hidden" (but we do accept ".hidden.ext") dot != len - 1 // and not deliberately suppressing extension ) { // exists? KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool result = statJob->exec(); if (result) { // qDebug() << "\tfile exists"; if (statJob->statResult().isDir()) { // qDebug() << "\tisDir - won't alter extension"; return; } // --- fall through --- } // // try to get rid of the current extension // // catch "double extensions" like ".tar.gz" if (lastExtension.length() && fileName.endsWith(lastExtension)) { fileName.chop(lastExtension.length()); } else if (extension.length() && fileName.endsWith(extension)) { fileName.chop(extension.length()); } // can only handle "single extensions" else { fileName.truncate(dot); } // add extension const QString newText = urlStr.leftRef(fileNameOffset) + fileName + extension; if (newText != locationEditCurrentText()) { locationEdit->setItemText(locationEdit->currentIndex(), newText); locationEdit->lineEdit()->setModified(true); } } } QString KFileWidgetPrivate::findMatchingFilter(const QString &filter, const QString &filename) const { const QStringList patterns = filter.left(filter.indexOf(QLatin1Char('|'))).split(QLatin1Char(' '), QString::SkipEmptyParts); // '*.foo *.bar|Foo type' -> '*.foo', '*.bar' for (const QString &p : patterns) { QRegExp rx(p); rx.setPatternSyntax(QRegExp::Wildcard); if (rx.exactMatch(filename)) { return p; } } return QString(); } // Updates the filter if the extension of the filename specified in d->locationEdit is changed // (this prevents you from accidently saving "file.kwd" as RTF, for example) void KFileWidgetPrivate::updateFilter() { // qDebug(); if ((operationMode == KFileWidget::Saving) && (ops->mode() & KFile::File)) { QString urlStr = locationEditCurrentText(); if (urlStr.isEmpty()) { return; } if (filterWidget->isMimeFilter()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(urlStr, QMimeDatabase::MatchExtension); if (mime.isValid() && !mime.isDefault()) { if (filterWidget->currentFilter() != mime.name() && filterWidget->filters().indexOf(mime.name()) != -1) { filterWidget->setCurrentFilter(mime.name()); } } } else { QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename // accept any match to honor the user's selection; see later code handling the "*" match if (!findMatchingFilter(filterWidget->currentFilter(), filename).isEmpty()) { return; } const QStringList list = filterWidget->filters(); for (const QString &filter : list) { QString match = findMatchingFilter(filter, filename); if (!match.isEmpty()) { if (match != QLatin1String("*")) { // never match the catch-all filter filterWidget->setCurrentFilter(filter); } return; // do not repeat, could match a later filter } } } } } // applies only to a file that doesn't already exist void KFileWidgetPrivate::appendExtension(QUrl &url) { // qDebug(); if (!autoSelectExtCheckBox->isChecked() || extension.isEmpty()) { return; } QString fileName = url.fileName(); if (fileName.isEmpty()) { return; } // qDebug() << "appendExtension(" << url << ")"; const int len = fileName.length(); const int dot = fileName.lastIndexOf(QLatin1Char('.')); const bool suppressExtension = (dot == len - 1); const bool unspecifiedExtension = (dot <= 0); // don't KIO::Stat if unnecessary if (!(suppressExtension || unspecifiedExtension)) { return; } // exists? KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (res) { // qDebug() << "\tfile exists - won't append extension"; return; } // suppress automatically append extension? if (suppressExtension) { // // Strip trailing dot // This allows lazy people to have autoSelectExtCheckBox->isChecked // but don't want a file extension to be appended // e.g. "README." will make a file called "README" // // If you really want a name like "README.", then type "README.." // and the trailing dot will be removed (or just stop being lazy and // turn off this feature so that you can type "README.") // // qDebug() << "\tstrip trailing dot"; QString path = url.path(); path.chop(1); url.setPath(path); } // evilmatically append extension :) if the user hasn't specified one else if (unspecifiedExtension) { // qDebug() << "\tappending extension \'" << extension << "\'..."; url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash url.setPath(url.path() + fileName + extension); // qDebug() << "\tsaving as \'" << url << "\'"; } } // adds the selected files/urls to 'recent documents' void KFileWidgetPrivate::addToRecentDocuments() { int m = ops->mode(); int atmost = KRecentDocument::maximumItems(); //don't add more than we need. KRecentDocument::add() is pretty slow if (m & KFile::LocalOnly) { const QStringList files = q->selectedFiles(); QStringList::ConstIterator it = files.begin(); for (; it != files.end() && atmost > 0; ++it) { KRecentDocument::add(QUrl::fromLocalFile(*it)); atmost--; } } else { // urls const QList urls = q->selectedUrls(); QList::ConstIterator it = urls.begin(); for (; it != urls.end() && atmost > 0; ++it) { if ((*it).isValid()) { KRecentDocument::add(*it); atmost--; } } } } KUrlComboBox *KFileWidget::locationEdit() const { return d->locationEdit; } KFileFilterCombo *KFileWidget::filterWidget() const { return d->filterWidget; } KActionCollection *KFileWidget::actionCollection() const { return d->ops->actionCollection(); } void KFileWidgetPrivate::_k_toggleSpeedbar(bool show) { if (show) { initSpeedbar(); placesDock->show(); setLafBoxColumnWidth(); // check to see if they have a home item defined, if not show the home button QUrl homeURL; homeURL.setPath(QDir::homePath()); KFilePlacesModel *model = static_cast(placesView->model()); for (int rowIndex = 0; rowIndex < model->rowCount(); rowIndex++) { QModelIndex index = model->index(rowIndex, 0); QUrl url = model->url(index); if (homeURL.matches(url, QUrl::StripTrailingSlash)) { toolbar->removeAction(ops->actionCollection()->action(QStringLiteral("home"))); break; } } } else { if (q->sender() == placesDock && placesDock && placesDock->isVisibleTo(q)) { // we didn't *really* go away! the dialog was simply hidden or // we changed virtual desktops or ... return; } if (placesDock) { placesDock->hide(); } QAction *homeAction = ops->actionCollection()->action(QStringLiteral("home")); QAction *reloadAction = ops->actionCollection()->action(QStringLiteral("reload")); if (!toolbar->actions().contains(homeAction)) { toolbar->insertAction(reloadAction, homeAction); } // reset the lafbox to not follow the width of the splitter lafBox->setColumnMinimumWidth(0, 0); } static_cast(q->actionCollection()->action(QStringLiteral("toggleSpeedbar")))->setChecked(show); // if we don't show the places panel, at least show the places menu urlNavigator->setPlacesSelectorVisible(!show); } void KFileWidgetPrivate::_k_toggleBookmarks(bool show) { if (show) { if (bookmarkHandler) { return; } bookmarkHandler = new KFileBookmarkHandler(q); q->connect(bookmarkHandler, SIGNAL(openUrl(QString)), SLOT(_k_enterUrl(QString))); bookmarkButton = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), q); bookmarkButton->setDelayed(false); q->actionCollection()->addAction(QStringLiteral("bookmark"), bookmarkButton); bookmarkButton->setMenu(bookmarkHandler->menu()); bookmarkButton->setWhatsThis(i18n("This button allows you to bookmark specific locations. " "Click on this button to open the bookmark menu where you may add, " "edit or select a bookmark.

" "These bookmarks are specific to the file dialog, but otherwise operate " "like bookmarks elsewhere in KDE.
")); toolbar->addAction(bookmarkButton); } else if (bookmarkHandler) { delete bookmarkHandler; bookmarkHandler = nullptr; delete bookmarkButton; bookmarkButton = nullptr; } static_cast(q->actionCollection()->action(QStringLiteral("toggleBookmarks")))->setChecked(show); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass) { QString fileName; // result discarded return getStartUrl(startDir, recentDirClass, fileName); } // static, overloaded QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName) { recentDirClass.clear(); fileName.clear(); QUrl ret; bool useDefaultStartDir = startDir.isEmpty(); if (!useDefaultStartDir) { if (startDir.scheme() == QLatin1String("kfiledialog")) { // The startDir URL with this protocol may be in the format: // directory() fileName() // 1. kfiledialog:///keyword "/" keyword // 2. kfiledialog:///keyword?global "/" keyword // 3. kfiledialog:///keyword/ "/" keyword // 4. kfiledialog:///keyword/?global "/" keyword // 5. kfiledialog:///keyword/filename /keyword filename // 6. kfiledialog:///keyword/filename?global /keyword filename QString keyword; QString urlDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString urlFile = startDir.fileName(); if (urlDir == QLatin1String("/")) { // '1'..'4' above keyword = urlFile; fileName.clear(); } else { // '5' or '6' above keyword = urlDir.mid(1); fileName = urlFile; } if (startDir.query() == QLatin1String("global")) { recentDirClass = QStringLiteral("::%1").arg(keyword); } else { recentDirClass = QStringLiteral(":%1").arg(keyword); } ret = QUrl::fromLocalFile(KRecentDirs::dir(recentDirClass)); } else { // not special "kfiledialog" URL // "foo.png" only gives us a file name, the default start dir will be used. // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same // (and is the reason why we don't just use QUrl::isRelative()). // In all other cases (startDir contains a directory path, or has no // fileName for us anyway, such as smb://), startDir is indeed a dir url. if (!startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) { // can use start directory ret = startDir; // will be checked by stat later // If we won't be able to list it (e.g. http), then use default if (!KProtocolManager::supportsListing(ret)) { useDefaultStartDir = true; fileName = startDir.fileName(); } } else { // file name only fileName = startDir.fileName(); useDefaultStartDir = true; } } } if (useDefaultStartDir) { if (lastDirectory()->isEmpty()) { *lastDirectory() = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); const QUrl home(QUrl::fromLocalFile(QDir::homePath())); // if there is no docpath set (== home dir), we prefer the current // directory over it. We also prefer the homedir when our CWD is // different from our homedirectory or when the document dir // does not exist if (lastDirectory()->adjusted(QUrl::StripTrailingSlash) == home.adjusted(QUrl::StripTrailingSlash) || QDir::currentPath() != QDir::homePath() || !QDir(lastDirectory()->toLocalFile()).exists()) { *lastDirectory() = QUrl::fromLocalFile(QDir::currentPath()); } } ret = *lastDirectory(); } // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName; return ret; } void KFileWidget::setStartDir(const QUrl &directory) { if (directory.isValid()) { *lastDirectory() = directory; } } void KFileWidgetPrivate::setNonExtSelection() { // Enhanced rename: Don't highlight the file extension. QString filename = locationEditCurrentText(); QMimeDatabase db; QString extension = db.suffixForFileName(filename); if (!extension.isEmpty()) { locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1); } else { int lastDot = filename.lastIndexOf(QLatin1Char('.')); if (lastDot > 0) { locationEdit->lineEdit()->setSelection(0, lastDot); } else { locationEdit->lineEdit()->selectAll(); } } } // Sets the filter text to "File type" if the dialog is saving and a mimetype // filter has been set; otherwise, the text is "Filter:" void KFileWidgetPrivate::updateFilterText() { QString label; QString whatsThisText; if (operationMode == KFileWidget::Saving && filterWidget->isMimeFilter()) { label = i18n("&File type:"); whatsThisText = i18n("This is the file type selector. It is used to select the format that the file will be saved as."); } else { label = i18n("&Filter:"); whatsThisText = i18n("This is the filter to apply to the file list. " "File names that do not match the filter will not be shown.

" "You may select from one of the preset filters in the " "drop down menu, or you may enter a custom filter " "directly into the text area.

" "Wildcards such as * and ? are allowed.

"); } if (filterLabel) { filterLabel->setText(label); filterLabel->setWhatsThis(whatsThisText); } if (filterWidget) { filterWidget->setWhatsThis(whatsThisText); } } KToolBar *KFileWidget::toolBar() const { return d->toolbar; } void KFileWidget::setCustomWidget(QWidget *widget) { delete d->bottomCustomWidget; d->bottomCustomWidget = widget; // add it to the dialog, below the filter list box. // Change the parent so that this widget is a child of the main widget d->bottomCustomWidget->setParent(this); d->vbox->addWidget(d->bottomCustomWidget); //d->vbox->addSpacing(3); // can't do this every time... // FIXME: This should adjust the tab orders so that the custom widget // comes after the Cancel button. The code appears to do this, but the result // somehow screws up the tab order of the file path combo box. Not a major // problem, but ideally the tab order with a custom widget should be // the same as the order without one. setTabOrder(d->cancelButton, d->bottomCustomWidget); setTabOrder(d->bottomCustomWidget, d->urlNavigator); } void KFileWidget::setCustomWidget(const QString &text, QWidget *widget) { delete d->labeledCustomWidget; d->labeledCustomWidget = widget; QLabel *label = new QLabel(text, this); label->setAlignment(Qt::AlignRight); d->lafBox->addWidget(label, 2, 0, Qt::AlignVCenter); d->lafBox->addWidget(widget, 2, 1, Qt::AlignVCenter); } KDirOperator *KFileWidget::dirOperator() { return d->ops; } void KFileWidget::readConfig(KConfigGroup &group) { d->configGroup = group; d->readViewConfig(); d->readRecentFiles(); } QString KFileWidgetPrivate::locationEditCurrentText() const { return QDir::fromNativeSeparators(locationEdit->currentText()); } QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url) { if (url.isLocalFile()) { return url; } KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, q); bool res = statJob->exec(); if (!res) { return url; } const QString path = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH); if (!path.isEmpty()) { QUrl newUrl; newUrl.setPath(path); return newUrl; } return url; } void KFileWidgetPrivate::setInlinePreviewShown(bool show) { ops->setInlinePreviewShown(show); } void KFileWidget::setConfirmOverwrite(bool enable) { d->confirmOverwrite = enable; } void KFileWidget::setInlinePreviewShown(bool show) { d->setInlinePreviewShown(show); } QSize KFileWidget::dialogSizeHint() const { int fontSize = fontMetrics().height(); QSize goodSize(48 * fontSize, 30 * fontSize); QSize screenSize = QApplication::desktop()->availableGeometry(this).size(); QSize minSize(screenSize / 2); QSize maxSize(screenSize * qreal(0.9)); return (goodSize.expandedTo(minSize).boundedTo(maxSize)); } void KFileWidget::setViewMode(KFile::FileView mode) { d->ops->setView(mode); d->hasView = true; } void KFileWidget::setSupportedSchemes(const QStringList &schemes) { d->model->setSupportedSchemes(schemes); d->ops->setSupportedSchemes(schemes); d->urlNavigator->setCustomProtocols(schemes); } QStringList KFileWidget::supportedSchemes() const { return d->model->supportedSchemes(); } #include "moc_kfilewidget.cpp" diff --git a/src/ioslaves/trash/trashimpl.cpp b/src/ioslaves/trash/trashimpl.cpp index 78ee8428..b83e28aa 100644 --- a/src/ioslaves/trash/trashimpl.cpp +++ b/src/ioslaves/trash/trashimpl.cpp @@ -1,1420 +1,1419 @@ /* This file is part of the KDE project Copyright (C) 2004 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "trashimpl.h" #include "discspaceutil.h" #include "trashsizecache.h" #include "kiotrashdebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TrashImpl::TrashImpl() : QObject(), m_lastErrorCode(0), m_initStatus(InitToBeDone), m_homeDevice(0), m_trashDirectoriesScanned(false), // not using kio_trashrc since KIO uses that one already for kio_trash // so better have a separate one, for faster parsing by e.g. kmimetype.cpp m_config(QStringLiteral("trashrc"), KConfig::SimpleConfig) { QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(QDir::homePath()).constData(), &buff) == 0) { m_homeDevice = buff.st_dev; } else { qCWarning(KIO_TRASH) << "Should never happen: couldn't stat $HOME" << strerror(errno); } } /** * Test if a directory exists, create otherwise * @param _name full path of the directory * @return errorcode, or 0 if the dir was created or existed already * Warning, don't use return value like a bool */ int TrashImpl::testDir(const QString &_name) const { DIR *dp = ::opendir(QFile::encodeName(_name).constData()); if (!dp) { QString name = _name; if (name.endsWith(QLatin1Char('/'))) { name.chop(1); } bool ok = QDir().mkdir(name); if (!ok && QFile::exists(name)) { #if 0 // this would require to use SlaveBase's method to ask the question //int ret = KMessageBox::warningYesNo( 0, i18n("%1 is a file, but KDE needs it to be a directory. Move it to %2.orig and create directory?").arg(name).arg(name) ); //if ( ret == KMessageBox::Yes ) { #endif QString new_name = name; name.append(QStringLiteral(".orig")); if (QFile::rename(name, new_name)) { ok = QDir().mkdir(name); } else { // foo.orig existed already. How likely is that? ok = false; } if (!ok) { return KIO::ERR_DIR_ALREADY_EXIST; } #if 0 //} else { // return 0; //} #endif } if (!ok) { //KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) ); qCWarning(KIO_TRASH) << "could not create" << name; return KIO::ERR_COULD_NOT_MKDIR; } else { //qCDebug(KIO_TRASH) << name << "created."; } } else { // exists already closedir(dp); } return 0; // success } void TrashImpl::deleteEmptyTrashInfrastructure() { #ifdef Q_OS_OSX // For each known trash directory... if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd() ; ++it) { const QString trashPath = it.value(); QString infoPath = trashPath + QLatin1String("/info"); //qCDebug(KIO_TRASH) << "empty Trash" << trashPath << "; removing infrastructure"; synchronousDel(infoPath, false, true); synchronousDel(trashPath + QLatin1String("/files"), false, true); if (trashPath.endsWith(QLatin1String("/KDE.trash"))) { synchronousDel(trashPath, false, true); } } #endif } bool TrashImpl::createTrashInfrastructure(int trashId, const QString &path) { int err; QString trashDir = path.isEmpty() ? trashDirectoryPath(trashId) : path; if ((err = testDir(trashDir))) { error(err, trashDir); return false; } if ((err = testDir(trashDir + QLatin1String("/info")))) { error(err, trashDir + QLatin1String("/info")); return false; } if ((err = testDir(trashDir + QLatin1String("/files")))) { error(err, trashDir + QLatin1String("/files")); return false; } return true; } bool TrashImpl::init() { if (m_initStatus == InitOK) { return true; } if (m_initStatus == InitError) { return false; } // Check the trash directory and its info and files subdirs // see also kdesktop/init.cc for first time initialization m_initStatus = InitError; #ifndef Q_OS_OSX // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default. const QString xdgDataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/'); if (!QDir().mkpath(xdgDataDir)) { qCWarning(KIO_TRASH) << "failed to create" << xdgDataDir; return false; } const QString trashDir = xdgDataDir + QLatin1String("Trash"); if (!createTrashInfrastructure(0, trashDir)) { return false; } #else // we DO NOT create ~/.Trash on OS X, that's the operating system's privilege QString trashDir = QDir::homePath() + QLatin1String("/.Trash"); if (!QFileInfo(trashDir).isDir()) { error(KIO::ERR_DOES_NOT_EXIST, trashDir); return false; } trashDir += QLatin1String("/KDE.trash"); // we don't have to call createTrashInfrastructure() here because it'll be called when needed. #endif m_trashDirectories.insert(0, trashDir); m_initStatus = InitOK; //qCDebug(KIO_TRASH) << "initialization OK, home trash dir:" << trashDir; return true; } void TrashImpl::migrateOldTrash() { qCDebug(KIO_TRASH); KConfigGroup g(KSharedConfig::openConfig(), "Paths"); const QString oldTrashDir = g.readPathEntry("Trash", QString()); if (oldTrashDir.isEmpty()) { return; } const QStringList entries = listDir(oldTrashDir); bool allOK = true; for (QString srcPath : entries) { if (srcPath == QLatin1Char('.') || srcPath == QLatin1String("..") || srcPath == QLatin1String(".directory")) { continue; } srcPath.prepend(oldTrashDir); // make absolute int trashId; QString fileId; if (!createInfo(srcPath, trashId, fileId)) { qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath; allOK = false; } else { bool ok = moveToTrash(srcPath, trashId, fileId); if (!ok) { (void)deleteInfo(trashId, fileId); qCWarning(KIO_TRASH) << "Trash migration: failed to create info for" << srcPath; allOK = false; } else { qCDebug(KIO_TRASH) << "Trash migration: moved" << srcPath; } } } if (allOK) { // We need to remove the old one, otherwise the desktop will have two trashcans... qCDebug(KIO_TRASH) << "Trash migration: all OK, removing old trash directory"; synchronousDel(oldTrashDir, false, true); } } bool TrashImpl::createInfo(const QString &origPath, int &trashId, QString &fileId) { //qCDebug(KIO_TRASH) << origPath; // Check source const QByteArray origPath_c(QFile::encodeName(origPath)); // off_t should be 64bit on Unix systems to have large file support // FIXME: on windows this gets disabled until trash gets integrated // BUG: 165449 #ifndef Q_OS_WIN Q_STATIC_ASSERT(sizeof(off_t) >= 8); #endif QT_STATBUF buff_src; if (QT_LSTAT(origPath_c.data(), &buff_src) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, origPath); } else { error(KIO::ERR_DOES_NOT_EXIST, origPath); } return false; } // Choose destination trash trashId = findTrashDirectory(origPath); if (trashId < 0) { qCWarning(KIO_TRASH) << "OUCH - internal error, TrashImpl::findTrashDirectory returned" << trashId; return false; // ### error() needed? } //qCDebug(KIO_TRASH) << "trashing to" << trashId; // Grab original filename QUrl url = QUrl::fromLocalFile(origPath); url = url.adjusted(QUrl::StripTrailingSlash); const QString origFileName = url.fileName(); // Make destination file in info/ #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif url.setPath(infoPath(trashId, origFileName)); // we first try with origFileName QUrl baseDirectory = QUrl::fromLocalFile(url.path()); // Here we need to use O_EXCL to avoid race conditions with other kioslave processes int fd = 0; QString fileName; do { //qCDebug(KIO_TRASH) << "trying to create" << url.path(); fd = ::open(QFile::encodeName(url.path()).constData(), O_WRONLY | O_CREAT | O_EXCL, 0600); if (fd < 0) { if (errno == EEXIST) { fileName = url.fileName(); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + KFileUtils::suggestName(baseDirectory, fileName)); // and try again on the next iteration } else { error(KIO::ERR_COULD_NOT_WRITE, url.path()); return false; } } } while (fd < 0); const QString infoPath = url.path(); fileId = url.fileName(); Q_ASSERT(fileId.endsWith(QLatin1String(".trashinfo"))); fileId.chop(10); // remove .trashinfo from fileId FILE *file = ::fdopen(fd, "w"); if (!file) { // can't see how this would happen error(KIO::ERR_COULD_NOT_WRITE, infoPath); return false; } // Contents of the info file. We could use KSimpleConfig, but that would // mean closing and reopening fd, i.e. opening a race condition... QByteArray info = "[Trash Info]\n"; info += "Path="; // Escape filenames according to the way they are encoded on the filesystem // All this to basically get back to the raw 8-bit representation of the filename... if (trashId == 0) { // home trash: absolute path info += QUrl::toPercentEncoding(origPath, "/"); } else { info += QUrl::toPercentEncoding(makeRelativePath(topDirectoryPath(trashId), origPath), "/"); } info += '\n'; info += "DeletionDate=" + QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1() + '\n'; size_t sz = info.size(); size_t written = ::fwrite(info.data(), 1, sz, file); if (written != sz) { ::fclose(file); QFile::remove(infoPath); error(KIO::ERR_DISK_FULL, infoPath); return false; } ::fclose(file); //qCDebug(KIO_TRASH) << "info file created in trashId=" << trashId << ":" << fileId; return true; } QString TrashImpl::makeRelativePath(const QString &topdir, const QString &path) { QString realPath = QFileInfo(path).canonicalFilePath(); if (realPath.isEmpty()) { // shouldn't happen realPath = path; } // topdir ends with '/' #ifndef Q_OS_WIN if (realPath.startsWith(topdir)) { #else if (realPath.startsWith(topdir, Qt::CaseInsensitive)) { #endif const QString rel = realPath.mid(topdir.length()); Q_ASSERT(rel[0] != QLatin1Char('/')); return rel; } else { // shouldn't happen... qCWarning(KIO_TRASH) << "Couldn't make relative path for" << realPath << "(" << path << "), with topdir=" << topdir; return realPath; } } void TrashImpl::enterLoop() { QEventLoop eventLoop; connect(this, &TrashImpl::leaveModality, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } QString TrashImpl::infoPath(int trashId, const QString &fileId) const { const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/info/") + fileId + QLatin1String(".trashinfo"); return trashPath; } QString TrashImpl::filesPath(int trashId, const QString &fileId) const { const QString trashPath = trashDirectoryPath(trashId) + QLatin1String("/files/") + fileId; return trashPath; } bool TrashImpl::deleteInfo(int trashId, const QString &fileId) { #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif bool ok = QFile::remove(infoPath(trashId, fileId)); if (ok) { fileRemoved(); } return ok; } bool TrashImpl::moveToTrash(const QString &origPath, int trashId, const QString &fileId) { //qCDebug(KIO_TRASH) << "Trashing" << origPath << trashId << fileId; if (!adaptTrashSize(origPath, trashId)) { return false; } const qulonglong pathSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif const QString dest = filesPath(trashId, fileId); if (!move(origPath, dest)) { // Maybe the move failed due to no permissions to delete source. // In that case, delete dest to keep things consistent, since KIO doesn't do it. if (QFileInfo(dest).isFile()) { QFile::remove(dest); } else { synchronousDel(dest, false, true); } return false; } if (QFileInfo(dest).isDir()) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.add(fileId, pathSize); } fileAdded(); return true; } bool TrashImpl::moveFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath) { QString src = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { src += QLatin1Char('/') + relativePath; } if (!move(src, dest)) { return false; } TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.remove(fileId); return true; } bool TrashImpl::move(const QString &src, const QString &dest) { if (directRename(src, dest)) { // This notification is done by KIO::moveAs when using the code below // But if we do a direct rename we need to do the notification ourselves org::kde::KDirNotify::emitFilesAdded(QUrl::fromLocalFile(dest)); return true; } if (m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION) { return false; } QUrl urlSrc = QUrl::fromLocalFile(src); QUrl urlDest = QUrl::fromLocalFile(dest); //qCDebug(KIO_TRASH) << urlSrc << "->" << urlDest; KIO::CopyJob *job = KIO::moveAs(urlSrc, urlDest, KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); return m_lastErrorCode == 0; } void TrashImpl::jobFinished(KJob *job) { //qCDebug(KIO_TRASH) << "error=" << job->error() << job->errorText(); error(job->error(), job->errorText()); emit leaveModality(); } bool TrashImpl::copyToTrash(const QString &origPath, int trashId, const QString &fileId) { //qCDebug(KIO_TRASH); if (!adaptTrashSize(origPath, trashId)) { return false; } const qulonglong pathSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif const QString dest = filesPath(trashId, fileId); if (!copy(origPath, dest)) { return false; } if (QFileInfo(dest).isDir()) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.add(fileId, pathSize); } fileAdded(); return true; } bool TrashImpl::copyFromTrash(const QString &dest, int trashId, const QString &fileId, const QString &relativePath) { QString src = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { src += QLatin1Char('/') + relativePath; } return copy(src, dest); } bool TrashImpl::copy(const QString &src, const QString &dest) { // kio_file's copy() method is quite complex (in order to be fast), let's just call it... m_lastErrorCode = 0; QUrl urlSrc = QUrl::fromLocalFile(src); QUrl urlDest = QUrl::fromLocalFile(dest); //qCDebug(KIO_TRASH) << "copying" << src << "to" << dest; KIO::CopyJob *job = KIO::copyAs(urlSrc, urlDest, KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); return m_lastErrorCode == 0; } bool TrashImpl::directRename(const QString &src, const QString &dest) { //qCDebug(KIO_TRASH) << src << "->" << dest; // Do not use QFile::rename here, we need to be able to move broken symlinks too // (and we need to make sure errno is set) if (::rename(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) != 0) { if (errno == EXDEV) { error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); } else { if ((errno == EACCES) || (errno == EPERM)) { error(KIO::ERR_ACCESS_DENIED, dest); } else if (errno == EROFS) { // The file is on a read-only filesystem error(KIO::ERR_CANNOT_DELETE, src); } else { error(KIO::ERR_CANNOT_RENAME, src); } } return false; } return true; } bool TrashImpl::moveInTrash(int trashId, const QString &oldFileId, const QString &newFileId) { m_lastErrorCode = 0; const QString oldInfo = infoPath(trashId, oldFileId); const QString oldFile = filesPath(trashId, oldFileId); const QString newInfo = infoPath(trashId, newFileId); const QString newFile = filesPath(trashId, newFileId); if (directRename(oldInfo, newInfo)) { if (directRename(oldFile, newFile)) { // success return true; } else { // rollback directRename(newInfo, oldInfo); } } return false; } #if 0 bool TrashImpl::mkdir(int trashId, const QString &fileId, int permissions) { const QString path = filesPath(trashId, fileId); if (KDE_mkdir(QFile::encodeName(path), permissions) != 0) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); return false; } else if (errno == ENOSPC) { error(KIO::ERR_DISK_FULL, path); return false; } else { error(KIO::ERR_COULD_NOT_MKDIR, path); return false; } } else { if (permissions != -1) { ::chmod(QFile::encodeName(path), permissions); } } return true; } #endif bool TrashImpl::del(int trashId, const QString &fileId) { #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif QString info = infoPath(trashId, fileId); QString file = filesPath(trashId, fileId); QByteArray info_c = QFile::encodeName(info); QT_STATBUF buff; if (QT_LSTAT(info_c.data(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, file); } else { error(KIO::ERR_DOES_NOT_EXIST, file); } return false; } const bool isDir = QFileInfo(file).isDir(); if (!synchronousDel(file, true, isDir)) { return false; } if (isDir) { TrashSizeCache trashSize(trashDirectoryPath(trashId)); trashSize.remove(fileId); } QFile::remove(info); fileRemoved(); return true; } bool TrashImpl::synchronousDel(const QString &path, bool setLastErrorCode, bool isDir) { const int oldErrorCode = m_lastErrorCode; const QString oldErrorMsg = m_lastErrorMessage; QUrl url = QUrl::fromLocalFile(path); // First ensure that all dirs have u+w permissions, // otherwise we won't be able to delete files in them (#130780). if (isDir) { // qCDebug(KIO_TRASH) << "chmod'ing" << url; KFileItem fileItem(url, QStringLiteral("inode/directory"), KFileItem::Unknown); KFileItemList fileItemList; fileItemList.append(fileItem); KIO::ChmodJob *chmodJob = KIO::chmod(fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo); connect(chmodJob, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); } KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo); connect(job, &KJob::result, this, &TrashImpl::jobFinished); enterLoop(); bool ok = m_lastErrorCode == 0; if (!setLastErrorCode) { m_lastErrorCode = oldErrorCode; m_lastErrorMessage = oldErrorMsg; } return ok; } bool TrashImpl::emptyTrash() { //qCDebug(KIO_TRASH); // The naive implementation "delete info and files in every trash directory" // breaks when deleted directories contain files owned by other users. // We need to ensure that the .trashinfo file is only removed when the // corresponding files could indeed be removed (#116371) // On the other hand, we certainly want to remove any file that has no associated // .trashinfo file for some reason (#167051) QSet unremovableFiles; int myErrorCode = 0; QString myErrorMsg; const TrashedFileInfoList fileInfoList = list(); TrashedFileInfoList::const_iterator it = fileInfoList.begin(); const TrashedFileInfoList::const_iterator end = fileInfoList.end(); for (; it != end; ++it) { const TrashedFileInfo &info = *it; const QString filesPath = info.physicalPath; if (synchronousDel(filesPath, true, true) || m_lastErrorCode == KIO::ERR_DOES_NOT_EXIST) { QFile::remove(infoPath(info.trashId, info.fileId)); } else { // error code is set by synchronousDel, let's remember it // (so that successfully removing another file doesn't erase the error) myErrorCode = m_lastErrorCode; myErrorMsg = m_lastErrorMessage; // and remember not to remove this file unremovableFiles.insert(filesPath); qCDebug(KIO_TRASH) << "Unremovable:" << filesPath; } TrashSizeCache trashSize(trashDirectoryPath(info.trashId)); trashSize.clear(); } // Now do the orphaned-files cleanup TrashDirMap::const_iterator trit = m_trashDirectories.constBegin(); for (; trit != m_trashDirectories.constEnd(); ++trit) { //const int trashId = trit.key(); QString filesDir = trit.value(); filesDir += QLatin1String("/files"); const QStringList list = listDir(filesDir); for (const QString &fileName : list) { if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) { continue; } const QString filePath = filesDir + QLatin1Char('/') + fileName; if (!unremovableFiles.contains(filePath)) { qCWarning(KIO_TRASH) << "Removing orphaned file" << filePath; QFile::remove(filePath); } } } m_lastErrorCode = myErrorCode; m_lastErrorMessage = myErrorMsg; fileRemoved(); return m_lastErrorCode == 0; } TrashImpl::TrashedFileInfoList TrashImpl::list() { // Here we scan for trash directories unconditionally. This allows // noticing plugged-in [e.g. removable] devices, or new mounts etc. scanTrashDirectories(); TrashedFileInfoList lst; // For each known trash directory... TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { const int trashId = it.key(); QString infoPath = it.value(); infoPath += QLatin1String("/info"); // Code taken from kio_file const QStringList entryNames = listDir(infoPath); //char path_buffer[PATH_MAX]; //getcwd(path_buffer, PATH_MAX - 1); //if ( chdir( infoPathEnc ) ) // continue; for (QStringList::const_iterator entryIt = entryNames.constBegin(), entryEnd = entryNames.constEnd(); entryIt != entryEnd; ++entryIt) { QString fileName = *entryIt; if (fileName == QLatin1Char('.') || fileName == QLatin1String("..")) { continue; } if (!fileName.endsWith(QLatin1String(".trashinfo"))) { qCWarning(KIO_TRASH) << "Invalid info file found in" << infoPath << ":" << fileName; continue; } fileName.chop(10); TrashedFileInfo info; if (infoForFile(trashId, fileName, info)) { lst << info; } } } return lst; } // Returns the entries in a given directory - including "." and ".." QStringList TrashImpl::listDir(const QString &physicalPath) { QDir dir(physicalPath); return dir.entryList(QDir::Dirs | QDir::Files | QDir::Hidden | QDir::System); } bool TrashImpl::infoForFile(int trashId, const QString &fileId, TrashedFileInfo &info) { //qCDebug(KIO_TRASH) << trashId << fileId; info.trashId = trashId; // easy :) info.fileId = fileId; // equally easy info.physicalPath = filesPath(trashId, fileId); return readInfoFile(infoPath(trashId, fileId), info, trashId); } bool TrashImpl::trashSpaceInfo(const QString &path, TrashSpaceInfo &info) { const int trashId = findTrashDirectory(path); if (trashId < 0) { qCWarning(KIO_TRASH) << "No trash directory found! TrashImpl::findTrashDirectory returned" << trashId; return false; } const KConfig config(QStringLiteral("ktrashrc")); const QString trashPath = trashDirectoryPath(trashId); const auto group = config.group(trashPath); const bool useSizeLimit = group.readEntry("UseSizeLimit", true); const double percent = group.readEntry("Percent", 10.0); DiscSpaceUtil util(trashPath + QLatin1String("/files/")); qulonglong total = util.size(); if (useSizeLimit) { total *= percent / 100.0; } TrashSizeCache trashSize(trashPath); const qulonglong used = trashSize.calculateSize(); info.totalSize = total; info.availableSize = total - used; return true; } bool TrashImpl::readInfoFile(const QString &infoPath, TrashedFileInfo &info, int trashId) { KConfig cfg(infoPath, KConfig::SimpleConfig); if (!cfg.hasGroup("Trash Info")) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath); return false; } const KConfigGroup group = cfg.group("Trash Info"); info.origPath = QUrl::fromPercentEncoding(group.readEntry("Path").toLatin1()); if (info.origPath.isEmpty()) { return false; // path is mandatory... } if (trashId == 0) { Q_ASSERT(info.origPath[0] == QLatin1Char('/')); } else { const QString topdir = topDirectoryPath(trashId); // includes trailing slash info.origPath.prepend(topdir); } const QString line = group.readEntry("DeletionDate"); if (!line.isEmpty()) { info.deletionDate = QDateTime::fromString(line, Qt::ISODate); } return true; } QString TrashImpl::physicalPath(int trashId, const QString &fileId, const QString &relativePath) { QString filePath = filesPath(trashId, fileId); if (!relativePath.isEmpty()) { filePath += QLatin1Char('/') + relativePath; } return filePath; } void TrashImpl::error(int e, const QString &s) { if (e) { qCDebug(KIO_TRASH) << e << s; } m_lastErrorCode = e; m_lastErrorMessage = s; } bool TrashImpl::isEmpty() const { // For each known trash directory... if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } TrashDirMap::const_iterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { QString infoPath = it.value(); infoPath += QLatin1String("/info"); DIR *dp = ::opendir(QFile::encodeName(infoPath).constData()); if (dp) { struct dirent *ep; ep = readdir(dp); ep = readdir(dp); // ignore '.' and '..' dirent ep = readdir(dp); // look for third file closedir(dp); if (ep != nullptr) { //qCDebug(KIO_TRASH) << ep->d_name << "in" << infoPath << "-> not empty"; return false; // not empty } } } return true; } void TrashImpl::fileAdded() { m_config.reparseConfiguration(); KConfigGroup group = m_config.group("Status"); if (group.readEntry("Empty", true) == true) { group.writeEntry("Empty", false); m_config.sync(); } // The apps showing the trash (e.g. kdesktop) will be notified // of this change when KDirNotify::FilesAdded("trash:/") is emitted, // which will be done by the job soon after this. } void TrashImpl::fileRemoved() { if (isEmpty()) { deleteEmptyTrashInfrastructure(); KConfigGroup group = m_config.group("Status"); group.writeEntry("Empty", true); m_config.sync(); org::kde::KDirNotify::emitFilesChanged({QUrl::fromEncoded("trash:/")}); } // The apps showing the trash (e.g. kdesktop) will be notified // of this change when KDirNotify::FilesRemoved(...) is emitted, // which will be done by the job soon after this. } #ifdef Q_OS_OSX #include #include #include int TrashImpl::idForMountPoint(const QString &mountPoint) const { DADiskRef disk; CFDictionaryRef descDict; DASessionRef session = DASessionCreate(NULL); int devId = -1; if (session) { QByteArray mp = QFile::encodeName(mountPoint); struct statfs statFS; statfs(mp.constData(), &statFS); disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, statFS.f_mntfromname); if (disk) { descDict = DADiskCopyDescription(disk); if (descDict) { CFNumberRef cfMajor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMajorKey); CFNumberRef cfMinor = (CFNumberRef)CFDictionaryGetValue(descDict, kDADiskDescriptionMediaBSDMinorKey); int major, minor; if (CFNumberGetValue(cfMajor, kCFNumberIntType, &major) && CFNumberGetValue(cfMinor, kCFNumberIntType, &minor)) { qCWarning(KIO_TRASH) << "major=" << major << " minor=" << minor; devId = 1000 * major + minor; } CFRelease(cfMajor); CFRelease(cfMinor); } else { qCWarning(KIO_TRASH) << "couldn't get DADiskCopyDescription from" << disk; } CFRelease(disk); } else { qCWarning(KIO_TRASH) << "DADiskCreateFromBSDName failed on statfs from" << mp; } CFRelease(session); } else { qCWarning(KIO_TRASH) << "couldn't create DASession"; } return devId; } #else int TrashImpl::idForDevice(const Solid::Device &device) const { const Solid::Block *block = device.as(); if (block) { //qCDebug(KIO_TRASH) << "major=" << block->deviceMajor() << "minor=" << block->deviceMinor(); return block->deviceMajor() * 1000 + block->deviceMinor(); } else { const Solid::NetworkShare *netshare = device.as(); if (netshare) { QString url = netshare->url().url(); QLockFile configLock(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/trashrc.nextid.lock")); if (!configLock.lock()) { return -1; } m_config.reparseConfiguration(); KConfigGroup group = m_config.group("NetworkShares"); int id = group.readEntry(url, -1); if (id == -1) { id = group.readEntry("NextID", 0); //qCDebug(KIO_TRASH) << "new share=" << url << " id=" << id; group.writeEntry(url, id); group.writeEntry("NextID", id + 1); group.sync(); } return 6000000 + id; } // Not a block device nor a network share return -1; } } void TrashImpl::refreshDevices() const { // this is needed because Solid's fstab backend uses QSocketNotifier // to get notifications about changes to mtab // otherwise we risk getting old device list qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } #endif int TrashImpl::findTrashDirectory(const QString &origPath) { //qCDebug(KIO_TRASH) << origPath; // First check if same device as $HOME, then we use the home trash right away. QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(origPath).constData(), &buff) == 0 && buff.st_dev == m_homeDevice) { return 0; } KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(origPath); if (!mp) { //qCDebug(KIO_TRASH) << "KMountPoint found no mount point for" << origPath; return 0; } QString mountPoint = mp->mountPoint(); const QString trashDir = trashForMountPoint(mountPoint, true); //qCDebug(KIO_TRASH) << "mountPoint=" << mountPoint << "trashDir=" << trashDir; #ifndef Q_OS_OSX if (trashDir.isEmpty()) { return 0; // no trash available on partition } #endif int id = idForTrashDirectory(trashDir); if (id > -1) { //qCDebug(KIO_TRASH) << "known with id" << id; return id; } // new trash dir found, register it // but we need stability in the trash IDs, so that restoring or asking // for properties works even kio_trash gets killed because idle. #if 0 qCDebug(KIO_TRASH) << "found" << trashDir; m_trashDirectories.insert(++m_lastId, trashDir); if (!mountPoint.endsWith('/')) { mountPoint += '/'; } m_topDirectories.insert(m_lastId, mountPoint); return m_lastId; #endif #ifdef Q_OS_OSX id = idForMountPoint(mountPoint); #else refreshDevices(); const QString query = QLatin1String("[StorageAccess.accessible == true AND StorageAccess.filePath == '") + mountPoint + QLatin1String("']"); //qCDebug(KIO_TRASH) << "doing solid query:" << query; const QList lst = Solid::Device::listFromQuery(query); //qCDebug(KIO_TRASH) << "got" << lst.count() << "devices"; if (lst.isEmpty()) { // not a device. Maybe some tmpfs mount for instance. return 0; // use the home trash instead } // Pretend we got exactly one... const Solid::Device device = lst[0]; // new trash dir found, register it id = idForDevice(device); #endif if (id == -1) { return 0; } m_trashDirectories.insert(id, trashDir); //qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << id; if (!mountPoint.endsWith(QLatin1Char('/'))) { mountPoint += QLatin1Char('/'); } m_topDirectories.insert(id, mountPoint); return idForTrashDirectory(trashDir); } void TrashImpl::scanTrashDirectories() const { #ifndef Q_OS_OSX refreshDevices(); #endif const QList lst = Solid::Device::listFromQuery(QStringLiteral("StorageAccess.accessible == true")); for (const Solid::Device &device : lst) { QString topdir = device.as()->filePath(); QString trashDir = trashForMountPoint(topdir, false); if (!trashDir.isEmpty()) { // OK, trashDir is a valid trash directory. Ensure it's registered. int trashId = idForTrashDirectory(trashDir); if (trashId == -1) { // new trash dir found, register it #ifdef Q_OS_OSX trashId = idForMountPoint(topdir); #else trashId = idForDevice(device); #endif if (trashId == -1) { continue; } m_trashDirectories.insert(trashId, trashDir); //qCDebug(KIO_TRASH) << "found" << trashDir << "gave it id" << trashId; if (!topdir.endsWith(QLatin1Char('/'))) { topdir += QLatin1Char('/'); } m_topDirectories.insert(trashId, topdir); } } } m_trashDirectoriesScanned = true; } TrashImpl::TrashDirMap TrashImpl::trashDirectories() const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } return m_trashDirectories; } TrashImpl::TrashDirMap TrashImpl::topDirectories() const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } return m_topDirectories; } QString TrashImpl::trashForMountPoint(const QString &topdir, bool createIfNeeded) const { // (1) Administrator-created $topdir/.Trash directory #ifndef Q_OS_OSX const QString rootTrashDir = topdir + QLatin1String("/.Trash"); #else const QString rootTrashDir = topdir + QLatin1String("/.Trashes"); #endif const QByteArray rootTrashDir_c = QFile::encodeName(rootTrashDir); // Can't use QFileInfo here since we need to test for the sticky bit uid_t uid = getuid(); QT_STATBUF buff; const unsigned int requiredBits = S_ISVTX; // Sticky bit required if (QT_LSTAT(rootTrashDir_c.constData(), &buff) == 0) { if ((S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && ((buff.st_mode & requiredBits) == requiredBits) && (::access(rootTrashDir_c.constData(), W_OK) == 0) // must be user-writable ) { #ifndef Q_OS_OSX const QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid); #else QString trashDir = rootTrashDir + QLatin1Char('/') + QString::number(uid); #endif const QByteArray trashDir_c = QFile::encodeName(trashDir); if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) { if ((buff.st_uid == uid) // must be owned by user && (S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && (buff.st_mode & 0777) == 0700) { // rwx for user #ifdef Q_OS_OSX trashDir += QStringLiteral("/KDE.trash"); #endif return trashDir; } qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it"; } else if (createIfNeeded && initTrashDirectory(trashDir_c)) { return trashDir; } } else { qCWarning(KIO_TRASH) << "Root trash dir" << rootTrashDir << "exists but didn't pass the security checks, can't use it"; } } #ifndef Q_OS_OSX // (2) $topdir/.Trash-$uid const QString trashDir = topdir + QLatin1String("/.Trash-") + QString::number(uid); const QByteArray trashDir_c = QFile::encodeName(trashDir); if (QT_LSTAT(trashDir_c.constData(), &buff) == 0) { if ((buff.st_uid == uid) // must be owned by user && (S_ISDIR(buff.st_mode)) // must be a dir && (!S_ISLNK(buff.st_mode)) // not a symlink && ((buff.st_mode & 0777) == 0700)) { // rwx for user, ------ for group and others if (checkTrashSubdirs(trashDir_c)) { return trashDir; } } qCWarning(KIO_TRASH) << "Directory" << trashDir << "exists but didn't pass the security checks, can't use it"; // Exists, but not useable return QString(); } if (createIfNeeded && initTrashDirectory(trashDir_c)) { return trashDir; } #endif return QString(); } int TrashImpl::idForTrashDirectory(const QString &trashDir) const { // If this is too slow we can always use a reverse map... TrashDirMap::ConstIterator it = m_trashDirectories.constBegin(); for (; it != m_trashDirectories.constEnd(); ++it) { if (it.value() == trashDir) { return it.key(); } } return -1; } bool TrashImpl::initTrashDirectory(const QByteArray &trashDir_c) const { //qCDebug(KIO_TRASH) << trashDir_c; if (mkdir(trashDir_c.constData(), 0700) != 0) { return false; } //qCDebug(KIO_TRASH); // This trash dir will be useable only if the directory is owned by user. // In theory this is the case, but not on e.g. USB keys... uid_t uid = getuid(); QT_STATBUF buff; if (QT_LSTAT(trashDir_c.constData(), &buff) != 0) { return false; // huh? } if ((buff.st_uid == uid) // must be owned by user && ((buff.st_mode & 0777) == 0700)) { // rwx for user, --- for group and others return checkTrashSubdirs(trashDir_c); } else { qCWarning(KIO_TRASH) << trashDir_c << "just created, by it doesn't have the right permissions, probably some strange unsupported filesystem"; ::rmdir(trashDir_c.constData()); return false; } return true; } bool TrashImpl::checkTrashSubdirs(const QByteArray &trashDir_c) const { // testDir currently works with a QString - ## optimize QString trashDir = QFile::decodeName(trashDir_c); const QString info = trashDir + QLatin1String("/info"); if (testDir(info) != 0) { return false; } const QString files = trashDir + QLatin1String("/files"); if (testDir(files) != 0) { return false; } return true; } QString TrashImpl::trashDirectoryPath(int trashId) const { // Never scanned for trash dirs? (This can happen after killing kio_trash // and reusing a directory listing from the earlier instance.) if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } Q_ASSERT(m_trashDirectories.contains(trashId)); return m_trashDirectories[trashId]; } QString TrashImpl::topDirectoryPath(int trashId) const { if (!m_trashDirectoriesScanned) { scanTrashDirectories(); } assert(trashId != 0); Q_ASSERT(m_topDirectories.contains(trashId)); return m_topDirectories[trashId]; } // Helper method. Creates a URL with the format trash:/trashid-fileid or // trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory. QUrl TrashImpl::makeURL(int trashId, const QString &fileId, const QString &relativePath) { QUrl url; url.setScheme(QStringLiteral("trash")); QString path = QLatin1Char('/') + QString::number(trashId) + QLatin1Char('-') + fileId; if (!relativePath.isEmpty()) { path += QLatin1Char('/') + relativePath; } url.setPath(path); return url; } // Helper method. Parses a trash URL with the URL scheme defined in makeURL. // The trash:/ URL itself isn't parsed here, must be caught by the caller before hand. bool TrashImpl::parseURL(const QUrl &url, int &trashId, QString &fileId, QString &relativePath) { if (url.scheme() != QLatin1String("trash")) { return false; } const QString path = url.path(); if (path.isEmpty()) { return false; } int start = 0; if (path[0] == QLatin1Char('/')) { // always true I hope start = 1; } int slashPos = path.indexOf(QLatin1Char('-'), 0); // don't match leading slash if (slashPos <= 0) { return false; } bool ok = false; trashId = path.midRef(start, slashPos - start).toInt(&ok); Q_ASSERT(ok); if (!ok) { return false; } start = slashPos + 1; slashPos = path.indexOf(QLatin1Char('/'), start); if (slashPos <= 0) { fileId = path.mid(start); relativePath.clear(); return true; } fileId = path.mid(start, slashPos - start); relativePath = path.mid(slashPos + 1); return true; } bool TrashImpl::adaptTrashSize(const QString &origPath, int trashId) { KConfig config(QStringLiteral("ktrashrc")); const QString trashPath = trashDirectoryPath(trashId); KConfigGroup group = config.group(trashPath); bool useTimeLimit = group.readEntry("UseTimeLimit", false); bool useSizeLimit = group.readEntry("UseSizeLimit", true); double percent = group.readEntry("Percent", 10.0); int actionType = group.readEntry("LimitReachedAction", 0); if (useTimeLimit) { // delete all files in trash older than X days const int maxDays = group.readEntry("Days", 7); const QDateTime currentDate = QDateTime::currentDateTime(); const TrashedFileInfoList trashedFiles = list(); for (int i = 0; i < trashedFiles.count(); ++i) { struct TrashedFileInfo info = trashedFiles.at(i); if (info.trashId != trashId) { continue; } if (info.deletionDate.daysTo(currentDate) > maxDays) { del(info.trashId, info.fileId); } } } if (useSizeLimit) { // check if size limit exceeded // calculate size of the files to be put into the trash qulonglong additionalSize = DiscSpaceUtil::sizeOfPath(origPath); #ifdef Q_OS_OSX createTrashInfrastructure(trashId); #endif TrashSizeCache trashSize(trashPath); DiscSpaceUtil util(trashPath + QLatin1String("/files/")); if (util.usage(trashSize.calculateSize() + additionalSize) >= percent) { // before we start to remove any files from the trash, // check whether the new file will fit into the trash // at all... qulonglong partitionSize = util.size(); if ((((double)additionalSize / (double)partitionSize) * 100) >= percent) { m_lastErrorCode = KIO::ERR_SLAVE_DEFINED; m_lastErrorMessage = i18n("The file is too large to be trashed."); return false; } if (actionType == 0) { // warn the user only m_lastErrorCode = KIO::ERR_SLAVE_DEFINED; m_lastErrorMessage = i18n("The trash has reached its maximum size!\nCleanup the trash manually."); return false; } else { // lets start removing some other files from the trash QDir dir(trashPath + QLatin1String("/files")); QFileInfoList infoList; if (actionType == 1) { // delete oldest files first infoList = dir.entryInfoList(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Time | QDir::Reversed); } else if (actionType == 2) { // delete biggest files first infoList = dir.entryInfoList(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Size); } else { qWarning("Should never happen!"); } bool deleteFurther = true; for (int i = 0; (i < infoList.count()) && deleteFurther; ++i) { const QFileInfo &info = infoList.at(i); del(trashId, info.fileName()); // delete trashed file TrashSizeCache trashSize(trashPath); if (util.usage(trashSize.calculateSize() + additionalSize) < percent) { // check whether we have enough space now deleteFurther = false; } } } } } return true; } #include "moc_trashimpl.cpp" diff --git a/src/widgets/kurlcompletion.h b/src/widgets/kurlcompletion.h index bcac2ef4..60da5fad 100644 --- a/src/widgets/kurlcompletion.h +++ b/src/widgets/kurlcompletion.h @@ -1,201 +1,201 @@ /* This file is part of the KDE libraries Copyright (C) 2000 David Smith This class was inspired by a previous KUrlCompletion by Henner Zeller This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KURLCOMPLETION_H #define KURLCOMPLETION_H #include "kiowidgets_export.h" #include #include #include namespace KIO { class Job; } class QStringList; class KUrlCompletionPrivate; /** * @class KUrlCompletion kurlcompletion.h * * This class does completion of URLs including user directories (~user) * and environment variables. Remote URLs are passed to KIO. * * @short Completion of a single URL * @author David Smith */ class KIOWIDGETS_EXPORT KUrlCompletion : public KCompletion { Q_OBJECT public: /** * Determines how completion is done. * @li ExeCompletion - executables in $PATH or with full path. * @li FileCompletion - all files with full path or in dir(), URLs * are listed using KIO. * @li DirCompletion - Same as FileCompletion but only returns directories. */ enum Mode { ExeCompletion = 1, FileCompletion, DirCompletion }; /** * Constructs a KUrlCompletion object in FileCompletion mode. */ KUrlCompletion(); /** * This overloaded constructor allows you to set the Mode to ExeCompletion * or FileCompletion without using setMode. Default is FileCompletion. */ KUrlCompletion(Mode); /** * Destructs the KUrlCompletion object. */ virtual ~KUrlCompletion(); /** * Finds completions to the given text. * * Remote URLs are listed with KIO. For performance reasons, local files * are listed with KIO only if KURLCOMPLETION_LOCAL_KIO is set. * The completion is done asynchronously if KIO is used. * * Returns the first match for user, environment, and local dir completion * and QString() for asynchronous completion (KIO or threaded). * * @param text the text to complete * @return the first match, or QString() if not found */ QString makeCompletion(const QString &text) override; /** * Sets the current directory (used as base for completion). * Default = $HOME. * @param dir the current directory, as a URL (use QUrl::fromLocalFile for local paths) */ virtual void setDir(const QUrl &dir); /** * Returns the current directory, as it was given in setDir * @return the current directory, as a URL (use QUrl::toLocalFile for local paths) */ virtual QUrl dir() const; /** * Check whether asynchronous completion is in progress. * @return true if asynchronous completion is in progress */ virtual bool isRunning() const; /** * Stops asynchronous completion. */ virtual void stop(); /** * Returns the completion mode: exe or file completion (default FileCompletion). * @return the completion mode */ virtual Mode mode() const; /** * Changes the completion mode: exe or file completion * @param mode the new completion mode */ virtual void setMode(Mode mode); /** * Checks whether environment variables are completed and * whether they are replaced internally while finding completions. * Default is enabled. - * @return true if environment vvariables will be replaced + * @return true if environment variables will be replaced */ virtual bool replaceEnv() const; /** * Enables/disables completion and replacement (internally) of * environment variables in URLs. Default is enabled. * @param replace true to replace environment variables */ virtual void setReplaceEnv(bool replace); /** * Returns whether ~username is completed and whether ~username * is replaced internally with the user's home directory while * finding completions. Default is enabled. * @return true to replace tilde with the home directory */ virtual bool replaceHome() const; /** * Enables/disables completion of ~username and replacement * (internally) of ~username with the user's home directory. * Default is enabled. * @param replace true to replace tilde with the home directory */ virtual void setReplaceHome(bool replace); /** * Replaces username and/or environment variables, depending on the * current settings and returns the filtered url. Only works with * local files, i.e. returns back the original string for non-local * urls. * @param text the text to process * @return the path or URL resulting from this operation. If you * want to convert it to a QUrl, use QUrl::fromUserInput. */ QString replacedPath(const QString &text) const; /** * @internal I'll let ossi add a real one to KShell :) */ static QString replacedPath(const QString &text, bool replaceHome, bool replaceEnv = true); /** * Sets the mimetype filters for the file dialog. * @see QFileDialog::setMimeTypeFilters() * @since 5.38 */ void setMimeTypeFilters(const QStringList &mimeTypes); /** * Returns the mimetype filters for the file dialog. * @see QFileDialog::mimeTypeFilters() * @since 5.38 */ QStringList mimeTypeFilters() const; protected: // Called by KCompletion, adds '/' to directories void postProcessMatch(QString *match) const override; void postProcessMatches(QStringList *matches) const override; void postProcessMatches(KCompletionMatches *matches) const override; void customEvent(QEvent *e) override; // KF6 TODO: remove private: KUrlCompletionPrivate *const d; Q_PRIVATE_SLOT(d, void _k_slotEntries(KIO::Job *, const KIO::UDSEntryList &)) Q_PRIVATE_SLOT(d, void _k_slotIOFinished(KJob *)) }; #endif // KURLCOMPLETION_H diff --git a/src/widgets/kurlrequester.cpp b/src/widgets/kurlrequester.cpp index 84538d8b..5b5bd31d 100644 --- a/src/widgets/kurlrequester.cpp +++ b/src/widgets/kurlrequester.cpp @@ -1,708 +1,707 @@ /* This file is part of the KDE libraries Copyright (C) 1999,2000,2001 Carsten Pfeiffer Copyright (C) 2013 Teo Mrnjavac This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kurlrequester.h" #include "kio_widgets_debug.h" #include "../pathhelpers_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include class KUrlDragPushButton : public QPushButton { Q_OBJECT public: explicit KUrlDragPushButton(QWidget *parent) : QPushButton(parent) { new DragDecorator(this); } ~KUrlDragPushButton() override {} void setURL(const QUrl &url) { m_urls.clear(); m_urls.append(url); } private: class DragDecorator : public KDragWidgetDecoratorBase { public: explicit DragDecorator(KUrlDragPushButton *button) : KDragWidgetDecoratorBase(button), m_button(button) {} protected: QDrag *dragObject() override { if (m_button->m_urls.isEmpty()) { return nullptr; } QDrag *drag = new QDrag(m_button); QMimeData *mimeData = new QMimeData; mimeData->setUrls(m_button->m_urls); drag->setMimeData(mimeData); return drag; } private: KUrlDragPushButton *m_button; }; QList m_urls; }; class Q_DECL_HIDDEN KUrlRequester::KUrlRequesterPrivate { public: explicit KUrlRequesterPrivate(KUrlRequester *parent) : m_fileDialogModeWasDirAndFile(false), m_parent(parent), edit(nullptr), combo(nullptr), fileDialogMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly), fileDialogAcceptMode(QFileDialog::AcceptOpen) { } ~KUrlRequesterPrivate() { delete myCompletion; delete myFileDialog; } void init(); void setText(const QString &text) { if (combo) { if (combo->isEditable()) { combo->setEditText(text); } else { int i = combo->findText(text); if (i == -1) { combo->addItem(text); combo->setCurrentIndex(combo->count() - 1); } else { combo->setCurrentIndex(i); } } } else { edit->setText(text); } } void connectSignals(KUrlRequester *receiver) { if (combo) { connect(combo, &QComboBox::currentTextChanged, receiver, &KUrlRequester::textChanged); connect(combo, &QComboBox::editTextChanged, receiver, &KUrlRequester::textEdited); connect(combo, QOverload<>::of(&KComboBox::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); connect(combo, QOverload::of(&KComboBox::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } else if (edit) { connect(edit, &QLineEdit::textChanged, receiver, &KUrlRequester::textChanged); connect(edit, &QLineEdit::textEdited, receiver, &KUrlRequester::textEdited); connect(edit, QOverload<>::of(&QLineEdit::returnPressed), receiver, QOverload<>::of(&KUrlRequester::returnPressed)); if (auto kline = qobject_cast(edit)) { connect(kline, QOverload::of(&KLineEdit::returnPressed), receiver, QOverload::of(&KUrlRequester::returnPressed)); } } } void setCompletionObject(KCompletion *comp) { if (combo) { combo->setCompletionObject(comp); } else { edit->setCompletionObject(comp); } } void updateCompletionStartDir(const QUrl &newStartDir) { myCompletion->setDir(newStartDir); } QString text() const { return combo ? combo->currentText() : edit->text(); } /** * replaces ~user or $FOO, if necessary * if text() is a relative path, make it absolute using startDir() */ QUrl url() const { const QString txt = text(); KUrlCompletion *comp; if (combo) { comp = qobject_cast(combo->completionObject()); } else { comp = qobject_cast(edit->completionObject()); } QString enteredPath; if (comp) enteredPath = comp->replacedPath(txt); else enteredPath = txt; if (QDir::isAbsolutePath(enteredPath)) { return QUrl::fromLocalFile(enteredPath); } const QUrl enteredUrl = QUrl(enteredPath); // absolute or relative if (enteredUrl.isRelative() && !txt.isEmpty()) { QUrl finalUrl(m_startDir); finalUrl.setPath(concatPaths(finalUrl.path(), enteredPath)); return finalUrl; } else { return enteredUrl; } } static void applyFileMode(QFileDialog *dlg, KFile::Modes m, QFileDialog::AcceptMode acceptMode) { QFileDialog::FileMode fileMode; bool dirsOnly = false; if (m & KFile::Directory) { fileMode = QFileDialog::Directory; if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) { dirsOnly = true; } } else if (m & KFile::Files && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFiles; } else if (m & KFile::File && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFile; } else { fileMode = QFileDialog::AnyFile; } dlg->setFileMode(fileMode); dlg->setAcceptMode(acceptMode); dlg->setOption(QFileDialog::ShowDirsOnly, dirsOnly); } // Converts from "*.foo *.bar|Comment" to "Comment (*.foo *.bar)" QStringList kToQFilters(const QString &filters) const { QStringList qFilters = filters.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (QString &qFilter : qFilters) { int sep = qFilter.indexOf(QLatin1Char('|')); const QStringRef globs = qFilter.leftRef(sep); const QStringRef desc = qFilter.midRef(sep + 1); qFilter = desc + QLatin1String(" (") + globs + QLatin1Char(')'); } return qFilters; } QUrl getDirFromFileDialog(const QUrl &openUrl) const { return QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly); } void createFileDialog() { //Creates the fileDialog if it doesn't exist yet QFileDialog *dlg = m_parent->fileDialog(); if (!url().isEmpty() && !url().isRelative()) { QUrl u(url()); // If we won't be able to list it (e.g. http), then don't try :) if (KProtocolManager::supportsListing(u)) { dlg->selectUrl(u); } } else { dlg->setDirectoryUrl(m_startDir); } dlg->setAcceptMode(fileDialogAcceptMode); //Update the file dialog window modality if (dlg->windowModality() != fileDialogModality) { dlg->setWindowModality(fileDialogModality); } if (fileDialogModality == Qt::NonModal) { dlg->show(); } else { dlg->exec(); } } // slots void _k_slotUpdateUrl(); void _k_slotOpenDialog(); void _k_slotFileDialogAccepted(); QUrl m_startDir; bool m_startDirCustomized; bool m_fileDialogModeWasDirAndFile; KUrlRequester * const m_parent; // TODO: rename to 'q' KLineEdit *edit; KComboBox *combo; KFile::Modes fileDialogMode; QFileDialog::AcceptMode fileDialogAcceptMode; QString fileDialogFilter; QStringList mimeTypeFilters; KEditListWidget::CustomEditor editor; KUrlDragPushButton *myButton; QFileDialog *myFileDialog; KUrlCompletion *myCompletion; Qt::WindowModality fileDialogModality; }; KUrlRequester::KUrlRequester(QWidget *editWidget, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { // must have this as parent editWidget->setParent(this); d->combo = qobject_cast(editWidget); d->edit = qobject_cast(editWidget); if (d->edit) { d->edit->setClearButtonEnabled(true); } d->init(); } KUrlRequester::KUrlRequester(QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); } KUrlRequester::KUrlRequester(const QUrl &url, QWidget *parent) : QWidget(parent), d(new KUrlRequesterPrivate(this)) { d->init(); setUrl(url); } KUrlRequester::~KUrlRequester() { delete d; } void KUrlRequester::KUrlRequesterPrivate::init() { myFileDialog = nullptr; fileDialogModality = Qt::ApplicationModal; if (!combo && !edit) { edit = new KLineEdit(m_parent); edit->setClearButtonEnabled(true); } QWidget *widget = combo ? static_cast(combo) : static_cast(edit); QHBoxLayout *topLayout = new QHBoxLayout(m_parent); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(-1); // use default spacing topLayout->addWidget(widget); myButton = new KUrlDragPushButton(m_parent); myButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int buttonSize = myButton->sizeHint().expandedTo(widget->sizeHint()).height(); myButton->setFixedSize(buttonSize, buttonSize); myButton->setToolTip(i18n("Open file dialog")); connect(myButton, SIGNAL(pressed()), m_parent, SLOT(_k_slotUpdateUrl())); widget->installEventFilter(m_parent); m_parent->setFocusProxy(widget); m_parent->setFocusPolicy(Qt::StrongFocus); topLayout->addWidget(myButton); connectSignals(m_parent); connect(myButton, SIGNAL(clicked()), m_parent, SLOT(_k_slotOpenDialog())); m_startDir = QUrl::fromLocalFile(QDir::currentPath()); m_startDirCustomized = false; myCompletion = new KUrlCompletion(); updateCompletionStartDir(m_startDir); setCompletionObject(myCompletion); QAction *openAction = new QAction(m_parent); openAction->setShortcut(QKeySequence::Open); m_parent->connect(openAction, SIGNAL(triggered(bool)), SLOT(_k_slotOpenDialog())); } void KUrlRequester::setUrl(const QUrl &url) { d->setText(url.toDisplayString(QUrl::PreferLocalFile)); } void KUrlRequester::setPath(const QString &path) { d->setText(path); } void KUrlRequester::setText(const QString &text) { d->setText(text); } void KUrlRequester::setStartDir(const QUrl &startDir) { d->m_startDir = startDir; d->m_startDirCustomized = true; d->updateCompletionStartDir(startDir); } void KUrlRequester::changeEvent(QEvent *e) { if (e->type() == QEvent::WindowTitleChange) { if (d->myFileDialog) { d->myFileDialog->setWindowTitle(windowTitle()); } } QWidget::changeEvent(e); } QUrl KUrlRequester::url() const { return d->url(); } QUrl KUrlRequester::startDir() const { return d->m_startDir; } QString KUrlRequester::text() const { return d->text(); } void KUrlRequester::KUrlRequesterPrivate::_k_slotOpenDialog() { if (myFileDialog) if (myFileDialog->isVisible()) { //The file dialog is already being shown, raise it and exit myFileDialog->raise(); myFileDialog->activateWindow(); return; } if (!m_fileDialogModeWasDirAndFile && (((fileDialogMode & KFile::Directory) && !(fileDialogMode & KFile::File)) || /* catch possible fileDialog()->setMode( KFile::Directory ) changes */ (myFileDialog && (myFileDialog->fileMode() == QFileDialog::Directory && myFileDialog->testOption(QFileDialog::ShowDirsOnly))))) { const QUrl openUrl = (!m_parent->url().isEmpty() && !m_parent->url().isRelative()) ? m_parent->url() : m_startDir; /* FIXME We need a new abstract interface for using KDirSelectDialog in a non-modal way */ QUrl newUrl; if (fileDialogMode & KFile::LocalOnly) { newUrl = QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly, QStringList() << QStringLiteral("file")); } else { newUrl = getDirFromFileDialog(openUrl); } if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); } } else { emit m_parent->openFileDialog(m_parent); if (((fileDialogMode & KFile::Directory) && (fileDialogMode & KFile::File)) || m_fileDialogModeWasDirAndFile) { QMenu *dirOrFileMenu = new QMenu(); QAction *fileAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("File")); QAction *dirAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Directory")); dirOrFileMenu->addAction(fileAction); dirOrFileMenu->addAction(dirAction); connect(fileAction, &QAction::triggered, [this]() { fileDialogMode = KFile::File; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); connect(dirAction, &QAction::triggered, [this]() { fileDialogMode = KFile::Directory; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); dirOrFileMenu->exec(m_parent->mapToGlobal(QPoint(m_parent->width(), m_parent->height()))); return; } createFileDialog(); } } void KUrlRequester::KUrlRequesterPrivate::_k_slotFileDialogAccepted() { if (!myFileDialog) { return; } const QUrl newUrl = myFileDialog->selectedUrls().constFirst(); if (newUrl.isValid()) { m_parent->setUrl(newUrl); emit m_parent->urlSelected(url()); // remember url as defaultStartDir and update startdir for autocompletion if (newUrl.isLocalFile() && !m_startDirCustomized) { m_startDir = newUrl.adjusted(QUrl::RemoveFilename); updateCompletionStartDir(m_startDir); } } } void KUrlRequester::setMode(KFile::Modes mode) { Q_ASSERT((mode & KFile::Files) == 0); d->fileDialogMode = mode; if ((mode & KFile::Directory) && !(mode & KFile::File)) { d->myCompletion->setMode(KUrlCompletion::DirCompletion); } if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, mode, d->fileDialogAcceptMode); } } KFile::Modes KUrlRequester::mode() const { return d->fileDialogMode; } void KUrlRequester::setAcceptMode(QFileDialog::AcceptMode mode) { d->fileDialogAcceptMode = mode; if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, d->fileDialogMode, mode); } } QFileDialog::AcceptMode KUrlRequester::acceptMode() const { return d->fileDialogAcceptMode; } void KUrlRequester::setFilter(const QString &filter) { d->fileDialogFilter = filter; if (d->myFileDialog) { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } } QString KUrlRequester::filter() const { return d->fileDialogFilter; } void KUrlRequester::setMimeTypeFilters(const QStringList &mimeTypes) { d->mimeTypeFilters = mimeTypes; if (d->myFileDialog) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } d->myCompletion->setMimeTypeFilters(d->mimeTypeFilters); } QStringList KUrlRequester::mimeTypeFilters() const { return d->mimeTypeFilters; } QFileDialog *KUrlRequester::fileDialog() const { if (d->myFileDialog && ( (d->myFileDialog->fileMode() == QFileDialog::Directory && !(d->fileDialogMode & KFile::Directory)) || (d->myFileDialog->fileMode() != QFileDialog::Directory && (d->fileDialogMode & KFile::Directory)))) { delete d->myFileDialog; d->myFileDialog = nullptr; } if (!d->myFileDialog) { d->myFileDialog = new QFileDialog(window(), windowTitle()); if (!d->mimeTypeFilters.isEmpty()) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } else { d->myFileDialog->setNameFilters(d->kToQFilters(d->fileDialogFilter)); } d->applyFileMode(d->myFileDialog, d->fileDialogMode, d->fileDialogAcceptMode); d->myFileDialog->setWindowModality(d->fileDialogModality); connect(d->myFileDialog, SIGNAL(accepted()), SLOT(_k_slotFileDialogAccepted())); } return d->myFileDialog; } void KUrlRequester::clear() { d->setText(QString()); } KLineEdit *KUrlRequester::lineEdit() const { return d->edit; } KComboBox *KUrlRequester::comboBox() const { return d->combo; } void KUrlRequester::KUrlRequesterPrivate::_k_slotUpdateUrl() { const QUrl visibleUrl = url(); QUrl u = visibleUrl; if (visibleUrl.isRelative()) { u = QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/')).resolved(visibleUrl); } myButton->setURL(u); } bool KUrlRequester::eventFilter(QObject *obj, QEvent *ev) { if ((d->edit == obj) || (d->combo == obj)) { if ((ev->type() == QEvent::FocusIn) || (ev->type() == QEvent::FocusOut)) // Forward focusin/focusout events to the urlrequester; needed by file form element in khtml { QApplication::sendEvent(this, ev); } } return QWidget::eventFilter(obj, ev); } QPushButton *KUrlRequester::button() const { return d->myButton; } KUrlCompletion *KUrlRequester::completionObject() const { return d->myCompletion; } void KUrlRequester::setClickMessage(const QString &msg) { setPlaceholderText(msg); } void KUrlRequester::setPlaceholderText(const QString &msg) { if (d->edit) { d->edit->setPlaceholderText(msg); } } QString KUrlRequester::clickMessage() const { return placeholderText(); } QString KUrlRequester::placeholderText() const { if (d->edit) { return d->edit->placeholderText(); } else { return QString(); } } Qt::WindowModality KUrlRequester::fileDialogModality() const { return d->fileDialogModality; } void KUrlRequester::setFileDialogModality(Qt::WindowModality modality) { d->fileDialogModality = modality; } const KEditListWidget::CustomEditor &KUrlRequester::customEditor() { setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); KLineEdit *edit = d->edit; if (!edit && d->combo) { edit = qobject_cast(d->combo->lineEdit()); } #ifndef NDEBUG if (!edit) { qCWarning(KIO_WIDGETS) << "KUrlRequester's lineedit is not a KLineEdit!??\n"; } #endif d->editor.setRepresentationWidget(this); d->editor.setLineEdit(edit); return d->editor; } KUrlComboRequester::KUrlComboRequester(QWidget *parent) : KUrlRequester(new KComboBox(false), parent), d(nullptr) { } #include "moc_kurlrequester.cpp" #include "kurlrequester.moc"