Integrating latest 47acbe8

This commit is contained in:
alexpete
2021-03-25 13:57:57 -07:00
parent 448c549698
commit 75dc720198
10312 changed files with 2711566 additions and 671451 deletions
+1
View File
@@ -11,3 +11,4 @@
add_subdirectory(detect_file_changes)
add_subdirectory(commit_validation)
add_subdirectory(project_manager)
@@ -3,43 +3,20 @@
"3rdParty.txt": "#include",
"AWS/AWSNativeSDK/1.7.167-az.2/**": "#include",
"AWS/GameLift/3.4.0/**": "#include",
"Blast/1.1.6-az.1/**": "#include",
"benchmark/1.5.0/**": "#include",
"cityhash/1.1-az.1/**": "#include",
"civetweb/civetweb-20160922-az.2/**": "#include",
"Clang/6.0.1-az/**": "#include",
"DirectXShaderCompiler/1.0.1-az.1/**": "#include",
"DirectXShaderCompiler/2020.08.07/**": "#include",
"DirectXShaderCompiler/5.0.0-az/**": "#include",
"dyad/0.2.0-17-amazon/**": "#include",
"etc2comp/2017_04_24-az.2/**": "#include",
"expat/2.1.0-pkg.3/**": "#include",
"FbxSdk/2016.1.2-az.1/**": "#include",
"glad/2.0.0-beta/**": "#include",
"googletest/1.8.1-az.3/**": "#include",
"jsmn/78b1dca/**": "#include",
"libav/11.7/**": "#include",
"LibTomCrypt/1.17-az.2/**": "#include",
"LibTomMath/0.42.0-az.2/**": "#include",
"lz4/r128-pkg.3/**": "#include",
"Lzma/unknown-pkg.3/**": "#include",
"LZSS/unknown-pkg.3/**": "#include",
"md5/2.0-pkg.3/**": "#include",
"mikkelsen/1.0.0-az.2/**": "#include",
"nvapi/R361-developer_V2/**": "#include",
"NvCloth/1.1.6-az.4/**": "#include",
"OpenSSL/1.1.1b-noasm-az/**": "#include",
"p4api/2019.1/**": "#include",
"PhysX/4.1.0.25992954-az.1/**": "#include",
"poly2tri/0.3.3-az.2/**": "#include",
"PVRTexTool/2016_r1.1/**": "#include",
"Qt/5.15.1.2-az/**": "#include",
"RadTelemetry/3.5.0.17/**": "#include",
"rapidjson/rapidjson-1.1.0/**": "#include",
"rapidxml/1.13-modified.1/**": "#include",
"SQLite/v3.32.2/**": "#include",
"tiff/3.9.5-az.3/**": "#include",
"unwind/1.2.1/**": "#include",
"Wwise/2019.2.8.7432/**": "#include",
"xxhash/0.7.4/**": "#include",
"zstd/1.35-pkg.1/**": "#include"
"Wwise/2019.2.8.7432/**": "#include"
}
}
@@ -14,16 +14,16 @@
"FILE_LIST": "symbols.json",
"FILE_LIST_TYPE": "All",
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-windows-all-symbols-${BUILD_NUMBER}.zip"
},
{
"FILE_LIST": "3rdParty.json",
"FILE_LIST_TYPE": "Windows",
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-windows-3rdparty-${BUILD_NUMBER}.zip"
}
],
"BOOTSTRAP_CFG_GAME_FOLDER":"CMakeTestbed",
"SKIP_BUILD": 0,
"BUILD_TARGETS":[
{
"BUILD_CONFIG_FILENAME": "build_config.json",
"PLATFORM": "Windows",
"TYPE": "profile_vs2017"
},
{
"BUILD_CONFIG_FILENAME": "build_config.json",
"PLATFORM": "Windows",
@@ -47,11 +47,6 @@
"BOOTSTRAP_CFG_GAME_FOLDER":"AtomSampleViewer;AtomTest",
"SKIP_BUILD": 1,
"BUILD_TARGETS":[
{
"BUILD_CONFIG_FILENAME": "build_config.json",
"PLATFORM": "Windows",
"TYPE": "profile_vs2017_atom"
},
{
"BUILD_CONFIG_FILENAME": "build_config.json",
"PLATFORM": "Windows",
@@ -0,0 +1,125 @@
{
"@3rdParty":{
"3rdParty.txt":"#include",
"AMD/AGS Lib/2.2":{
"*":"#include",
"inc/**":"#include",
"lib/x64/**":"#include"
},
"AWS/AWSNativeSDK/1.7.167-az.2":{
"*":"#include",
"include/**":"#include",
"LICENSE*":"#include",
"lib/windows/**":"#include",
"bin/windows/**":"#include",
"lib/android/arm64-v8a/**":"#include",
"bin/android/arm64-v8a/**":"#include",
"bin/linux/**":"#include",
"lib/linux/**":"#include"
},
"AWS/GameLift/3.4.0":{
"*":"#include",
"include/**":"#include",
"bin/windows/**":"#include",
"lib/linux/libstdcxx/**":"#include"
},
"civetweb/civetweb-20160922-az.2":{
"src/**":"#include",
"include/**":"#include",
"lib/Windows/**":"#include",
"*":"#include"
},
"DirectXShaderCompiler/1.0.1-az.1":{
"*":"#include",
"src/**":"#include",
"bin/win_x64/**":"#include"
},
"DirectXShaderCompiler/2020.08.07":{
"*":"#include",
"bin/win_x64/**":"#include"
},
"DirectXShaderCompiler/5.0.0-az":{
"*":"#include",
"bin/win_x64/**":"#include"
},
"dyad/0.2.0-17-amazon":{
"*":"#include",
"doc/**":"#include",
"example/**":"#include",
"projects/**":"#include",
"src/**":"#include",
"lib/x64_v140_Debug/**":"#include",
"lib/x64_v140_Release/**":"#include",
"lib/linux_debug/**":"#include",
"lib/linux_release/**":"#include"
},
"etc2comp/2017_04_24-az.2":{
"EtcLib/Etc/**":"#include",
"EtcLib/EtcCodec/**":"#include",
"LICENSE":"#include",
"EtcLib/Windows_x86_64/**":"#include",
"EtcLib/Linux_x64_linux/**":"#include"
},
"expat/2.1.0-pkg.3":{
"*":"#include",
"amiga/**":"#include",
"bcb5/**":"#include",
"conftools/**":"#include",
"doc/**":"#include",
"examples/**":"#include",
"lib/**":"#include",
"m4/**":"#include",
"tests/**":"#include",
"vms/**":"#include",
"win32/**":"#include",
"xmlwf/**":"#include",
"build/win_x64/vc140/**":"#include",
"build/win_x64/android_ndk_r12/android-*/**":"#include",
"build/linux/**":"#include"
},
"FreeType2/2.5.0.1-pkg.3":{
"freetype-2.5.0.1/**":"#include",
"dist/**":"#include",
"vc140_x64/**":"#include",
"build/win_x64/vc140/**":"#include",
"android*/**":"#include",
"build/win_x64/android_ndk_r12/android-*/**":"#include",
"build/linux/clang-3.4/**":"#include"
},
"Redistributables/FbxSdk/2016.1.2":{
"*win*":"#include",
"*vs2013*":"#exclude"
},
"OpenSSL/1.1.1b-noasm-az":{
"include/**":"#include",
"ssl/**":"#include",
"LICENSE":"#include",
"bin/**":"#include",
"lib/vc140_x64_debug/**":"#include",
"lib/vc140_x64_release/**":"#include",
"lib/android_ndk_r15c/android-*/**":"#include",
"lib/linux-x86_64-clang-debug/**":"#include",
"lib/linux-x86_64-clang-release/**":"#include"
},
"Qt/5.15.1.2-az":{
"LICENSE":"#include",
"LGPL_EXCEPTION.TXT":"#include",
"LICENSE.GPLV3":"#include",
"LICENSE.LGPLV3":"#include",
"QT-NOTICE.TXT":"#include",
"msvc*/**":"#include",
"gcc_64/**":"#include"
},
"tiff/3.9.5-az.3":{
"COPYRIGHT":"#include",
"README":"#include",
"RELEASE-DATE":"#include",
"VERSION":"#include",
"libtiff/buildLib32Lib64.bat":"#include",
"libtiff/readme.txt":"#include",
"include/libtiff/*.h":"#include",
"libtiff/*vc140.lib":"#include",
"libtiff/linux_gcc/**":"#include"
}
}
}
@@ -1,28 +1,13 @@
{
"@3rdParty": {
"**/.owner": "#exclude",
"3rdParty.txt": "#move:3rdParty",
"OpenEXR/**": "#move:3rdParty",
"CMake/3.19.1/**": "#move:3rdParty",
"Redistributables":{
"WwiseLTX": {
"LTX_2018.1.2.6762": {
"**": "#move:dev/Tools/Redistributables/WwiseLTX/LTX_2018.1.2.6762",
"*.app.zip": "#exclude",
"WwiseLauncher.pkg": "#exclude"
}
},
"FbxSdk": {
"2016.1.2": {
"*win*": "#move:dev/Tools/Redistributables/FbxSdk/2016.1.2-az.1",
"*vs2013*": "#exclude"
}
}
}
},
"@lyengine": {
"**/*.pyc": "#exclude",
"*": "#include",
".gitattributes": "#exclude",
".gitignore": "#exclude",
".gitmodules": "#exclude",
".lfsconfig": "#exclude",
".p4ignore": "#exclude",
".submodules": "#exclude",
"AtomTest":
{
"**":"#include",
@@ -167,11 +152,6 @@
"3dsmax/**": "#include",
"7za.exe": "#include",
"7za_legal_notice.txt": "#include",
"AWSNativeSDK": {
"**": "#include",
"Upgrader/restricted_platforms.py": "#exclude"
},
"AWSPythonSDK/**": "#include",
"Crashpad/**": "#include",
"CrySCompileServer/**": "#include",
"PakShaders/**": "#include",
+13 -7
View File
@@ -22,6 +22,9 @@ except ImportError:
from pathlib import Path
this_file_path = os.path.dirname(os.path.realpath(__file__))
# resolve symlinks and eliminate ".." components
engine_root_path = Path(__file__).resolve().parents[3]
def convert_glob_pattern_to_regex_pattern(glob_pattern):
# switch to forward slashes because way easier to pattern match against
@@ -75,12 +78,13 @@ def generate_excludes_for_platform(root, platform):
with open(platform_exclusions_filename, 'r') as platform_exclusions_file:
platform_exclusions = json.load(platform_exclusions_file)
else:
# Use real path in case engine root is a symlink path
# Use real path in case root is a symlink path
if os.name == 'posix' and os.path.islink(root):
root = os.readlink(root)
cur_dir = os.path.dirname(os.path.abspath(__file__))
relative_folder = os.path.relpath(cur_dir, root)
platform_exclusions_filename = os.path.join(root, 'restricted', platform, relative_folder, platform.lower() + '_exclusions.json')
# "root" is the root of the folder structure we're validating
# "engine_root_path" is the engine root where the restricted platform folder is linked
relative_folder = os.path.relpath(this_file_path, engine_root_path)
platform_exclusions_filename = os.path.join(engine_root_path, 'restricted', platform, relative_folder, platform.lower() + '_exclusions.json')
with open(platform_exclusions_filename, 'r') as platform_exclusions_file:
platform_exclusions = json.load(platform_exclusions_file)
@@ -100,11 +104,13 @@ def generate_include_exclude_regexes(package_platform, package_type, root, prohi
# Search non-restricted platform first
package_file_list = os.path.join(this_file_path, 'Platform', package_platform, 'package_filelists', f'{package_type}.json')
if not os.path.exists(filelist):
# Use real path in case engine root is a symlink path
# Use real path in case root is a symlink path
if os.name == 'posix' and os.path.islink(root):
root = os.readlink(root)
rel_path = os.path.relpath(cur_dir, root)
package_file_list = os.path.join(root, 'restricted', package_platform, rel_path, 'package_filelists',
# "root" is the root of the folder structure we're validating
# "engine_root_path" is the engine root where the restricted platform folder is linked
rel_path = os.path.relpath(this_file_path, engine_root_path)
package_file_list = os.path.join(engine_root_path, 'restricted', package_platform, rel_path, 'package_filelists',
f'{package_type}.json')
with open(package_file_list, 'r') as package_file:
package = json.load(package_file)
@@ -1,6 +1,13 @@
{
"@lyengine": {
"**": "#include",
".git/**": "#exclude",
".gitattributes": "#exclude",
".gitignore": "#exclude",
".gitmodules": "#exclude",
".lfsconfig": "#exclude",
".p4ignore": "#exclude",
".submodules": "#exclude",
"**/*.pyc": "#exclude",
"**/*.pdb": "#exclude"
}
@@ -186,6 +186,5 @@ EXCLUDED_VALIDATION_PATTERNS = [
'python/runtime',
'restricted/*/Tools/*RemoteControl',
'Tools/3dsmax',
'Tools/AWSPythonSDK',
'Tools/Crashpad',
]
@@ -31,7 +31,6 @@
*/Code/Framework/AzCore/Platform/Provo/AzCore/AzCore_Traits_Provo.h
*/Code/Framework/AzCore/Platform/Salem/AzCore/AzCore_Traits_Salem.h
*/Code/Framework/AzCore/Platform/Windows/AzCore/AzCore_Traits_Windows.h
*/Code/Framework/AzCore/Platform/Xenia/AzCore/AzCore_Traits_Xenia.h
*/Code/Framework/AzCore/Tests/AZStd/Examples.cpp
*/Code/Framework/AzCore/Tests/Memory.cpp
*/Code/Framework/AzFramework/AzFramework/Asset/AssetSystemComponentHelper.cpp
@@ -31,7 +31,6 @@ platform_macros = [
'WIN32',
'WIN32_LEAN_AND_MEAN',
'WIN64',
'XENIA',
'_WIN32',
'_WIN32_WINDOWS',
'_WIN32_WINNT',
+1 -1
View File
@@ -20,7 +20,7 @@ GOTO pythonDirNotFound
:pythonPathAvailable
SET PYTHON_EXECUTABLE=%PYTHON_DIRECTORY%\python.cmd
IF NOT EXIST "%PYTHON_EXECUTABLE%" GOTO pythonExeNotFound
CALL "%PYTHON_EXECUTABLE%" %BASE_PATH%\scripts\lmbr.py %*
CALL "%PYTHON_EXECUTABLE%" %BASE_PATH%\scripts\o3de.py %*
GOTO end
:pythonDirNotFound
ECHO Python directory not found: %PYTHON_DIRECTORY%
+2
View File
@@ -21,12 +21,14 @@ if ROOT_DEV_PATH not in sys.path:
from cmake.Tools import engine_template
from cmake.Tools import current_project
from cmake.Tools import add_remove_gem
from cmake.Tools import registration
def add_args(parser, subparsers) -> None:
current_project.add_args(parser, subparsers)
engine_template.add_args(parser, subparsers)
add_remove_gem.add_args(parser, subparsers)
registration.add_args(parser, subparsers)
if __name__ == "__main__":
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
#Use $(cd dirname $0) to get the absolute path of this scripts folder
#Note this does not actually change the working directory
SCRIPT_DIR=$(cd `dirname $0` && pwd)
#The engine root is always the parent of the scripts directory, so $(dirname "$SCRIPT_DIR") should get us engine root
ENGINE_ROOT=$(dirname "$SCRIPT_DIR")
#The engines python is in the engineroot/python
PYTHON_DIRECTORY="$ENGINE_ROOT/python"
#If engine python exists use it, if not try the system python
if [ ! -d "$PYTHON_DIRECTORY" ]; then
PYTHON_EXECUTABLE="python"
else
PYTHON_EXECUTABLE="$PYTHON_DIRECTORY/python.sh"
fi
if [ ! -f "$PYTHON_EXECUTABLE" ]; then
echo "Python executable not found: $PYTHON_EXECUTABLE"
exit 1
fi
#run the o3de.py pass along the command
$PYTHON_EXECUTABLE "$SCRIPT_DIR/o3de.py" $*
@@ -1,5 +1,3 @@
#!/bin/sh
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
@@ -11,20 +9,14 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
CURRENT=$PWD
cd "$PWD/.." || exit
BASE_PATH=$PWD
cd "$CURRENT" || exit
PYTHON_DIRECTORY="${BASE_PATH}/python"
if [ ! -d "$PYTHON_DIRECTORY" ]; then
echo "Python dir not found: $PYTHON_DIRECTORY"
exit 1
fi
PYTHON_EXECUTABLE="$PYTHON_DIRECTORY/python.sh"
if [ ! -f "$PYTHON_EXECUTABLE" ]; then
echo "Python executable not found: $PYTHON_EXECUTABLE"
exit 1
fi
$PYTHON_EXECUTABLE "$BASE_PATH/scripts/lmbr.py" $*
cd "$CURRENT" || exit
ly_download_associated_package(pyside2)
ly_add_pytest(
NAME test_pyside
PATH ${CMAKE_CURRENT_LIST_DIR}/tests/test_pyside.py
)
ly_add_pytest(
NAME test_projects
PATH ${CMAKE_CURRENT_LIST_DIR}/tests/test_projects.py
)
+10
View File
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
+642
View File
@@ -0,0 +1,642 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# PySide project and gem selector GUI
import os
import sys
import argparse
import json
import logging
import subprocess
import threading
import platform
from logging.handlers import RotatingFileHandler
from typing import List
from pathlib import Path
from pyside import add_pyside_environment, is_pyside_ready, uninstall_env
logger = logging.getLogger()
dev_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
sys.path.append(dev_path)
executable_path = ''
def initialize_pyside_from_parser():
# Parse arguments up top. We need to know the path to our binaries and QT libs in particular to load up
# PySide
parser = argparse.ArgumentParser()
parser.add_argument('--executable_path', required=True, help='Path to Executable to launch with project')
parser.add_argument('--binaries_path', default=None, help='Path to QT Binaries necessary for PySide. If not'
'provided executable_path folder is assumed')
args = parser.parse_args()
global executable_path
executable_path = args.executable_path
binaries_path = args.binaries_path or os.path.dirname(executable_path)
# Initialize PySide before imports below. This adds both PySide python modules to the python system interpreter
# path and adds the necessary paths to binaries for the DLLs to be found and load their dependencies
add_pyside_environment(binaries_path)
if not is_pyside_ready():
initialize_pyside_from_parser()
try:
from PySide2.QtWidgets import QApplication, QDialogButtonBox, QPushButton, QComboBox, QMessageBox, QFileDialog
from PySide2.QtWidgets import QListView, QLabel
from PySide2.QtUiTools import QUiLoader
from PySide2.QtCore import QFile, QObject, Qt, Signal, Slot
from PySide2.QtGui import QIcon, QStandardItemModel, QStandardItem
except ImportError as e:
logger.error(f"Failed to import PySide2 with error {e}")
from cmake.Tools import engine_template
from cmake.Tools import add_remove_gem
ui_path = os.path.join(os.path.join(os.path.dirname(__file__), 'ui'))
ui_file = 'projects.ui'
ui_icon_file = 'projects.ico'
manage_gems_file = 'manage_gems.ui'
create_project_file = 'create_project.ui'
mru_file_name = 'o3de_projects.json'
default_settings_folder = os.path.join(Path.home(), '.o3de')
# Used to indicate a folder which appears to be a valid o3de project
project_marker_file = 'project.json'
class DialogLoggerSignaller(QObject):
send_to_dialog = Signal(str)
def __init__(self, dialog_logger):
super(DialogLoggerSignaller, self).__init__()
self.dialog_logger = dialog_logger
# Independent class to handle log forwarding. Logger and qt signals both use emit method.
# This class's job is to receive the logger record and then emit the formatted message through
# DialogLoggerSignaller which is what the ProjectDialog handler listens for
class DialogLogger(logging.Handler):
def __init__(self, log_dialog, log_level=logging.INFO, forward_log_level=logging.WARNING,
message_box_log_level=logging.ERROR):
super(DialogLogger, self).__init__()
self.log_dialog = log_dialog
self.log_level = log_level
self.forward_log_level = forward_log_level
self.message_box_log_level = message_box_log_level
self.log_records = []
self.formatter = logging.Formatter('%(levelname)s : %(message)s')
self.setFormatter(self.formatter)
self.signaller = DialogLoggerSignaller(self)
def emit(self, record):
self.log_records.append(record)
if record.levelno >= self.message_box_log_level:
QMessageBox.warning(None, record.levelname, record.message)
elif record.levelno >= self.forward_log_level:
self.signaller.send_to_dialog.emit(self.format(record))
class ProjectDialog(QObject):
"""
Main project dialog class responsible for displaying the project selection list
"""
def __init__(self, parent=None, my_ui_path=ui_path, settings_folder=default_settings_folder):
super(ProjectDialog, self).__init__(parent)
self.log_display = None
self.dialog_logger = DialogLogger(self)
logger.addHandler(self.dialog_logger)
logger.setLevel(logging.INFO)
log_path = os.path.join(os.path.dirname(__file__), "logs")
if not os.path.isdir(log_path):
os.makedirs(log_path)
log_path = os.path.join(log_path, "project_manager.log")
log_file_handler = RotatingFileHandler(filename=log_path, maxBytes=1024 * 1024, backupCount=1)
formatter = logging.Formatter('%(asctime)s | %(levelname)s : %(message)s')
log_file_handler.setFormatter(formatter)
logger.addHandler(log_file_handler)
logger.info("Starting Project Manager")
self.dialog_logger.signaller.send_to_dialog.connect(self.handle_log_message)
self.displayed_projects = []
self.mru_file_path = os.path.join(settings_folder, mru_file_name)
self.my_ui_file_path = os.path.join(my_ui_path, ui_file)
self.my_ui_icon_path = os.path.join(my_ui_path, ui_icon_file)
self.my_ui_manage_gems_path = os.path.join(my_ui_path, manage_gems_file)
self.my_ui_create_project_path = os.path.join(my_ui_path, create_project_file)
self.ui_dir = my_ui_path
self.ui_file = QFile(self.my_ui_file_path)
self.ui_file.open(QFile.ReadOnly)
loader = QUiLoader()
self.dialog = loader.load(self.ui_file)
self.dialog.setWindowIcon(QIcon(self.my_ui_icon_path))
self.dialog.setFixedSize(self.dialog.size())
self.browse_projects_button = self.dialog.findChild(QPushButton, 'browseProjectsButton')
self.browse_projects_button.clicked.connect(self.browse_projects_handler)
self.log_display = self.dialog.findChild(QLabel, 'logDisplay')
self.create_project_button = self.dialog.findChild(QPushButton, 'createProjectButton')
self.create_project_button.clicked.connect(self.create_project_handler)
self.ok_cancel_button = self.dialog.findChild(QDialogButtonBox, 'okCancel')
self.ok_cancel_button.accepted.connect(self.accepted_handler)
self.manage_gems_button = self.dialog.findChild(QPushButton, 'manageGemsButton')
self.manage_gems_button.clicked.connect(self.manage_gems_handler)
self.project_list_box = self.dialog.findChild(QComboBox, 'projectListBox')
self.add_projects(self.get_mru_list())
self.project_gem_list = []
self.dialog.show()
self.load_thread = None
self.gems_list = []
self.load_gems()
def update_gems(self) -> None:
"""
Perform a full refresh of active project and available gems. Loads both from
data on disk and refreshes UI
:return: None
"""
project_path = self.path_for_selection()
if not project_path:
self.project_gem_list = set()
else:
self.project_gem_list = add_remove_gem.get_project_gem_list(self.path_for_selection())
project_gem_model = QStandardItemModel()
for item in sorted(self.project_gem_list):
project_gem_model.appendRow(QStandardItem(item))
self.project_gems.setModel(project_gem_model)
add_gem_model = QStandardItemModel()
for item in self.gems_list:
if item.get('Name') in self.project_gem_list:
continue
model_item = QStandardItem(item.get('Name'))
model_item.setData(item.get('Path'), Qt.UserRole)
add_gem_model.appendRow(model_item)
self.add_gems_list.setModel(add_gem_model)
def get_selected_project_name(self) -> str:
return os.path.basename(self.path_for_selection())
def get_launch_project(self) -> str:
# We can't currently pass an absolute path, so we need to assume the folder lives under dev and just pass
# the base name of the selected project
return os.path.basename(os.path.normpath(self.path_for_selection()))
def get_executable_launch_params(self) -> list:
"""
Retrieve the necessary launch parameters to make the subprocess launch call with - this is the path
to the executable such as the Editor and the path to the selected project
:return: list of params
"""
launch_params = [executable_path,
f'-regset="/Amazon/AzCore/Bootstrap/sys_game_folder={self.get_launch_project()}"']
return launch_params
def load_gems(self) -> None:
"""
Starts another thread to discover available gems. This requires file discovery/parsing and would likely
cause a noticeable pause if it were not on another thread
:return: None
"""
self.load_thread = threading.Thread(target=self.load_gems_thread_func)
self.load_thread.start()
def load_gems_thread_func(self) -> None:
"""
Actual load function for load_gems thread. Blocking call to lower level find method to fill out gems_list
:return: None
"""
self.gems_list = add_remove_gem.find_all_gems([os.path.join(dev_path, 'Gems')])
def load_template_list(self) -> None:
"""
Search for available templates to fill out template list
:return: None
"""
self.project_templates = engine_template.find_all_project_templates([os.path.join(dev_path, 'Templates')])
def get_gem_info(self, gem_name: str) -> str:
"""
Provided a known gem name provides gem info
:param gem_name: Name of known gem. Names are based on Gem.<Gemname> module names in CMakeLists.txt rather
than folders a Gem lives in.
:return: Dictionary with data about gem if known
"""
for this_gem in self.gems_list:
if this_gem.get('Name') == gem_name:
this_gem
return None
def get_gem_info_by_path(self, gem_path: str) -> str:
"""
Provided a gem path returns the gem info if known
:param gem_path: Path to gem
:return: Dictionary with data about gem if known
"""
for this_gem in self.gems_list:
if this_gem.get('Path') == gem_path:
return this_gem
return None
def path_for_gem(self, gem_name: str) -> str:
"""
Provided a known gem name provides the full path on disk
:param gem_name: Name of known gem. Names are based on Gem.<Gemname> module names in CMakeLists.txt rather
than folders a Gem lives in.
:return: Path to CMakeLists.txt containing the Gem modules
"""
for this_gem in self.gems_list:
if this_gem.get('Name') == gem_name:
return this_gem.get('Path')
return ''
def open_project(self, project_path: str) -> None:
"""
Launch the desired application given the selected project
:param project_path: Path to currently selected project
:return: None
"""
logger.info(f'Attempting to open {project_path}')
self.update_mru_list(project_path)
launch_params = self.get_executable_launch_params()
try:
logger.info(f'Launching with params {launch_params}')
subprocess.Popen(launch_params, env=uninstall_env())
quit(0)
except subprocess.CalledProcessError as e:
logger.error(f'Failed to start executable with launch params {launch_params} error {e}')
def path_for_selection(self) -> str:
"""
Retrive the full path to the project the user currently has selected in the drop down
:return: str path to project
"""
if self.project_list_box.currentIndex() == -1:
logger.warning("No project selected")
return ""
return self.project_list_box.itemData(self.project_list_box.currentIndex(), Qt.ToolTipRole)
def accepted_handler(self) -> None:
"""
Override for handling "Ok" on main project dialog to first check whether the user has selected a project and
prompt them to if not. If a project is selected will attempt to open it.
:return: None
"""
if not self.project_list_box.currentText():
msg_box = QMessageBox(parent=self.dialog)
msg_box.setWindowTitle("O3DE")
msg_box.setText("Please select a project")
msg_box.exec()
return
self.open_project(self.path_for_selection())
def is_project_folder(self, project_path: str) -> bool:
"""
Checks whether the supplied path appears to be a canonical project folder.
Root of a valid project should contain the canonical file
:param project_path:
:return:
"""
return os.path.isfile(os.path.join(project_path, project_marker_file))
def add_new_project(self, project_folder, update_mru=True, reset_selected=True, validate=True):
"""
Handle request to add a new project to our display and mru lists. Validation checks whether the folder
appears to be a project. Duplicates should never be added with or without validation.
:param project_folder: Absolute path to project folder
:param update_mru: Should the project also be promoted to "most recent" in our mru list
:param reset_selected: Update our drop down to show this as our current selection
:param validate: Verify the folder appears to be a valid project folder
:return:
"""
if validate:
if not self.is_project_folder(project_folder):
QMessageBox.warning(self.dialog, "Invalid Project Folder",
f"{project_folder} does not contain a {project_marker_file}"
f" and does not appear to be a valid project")
return
self.add_projects([project_folder])
if reset_selected:
self.project_list_box.setCurrentIndex(self.project_list_box.count() - 1)
if update_mru:
self.update_mru_list(project_folder)
def browse_projects_handler(self):
"""
Open a file search dialog looking for a folder which contains a valid project marker. If valid
will update the mru list with the new entry, if invalid will warn the user.
:return: None
"""
project_folder = QFileDialog.getExistingDirectory(self.dialog, "Select Project Folder", dev_path)
if project_folder:
self.add_new_project(project_folder)
return
def get_selected_project_gems(self) -> list:
"""
:return: List of (GemName, GemPath) of currently selected gems in the project gems list
"""
selected_items = self.project_gems.selectionModel().selectedRows()
return [self.project_gems.model().data(item) for item in selected_items]
def remove_gems_handler(self):
"""
Finds the currently selected gems in the active gems list and attempts to remove each and updates the UI.
:return: None
"""
remove_gems = self.get_selected_project_gems()
for this_gem in remove_gems:
gem_path = self.path_for_gem(this_gem)
add_remove_gem.add_remove_gem(False, dev_path, gem_path or os.path.join(dev_path, 'Gems', this_gem),
self.path_for_selection(), gem_name=this_gem)
self.update_gems()
def manage_gems_handler(self):
"""
Opens the Gem management pane. Waits for the load thread to complete if still running and displays all
active gems for the current project as well as all available gems which aren't currently active.
:return: None
"""
if not self.path_for_selection():
msg_box = QMessageBox(parent=self.dialog)
msg_box.setWindowTitle("O3DE")
msg_box.setText("Please select a project")
msg_box.exec()
return
self.load_thread.join()
loader = QUiLoader()
self.manage_gems_file = QFile(self.my_ui_manage_gems_path)
if not self.manage_gems_file:
logger.error(f'Failed to load gems UI file at {self.manage_gems_file}')
return
self.manage_gems_dialog = loader.load(self.manage_gems_file)
if not self.manage_gems_dialog:
logger.error(f'Failed to load gems dialog file at {self.manage_gems_file}')
return
self.manage_gems_dialog.setWindowTitle(f"Manage Gems for {self.get_selected_project_name()}")
self.add_gems_button = self.manage_gems_dialog.findChild(QPushButton, 'addGemsButton')
self.add_gems_button.clicked.connect(self.add_gems_handler)
self.add_gems_list = self.manage_gems_dialog.findChild(QListView, 'addGemsList')
self.remove_gems_button = self.manage_gems_dialog.findChild(QPushButton, 'removeGemsButton')
self.remove_gems_button.clicked.connect(self.remove_gems_handler)
self.project_gems = self.manage_gems_dialog.findChild(QListView, 'projectGems')
self.update_gems()
self.manage_gems_dialog.exec()
def get_selected_add_gems(self) -> list:
"""
Find returns a list of currently selected gems in the add listas (GemName, GemPath)
:return:
"""
selected_items = self.add_gems_list.selectionModel().selectedRows()
return [(self.add_gems_list.model().data(item), self.add_gems_list.model().data(item, Qt.UserRole)) for
item in selected_items]
def add_gems_handler(self) -> None:
"""
Searches the available gems list for selected gems and attempts to add each one to the current project.
Updates UI after completion.
:return: None
"""
add_gems_list = self.get_selected_add_gems()
for this_gem in add_gems_list:
gem_info = self.get_gem_info_by_path(this_gem[1])
if not gem_info:
logger.error(f'Unknown gem {this_gem}!')
continue
add_remove_gem.add_remove_gem(True, dev_path, this_gem[1], self.path_for_selection(),
gem_name=gem_info.get('Name'),
runtime_dependency=gem_info.get('Runtime', False),
tool_dependency=gem_info.get('Tools', False))
self.update_gems()
def create_project_handler(self):
"""
Opens the Create Project pane. Retrieves a list of available templates for display
:return: None
"""
loader = QUiLoader()
self.create_project_file = QFile(self.my_ui_create_project_path)
if not self.create_project_file:
logger.error(f'Failed to create project UI file at {self.create_project_file}')
return
self.create_project_dialog = loader.load(self.create_project_file)
if not self.create_project_dialog:
logger.error(f'Failed to load create project dialog file at {self.create_project_file}')
return
self.create_project_ok_button = self.create_project_dialog.findChild(QDialogButtonBox, 'okCancel')
self.create_project_ok_button.accepted.connect(self.create_project_accepted_handler)
self.project_template_list = self.create_project_dialog.findChild(QListView, 'projectTemplates')
self.load_template_list()
self.load_template_model = QStandardItemModel()
for item in self.project_templates:
model_item = QStandardItem(item[0])
model_item.setData(item[1], Qt.UserRole)
self.load_template_model.appendRow(model_item)
self.project_template_list.setModel(self.load_template_model)
self.create_project_dialog.exec()
def get_selected_project_template(self) -> tuple:
"""
Get the current pair templatename, path to template selecte dby the user
:return: pair
"""
selected_item = self.project_template_list.selectionModel().currentIndex()
if not selected_item.isValid():
logger.warning("Select a template to create from")
return None
create_project_item = (self.project_template_list.model().data(selected_item),
self.project_template_list.model().data(selected_item, Qt.UserRole))
return create_project_item
def create_project_accepted_handler(self) -> None:
"""
Searches the available gems list for selected gems and attempts to add each one to the current project.
Updates UI after completion.
:return: None
"""
create_project_item = self.get_selected_project_template()
if not create_project_item:
return
folder_dialog = QFileDialog(self.dialog, "Select a Folder and Enter a New Project Name", dev_path)
folder_dialog.setFileMode(QFileDialog.AnyFile)
folder_dialog.setOptions(QFileDialog.ShowDirsOnly)
project_count = 0
project_name = "MyNewProject"
while os.path.exists(os.path.join(dev_path, project_name)):
project_name = f"MyNewProject{project_count}"
project_count += 1
folder_dialog.selectFile(project_name)
project_folder = None
if folder_dialog.exec():
project_folder = folder_dialog.selectedFiles()
if project_folder:
if engine_template.create_project(dev_path, project_folder[0], create_project_item[1]) == 0:
# Success
self.add_new_project(project_folder[0], validate=False)
msg_box = QMessageBox(parent=self.dialog)
msg_box.setWindowTitle("O3DE")
msg_box.setText(f"Project {os.path.basename(os.path.normpath(project_folder[0]))} created."
" Build your\nnew project before hitting OK to launch.")
msg_box.exec()
return
def get_display_name(self, project_path: str) -> str:
"""
Returns the project path in the format to be displayed in the projects MRU list
:param project_path: Path to the project folder
:return: Formatted path
"""
return f'{os.path.basename(os.path.normpath(project_path))} ({project_path})'
def add_projects(self, new_list: List[str]) -> None:
"""
Attempt to add a list of known projects. Performs validation first that the supplied folder appears valid and
silently drops invalid folders - these can simply be folders which were previously valid and are in the MRU list
but have been moved or deleted. Used both when loading the MRU list or when a user browses or creates a new
project
:param new_list: List of full paths to projects to add
:return: None
"""
new_display_items = []
new_display_paths = []
for this_item in new_list:
if self.is_project_folder(this_item) and this_item not in self.displayed_projects:
self.displayed_projects.append(this_item)
new_display_items.append(self.get_display_name(this_item))
new_display_paths.append(this_item)
# Storing the full path in the tooltip, if this is altered we need to store this elsewhere
# as it's used when selecting a project to open
for this_slot in range(len(new_display_items)):
self.project_list_box.addItem(new_display_items[this_slot])
self.project_list_box.setItemData(self.project_list_box.count() - 1, new_display_paths[this_slot],
Qt.ToolTipRole)
def update_mru_list(self, used_project: str) -> None:
"""
Promote a supplied project name to the "most recent" in a given MRU list.
:param used_project: path to project to promote
:param file_path: path to mru list file
:return: None
"""
used_project = os.path.normpath(used_project)
if not os.path.exists(os.path.dirname(self.mru_file_path)):
os.makedirs(os.path.dirname(self.mru_file_path), exist_ok=True)
mru_data = {}
try:
with open(self.mru_file_path, 'r') as mru_file:
mru_data = json.loads(mru_file.read())
except FileNotFoundError:
pass
except json.JSONDecodeError:
pass
recent_list = mru_data.get('Projects', [])
recent_list = [item for item in recent_list if item.get('Path') != used_project and
self.is_project_folder(item.get('Path'))]
new_list = [{'Path': used_project}]
new_list.extend(recent_list)
mru_data['Projects'] = new_list
try:
with open(self.mru_file_path, 'w') as mru_file:
mru_file.write(json.dumps(mru_data, indent=1))
except PermissionError as e:
logger.warning(f"Failed to write {self.mru_file_path} with error {e}")
def get_mru_list(self) -> List[str]:
"""
Retrive the current MRU list. Does not perform validation that the projects still appear valid
:return: list of full path strings to project folders
"""
if not os.path.exists(os.path.dirname(self.mru_file_path)):
return []
try:
with open(self.mru_file_path, 'r') as mru_file:
mru_data = json.loads(mru_file.read())
except FileNotFoundError:
return []
except json.JSONDecodeError:
logger.error(f'MRU list at {self.mru_file_path} is not valid JSON')
return []
recent_list = mru_data.get('Projects', [])
return [item.get('Path') for item in recent_list if item.get('Path') is not None]
@Slot(str)
def handle_log_message(self, message: str) -> None:
"""
Signal handler for messages from the logger. Displays the most recent warning/error
:param message: formatted log message from DialogLoggerSignaller
:return:
"""
if not self.log_display:
return
self.log_display.setText(message)
self.log_display.setToolTip(message)
if __name__ == "__main__":
dialog_app = QApplication(sys.argv)
my_dialog = ProjectDialog()
dialog_app.exec_()
sys.exit(0)
+60
View File
@@ -0,0 +1,60 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import os
import logging
from pathlib import Path
logger = logging.getLogger()
pyside_initialized = False
old_env = os.environ.copy()
# Helper to extend OS PATH for pyside to locate our QT binaries based on our build folder
def add_pyside_environment(bin_path):
if is_pyside_ready():
# No need to reinitialize currently
logger.info("Pyside environment already initialized")
return
global old_env
old_env = os.environ.copy()
binaries_path = Path(os.path.normpath(bin_path))
platforms_path = binaries_path.joinpath("platforms")
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(platforms_path)
path = os.environ['PATH']
new_path = os.pathsep.join([str(binaries_path), str(platforms_path), path])
os.environ['PATH'] = new_path
global pyside_initialized
pyside_initialized = True
def is_pyside_ready():
return pyside_initialized
def is_configuration_valid(workspace):
return os.path.basename(workspace.paths.build_directory()) != "debug"
def uninstall_env():
if not is_pyside_ready():
logger.warning("Pyside not initialized")
return os.environ
global old_env
if old_env.get("QT_QPA_PLATFORM_PLUGIN_PATH"):
old_env.pop("QT_QPA_PLATFORM_PLUGIN_PATH")
os.environ = old_env
return old_env
+10
View File
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,234 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import pytest
import os
import sys
import tempfile
import logging
from unittest.mock import MagicMock
logger = logging.getLogger()
# Code lives one folder above
projects_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.append(projects_path)
from pyside import add_pyside_environment, is_configuration_valid
from ly_test_tools import WINDOWS
project_marker_file = "project.json"
class ProjectHelper:
def __init__(self):
self._temp_directory = tempfile.TemporaryDirectory()
self.temp_project_root = self._temp_directory.name
self.temp_file_dir = os.path.join(self.temp_project_root, "o3de")
self.application = None
self.dialog = None
if not os.path.exists(self.temp_file_dir):
os.makedirs(self.temp_file_dir)
def create_empty_projects(self):
self.project_1_dir = os.path.join(self.temp_project_root, "Project1")
if not os.path.exists(self.project_1_dir):
os.makedirs(self.project_1_dir)
with open(os.path.join(self.project_1_dir,project_marker_file), 'w') as marker_file:
marker_file.write("{}")
self.project_2_dir = os.path.join(self.temp_project_root, "Project2")
if not os.path.exists(self.project_2_dir):
os.makedirs(self.project_2_dir)
with open(os.path.join(self.project_2_dir, project_marker_file), 'w') as marker_file:
marker_file.write("{}")
self.project_3_dir = os.path.join(self.temp_project_root, "Project3")
if not os.path.exists(self.project_3_dir):
os.makedirs(self.project_3_dir)
with open(os.path.join(self.project_3_dir, project_marker_file), 'w') as marker_file:
marker_file.write("{}")
self.invalid_project_dir = os.path.join(self.temp_project_root, "InvalidProject")
def setup_dialog_test(self, workspace):
add_pyside_environment(workspace.paths.build_directory())
if not is_configuration_valid(workspace):
# This is essentially skipif debug. Our debug tests use our profile version of python, but that means we'd
# need to use the profile version of PySide which works with the profile QT libs which aren't in the debug
# folder we've built.
return None
from PySide2.QtWidgets import QApplication, QMessageBox
if QApplication.instance():
self.application = QApplication.instance()
else:
self.application = QApplication(sys.argv)
assert self.application
from projects import ProjectDialog
try:
self.dialog = ProjectDialog(settings_folder=self.temp_file_dir)
return self.dialog
except Exception as e:
logger.error(f'Failed to create ProjectDialog with error {e}')
return None
def create_project_from_template(self, project_name) -> bool:
"""
Uses the dialog to create a temporary project based on the first template found
:param project_name: Name of project to create. Will be created under temp_project_root
:return: True for Success, False for failure
"""
from PySide2.QtWidgets import QWidget, QFileDialog
from projects import ProjectDialog
QWidget.exec = MagicMock()
self.dialog.create_project_handler()
QWidget.exec.assert_called_once()
assert len(self.dialog.project_templates), 'Failed to find any project templates'
ProjectDialog.get_selected_project_template = MagicMock(return_value=self.dialog.project_templates[0])
QFileDialog.exec = MagicMock()
create_project_dir = os.path.join(self.temp_project_root, project_name)
QFileDialog.selectedFiles = MagicMock(return_value=[create_project_dir])
self.dialog.create_project_accepted_handler()
assert os.path.isdir(create_project_dir), f"Expected project folder not found at {create_project_dir}"
assert QWidget.exec.call_count == 2, "Message box confirming project creation failed to show"
@pytest.fixture
def project_helper():
return ProjectHelper()
@pytest.mark.skipif(not WINDOWS, reason="PySide2 only works on windows currently")
@pytest.mark.parametrize('project', ['']) # Workspace wants a project, but this test is not project dependent
def test_logger_handler(workspace, project_helper):
my_dialog = project_helper.setup_dialog_test(workspace)
if not my_dialog:
return
from PySide2.QtWidgets import QMessageBox
QMessageBox.warning = MagicMock()
logger.error(f'Testing logger')
QMessageBox.warning.assert_called_once()
@pytest.mark.skipif(not WINDOWS, reason="PySide2 only works on windows currently")
@pytest.mark.parametrize('project', ['']) # Workspace wants a project, but this test is not project dependent
def test_mru_list(workspace, project_helper):
my_dialog = project_helper.setup_dialog_test(workspace)
if not my_dialog:
return
project_helper.create_empty_projects()
from PySide2.QtWidgets import QMessageBox
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 0, f'MRU list unexpectedly had entries: {mru_list}'
QMessageBox.warning = MagicMock()
my_dialog.add_new_project('TestProjectInvalid')
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 0, f'MRU list unexpectedly added an invalid project : {mru_list}'
QMessageBox.warning.assert_called_once()
my_dialog.add_new_project(project_helper.project_1_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 1, f'MRU list failed to add project at {project_helper.project_1_dir}'
my_dialog.add_new_project(project_helper.project_1_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 1, f'MRU list added project at {project_helper.project_1_dir} a second time : {mru_list}'
my_dialog.update_mru_list(project_helper.project_1_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 1, f'MRU list added project at {project_helper.project_1_dir} a second time : {mru_list}'
my_dialog.add_new_project(project_helper.project_2_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 2, f'MRU list failed to add project at {project_helper.project_2_dir}'
assert mru_list[0] == project_helper.project_2_dir, f"{project_helper.project_2_dir} wasn't first item"
assert mru_list[1] == project_helper.project_1_dir, f"{project_helper.project_1_dir} wasn't second item"
my_dialog.update_mru_list(project_helper.project_1_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 2, f'MRU list added wrong items {mru_list}'
assert mru_list[0] == project_helper.project_1_dir, f"{project_helper.project_1_dir} wasn't first item"
assert mru_list[1] == project_helper.project_2_dir, f"{project_helper.project_2_dir} wasn't second item"
my_dialog.add_new_project(project_helper.invalid_project_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 2, f'MRU list added invalid item {mru_list}'
assert mru_list[0] == project_helper.project_1_dir, f"{project_helper.project_1_dir} wasn't first item"
assert mru_list[1] == project_helper.project_2_dir, f"{project_helper.project_2_dir} wasn't second item"
my_dialog.add_new_project(project_helper.project_3_dir)
mru_list = my_dialog.get_mru_list()
assert len(mru_list) == 3, f'MRU list failed to add {project_helper.project_3_dir} : {mru_list}'
assert mru_list[0] == project_helper.project_3_dir, f"{project_helper.project_3_dir} wasn't first item"
assert mru_list[1] == project_helper.project_1_dir, f"{project_helper.project_1_dir} wasn't second item"
assert mru_list[2] == project_helper.project_2_dir, f"{project_helper.project_2_dir} wasn't third item"
@pytest.mark.skipif(not WINDOWS, reason="PySide2 only works on windows currently")
@pytest.mark.parametrize('project', ['']) # Workspace wants a project, but this test is not project dependent
def test_create_project(workspace, project_helper):
my_dialog = project_helper.setup_dialog_test(workspace)
if not my_dialog:
return
project_helper.create_project_from_template("TestCreateProject")
@pytest.mark.skipif(not WINDOWS, reason="PySide2 only works on windows currently")
@pytest.mark.parametrize('project', ['']) # Workspace wants a project, but this test is not project dependent
def test_add_remove_gems(workspace, project_helper):
my_dialog = project_helper.setup_dialog_test(workspace)
if not my_dialog:
return
my_project_name = "TestAddRemoveGems"
project_helper.create_project_from_template(my_project_name)
my_project_path = os.path.join(project_helper.temp_project_root, my_project_name)
from PySide2.QtWidgets import QWidget, QFileDialog
from projects import ProjectDialog
assert my_dialog.path_for_selection() == my_project_path, "Gems project not selected"
QWidget.exec = MagicMock()
my_dialog.manage_gems_handler()
assert my_dialog.manage_gems_dialog, "No gem management dialog created"
QWidget.exec.assert_called_once()
assert len(my_dialog.gems_list), 'Failed to find any gems'
my_test_gem = my_dialog.gems_list[0]
my_test_gem_name = my_test_gem.get("Name")
my_test_gem_path = my_test_gem.get("Path")
my_test_gem_selection = (my_test_gem_name, my_test_gem_path)
ProjectDialog.get_selected_add_gems = MagicMock(return_value=[my_test_gem_selection])
assert my_test_gem_name, "No Name set in test gem"
assert my_test_gem_name not in my_dialog.project_gem_list, f'Gem {my_test_gem_name} already in project gem list'
my_dialog.add_gems_handler()
assert my_test_gem_name in my_dialog.project_gem_list, f'Gem {my_test_gem_name} failed to add to gem list'
ProjectDialog.get_selected_project_gems = MagicMock(return_value=[my_test_gem_name])
my_dialog.remove_gems_handler()
assert my_test_gem_name not in my_dialog.project_gem_list, f'Gem {my_test_gem_name} still in project gem list'
@@ -0,0 +1,54 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import pytest
import sys
import os
pyside_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.append(pyside_path)
from pyside import add_pyside_environment, is_pyside_ready, is_configuration_valid
from ly_test_tools import WINDOWS
import logging
logger = logging.getLogger()
@pytest.mark.skipif(not WINDOWS, reason="PySide2 only works on windows currently")
@pytest.mark.parametrize('project', ['']) # Workspace wants a project, but this test is not project dependent
def test_add_pyside_environment(workspace):
import_failed = False
try:
import PySide2
except ImportError:
import_failed = True
if not import_failed:
cur_path = sys.path
logger.warning(f"Expected to fail initial import but passed. Sys path was {cur_path}")
assert is_pyside_ready() is False, "Expected pyside not to be initialized yet"
add_pyside_environment(workspace.paths.build_directory())
assert is_pyside_ready() is True, "Expected pyside to be initialized yet"
try:
import PySide2
if not is_configuration_valid(workspace):
return
from PySide2.QtWidgets import QApplication
except ImportError as e:
assert False, f"Failed to import PySide2 with error {e}"
try:
from PySide2.QtWidgets import QApplication
except ImportError as e:
assert False, f"Failed to import QApplication from PySide2.QtWidgets with error {e}"
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>createProjectDialog</class>
<widget class="QDialog" name="createProjectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>226</width>
<height>288</height>
</rect>
</property>
<property name="windowTitle">
<string>Create Project</string>
</property>
<widget class="QDialogButtonBox" name="okCancel">
<property name="geometry">
<rect>
<x>50</x>
<y>250</y>
<width>171</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QListView" name="projectTemplates">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>211</width>
<height>221</height>
</rect>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
</widget>
<widget class="QLabel" name="label_4">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>101</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Available Templates</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>okCancel</sender>
<signal>accepted()</signal>
<receiver>createProjectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>okCancel</sender>
<signal>rejected()</signal>
<receiver>createProjectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+165
View File
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>addGemsDialog</class>
<widget class="QDialog" name="addGemsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>635</width>
<height>327</height>
</rect>
</property>
<property name="windowTitle">
<string>Manage Gems</string>
</property>
<widget class="QDialogButtonBox" name="close">
<property name="geometry">
<rect>
<x>460</x>
<y>290</y>
<width>171</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
<widget class="QListView" name="addGemsList">
<property name="geometry">
<rect>
<x>380</x>
<y>50</y>
<width>250</width>
<height>221</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
<widget class="QListView" name="projectGems">
<property name="geometry">
<rect>
<x>10</x>
<y>50</y>
<width>250</width>
<height>221</height>
</rect>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>380</x>
<y>30</y>
<width>91</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Available Gems</string>
</property>
</widget>
<widget class="QLabel" name="label_4">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>71</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Enabled Gems</string>
</property>
</widget>
<widget class="QPushButton" name="removeGemsButton">
<property name="geometry">
<rect>
<x>267</x>
<y>130</y>
<width>105</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>Remove Gems &gt;&gt;</string>
</property>
</widget>
<widget class="QPushButton" name="addGemsButton">
<property name="geometry">
<rect>
<x>267</x>
<y>100</y>
<width>105</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>&lt;&lt; Add Gems</string>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>5</y>
<width>241</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Adding new Gems may require rebuilding</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>close</sender>
<signal>accepted()</signal>
<receiver>addGemsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>close</sender>
<signal>rejected()</signal>
<receiver>addGemsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:113be7ded1969e0d535722382b6d3730eed052a10430cf23804ae7f20414b999
size 108278
+167
View File
@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>136</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>O3DE</string>
</property>
<property name="whatsThis">
<string>Select and manage your projects for O3DE</string>
</property>
<widget class="QDialogButtonBox" name="okCancel">
<property name="geometry">
<rect>
<x>290</x>
<y>100</y>
<width>171</width>
<height>31</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QComboBox" name="projectListBox">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>451</width>
<height>31</height>
</rect>
</property>
<property name="whatsThis">
<string>Current project to launch or manage gems for.</string>
</property>
</widget>
<widget class="QPushButton" name="createProjectButton">
<property name="geometry">
<rect>
<x>10</x>
<y>70</y>
<width>91</width>
<height>23</height>
</rect>
</property>
<property name="whatsThis">
<string>Create a new O3DE project from a pre configured template.</string>
</property>
<property name="text">
<string>Create New</string>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>47</width>
<height>13</height>
</rect>
</property>
<property name="text">
<string>Project</string>
</property>
</widget>
<widget class="QPushButton" name="browseProjectsButton">
<property name="geometry">
<rect>
<x>110</x>
<y>70</y>
<width>91</width>
<height>23</height>
</rect>
</property>
<property name="whatsThis">
<string>Browse for an existing O3DE project.</string>
</property>
<property name="text">
<string>Browse</string>
</property>
</widget>
<widget class="QPushButton" name="manageGemsButton">
<property name="geometry">
<rect>
<x>350</x>
<y>70</y>
<width>91</width>
<height>23</height>
</rect>
</property>
<property name="whatsThis">
<string>Add or remove gems from your selected project. Gems add and remove additional assets and features to projects.</string>
</property>
<property name="text">
<string>Manage Gems</string>
</property>
</widget>
<widget class="QLabel" name="logDisplay">
<property name="geometry">
<rect>
<x>5</x>
<y>110</y>
<width>275</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string/>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>okCancel</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>okCancel</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>