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 <spham@amazon.com>
This commit is contained in:
Steve Pham
2021-10-08 11:41:46 -07:00
committed by GitHub
parent 960edf857d
commit 73f8537030
15 changed files with 234 additions and 6 deletions
@@ -6,10 +6,11 @@
*
*/
#include <AzQtComponents/AzQtComponents_Traits_Platform.h>
#include <AzQtComponents/Components/Widgets/FileDialog.h>
#include <QMessageBox>
#include <QRegExp>
#include <QRegularExpression>
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
// <Filter Name> (<filter pattern1> <filter pattern2> .. <filter patternN> )
//
// For example:
// "Images (*.gif *.png *.jpg)"
//
// Extract the contents of the <filter pattern>(s) inside the parenthesis and split them based on a whitespace or comma
const QRegularExpression filterContent(".*\\((?<filters>[^\\)]+)\\)");
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
@@ -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
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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 <AzTest/AzTest.h>
#include <AzQtComponents/Components/Widgets/FileDialog.h>
#include <QString>
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());
}
@@ -9,6 +9,7 @@
set(FILES
Tests/AzQtComponentTests.cpp
Tests/ColorControllerTests.cpp
Tests/FileDialogTests.cpp
Tests/FloatToStringConversionTests.cpp
Tests/HexParsingTests.cpp
Tests/StyleSheetCacheTests.cpp
@@ -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
@@ -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
@@ -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 <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 0
#define AZ_TRAIT_AZQTCOMPONENTS_FILE_DIALOG_FILTER_CASE_SENSITIVITY QRegularExpression::NoPatternOption
@@ -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 <AzQtComponents/AzQtComponents_Traits_Mac.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 <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
@@ -410,7 +410,7 @@ namespace AzToolsFramework
filter.append(ext);
if (i < n - 1)
{
filter.append(", ");
filter.append(" ");
}
}
filter.append(")");