From 73f8537030aaa5efd126c991e28e61c803fc560e Mon Sep 17 00:00:00 2001 From: Steve Pham <82231385+spham-amzn@users.noreply.github.com> Date: Fri, 8 Oct 2021 11:41:46 -0700 Subject: [PATCH] Fix Issue saving new assets in Asset Editor on Linux (#4537) * Add helper function to apply a selected file filter from a file dialog to the result filename if needed * Add platform traits to restrict the use of the helper function on platforms that need to apply it * Fix building of file filters of multiple extensions for a file type Signed-off-by: Steve Pham --- .../Components/Widgets/FileDialog.cpp | 92 ++++++++++++++++++- .../Components/Widgets/FileDialog.h | 6 ++ .../Platform/Linux/platform_linux_files.cmake | 2 + .../Platform/Mac/platform_mac_files.cmake | 2 + .../Windows/platform_windows_files.cmake | 2 + .../AzQtComponents/Tests/FileDialogTests.cpp | 65 +++++++++++++ .../azqtcomponents_testing_files.cmake | 1 + Code/Framework/AzQtComponents/CMakeLists.txt | 5 + .../AzQtComponents_Traits_Linux.h | 11 +++ .../AzQtComponents_Traits_Platform.h | 10 ++ .../AzQtComponents_Traits_Mac.h | 11 +++ .../AzQtComponents_Traits_Platform.h | 10 ++ .../AzQtComponents_Traits_Platform.h | 10 ++ .../AzQtComponents_Traits_Windows.h | 11 +++ .../AssetEditor/AssetEditorWidget.cpp | 2 +- 15 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 Code/Framework/AzQtComponents/AzQtComponents/Tests/FileDialogTests.cpp create mode 100644 Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h create mode 100644 Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h create mode 100644 Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h create mode 100644 Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h create mode 100644 Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h create mode 100644 Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.cpp index d2d773ff93..3061ceedd5 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.cpp @@ -6,10 +6,11 @@ * */ +#include #include #include -#include +#include namespace AzQtComponents { @@ -24,7 +25,12 @@ namespace AzQtComponents // Trigger Qt's save filename dialog // If filePath isn't empty, it means we are prompting again because the filename was invalid, // so pass it instead of the directory so the filename is pre-filled in for the user - filePath = QFileDialog::getSaveFileName(parent, caption, (filePath.isEmpty()) ? dir : filePath, filter, selectedFilter, options); + QString localSelectedFilter; + filePath = QFileDialog::getSaveFileName(parent, caption, (filePath.isEmpty()) ? dir : filePath, filter, &localSelectedFilter, options); + if (selectedFilter) + { + *selectedFilter = localSelectedFilter; + } if (!filePath.isEmpty()) { @@ -32,15 +38,39 @@ namespace AzQtComponents QString fileName = fileInfo.fileName(); // Check if the filename has any invalid characters - QRegExp validFileNameRegex("^[a-zA-Z0-9_\\-./]*$"); - shouldPromptAgain = !validFileNameRegex.exactMatch(fileName); + QRegularExpression validFileNameRegex("^[a-zA-Z0-9_\\-./]*$"); + QRegularExpressionMatch validFileNameMatch = validFileNameRegex.match(fileName); // If the filename had invalid characters, then show a warning message and then we will re-prompt the save filename dialog - if (shouldPromptAgain) + if (!validFileNameMatch.hasMatch()) { QMessageBox::warning(parent, QObject::tr("Invalid filename"), QObject::tr("O3DE assets are restricted to alphanumeric characters, hyphens (-), underscores (_), and dots (.)\n\n%1").arg(fileName)); + shouldPromptAgain = true; + continue; } + else + { + shouldPromptAgain = false; + } +#if AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION + // If a filter was selected, then make sure that the resulting filename ends with that extension. On systems that use the default QFileDialog, + // the extension is not guaranteed to be set in the resulting filename + if (FileDialog::ApplyMissingExtension(localSelectedFilter, filePath)) + { + // If an extension had to be applied, then the file dialog did not handle the case of overwriting existing files. + // We need to check that condition before we proceed + QFileInfo updatedFilePath(filePath); + + if (updatedFilePath.exists()) + { + QMessageBox::StandardButton overwriteSelection = QMessageBox::question(parent, + QObject::tr("File exists"), + QObject::tr("%1 exists. Do you want to overwrite the existing file?").arg(updatedFilePath.fileName())); + shouldPromptAgain = (overwriteSelection == QMessageBox::No); + } + } +#endif // AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION } else { @@ -51,4 +81,56 @@ namespace AzQtComponents return filePath; } + + bool FileDialog::ApplyMissingExtension(const QString& selectedFilter, QString& filePath) + { + if (selectedFilter.isEmpty()) + { + return false; + } + + // According to the QT documentation for QFileDialog, the selected filter will come in the form + // ( .. ) + // + // For example: + // "Images (*.gif *.png *.jpg)" + // + // Extract the contents of the (s) inside the parenthesis and split them based on a whitespace or comma + const QRegularExpression filterContent(".*\\((?[^\\)]+)\\)"); + QRegularExpressionMatch filterContentMatch = filterContent.match(selectedFilter); + if (!filterContentMatch.hasMatch()) + { + return false; + } + QString filterExtensionsString = filterContentMatch.captured("filters"); + QStringList filterExtensionsFull = filterExtensionsString.split(" ", Qt::SkipEmptyParts); + if (filterExtensionsFull.length() <= 0) + { + return false; + } + + // If there are multiple suffixes in the selected filter, then default to the first one if a suffix needs to be appended + QString defaultSuffix = filterExtensionsFull[0].mid(1); + + // Iterate through the filter patterns to see if the current filename matches + QFileInfo fileInfo(filePath); + bool extensionNeeded = true; + for (const QString& filterExtensionFull : filterExtensionsFull) + { + QString wildcardExpression = QRegularExpression::wildcardToRegularExpression(filterExtensionFull); + QRegularExpression filterPattern(wildcardExpression, AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY); + QRegularExpressionMatch filterPatternMatch = filterPattern.match(fileInfo.fileName()); + if (filterPatternMatch.hasMatch()) + { + // The filename matches one of the filter patterns already, the extension does not need to be added to the filename + extensionNeeded = false; + } + } + if (extensionNeeded) + { + // If the current (if any) suffix does not match, automatically add the default suffix for the selected filter + filePath.append(defaultSuffix); + } + return extensionNeeded; + } } // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.h index 6b63404949..9f27431400 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/FileDialog.h @@ -24,6 +24,12 @@ namespace AzQtComponents static QString GetSaveFileName(QWidget* parent = nullptr, const QString& caption = QString(), const QString& dir = QString(), const QString& filter = QString(), QString* selectedFilter = nullptr, QFileDialog::Options options = QFileDialog::Options()); + + //! Helper method that parses a selected filter from Qt's QFileDialog::getSaveFileName and applies the + //! selected filter's extension to the filePath if it doesnt already have the extension. This is needed + //! on platforms that do not have a default file dialog (These platforms uses Qt's custom file dialog which will + //! not apply the filter's extension automatically on user entered filenames) + static bool ApplyMissingExtension(const QString& selectedFilter, QString& filePath); }; } // namespace AzQtComponents diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake index 7f204e5022..ec71d7b508 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Linux/platform_linux_files.cmake @@ -12,4 +12,6 @@ set(FILES ../../Utilities/QtWindowUtilities_linux.cpp ../../Utilities/ScreenGrabber_linux.cpp ../../../Platform/Linux/AzQtComponents/Components/StyledDockWidget_Linux.cpp + ../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h + ../../../Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake index 50084d7a1e..18f9479289 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Mac/platform_mac_files.cmake @@ -12,4 +12,6 @@ set(FILES ../../Utilities/QtWindowUtilities_mac.mm ../../Utilities/ScreenGrabber_mac.mm ../../../Platform/Mac/AzQtComponents/Components/StyledDockWidget_Mac.cpp + ../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h + ../../../Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake index 37d1d0f390..ca4145ef35 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/Platform/Windows/platform_windows_files.cmake @@ -17,4 +17,6 @@ set(FILES ../../Components/TitleBarOverdrawScreenHandler_win.h ../../Components/TitleBarOverdrawScreenHandler_win.cpp ../../../Platform/Windows/AzQtComponents/Components/StyledDockWidget_Windows.cpp + ../../../Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h + ../../../Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h ) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Tests/FileDialogTests.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Tests/FileDialogTests.cpp new file mode 100644 index 0000000000..e3dc4b4f0d --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Tests/FileDialogTests.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +TEST(AzQtComponents, ApplyMissingExtension_UpdateMissingExtension_Success) +{ + const QString textFiler{"Text Files (*.txt)"}; + QString testPath{"testFile"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_TRUE(result); + EXPECT_STRCASEEQ("testFile.txt", testPath.toUtf8().constData()); +} + +TEST(AzQtComponents, ApplyMissingExtension_NoUpdateExistingExtension_Success) +{ + const QString textFiler{"Text Files (*.txt)"}; + QString testPath{"testFile.txt"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_FALSE(result); + EXPECT_STRCASEEQ("testFile.txt", testPath.toUtf8().constData()); +} + +TEST(AzQtComponents, ApplyMissingExtension_UpdateMissingExtensionMultipleExtensionFilter_Success) +{ + const QString textFiler{"Image Files (*.jpg *.bmp *.png)"}; + QString testPath{"testFile"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_TRUE(result); + EXPECT_STRCASEEQ("testFile.jpg", testPath.toUtf8().constData()); +} + +TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionMultipleExtensionFilter_Success) +{ + const QString textFiler{"Image Files (*.jpg *.bmp *.png)"}; + QString testPath{"testFile.png"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_FALSE(result); + EXPECT_STRCASEEQ("testFile.png", testPath.toUtf8().constData()); +} + +TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionEmptyFilter_Success) +{ + const QString textFiler{""}; + QString testPath{"testFile"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_FALSE(result); + EXPECT_STRCASEEQ("testFile", testPath.toUtf8().constData()); +} + +TEST(AzQtComponents, ApplyMissingExtension_NoUpdateMissingExtensionInvalidFilter_Success) +{ + const QString textFiler{"Bad Filter!!"}; + QString testPath{"testFile"}; + bool result = AzQtComponents::FileDialog::ApplyMissingExtension(textFiler, testPath); + EXPECT_FALSE(result); + EXPECT_STRCASEEQ("testFile", testPath.toUtf8().constData()); +} diff --git a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_testing_files.cmake b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_testing_files.cmake index 2611063305..b4ab487c79 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_testing_files.cmake +++ b/Code/Framework/AzQtComponents/AzQtComponents/azqtcomponents_testing_files.cmake @@ -9,6 +9,7 @@ set(FILES Tests/AzQtComponentTests.cpp Tests/ColorControllerTests.cpp + Tests/FileDialogTests.cpp Tests/FloatToStringConversionTests.cpp Tests/HexParsingTests.cpp Tests/StyleSheetCacheTests.cpp diff --git a/Code/Framework/AzQtComponents/CMakeLists.txt b/Code/Framework/AzQtComponents/CMakeLists.txt index dd217a7be2..a24b68bcd7 100644 --- a/Code/Framework/AzQtComponents/CMakeLists.txt +++ b/Code/Framework/AzQtComponents/CMakeLists.txt @@ -10,6 +10,8 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS) return() endif() +ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME}) + ly_add_target( NAME AzQtComponents SHARED NAMESPACE AZ @@ -26,6 +28,7 @@ ly_add_target( AzQtComponents PUBLIC . + ${pal_dir} COMPILE_DEFINITIONS PRIVATE AZ_QT_COMPONENTS_EXPORT_SYMBOLS @@ -53,6 +56,7 @@ ly_add_target( . AzQtComponents AzQtComponents/Gallery + ${pal_dir} BUILD_DEPENDENCIES PRIVATE 3rdParty::Qt::Svg @@ -86,6 +90,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE Tests AzQtComponents + ${pal_dir} BUILD_DEPENDENCIES PRIVATE AZ::AzQtComponents diff --git a/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h new file mode 100644 index 0000000000..685ac4dfed --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Linux.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 1 +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::NoPatternOption diff --git a/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h new file mode 100644 index 0000000000..101fe3a494 --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Linux/AzQtComponents/AzQtComponents_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include diff --git a/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h new file mode 100644 index 0000000000..1cb91ec09e --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Mac.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 0 +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::NoPatternOption diff --git a/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h new file mode 100644 index 0000000000..9b285476c1 --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Mac/AzQtComponents/AzQtComponents_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include diff --git a/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h new file mode 100644 index 0000000000..2c3efc5e6c --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include diff --git a/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h new file mode 100644 index 0000000000..edba0043b4 --- /dev/null +++ b/Code/Framework/AzQtComponents/Platform/Windows/AzQtComponents/AzQtComponents_Traits_Windows.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_APPLY_MISSING_EXTENSION 0 +#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::CaseInsensitiveOption diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp index 8856989911..a159882f72 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp @@ -410,7 +410,7 @@ namespace AzToolsFramework filter.append(ext); if (i < n - 1) { - filter.append(", "); + filter.append(" "); } } filter.append(")");