From f9421abbdf4d2e2be06febf9f75a2b62e6875835 Mon Sep 17 00:00:00 2001 From: Mitch Curtis Date: Mon, 16 Nov 2020 16:34:56 +0100 Subject: Add QtQuick.Dialogs [ChangeLog][QtQuickDialogs] Added FileDialog. This is a native FileDialog on platforms that support it, and a non-native Qt Quick FileDialog on platforms that don't. Fixes: QTBUG-87797 Change-Id: Ia3a98b616479b818c96c232a0329750023079642 Reviewed-by: Fabian Kosmale --- tests/auto/CMakeLists.txt | 1 + tests/auto/dialogs/CMakeLists.txt | 1 + tests/auto/dialogs/dialogs.pro | 3 + tests/auto/dialogs/qquickfiledialogimpl/BLACKLIST | 7 + .../dialogs/qquickfiledialogimpl/CMakeLists.txt | 40 + .../data/acceptRejectLabel.qml | 67 + .../data/bindAllTxtHtmlNameFilters.qml | 66 + .../data/bindCurrentFolder.qml | 68 + .../qquickfiledialogimpl/data/bindTitle.qml | 65 + .../data/bindTxtHtmlNameFilters.qml | 66 + .../qquickfiledialogimpl/data/fileDialog.qml | 73 ++ .../qquickfiledialogimpl/qquickfiledialogimpl.pro | 14 + .../tst_qquickfiledialogimpl.cpp | 1349 ++++++++++++++++++++ tests/auto/shared/dialogtestutil.h | 144 +++ tests/auto/shared/visualtestutil.h | 14 + tests/manual/dialogs/CMakeLists.txt | 31 + tests/manual/dialogs/FileDialogPage.qml | 357 ++++++ tests/manual/dialogs/StringListView.qml | 79 ++ tests/manual/dialogs/Theme.qml | 57 + tests/manual/dialogs/dialogs.cpp | 80 ++ tests/manual/dialogs/dialogs.pro | 11 + tests/manual/dialogs/dialogs.qml | 127 ++ tests/manual/dialogs/qmldir | 3 + 23 files changed, 2723 insertions(+) create mode 100644 tests/auto/dialogs/CMakeLists.txt create mode 100644 tests/auto/dialogs/dialogs.pro create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/BLACKLIST create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/CMakeLists.txt create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/acceptRejectLabel.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/bindAllTxtHtmlNameFilters.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/bindCurrentFolder.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/bindTitle.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/bindTxtHtmlNameFilters.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/data/fileDialog.qml create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/qquickfiledialogimpl.pro create mode 100644 tests/auto/dialogs/qquickfiledialogimpl/tst_qquickfiledialogimpl.cpp create mode 100644 tests/auto/shared/dialogtestutil.h create mode 100644 tests/manual/dialogs/CMakeLists.txt create mode 100644 tests/manual/dialogs/FileDialogPage.qml create mode 100644 tests/manual/dialogs/StringListView.qml create mode 100644 tests/manual/dialogs/Theme.qml create mode 100644 tests/manual/dialogs/dialogs.cpp create mode 100644 tests/manual/dialogs/dialogs.pro create mode 100644 tests/manual/dialogs/dialogs.qml create mode 100644 tests/manual/dialogs/qmldir (limited to 'tests') diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt index 5d7302c3..ef299bb7 100644 --- a/tests/auto/CMakeLists.txt +++ b/tests/auto/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(controls) add_subdirectory(cursor) add_subdirectory(customization) add_subdirectory(designer) +add_subdirectory(dialogs) add_subdirectory(focus) add_subdirectory(font) add_subdirectory(palette) diff --git a/tests/auto/dialogs/CMakeLists.txt b/tests/auto/dialogs/CMakeLists.txt new file mode 100644 index 00000000..26eb933f --- /dev/null +++ b/tests/auto/dialogs/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(qquickfiledialogimpl) diff --git a/tests/auto/dialogs/dialogs.pro b/tests/auto/dialogs/dialogs.pro new file mode 100644 index 00000000..004641ec --- /dev/null +++ b/tests/auto/dialogs/dialogs.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs +SUBDIRS += \ + qquickfiledialogimpl diff --git a/tests/auto/dialogs/qquickfiledialogimpl/BLACKLIST b/tests/auto/dialogs/qquickfiledialogimpl/BLACKLIST new file mode 100644 index 00000000..822b92dd --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/BLACKLIST @@ -0,0 +1,7 @@ +# QTBUG-92094 +[tabFocusNavigation] +* + +# QTBUG-92585 +[fileMode:OpenFiles] +* diff --git a/tests/auto/dialogs/qquickfiledialogimpl/CMakeLists.txt b/tests/auto/dialogs/qquickfiledialogimpl/CMakeLists.txt new file mode 100644 index 00000000..631cb9e8 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/CMakeLists.txt @@ -0,0 +1,40 @@ +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qquickfiledialogimpl + SOURCES + ../../shared/qtest_quickcontrols.h + ../../shared/util.cpp ../../shared/util.h + ../../shared/visualtestutil.cpp ../../shared/visualtestutil.h + tst_qquickfiledialogimpl.cpp + DEFINES + QQC2_IMPORT_PATH=\\\"${CMAKE_CURRENT_SOURCE_DIR}/../../../../src/imports\\\" + PUBLIC_LIBRARIES + Qt::CorePrivate + Qt::Gui + Qt::GuiPrivate + Qt::QmlPrivate + Qt::QuickControls2 + Qt::QuickControls2Private + Qt::QuickDialogs2Private + Qt::QuickDialogs2QuickImplPrivate + Qt::QuickDialogs2UtilsPrivate + Qt::QuickPrivate + Qt::QuickTemplates2Private + Qt::QuickTest + Qt::TestPrivate + TESTDATA ${test_data} +) + +qt_internal_extend_target(tst_qquickfiledialogimpl CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=\\\":/data\\\" +) + +qt_internal_extend_target(tst_qquickfiledialogimpl CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\" +) diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/acceptRejectLabel.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/acceptRejectLabel.qml new file mode 100644 index 00000000..3cd526e3 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/acceptRejectLabel.qml @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + id: window + width: 640 + height: 480 + + property alias dialog: dialog + + FileDialog { + id: dialog + acceptLabel: "AcceptTest" + rejectLabel: "RejectTest" + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/bindAllTxtHtmlNameFilters.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/bindAllTxtHtmlNameFilters.qml new file mode 100644 index 00000000..5010c422 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/bindAllTxtHtmlNameFilters.qml @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + width: 640 + height: 480 + + property alias dialog: dialog + + FileDialog { + id: dialog + objectName: "FileDialog" + nameFilters: ["All files (*)", "Text files (*.txt)", "HTML files (*.html)"] + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/bindCurrentFolder.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/bindCurrentFolder.qml new file mode 100644 index 00000000..03cd61bc --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/bindCurrentFolder.qml @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + id: window + width: 640 + height: 480 + + property alias dialog: dialog + required property url initialFolder + + FileDialog { + id: dialog + objectName: "FileDialog" + currentFolder: window.initialFolder + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/bindTitle.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/bindTitle.qml new file mode 100644 index 00000000..60657f58 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/bindTitle.qml @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + width: 640 + height: 480 + + property alias dialog: dialog + + FileDialog { + id: dialog + title: "Test Title" + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/bindTxtHtmlNameFilters.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/bindTxtHtmlNameFilters.qml new file mode 100644 index 00000000..1e312fbc --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/bindTxtHtmlNameFilters.qml @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + width: 640 + height: 480 + + property alias dialog: dialog + + FileDialog { + id: dialog + objectName: "FileDialog" + nameFilters: ["Text files (*.txt)", "HTML files (*.html)"] + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/data/fileDialog.qml b/tests/auto/dialogs/qquickfiledialogimpl/data/fileDialog.qml new file mode 100644 index 00000000..febad45c --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/data/fileDialog.qml @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +ApplicationWindow { + width: 640 + height: 480 + + property alias dialog: dialog + + function doneAccepted() { + dialog.done(FileDialog.Accepted) + } + + function doneRejected() { + dialog.done(FileDialog.Rejected) + } + + FileDialog { + id: dialog + objectName: "FileDialog" + } +} diff --git a/tests/auto/dialogs/qquickfiledialogimpl/qquickfiledialogimpl.pro b/tests/auto/dialogs/qquickfiledialogimpl/qquickfiledialogimpl.pro new file mode 100644 index 00000000..3f6e83d7 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/qquickfiledialogimpl.pro @@ -0,0 +1,14 @@ +CONFIG += testcase +TARGET = tst_qquickfiledialogimpl +SOURCES += tst_qquickfiledialogimpl.cpp + +macos:CONFIG -= app_bundle + +QT += core-private gui-private testlib qml-private quick-private qmltest quicktemplates2-private + +include (../../shared/util.pri) + +TESTDATA = data/* + +OTHER_FILES += \ + data/*.qml diff --git a/tests/auto/dialogs/qquickfiledialogimpl/tst_qquickfiledialogimpl.cpp b/tests/auto/dialogs/qquickfiledialogimpl/tst_qquickfiledialogimpl.cpp new file mode 100644 index 00000000..a8fe4c15 --- /dev/null +++ b/tests/auto/dialogs/qquickfiledialogimpl/tst_qquickfiledialogimpl.cpp @@ -0,0 +1,1349 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "../../shared/dialogtestutil.h" +#include "../../shared/util.h" +#include "../../shared/visualtestutil.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QQuickDialogTestUtil; +using namespace QQuickVisualTestUtil; + +class tst_QQuickFileDialogImpl : public QQmlDataTest +{ + Q_OBJECT + +private slots: + void initTestCase() override; + void cleanupTestCase(); + + void defaults(); + void chooseFileViaStandardButtons(); + void chooseFileViaDoubleClick(); + void chooseFileViaTextEdit(); + void chooseFileViaEnter(); + void bindCurrentFolder_data(); + void bindCurrentFolder(); + void changeFolderViaStandardButtons(); + void changeFolderViaDoubleClick(); + void chooseFolderViaTextEdit(); + void chooseFolderViaEnter(); + void cancelDialogWhileTextEditHasFocus(); + void goUp(); + void goUpWhileTextEditHasFocus(); + void goIntoLargeFolder(); + void keyAndShortcutHandling(); + void bindNameFilters(); + void changeNameFilters(); + void changeNameFiltersAfterChangingFolder(); + void tabFocusNavigation(); + void acceptRejectLabel(); + void bindTitle(); + void itemsDisabledWhenNecessary(); + void fileMode_data(); + void fileMode(); + void defaultSuffix_data(); + void defaultSuffix(); + void done_data(); + void done(); + +private: + QQuickAbstractButton *findDialogButton(QQuickDialogButtonBox *box, const QString &buttonText); + void enterText(QWindow *window, const QString &textToEnter); + + QTemporaryDir tempDir; + QScopedPointer tempFile1; + QScopedPointer tempFile2; + QDir tempSubDir; + QDir tempSubSubDir; + QScopedPointer tempSubFile1; + QScopedPointer tempSubFile2; + QDir oldCurrentDir; +}; + +QQuickAbstractButton *tst_QQuickFileDialogImpl::findDialogButton(QQuickDialogButtonBox *box, const QString &buttonText) +{ + for (int i = 0; i < box->count(); ++i) { + auto button = qobject_cast(box->itemAt(i)); + if (button && button->text().toUpper() == buttonText.toUpper()) + return button; + } + return nullptr; +} + +void tst_QQuickFileDialogImpl::enterText(QWindow *window, const QString &textToEnter) +{ + for (int i = 0; i < textToEnter.size(); ++i) { + const QChar key = textToEnter.at(i); + QTest::keyClick(window, key.toLatin1()); + } +} + +void tst_QQuickFileDialogImpl::initTestCase() +{ + QQmlDataTest::initTestCase(); + + QVERIFY(tempDir.isValid()); + // QTEST_QUICKCONTROLS_MAIN constructs the test case object once, + // and then calls qRun() for each style, and qRun() calls initTestCase(). + // So, we need to check if we've already made the temporary directory. + // Note that this is only necessary if the test is run with more than one style. + if (!QDir(tempDir.path()).isEmpty()) + return; + + /* + Create a couple of files within the temporary directory. The structure is: + + [temp directory] + ├── sub-dir + │ ├── sub-sub-dir + │ ├── sub-file1.txt + │ └── sub-file2.txt + ├── file1.txt + └── file2.txt + */ + tempSubDir = QDir(tempDir.path()); + QVERIFY2(tempSubDir.mkdir("sub-dir"), qPrintable(QString::fromLatin1( + "Failed to make sub-directory \"sub-dir\" in %1. Permissions are: %2") + .arg(tempDir.path()).arg(QDebug::toString(QFileInfo(tempDir.path()).permissions())))); + QVERIFY(tempSubDir.cd("sub-dir")); + + tempSubSubDir = QDir(tempSubDir.path()); + QVERIFY2(tempSubSubDir.mkdir("sub-sub-dir"), qPrintable(QString::fromLatin1( + "Failed to make sub-directory \"sub-sub-dir\" in %1. Permissions are: %2") + .arg(tempSubDir.path()).arg(QDebug::toString(QFileInfo(tempSubDir.path()).permissions())))); + QVERIFY(tempSubSubDir.cd("sub-sub-dir")); + + tempSubFile1.reset(new QFile(tempSubDir.path() + "/sub-file1.txt")); + QVERIFY(tempSubFile1->open(QIODevice::ReadWrite)); + + tempSubFile2.reset(new QFile(tempSubDir.path() + "/sub-file2.txt")); + QVERIFY(tempSubFile2->open(QIODevice::ReadWrite)); + + tempFile1.reset(new QFile(tempDir.path() + "/file1.txt")); + QVERIFY(tempFile1->open(QIODevice::ReadWrite)); + + tempFile2.reset(new QFile(tempDir.path() + "/file2.txt")); + QVERIFY(tempFile2->open(QIODevice::ReadWrite)); + + // Ensure that each test starts off in the temporary directory. + oldCurrentDir = QDir::current(); + QDir::setCurrent(tempDir.path()); +} + +void tst_QQuickFileDialogImpl::cleanupTestCase() +{ + // Just in case... + QDir::setCurrent(oldCurrentDir.path()); +} + +bool verifyFileDialogDelegates(QQuickListView *fileDialogListView, const QStringList &expectedFiles, + QString &failureMessage) +{ + if (QQuickTest::qIsPolishScheduled(fileDialogListView)) { + if (!QQuickTest::qWaitForItemPolished(fileDialogListView)) { + failureMessage = QLatin1String("Failed to polish fileDialogListView"); + return false; + } + } + + QStringList actualFiles; + for (int i = 0; i < fileDialogListView->count(); ++i) { + auto delegate = qobject_cast(findViewDelegateItem(fileDialogListView, i)); + if (!delegate) { + failureMessage = QString::fromLatin1("Delegate at index %1 is null").arg(i); + return false; + } + + // Need to call absoluteFilePath on Windows; see comment in dialogtestutil.h. + actualFiles.append(QFileInfo(delegate->file().toLocalFile()).absoluteFilePath()); + } + + if (actualFiles != expectedFiles) { + failureMessage = QString::fromLatin1("Mismatch in actual vs expected " + "delegates in fileDialogListView:\n expected: %1\n actual: %2") + .arg(QDebug::toString(expectedFiles)).arg(QDebug::toString(actualFiles)); + return false; + } + + return true; +} + +bool verifyBreadcrumbDelegates(QQuickFolderBreadcrumbBar *breadcrumbBar, const QUrl &expectedFolder, + QString &failureMessage) +{ + if (!breadcrumbBar) { + failureMessage = QLatin1String("breadcrumbBar is null"); + return false; + } + + auto breadcrumbBarListView = qobject_cast(breadcrumbBar->contentItem()); + if (!breadcrumbBarListView) { + failureMessage = QLatin1String("breadcrumbBar's ListView is null"); + return false; + } + + if (QQuickTest::qIsPolishScheduled(breadcrumbBarListView)) { + if (!QQuickTest::qWaitForItemPolished(breadcrumbBarListView)) { + failureMessage = QLatin1String("Failed to polish breadcrumbBarListView"); + return false; + } + } + + QStringList actualCrumbs; + for (int i = 0; i < breadcrumbBarListView->count(); ++i) { + auto delegate = qobject_cast(findViewDelegateItem(breadcrumbBarListView, i)); + if (!delegate) { + // It's a separator or some other non-crumb item. + continue; + } + + actualCrumbs.append(delegate->text()); + } + + QStringList expectedCrumbs = QQuickFolderBreadcrumbBarPrivate::crumbPathsForFolder(expectedFolder); + for (int i = 0; i < expectedCrumbs.size(); ++i) { + QString &crumbPath = expectedCrumbs[i]; + crumbPath = QQuickFolderBreadcrumbBarPrivate::folderBaseName(crumbPath); + } + + if (actualCrumbs != expectedCrumbs) { + failureMessage = QString::fromLatin1("Mismatch in actual vs expected " + "delegates in breadcrumbBarListView:\n expected: %1\n actual: %2") + .arg(QDebug::toString(expectedCrumbs)).arg(QDebug::toString(actualCrumbs)); + return false; + } + + return true; +} + +void tst_QQuickFileDialogImpl::defaults() +{ + QQuickApplicationHelper helper(this, "fileDialog.qml"); + QVERIFY2(helper.ready, helper.failureMessage()); + + QQuickWindow *window = helper.window; + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowExposed(window)); + + QQuickFileDialog *dialog = window->property("dialog").value(); + QVERIFY(dialog); + COMPARE_URL(dialog->selectedFile(), QUrl()); + // It should default to the current directory. + COMPARE_URL(dialog->currentFolder(), QUrl::fromLocalFile(QDir().absolutePath())); + // The first file in the directory should be selected, but not until the dialog is actually open, + // as QQuickFileDialogImpl hasn't been created yet. + COMPARE_URL(dialog->currentFile(), QUrl()); + COMPARE_URLS(dialog->currentFiles(), {}); + QCOMPARE(dialog->title(), QString()); + + dialog->open(); + QQuickFileDialogImpl *quickDialog = window->findChild(); + QTRY_VERIFY(quickDialog->isOpened()); + QVERIFY(quickDialog); + COMPARE_URL(quickDialog->selectedFile(), QUrl()); + COMPARE_URL(quickDialog->currentFolder(), QUrl::fromLocalFile(QDir().absolutePath())); + COMPARE_URL(dialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + COMPARE_URLS(dialog->currentFiles(), { QUrl::fromLocalFile(tempSubDir.path()) }); + COMPARE_URL(quickDialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + QCOMPARE(quickDialog->title(), QString()); +} + +void tst_QQuickFileDialogImpl::chooseFileViaStandardButtons() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Select the delegate by clicking once. + QSignalSpy dialogFileChangedSpy(dialogHelper.dialog, SIGNAL(selectedFileChanged())); + QVERIFY(dialogFileChangedSpy.isValid()); + QSignalSpy dialogCurrentFileChangedSpy(dialogHelper.dialog, SIGNAL(currentFileChanged())); + QVERIFY(dialogCurrentFileChangedSpy.isValid()); + QSignalSpy quickDialogCurrentFileChangedSpy(dialogHelper.quickDialog, SIGNAL(currentFileChanged(const QUrl &))); + QVERIFY(quickDialogCurrentFileChangedSpy.isValid()); + + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 2, delegate)); + COMPARE_URL(delegate->file(), QUrl::fromLocalFile(tempFile2->fileName())); + QVERIFY(clickButton(delegate)); + COMPARE_URL(dialogHelper.quickDialog->currentFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }); + // Only currentFile-related signals should be emitted. + QCOMPARE(dialogFileChangedSpy.count(), 0); + QCOMPARE(dialogCurrentFileChangedSpy.count(), 1); + QCOMPARE(quickDialogCurrentFileChangedSpy.count(), 1); + + // Click the "Open" button. + QVERIFY(dialogHelper.quickDialog->footer()); + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); + QVERIFY(openButton); + QVERIFY(clickButton(openButton)); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }); + QCOMPARE(dialogFileChangedSpy.count(), 1); + COMPARE_URL(dialogHelper.quickDialog->selectedFile(), QUrl::fromLocalFile(tempFile2->fileName())); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + QVERIFY(!dialogHelper.dialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::chooseFileViaDoubleClick() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Select the delegate by double-clicking. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 2, delegate)); + COMPARE_URL(delegate->file(), QUrl::fromLocalFile(tempFile2->fileName())) + QVERIFY(doubleClickButton(delegate)); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile2->fileName())) + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }) + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl::fromLocalFile(tempFile2->fileName())) + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }) + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::chooseFileViaTextEdit() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->isVisible()); + QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); + QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); + + // Enter the path to the file in the text edit. + enterText(dialogHelper.window(), tempFile2->fileName()); + QCOMPARE(breadcrumbBar->textField()->text(), tempFile2->fileName()); + + // Hit enter to accept. + QTest::keyClick(dialogHelper.window(), Qt::Key_Return); + COMPARE_URL(dialogHelper.quickDialog->selectedFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::chooseFileViaEnter() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Before moving down, the first delegate in the view should be selected and have focus. + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *subDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subDirDelegate)); + COMPARE_URL(subDirDelegate->file(), QUrl::fromLocalFile(tempSubDir.path())); + QVERIFY(subDirDelegate->hasActiveFocus()); + + // Select the first file in the view by navigating with the down key. + QTest::keyClick(dialogHelper.window(), Qt::Key_Down); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile1->fileName())); + + // Select the delegate by pressing enter. + QTest::keyClick(dialogHelper.window(), Qt::Key_Return); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl::fromLocalFile(tempFile1->fileName())); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { QUrl::fromLocalFile(tempFile1->fileName()) }); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + QCOMPARE(dialogHelper.dialog->result(), QQuickFileDialog::Accepted); +} + +void tst_QQuickFileDialogImpl::bindCurrentFolder_data() +{ + QTest::addColumn("initialFolder"); + QTest::addColumn("expectedFolder"); + QTest::addColumn("expectedVisibleFiles"); + + const auto currentDirUrl = QUrl::fromLocalFile(QDir::current().path()); + const auto tempSubDirUrl = QUrl::fromLocalFile(tempSubDir.path()); + const auto tempSubFile1Url = QUrl::fromLocalFile(tempSubFile1->fileName()); + + const QStringList currentDirFiles = { tempSubDir.path(), tempFile1->fileName(), tempFile2->fileName() }; + const QStringList tempSubDirFiles = { tempSubSubDir.path(), tempSubFile1->fileName(), tempSubFile2->fileName() }; + + // Setting the folder to "sub-dir" should result in "sub-file1.txt" and "sub-file2.txt" being visible. + QTest::addRow("sub-dir") << tempSubDirUrl << tempSubDirUrl << tempSubDirFiles; + // Setting a file as the folder shouldn't work, and the dialog shouldn't change its folder. + QTest::addRow("sub-dir/sub-file1.txt") << tempSubFile1Url << currentDirUrl << currentDirFiles; +} + +void tst_QQuickFileDialogImpl::bindCurrentFolder() +{ + QFETCH(QUrl, initialFolder); + QFETCH(QUrl, expectedFolder); + QFETCH(QStringList, expectedVisibleFiles); + + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", initialFolder }}); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), expectedFolder); + + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QString failureMessage; + // Even waiting for ListView polish and that the FolderListModel's status is ready aren't enough + // on Windows, apparently, as sometimes there just aren't any delegates by the time we do the check. + // So, we use QTRY_VERIFY2 each time we call this function just to be safe. + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, expectedVisibleFiles, failureMessage), qPrintable(failureMessage)); + + // Check that the breadcrumb bar is correct by constructing the expected files from the expectedFolder. + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY2(verifyBreadcrumbDelegates(breadcrumbBar, expectedFolder, failureMessage), qPrintable(failureMessage)); +} + +void tst_QQuickFileDialogImpl::changeFolderViaStandardButtons() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Select the delegate by clicking once. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, delegate)); + COMPARE_URL(delegate->file(), QUrl::fromLocalFile(tempSubDir.path())); + QVERIFY(clickButton(delegate)); + // The selectedFile and currentFolder shouldn't change yet. + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl()); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), {}); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDir.path())); + // The currentFile should, though. + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempSubDir.path()) }); + + // Click the "Open" button. The dialog should navigate to that directory, but still be open. + QVERIFY(dialogHelper.quickDialog->footer()); + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); + QVERIFY(openButton); + QVERIFY(clickButton(openButton)); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + QVERIFY(dialogHelper.dialog->isVisible()); + + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::changeFolderViaDoubleClick() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Select the "sub-dir" delegate by double-clicking. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *subDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subDirDelegate)); + COMPARE_URL(subDirDelegate->file(), QUrl::fromLocalFile(tempSubDir.path())); + QVERIFY(doubleClickButton(subDirDelegate)); + // The first file in the directory should be selected, which is "sub-sub-dir". + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubSubDir.path())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempSubSubDir.path()) }); + QQuickFileDialogDelegate *subSubDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subSubDirDelegate)); + QCOMPARE(subSubDirDelegate->isHighlighted(), true); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl()); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), {}); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + // Since we only chose a folder, the dialog should still be open. + QVERIFY(dialogHelper.dialog->isVisible()); + + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::chooseFolderViaTextEdit() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->isVisible()); + QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); + QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); + + // Enter the path to the folder in the text edit. + enterText(dialogHelper.window(), tempSubDir.path()); + QCOMPARE(breadcrumbBar->textField()->text(), tempSubDir.path()); + + // Hit enter to accept. + QTest::keyClick(dialogHelper.window(), Qt::Key_Return); + // The first file in the directory should be selected, which is "sub-sub-dir". + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubSubDir.path())); + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *subSubDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subSubDirDelegate)); + QCOMPARE(subSubDirDelegate->isHighlighted(), true); + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl()); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), {}); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + QTRY_VERIFY(dialogHelper.dialog->isVisible()); + + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::chooseFolderViaEnter() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Fhe first delegate in the view should be selected and have focus. + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDir.path())); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempSubDir.path()) }); + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *subDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subDirDelegate)); + COMPARE_URL(subDirDelegate->file(), QUrl::fromLocalFile(tempSubDir.path())); + QVERIFY(subDirDelegate->hasActiveFocus()); + + // Select the delegate by pressing enter. + QTest::keyClick(dialogHelper.window(), Qt::Key_Return); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + // The first file in the new directory should be selected, which is "sub-sub-dir". + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubSubDir.path())); + // Since we only chose a folder, the dialog should still be open. + QVERIFY(dialogHelper.dialog->isVisible()); + + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::cancelDialogWhileTextEditHasFocus() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->hasActiveFocus()); + + // Close it via the cancel button. + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + const QString cancelText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Cancel); + auto cancelButton = findDialogButton(dialogButtonBox, cancelText); + QVERIFY(cancelButton); + QVERIFY(clickButton(cancelButton)); + + // Open it again. The text field should not be visible, but the breadcrumb bar itself should be. + dialogHelper.dialog->open(); + QVERIFY(dialogHelper.dialog->isVisible()); + QTRY_VERIFY(dialogHelper.quickDialog->isOpened()); + QVERIFY(breadcrumbBar->isVisible()); + // The ListView that contains the breadcrumb delegates should be visible. + QVERIFY(breadcrumbBar->contentItem()->isVisible()); + QVERIFY(!breadcrumbBar->textField()->isVisible()); +} + +void tst_QQuickFileDialogImpl::goUp() +{ + // Open the dialog. Start off in "sub-dir". + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", QUrl::fromLocalFile(tempSubDir.path()) }}); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Go up a directory via the button next to the breadcrumb bar. + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + auto barListView = qobject_cast(breadcrumbBar->contentItem()); + QVERIFY(barListView); + if (QQuickTest::qIsPolishScheduled(barListView)) + QVERIFY(QQuickTest::qWaitForItemPolished(barListView)); + QVERIFY(clickButton(breadcrumbBar->upButton())); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDir.path())); + // The previous directory that we were in should now be selected (matches e.g. Windows and Ubuntu). + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempSubDir.path())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempSubDir.path()) }); + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *subDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subDirDelegate)); + QCOMPARE(subDirDelegate->isHighlighted(), true); + + // Go up a directory via the keyboard shortcut next to the breadcrumb bar. + const auto goUpKeySequence = QKeySequence(Qt::ALT | Qt::Key_Up); + QTest::keySequence(dialogHelper.window(), goUpKeySequence); + QDir tempParentDir(tempDir.path()); + QVERIFY(tempParentDir.cdUp()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempParentDir.path())); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempDir.path())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempDir.path()) }); +} + +void tst_QQuickFileDialogImpl::goUpWhileTextEditHasFocus() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", QUrl::fromLocalFile(tempSubDir.path()) }}); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->hasActiveFocus()); + + // Go up a directory via the button next to the breadcrumb bar. + auto barListView = qobject_cast(breadcrumbBar->contentItem()); + QVERIFY(barListView); + if (QQuickTest::qIsPolishScheduled(barListView)) + QVERIFY(QQuickTest::qWaitForItemPolished(barListView)); + QVERIFY(clickButton(breadcrumbBar->upButton())); + // The path should have changed to the parent directory. + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDir.path())); + // The text edit should be hidden when it loses focus. + QVERIFY(!breadcrumbBar->textField()->hasActiveFocus()); + QVERIFY(!breadcrumbBar->textField()->isVisible()); + // The focus should be given to the first delegate. + QVERIFY(dialogHelper.window()->activeFocusItem()); + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *firstDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, firstDelegate)); + QCOMPARE(dialogHelper.window()->activeFocusItem(), firstDelegate); +} + +void tst_QQuickFileDialogImpl::goIntoLargeFolder() +{ + // Create a throwaway directory with a lot of folders within it... + QTemporaryDir anotherTempDir; + QVERIFY(anotherTempDir.isValid()); + for (int i = 0; i < 30; ++i) { + QDir newDir(anotherTempDir.path()); + QVERIFY(newDir.exists()); + // Pad with zeroes so that the directories are ordered as we expect. + QVERIFY(newDir.mkdir(QString::fromLatin1("dir%1").arg(i, 2, 10, QLatin1Char('0')))); + } + + // ... and within one of those folders, more folders. + QDir dir20(anotherTempDir.path() + "/dir20"); + QVERIFY(dir20.exists()); + for (int i = 0; i < 30; ++i) { + QDir newDir(dir20.path()); + QVERIFY(newDir.exists()); + QVERIFY(newDir.mkdir(QString::fromLatin1("subdir%1").arg(i, 2, 10, QLatin1Char('0')))); + } + + // Open the dialog. Start off in the throwaway directory. + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", QUrl::fromLocalFile(anotherTempDir.path()) }}); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // If the screen is so tall that the contentItem is not vertically larger than the view, + // then the test makes no sense. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + if (QQuickTest::qIsPolishScheduled(fileDialogListView)) + QVERIFY(QQuickTest::qWaitForItemPolished(fileDialogListView)); + // Just to be safe, make sure it's at least twice as big. + if (fileDialogListView->contentItem()->height() < fileDialogListView->height() * 2) { + QSKIP(qPrintable(QString::fromLatin1("Expected height of fileDialogListView's contentItem (%1)" \ + " to be at least twice as big as the ListView's height (%2)") + .arg(fileDialogListView->contentItem()->height()).arg(fileDialogListView->height()))); + } + + // Scroll down to dir20. The view should be somewhere past the middle. + QVERIFY(QMetaObject::invokeMethod(fileDialogListView, "positionViewAtIndex", Qt::DirectConnection, + Q_ARG(int, 20), Q_ARG(int, QQuickItemView::PositionMode::Center))); + QVERIFY(fileDialogListView->contentY() > 0); + + // Go into it. The view should start at the top of the directory, not at the same contentY + // that it had in the previous directory. + QQuickFileDialogDelegate *dir20Delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 20, dir20Delegate)); + COMPARE_URL(dir20Delegate->file(), QUrl::fromLocalFile(dir20.path())); + QVERIFY(doubleClickButton(dir20Delegate)); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(dir20.path())); + COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(dir20.path())); + QCOMPARE(fileDialogListView->contentY(), 0); +} + +void tst_QQuickFileDialogImpl::keyAndShortcutHandling() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->isVisible()); + QCOMPARE(breadcrumbBar->textField()->text(), dialogHelper.dialog->currentFolder().toLocalFile()); + QCOMPARE(breadcrumbBar->textField()->selectedText(), breadcrumbBar->textField()->text()); + + // Ctrl+L shouldn't hide it. + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + QVERIFY(breadcrumbBar->textField()->isVisible()); + + // Cancel it with the escape key. + QTest::keyClick(dialogHelper.window(), Qt::Key_Escape); + QVERIFY(!breadcrumbBar->textField()->isVisible()); + QVERIFY(dialogHelper.dialog->isVisible()); + + // Make it visible. + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + QVERIFY(breadcrumbBar->textField()->isVisible()); + + // Cancel it with the escape key again. + QTest::keyClick(dialogHelper.window(), Qt::Key_Escape); + QVERIFY(!breadcrumbBar->textField()->isVisible()); + QVERIFY(dialogHelper.dialog->isVisible()); + + // Pressing escape now should close the dialog. + QTest::keyClick(dialogHelper.window(), Qt::Key_Escape); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); +} + +void tst_QQuickFileDialogImpl::bindNameFilters() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindTxtHtmlNameFilters.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Only "sub-dir", "text1.txt" and "text2.txt" should be visible, since *.txt is the first filter. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QString failureMessage; + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, + { tempSubDir.path(), tempFile1->fileName(), tempFile2->fileName() }, failureMessage), qPrintable(failureMessage)); +} + +void tst_QQuickFileDialogImpl::changeNameFilters() +{ + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + + // Open the dialog and check that selectedNameFilter is correct. + // By default, QFileDialogOptions::defaultNameFilterString() is used. + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->name(), "All Files"); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->name(), "All Files"); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->index(), 0); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->index(), 0); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->extensions(), { "*" }); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->extensions(), { "*" }); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->globs(), { "*" }); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->globs(), { "*" }); + + // Close the dialog. + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + + // Set .txt and .html filters. + QSignalSpy nameFiltersChangedSpy(dialogHelper.dialog, SIGNAL(nameFiltersChanged())); + QVERIFY(nameFiltersChangedSpy.isValid()); + const QStringList nameFilters = { "Text files (*.txt)", "HTML files (*.html)" }; + dialogHelper.dialog->setNameFilters(nameFilters); + QCOMPARE(dialogHelper.dialog->nameFilters(), nameFilters); + QCOMPARE(nameFiltersChangedSpy.count(), 1); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->name(), "Text files"); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->index(), 0); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->extensions(), { "txt" }); + QCOMPARE(dialogHelper.dialog->selectedNameFilter()->globs(), { "*.txt" }); + + // Re-open the dialog. + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + // QQuickFileDialogImpl's values only get set before opening. + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->name(), "Text files"); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->index(), 0); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->extensions(), { "txt" }); + QCOMPARE(dialogHelper.quickDialog->selectedNameFilter()->globs(), { "*.txt" }); + + // Only "sub-dir", "text1.txt" and "text2.txt" should be visible, since *.txt is the first filter. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QString failureMessage; + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, + { tempSubDir.path(), tempFile1->fileName(), tempFile2->fileName() }, failureMessage), qPrintable(failureMessage)); + + // Open the ComboBox's popup. + const QQuickComboBox *comboBox = dialogHelper.quickDialog->findChild(); + QVERIFY(comboBox); + const QPoint comboBoxCenterPos = comboBox->mapToScene({ comboBox->width() / 2, comboBox->height() / 2 }).toPoint(); + QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier, comboBoxCenterPos); + QTRY_VERIFY(comboBox->popup()->isOpened()); + + // Select the .html delegate and close the combobox popup. The only visible entry should be the sub-dir. + QQuickListView *comboBoxPopupListView = qobject_cast(comboBox->popup()->contentItem()); + QVERIFY(comboBoxPopupListView); + { + QQuickAbstractButton *htmlDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(comboBoxPopupListView, 1, htmlDelegate)); + QVERIFY(clickButton(htmlDelegate)); + } + QTRY_VERIFY(!comboBox->popup()->isVisible()); + // Use QTRY_VERIFY2 here to fix a failure on QEMU armv7 (QT_QPA_PLATFORM=offscreen). + // Not sure why it's necessary. + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, { tempSubDir.path() }, failureMessage), qPrintable(failureMessage)); + + // Open the popup again. + QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier, comboBoxCenterPos); + QTRY_VERIFY(comboBox->popup()->isOpened()); + + // Select .txt and close the combobox popup. The original entries should be visible. + { + QQuickAbstractButton *txtDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(comboBoxPopupListView, 0, txtDelegate)); + QCOMPARE(txtDelegate->text(), nameFilters.at(0)); + QVERIFY(clickButton(txtDelegate)); + } + QTRY_VERIFY(!comboBox->popup()->isVisible()); + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, + { tempSubDir.path(), tempFile1->fileName(), tempFile2->fileName() }, failureMessage), qPrintable(failureMessage)); +} + +void tst_QQuickFileDialogImpl::changeNameFiltersAfterChangingFolder() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindAllTxtHtmlNameFilters.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Go into the "sub-dir" folder. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QString failureMessage; + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, + { tempSubDir.path(), tempFile1->fileName(), tempFile2->fileName() }, failureMessage), qPrintable(failureMessage)); + QQuickFileDialogDelegate *subDirDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, subDirDelegate)); + QVERIFY(doubleClickButton(subDirDelegate)); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(tempSubDir.path())); + + // Open the ComboBox's popup. + const QQuickComboBox *comboBox = dialogHelper.quickDialog->findChild(); + QVERIFY(comboBox); + const QPoint comboBoxCenterPos = comboBox->mapToScene({ comboBox->width() / 2, comboBox->height() / 2 }).toPoint(); + QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier, comboBoxCenterPos); + QTRY_VERIFY(comboBox->popup()->isOpened()); + + // Select the .html delegate, close the combobox popup, and ensure that the change had an effect. + QQuickListView *comboBoxPopupListView = qobject_cast(comboBox->popup()->contentItem()); + QVERIFY(comboBoxPopupListView); + { + QQuickAbstractButton *htmlDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(comboBoxPopupListView, 2, htmlDelegate)); + QVERIFY(clickButton(htmlDelegate)); + } + QTRY_VERIFY(!comboBox->popup()->isVisible()); + // There are no HTML files in "sub-dir", so there should only be the one "sub-sub-dir" delegate. + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, { tempSubSubDir.path() }, failureMessage), qPrintable(failureMessage)); +} + +void tst_QQuickFileDialogImpl::tabFocusNavigation() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindTxtHtmlNameFilters.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + QList expectedFocusItems; + + // The initial focus should be on the first delegate. + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *firstDelegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, firstDelegate)); + expectedFocusItems << firstDelegate; + + // Tab should move to the name filters combobox. + QQuickComboBox *comboBox = dialogHelper.quickDialog->findChild(); + QVERIFY(comboBox); + expectedFocusItems << comboBox; + + // Next, the left-most dialog button. + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QCOMPARE(dialogButtonBox->count(), 2); + auto leftMostButton = qobject_cast(dialogButtonBox->itemAt(0)); + QVERIFY(leftMostButton); + expectedFocusItems << leftMostButton; + + // Then, the right-most dialog button. + auto rightMostButton = qobject_cast(dialogButtonBox->itemAt(1)); + QVERIFY(rightMostButton); + expectedFocusItems << rightMostButton; + + // Then, the up button. + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + expectedFocusItems << breadcrumbBar->upButton(); + + // Finally, add each bread crumb delegate. + for (int i = 0; i < fileDialogListView->count(); ++i) { + QQuickFileDialogDelegate *delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, i, delegate)); + expectedFocusItems << delegate; + } + + // Tab through each item, checking the focus after each. + for (auto expectedFocusItem : qAsConst(expectedFocusItems)) { + // Check the focus item first so that we account for the first item. + // Print detailed failure message as workaround for QTBUG-92102. + QVERIFY2(dialogHelper.window()->activeFocusItem() == expectedFocusItem, qPrintable(QString::fromLatin1( + "\n Actual: %1\n Expected: %2").arg(QDebug::toString(dialogHelper.window()->activeFocusItem())) + .arg(QDebug::toString(expectedFocusItem)))); + + if (expectedFocusItem != expectedFocusItems.last()) + QTest::keyClick(dialogHelper.window(), Qt::Key_Tab); + } + + // Ensure the order is reversed when shift-tabbing. + std::reverse(expectedFocusItems.begin(), expectedFocusItems.end()); + // We know the first (last) item has focus already, so skip it. + expectedFocusItems.removeFirst(); + for (auto expectedFocusItem : qAsConst(expectedFocusItems)) { + QTest::keyClick(dialogHelper.window(), Qt::Key_Tab, Qt::ShiftModifier); + + QCOMPARE(dialogHelper.window()->activeFocusItem(), expectedFocusItem); + } +} + +void tst_QQuickFileDialogImpl::acceptRejectLabel() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "acceptRejectLabel.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Check that the accept and reject buttons' labels have changed. + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QVERIFY(findDialogButton(dialogButtonBox, "AcceptTest")); + QVERIFY(findDialogButton(dialogButtonBox, "RejectTest")); + + // Close the dialog. + dialogHelper.dialog->close(); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + + // Reset back to the default text. + dialogHelper.dialog->resetAcceptLabel(); + dialogHelper.dialog->resetRejectLabel(); + + // Re-open the dialog. + dialogHelper.dialog->open(); + QVERIFY(dialogHelper.dialog->isVisible()); + QTRY_VERIFY(dialogHelper.quickDialog->isOpened()); + + // Check that the defaults are back. + const QString openText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Open); + QVERIFY2(findDialogButton(dialogButtonBox, openText), qPrintable(QString::fromLatin1( + "Failed to find dialog button with text \"%1\"").arg(openText))); + const QString cancelText = QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Cancel); + QVERIFY2(findDialogButton(dialogButtonBox, cancelText), qPrintable(QString::fromLatin1( + "Failed to find dialog button with text \"%1\"").arg(cancelText))); +} + +void tst_QQuickFileDialogImpl::bindTitle() +{ + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindTitle.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Open the dialog and check that the correct title is displayed. + QQuickFileDialog *dialog = dialogHelper.window()->property("dialog").value(); + QVERIFY(dialog); + const QString expectedTitle = QLatin1String("Test Title"); + QCOMPARE(dialogHelper.dialog->title(), expectedTitle); + QCOMPARE(dialogHelper.quickDialog->title(), expectedTitle); + auto header = dialogHelper.quickDialog->header(); + QVERIFY(header); + auto dialogTitleBarLabel = dialogHelper.quickDialog->header()->findChild("dialogTitleBarLabel"); + QVERIFY(dialogTitleBarLabel); + QCOMPARE(dialogTitleBarLabel->text(), expectedTitle); +} + +void tst_QQuickFileDialogImpl::itemsDisabledWhenNecessary() +{ + QTemporaryDir anotherTempDir; + QVERIFY(anotherTempDir.isValid()); + QDir subDir(anotherTempDir.path()); + QVERIFY(subDir.mkdir("emptyDir")); + QVERIFY(subDir.cd("emptyDir")); + + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", QUrl::fromLocalFile(subDir.path()) }}); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(subDir.path())); + COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(subDir.path())); + + // We opened it in a folder that has no files, so the Open button should be disabled. + QVERIFY(dialogHelper.quickDialog->footer()); + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); + QVERIFY(openButton); + QCOMPARE(openButton->isEnabled(), false); + + // Now go up. The Open button should now be enabled. + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(clickButton(breadcrumbBar->upButton())); + QCOMPARE(openButton->isEnabled(), true); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(anotherTempDir.path())); + COMPARE_URL(dialogHelper.quickDialog->currentFolder(), QUrl::fromLocalFile(anotherTempDir.path())); + + // Get the text edit visible with Ctrl+L. The Open button should now be disabled. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + QVERIFY(breadcrumbBar->textField()->isVisible()); + QCOMPARE(openButton->isEnabled(), false); + + // Hide it with the escape key. The Open button should now be enabled. + QTest::keyClick(dialogHelper.window(), Qt::Key_Escape); + QVERIFY(!breadcrumbBar->textField()->isVisible()); + QCOMPARE(openButton->isEnabled(), true); +} + +void tst_QQuickFileDialogImpl::fileMode_data() +{ + QTest::addColumn("fileMode"); + + QTest::newRow("OpenFile") << QQuickFileDialog::OpenFile; + QTest::newRow("OpenFiles") << QQuickFileDialog::OpenFiles; + QTest::newRow("SaveFile") << QQuickFileDialog::SaveFile; +} + +void tst_QQuickFileDialogImpl::fileMode() +{ + QFETCH(QQuickFileDialog::FileMode, fileMode); + + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + dialogHelper.dialog->setFileMode(fileMode); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + // Select the first file (not a directory). + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QQuickFileDialogDelegate *tempFile1Delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 1, tempFile1Delegate)); + COMPARE_URL(tempFile1Delegate->file(), QUrl::fromLocalFile(tempFile1->fileName())); + QVERIFY(clickButton(tempFile1Delegate)); + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile1->fileName())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempFile1->fileName()) }); + + // All modes should support opening an existing file, so the Open button should be enabled. + QVERIFY(dialogHelper.quickDialog->footer()); + auto dialogButtonBox = dialogHelper.quickDialog->footer()->findChild(); + QVERIFY(dialogButtonBox); + QQuickAbstractButton* openButton = findDialogButton(dialogButtonBox, "Open"); + QVERIFY(openButton); + QCOMPARE(openButton->isEnabled(), true); + + // Only the OpenFiles mode should allow multiple files to be selected, however. + QQuickFileDialogDelegate *tempFile2Delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 2, tempFile2Delegate)); + COMPARE_URL(tempFile2Delegate->file(), QUrl::fromLocalFile(tempFile2->fileName())); + QTest::keyPress(dialogHelper.window(), Qt::Key_Shift); + QVERIFY(clickButton(tempFile2Delegate)); + QTest::keyRelease(dialogHelper.window(), Qt::Key_Shift); + if (fileMode == QQuickFileDialog::OpenFiles) { + // currentFile() always points to the first file in the list of selected files. + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile1->fileName())); + const QList expectedSelectedFiles = { + QUrl::fromLocalFile(tempFile1->fileName()), QUrl::fromLocalFile(tempFile2->fileName()) }; + COMPARE_URLS(dialogHelper.dialog->currentFiles(), expectedSelectedFiles); + } else { + // OpenFile and SaveFile dialogs should have tempFile2 selected since it was clicked, + // but the shift should do nothing, so tempFile1 should no longer be selected. + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }); + } + + // Get the text edit visible with Ctrl+L. + const auto editPathKeySequence = QKeySequence(Qt::CTRL | Qt::Key_L); + QTest::keySequence(dialogHelper.window(), editPathKeySequence); + auto breadcrumbBar = dialogHelper.quickDialog->findChild(); + QVERIFY(breadcrumbBar); + QVERIFY(breadcrumbBar->textField()->isVisible()); + + // Typing in the name of an non-existent file should only work for SaveFile. + const QString nonExistentFilePath = "/foo/bar.txt"; + enterText(dialogHelper.window(), nonExistentFilePath); + QCOMPARE(breadcrumbBar->textField()->text(), nonExistentFilePath); + QTest::keyClick(dialogHelper.window(), Qt::Key_Return); + if (fileMode == QQuickFileDialog::SaveFile) { + COMPARE_URL(dialogHelper.dialog->selectedFile(), QUrl::fromLocalFile(nonExistentFilePath)); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { QUrl::fromLocalFile(nonExistentFilePath) }); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + } else { + // For OpenFile(s), we do what Qt Quick Dialogs 1.x did, and restore the previous (valid) dir path. + // The currentFile(s) should remain unchanged too. + QVERIFY(dialogHelper.dialog->isVisible()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempDir.path())); + QCOMPARE(breadcrumbBar->textField()->text(), tempDir.path()); + + // Should be unchanged from the last time. + if (fileMode == QQuickFileDialog::OpenFiles) { + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile1->fileName())); + const QList expectedSelectedFiles = { + QUrl::fromLocalFile(tempFile1->fileName()), QUrl::fromLocalFile(tempFile2->fileName()) }; + COMPARE_URLS(dialogHelper.dialog->currentFiles(), expectedSelectedFiles); + } else { // OpenFile + COMPARE_URL(dialogHelper.dialog->currentFile(), QUrl::fromLocalFile(tempFile2->fileName())); + COMPARE_URLS(dialogHelper.dialog->currentFiles(), { QUrl::fromLocalFile(tempFile2->fileName()) }); + } + } +} + +void tst_QQuickFileDialogImpl::defaultSuffix_data() +{ + QTest::addColumn("defaultSuffix"); + + QTest::newRow("txt") << "txt"; + QTest::newRow(".txt") << ".txt"; +} + +void tst_QQuickFileDialogImpl::defaultSuffix() +{ + QFETCH(QString, defaultSuffix); + + // Simplify the test by using a directory with no files, and add a single file there. + QFile tempFile1(tempSubSubDir.path() + "/file1"); + QVERIFY(tempFile1.open(QIODevice::ReadWrite)); + + // Open the dialog. + DialogTestHelper dialogHelper(this, "bindCurrentFolder.qml", {}, + {{ "initialFolder", QUrl::fromLocalFile(tempSubSubDir.path()) }}); + dialogHelper.dialog->setDefaultSuffix(defaultSuffix); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + COMPARE_URL(dialogHelper.dialog->currentFolder(), QUrl::fromLocalFile(tempSubSubDir.path())); + + // There should be one extension-less file: "file1". + auto fileDialogListView = dialogHelper.quickDialog->findChild("fileDialogListView"); + QVERIFY(fileDialogListView); + QString failureMessage; + const QStringList expectedVisibleFiles = { tempFile1.fileName() }; + QTRY_VERIFY2(verifyFileDialogDelegates(fileDialogListView, expectedVisibleFiles, failureMessage), qPrintable(failureMessage)); + + // Choose the delegate. The suffix should be added to the delegates. + QQuickFileDialogDelegate *file1Delegate = nullptr; + QTRY_VERIFY(findViewDelegateItem(fileDialogListView, 0, file1Delegate)); + COMPARE_URL(file1Delegate->file(), QUrl::fromLocalFile(tempFile1.fileName())); + QVERIFY(doubleClickButton(file1Delegate)); + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + const QUrl fileUrlWithSuffix = QUrl::fromLocalFile(tempFile1.fileName() + ".txt"); + COMPARE_URL(dialogHelper.dialog->selectedFile(), fileUrlWithSuffix); + COMPARE_URLS(dialogHelper.dialog->selectedFiles(), { fileUrlWithSuffix }); +} + +void tst_QQuickFileDialogImpl::done_data() +{ + QTest::addColumn("result"); + + QTest::newRow("Accepted") << QQuickFileDialog::Accepted; + QTest::newRow("Rejected") << QQuickFileDialog::Rejected; +} + +void tst_QQuickFileDialogImpl::done() +{ + QFETCH(QQuickFileDialog::StandardCode, result); + + // Open the dialog. + DialogTestHelper dialogHelper(this, "fileDialog.qml"); + QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); + QVERIFY(dialogHelper.waitForWindowActive()); + QVERIFY(dialogHelper.openDialog()); + QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); + + switch (result) { + case QQuickFileDialog::Accepted: + QVERIFY(QMetaObject::invokeMethod(dialogHelper.window(), "doneAccepted")); + break; + case QQuickFileDialog::Rejected: + QVERIFY(QMetaObject::invokeMethod(dialogHelper.window(), "doneRejected")); + break; + } + + QVERIFY(!dialogHelper.dialog->isVisible()); + QTRY_VERIFY(!dialogHelper.quickDialog->isVisible()); + QCOMPARE(dialogHelper.dialog->result(), result); +} + +int main(int argc, char *argv[]) +{ + // We need to set this attribute, and this (defining main() ourselves and + // calling QTEST_MAIN_IMPL) seems to be the nicest way to do it without + // duplicating too much code. + // We also don't want to run this for every style, as each one will have + // different ways of implementing the dialogs. + QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + // For now we only test one style. + QQuickStyle::setStyle("Basic"); + QTEST_MAIN_IMPL(tst_QQuickFileDialogImpl) +} + +#include "tst_qquickfiledialogimpl.moc" diff --git a/tests/auto/shared/dialogtestutil.h b/tests/auto/shared/dialogtestutil.h new file mode 100644 index 00000000..c10c4001 --- /dev/null +++ b/tests/auto/shared/dialogtestutil.h @@ -0,0 +1,144 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKDIALOGTESTUTIL_H +#define QQUICKDIALOGTESTUTIL_H + +#include + +#include "util.h" +#include "visualtestutil.h" + +// We need these for Windows, because FolderListModel returns a lowercase drive letter; e.g.: +// "file:///c:/blah.txt", whereas other API returns "file:///C:/blah.txt". +#define COMPARE_URL(url1, url2) \ + QCOMPARE(QFileInfo(url1.toLocalFile()).absoluteFilePath(), QFileInfo(url2.toLocalFile()).absoluteFilePath()); + +// Store a copy of the arguments in case { ... } list initializer syntax is used as an argument, +// which could result in two different lists being created and passed to std::transform() +// (and would also require it to be enclosed in parentheses everywhere it's used). +#define COMPARE_URLS(actualUrls, expectedUrls) \ +{ \ + const QList actualUrlsCopy = actualUrls; \ + QList actualPaths; \ + std::transform(actualUrlsCopy.begin(), actualUrlsCopy.end(), std::back_insert_iterator(actualPaths), \ + [](const QUrl &url) { return QFileInfo(url.toLocalFile()).absoluteFilePath(); }); \ + const QList expectedUrlsCopy = expectedUrls; \ + QList expectedPaths; \ + std::transform(expectedUrlsCopy.begin(), expectedUrlsCopy.end(), std::back_insert_iterator(expectedPaths), \ + [](const QUrl &url) { return QFileInfo(url.toLocalFile()).absoluteFilePath(); }); \ + QCOMPARE(actualPaths, expectedPaths); \ +} + +namespace QQuickDialogTestUtil +{ + +// Saves duplicating a bunch of code in every test. +template +class DialogTestHelper +{ +public: + DialogTestHelper(QQmlDataTest *testCase, const QString &testFilePath, + const QStringList &qmlImportPaths = {}, const QVariantMap &initialProperties = {}) : + appHelper(testCase, testFilePath, qmlImportPaths, initialProperties) + { + if (!appHelper.ready) + return; + + dialog = appHelper.window->property("dialog").value(); + if (!dialog) { + appHelper.errorMessage = "\"dialog\" property is not valid"; + return; + } + + appHelper.window->show(); + appHelper.window->requestActivate(); + } + + Q_REQUIRED_RESULT bool isWindowInitialized() const + { + return appHelper.ready; + } + + Q_REQUIRED_RESULT bool waitForWindowActive() + { + return QTest::qWaitForWindowActive(appHelper.window); + } + + bool openDialog() + { + dialog->open(); + if (!dialog->isVisible()) { + appHelper.errorMessage = "Dialog is not visible"; + return false; + } + + // We might want to call this function more than once, + // and we only need to get these members the first time. + if (!quickDialog) { + quickDialog = appHelper.window->findChild(); + if (!quickDialog) { + appHelper.errorMessage = "Can't find Qt Quick-based dialog"; + return false; + } + } + + return true; + } + + bool isQuickDialogOpen() const + { + return quickDialog->isOpened(); + } + + QQuickWindow *window() const + { + return appHelper.window; + } + + const char *failureMessage() const + { + return appHelper.errorMessage.constData(); + } + + QQuickVisualTestUtil::QQuickApplicationHelper appHelper; + DialogType *dialog = nullptr; + QuickDialogType *quickDialog = nullptr; +}; + +} + +#endif // QQUICKDIALOGTESTUTIL_H diff --git a/tests/auto/shared/visualtestutil.h b/tests/auto/shared/visualtestutil.h index 215a487b..76c44821 100644 --- a/tests/auto/shared/visualtestutil.h +++ b/tests/auto/shared/visualtestutil.h @@ -127,6 +127,20 @@ namespace QQuickVisualTestUtil QQuickItem* findViewDelegateItem(QQuickItemView *itemView, int index, FindViewDelegateItemFlags flags = FindViewDelegateItemFlag::PositionViewAtIndex); + /*! + \internal + + Same as above except allows use in QTRY_* functions without having to call it again + afterwards to assign the delegate. + */ + template + bool findViewDelegateItem(QQuickItemView *itemView, int index, T &delegateItem, + FindViewDelegateItemFlags flags = FindViewDelegateItemFlag::PositionViewAtIndex) + { + delegateItem = qobject_cast(findViewDelegateItem(itemView, index, flags)); + return delegateItem != nullptr; + } + class QQuickApplicationHelper { public: diff --git a/tests/manual/dialogs/CMakeLists.txt b/tests/manual/dialogs/CMakeLists.txt new file mode 100644 index 00000000..d99d64b9 --- /dev/null +++ b/tests/manual/dialogs/CMakeLists.txt @@ -0,0 +1,31 @@ +if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(dialogs LANGUAGES C CXX ASM) + find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_manual_test(dialogs + GUI + SOURCES + dialogs.cpp + PUBLIC_LIBRARIES + Qt::Gui + Qt::Qml + Qt::QuickControls2 +) + +# Resources: +set(qmake_immediate_resource_files + "dialogs.qml" + "FileDialogPage.qml" + "StringListView.qml" + "qmldir" + "Theme.qml" +) + +qt_internal_add_resource(dialogs "qmake_immediate" + PREFIX + "/" + FILES + ${qmake_immediate_resource_files} +) diff --git a/tests/manual/dialogs/FileDialogPage.qml b/tests/manual/dialogs/FileDialogPage.qml new file mode 100644 index 00000000..9faad47e --- /dev/null +++ b/tests/manual/dialogs/FileDialogPage.qml @@ -0,0 +1,357 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts + +import "." + +ColumnLayout { + property alias dialog: fileDialog + + // Put it all in another ColumnLayout so we can easily add margins. + ColumnLayout { + Layout.leftMargin: 12 + Layout.rightMargin: 12 + Layout.topMargin: 12 + Layout.bottomMargin: 12 + + GroupBox { + title: qsTr("Dialog properties") + + Layout.fillWidth: true + + GridLayout { + columns: 2 + anchors.fill: parent + + Label { + text: qsTr("modality") + + Layout.alignment: Qt.AlignTop + Layout.minimumWidth: ApplicationWindow.window.width * 0.2 + Layout.maximumWidth: ApplicationWindow.window.width * 0.2 + } + ButtonGroup { + id: modalityButtonGroup + buttons: modalityColumnLayout.children + } + ColumnLayout { + id: modalityColumnLayout + + RadioButton { + text: qsTr("Qt.NonModal") + + readonly property int modality: Qt.NonModal + } + RadioButton { + text: qsTr("Qt.WindowModal") + checked: true + + readonly property int modality: Qt.WindowModal + } + RadioButton { + text: qsTr("Qt.ApplicationModal") + + readonly property int modality: Qt.ApplicationModal + } + } + + Label { + text: qsTr("result") + } + TextField { + id: resultTextField + text: fileDialog.result === 1 ? qsTr("Accepted") : qsTr("Rejected") + readOnly: true + enabled: false + } + + Label { + text: qsTr("title") + } + TextField { + id: titleTextField + text: qsTr("Choose a file") + } + } + } + + GroupBox { + title: qsTr("FileDialog properties") + + Layout.fillWidth: true + + GridLayout { + columns: 2 + anchors.fill: parent + + Label { + text: qsTr("acceptLabel") + + Layout.minimumWidth: ApplicationWindow.window.width * 0.2 + Layout.maximumWidth: ApplicationWindow.window.width * 0.2 + } + TextField { + id: acceptLabelTextField + text: qsTr("OK") + } + + Label { + text: qsTr("currentFile") + } + TextField { + id: currentFileTextField + text: fileDialog.currentFile + readOnly: true + selectByMouse: true + + Layout.fillWidth: true + } + + Label { + text: qsTr("currentFolder") + } + TextField { + id: currentFolderTextField + text: fileDialog.currentFolder + readOnly: true + selectByMouse: true + + Layout.fillWidth: true + } + + Label { + text: qsTr("currentFiles") + + Layout.alignment: Qt.AlignTop + } + StringListView { + id: currentFilesListView + // QTBUG-72906 + model: [].concat(fileDialog.currentFiles) + } + + Label { + text: qsTr("fileOptions") + + Layout.alignment: Qt.AlignTop + } + ColumnLayout { + id: fileOptionsColumnLayout + + CheckBox { + id: dontResolveSymlinksCheckBox + text: qsTr("DontResolveSymlinks") + + readonly property int fileOption: checked ? FileDialog.DontResolveSymlinks : 0 + } + CheckBox { + id: dontConfirmOverwriteCheckBox + text: qsTr("DontConfirmOverwrite") + + readonly property int fileOption: checked ? FileDialog.DontConfirmOverwrite : 0 + } + CheckBox { + id: readOnlyCheckBox + text: qsTr("ReadOnly") + + readonly property int fileOption: checked ? FileDialog.ReadOnly : 0 + } + CheckBox { + id: hideNameFilterDetailsCheckBox + text: qsTr("HideNameFilterDetails") + + readonly property int fileOption: checked ? FileDialog.HideNameFilterDetails : 0 + } + } + + Label { + text: qsTr("fileMode") + + Layout.alignment: Qt.AlignTop + } + ButtonGroup { + id: fileModeButtonGroup + buttons: fileModeColumnLayout.children + } + ColumnLayout { + id: fileModeColumnLayout + + RadioButton { + text: qsTr("OpenFile") + + readonly property int fileMode: FileDialog.OpenFile + } + RadioButton { + text: qsTr("OpenFiles") + checked: true + + readonly property int fileMode: FileDialog.OpenFiles + } + RadioButton { + text: qsTr("SaveFile") + + readonly property int fileMode: FileDialog.SaveFile + } + } + + Label { + text: qsTr("nameFilters") + } + TextField { + id: nameFiltersTextField + text: ["Text files (*.txt)", "HTML files (*.html)"].join(",") + + Layout.fillWidth: true + + ToolTip.text: qsTr("For this example, a comma-separated string") + ToolTip.visible: hovered + ToolTip.delay: Theme.toolTipDelay + } + + Label { + text: qsTr("rejectLabel") + } + TextField { + id: rejectLabelTextField + text: qsTr("Cancel") + } + + Label { + text: qsTr("selectedFile") + } + TextField { + id: selectedFileTextField + text: fileDialog.selectedFile + readOnly: true + selectByMouse: true + + Layout.fillWidth: true + } + + Label { + text: qsTr("selectedFiles") + + Layout.alignment: Qt.AlignTop + } + StringListView { + id: selectedFilesListView + // QTBUG-72906 + model: [].concat(fileDialog.selectedFiles) + } + + Label { + text: qsTr("selectedNameFilter.name") + } + TextField { + id: selectedNameFilterNameTextField + text: fileDialog.selectedNameFilter.name + readOnly: true + selectByMouse: true + + Layout.fillWidth: true + } + + Label { + text: qsTr("selectedNameFilter.globs") + + Layout.alignment: Qt.AlignTop + } + StringListView { + id: selectedNameFilterGlobsListView + // QTBUG-72906 + model: [].concat(fileDialog.selectedNameFilter.globs) + } + + Label { + text: qsTr("selectedNameFilter.index") + } + TextField { + id: selectedNameFilterIndexTextField + text: fileDialog.selectedNameFilter.index + readOnly: true + selectByMouse: true + + Layout.fillWidth: true + } + + Label { + text: qsTr("selectedNameFilter.extensions") + + Layout.alignment: Qt.AlignTop + } + StringListView { + id: selectedNameFilterExtensionsListView + // QTBUG-72906 + model: [].concat(fileDialog.selectedNameFilter.extensions) + } + } + } + + FileDialog { + id: fileDialog + + modality: modalityButtonGroup.checkedButton.modality + title: titleTextField.text + + acceptLabel: acceptLabelTextField.text + fileMode: fileModeButtonGroup.checkedButton.fileMode + options: dontResolveSymlinksCheckBox.fileOption + | dontConfirmOverwriteCheckBox.fileOption + | readOnlyCheckBox.fileOption + | hideNameFilterDetailsCheckBox.fileOption + nameFilters: nameFiltersTextField.text.split(",") + rejectLabel: rejectLabelTextField.text + } + } +} diff --git a/tests/manual/dialogs/StringListView.qml b/tests/manual/dialogs/StringListView.qml new file mode 100644 index 00000000..4a229404 --- /dev/null +++ b/tests/manual/dialogs/StringListView.qml @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "." + +ListView { + id: root + clip: true + boundsBehavior: Flickable.StopAtBounds + + Layout.fillWidth: true + Layout.preferredHeight: count > 0 ? 128 : noneLabel.implicitHeight + + ScrollBar.vertical: ScrollBar {} + + delegate: TextField { + width: root.width + text: modelData + readOnly: true + selectByMouse: true + } + + Label { + id: noneLabel + text: qsTr("(None)") + visible: root.count === 0 + } +} diff --git a/tests/manual/dialogs/Theme.qml b/tests/manual/dialogs/Theme.qml new file mode 100644 index 00000000..8114f14e --- /dev/null +++ b/tests/manual/dialogs/Theme.qml @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +pragma Singleton + +import QtQml + +QtObject { + readonly property int toolTipDelay: 1000 +} diff --git a/tests/manual/dialogs/dialogs.cpp b/tests/manual/dialogs/dialogs.cpp new file mode 100644 index 00000000..8bab70e2 --- /dev/null +++ b/tests/manual/dialogs/dialogs.cpp @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QCoreApplication::setApplicationName("dialogs-manual-test"); + QCoreApplication::setOrganizationName("QtProject"); + + // Use native dialogs by default unless the user asked us not to. + QSettings settings; + const QVariant useNativeDialogs = settings.value("useNativeDialogs"); + if (useNativeDialogs.isValid()) { + if (!useNativeDialogs.toBool()) + QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); + } else { + // Set the default here so that we can use an alias in QML. + // Without this it defaults to CheckBox's checked default value, which is false. + settings.setValue("useNativeDialogs", true); + } + + QQmlApplicationEngine engine; + engine.setInitialProperties({{ "style", QQuickStyle::name() }}); + engine.load(QUrl(QStringLiteral("qrc:/dialogs.qml"))); + + return app.exec(); +} diff --git a/tests/manual/dialogs/dialogs.pro b/tests/manual/dialogs/dialogs.pro new file mode 100644 index 00000000..abb31492 --- /dev/null +++ b/tests/manual/dialogs/dialogs.pro @@ -0,0 +1,11 @@ +TEMPLATE = app +TARGET = dialogs +QT += qml quickcontrols2 + +SOURCES += dialogs.cpp +RESOURCES += \ + dialogs.qml \ + FileDialogPage.qml \ + StringListView.qml \ + qmldir \ + Theme.qml diff --git a/tests/manual/dialogs/dialogs.qml b/tests/manual/dialogs/dialogs.qml new file mode 100644 index 00000000..d1f54c08 --- /dev/null +++ b/tests/manual/dialogs/dialogs.qml @@ -0,0 +1,127 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Qt.labs.settings + +ApplicationWindow { + id: window + width: 800 + height: 600 + title: "dialogs - style: " + style + visible: true + + required property string style + + Component.onCompleted: { + x = Screen.width / 2 - width / 2 + y = Screen.height / 2 - height / 2 + } + + Settings { + id: settings + + property alias useNativeDialogs: useNativeDialogsCheckBox.checked + } + + Page { + anchors.fill: parent + + header: TabBar { + id: tabBar + + TabButton { + text: qsTr("FileDialog") + } + TabButton { + text: qsTr("Coming Soon...") + } + } + + ScrollView { + id: scrollView + anchors.fill: parent + clip: true + + StackLayout { + id: stackLayout + currentIndex: tabBar.currentIndex + width: scrollView.width + + FileDialogPage {} + } + } + } + + footer: ToolBar { + RowLayout { + anchors.fill: parent + + CheckBox { + id: useNativeDialogsCheckBox + text: qsTr("Use Native Dialogs (requires restart)") + checked: settings.useNativeDialogs + } + + Item { + Layout.fillWidth: true + } + + Button { + text: qsTr("Open") + + onClicked: stackLayout.children[stackLayout.currentIndex].dialog.open() + } + } + } +} diff --git a/tests/manual/dialogs/qmldir b/tests/manual/dialogs/qmldir new file mode 100644 index 00000000..4f4a6b7b --- /dev/null +++ b/tests/manual/dialogs/qmldir @@ -0,0 +1,3 @@ +module org.qtproject.examples.dialogs + +singleton Theme 1.0 Theme.qml -- cgit v1.2.3