diff --git a/.github/workflows/build-nightly.yaml b/.github/workflows/build-nightly.yaml index 0a5731a2953..9b1fb338067 100644 --- a/.github/workflows/build-nightly.yaml +++ b/.github/workflows/build-nightly.yaml @@ -6,7 +6,7 @@ on: - 'nightly_*' env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.2 jobs: build_linux: @@ -38,7 +38,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} run: | - LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Run Tests working-directory: ./build env: @@ -125,15 +125,13 @@ jobs: # arch: win32_msvc2017 # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} # aqtversion: ==0.8 - # - name: Install Qt (64 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v4 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2022_64 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -276,7 +274,7 @@ jobs: run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja all - name: Run Tests working-directory: ./build env: diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml index a160fec7033..34faf616733 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yaml @@ -6,7 +6,7 @@ on: - 'release_*' env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.2 jobs: create_release: @@ -69,7 +69,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} run: | - LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Run Tests working-directory: ./build env: @@ -180,16 +180,14 @@ jobs: # arch: win32_msvc2017 # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} # aqtversion: ==0.8 - # - name: Install Qt (64 bit) + - name: Install Qt (64 bit) # # Install Qt 64bit if matrix.arch == x64 - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + uses: jurplel/install-qt-action@v4 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2022_64 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -346,7 +344,7 @@ jobs: run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja all - name: Run Tests working-directory: ./build env: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 99f7fc1144c..9956259eb1a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -6,7 +6,7 @@ on: - 'test/*' env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.2 jobs: build_linux: @@ -38,7 +38,7 @@ jobs: env: CONFIGURATION: ${{ matrix.configuration }} run: | - LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Run Tests working-directory: ./build env: @@ -123,15 +123,13 @@ jobs: # arch: win32_msvc2017 # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} # aqtversion: ==0.8 - # - name: Install Qt (64 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v4 + if: ${{ matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2022_64 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -274,7 +272,7 @@ jobs: run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja all - name: Run Tests working-directory: ./build env: diff --git a/.github/workflows/cache-master.yaml b/.github/workflows/cache-master.yaml index 9a4c9124944..f1a634ae8e7 100644 --- a/.github/workflows/cache-master.yaml +++ b/.github/workflows/cache-master.yaml @@ -6,7 +6,7 @@ on: - master env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.1 jobs: build_linux: @@ -47,10 +47,10 @@ jobs: JOB_CMAKE_OPTIONS: ${{ matrix.cmake_options }} CCACHE_PATH: /usr/local/bin/ccache ENABLE_QTFRED: ON - Qt5_DIR: /qt/${{ env.QT_VERSION }}/gcc_64/lib/cmake/Qt5 + Qt6_DIR: /qt/${{ env.QT_VERSION }}/gcc_64/lib/cmake/Qt6 run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Show CCache statistics run: ccache --show-stats diff --git a/.github/workflows/test-pull_request.yaml b/.github/workflows/test-pull_request.yaml index dbac49babbb..087d56773e7 100644 --- a/.github/workflows/test-pull_request.yaml +++ b/.github/workflows/test-pull_request.yaml @@ -3,7 +3,7 @@ name: Check a pull request on: [pull_request] env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.2 jobs: build_linux: @@ -50,11 +50,11 @@ jobs: JOB_CMAKE_OPTIONS: ${{ matrix.cmake_options }} CCACHE_PATH: /usr/local/bin/ccache ENABLE_QTFRED: ON - Qt5_DIR: /qt/${{ env.QT_VERSION }}/gcc_64/lib/cmake/Qt5 + Qt6_DIR: /qt/${{ env.QT_VERSION }}/gcc_64/lib/cmake/Qt6 run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja -k 20 all - name: Run Tests working-directory: ./build env: @@ -88,30 +88,26 @@ jobs: name: Checkout with: submodules: true - # - name: Cache Qt - # id: cache-qt-win - # uses: actions/cache@v1 - # with: - # path: ${{ github.workspace }}/../Qt + #- name: Cache Qt + #id: cache-qt-win + #uses: actions/cache@v4https://github.com/jurplel/install-qt-action + #with: + #path: ${{ github.workspace }}/../Qt # key: ${{ runner.os }}-${{ matrix.arch }}-QtCache-${{ env.QT_VERSION }} - # - name: Install Qt (32 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.compiler == 'MSVC' && matrix.arch == 'Win32' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win32_msvc2017 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 - # - name: Install Qt (64 bit) - # uses: jurplel/install-qt-action@v2 - # if: ${{ matrix.compiler == 'MSVC' && matrix.arch == 'x64' }} - # with: - # version: ${{ env.QT_VERSION }} - # dir: ${{ github.workspace }}/.. - # arch: win64_msvc2017_64 - # cached: ${{ steps.cache-qt-win.outputs.cache-hit }} - # aqtversion: ==0.8 + #- name: Install Qt (32 bit) + # uses: jurplel/install-qt-action@v4 + # if: ${{ matrix.compiler == 'MSVC' && matrix.arch == 'Win32' }} + # with: + # version: ${{ env.QT_VERSION }} + # dir: ${{ github.workspace }}/.. + # arch: win32_msvc2022 + - name: Install Qt (64 bit) + uses: jurplel/install-qt-action@v4 + if: ${{ matrix.compiler == 'MSVC' && matrix.arch == 'x64' }} + with: + version: ${{ env.QT_VERSION }} + dir: ${{ github.workspace }}/.. + arch: win64_msvc2022_64 - name: Prepare Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.1 with: @@ -144,7 +140,7 @@ jobs: else cmake -DFSO_USE_SPEECH="ON" -DFSO_FATAL_WARNINGS="ON" -DFSO_USE_VOICEREC="OFF" -DFSO_BUILD_TESTS="ON" \ -DFORCED_SIMD_INSTRUCTIONS=SSE2 -DFSO_BUILD_FRED2="ON" -G "Visual Studio 17 2022" \ - -DFSO_BUILD_QTFRED=OFF -T "v143" -A "$ARCHITECTURE" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. + -DFSO_BUILD_QTFRED="ON" -T "v143" -A "$ARCHITECTURE" -DCMAKE_BUILD_TYPE=$CONFIGURATION .. fi fi - name: Compile @@ -223,7 +219,7 @@ jobs: run: $GITHUB_WORKSPACE/ci/linux/configure_cmake.sh - name: Compile working-directory: ./build - run: LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH ninja all + run: LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH ninja all - name: Run Tests working-directory: ./build env: diff --git a/.github/workflows/weekly-coverity-scan.yaml b/.github/workflows/weekly-coverity-scan.yaml index 7e662dd0a80..01c685caa0c 100644 --- a/.github/workflows/weekly-coverity-scan.yaml +++ b/.github/workflows/weekly-coverity-scan.yaml @@ -11,7 +11,7 @@ on: - cron: "5 0 * * 5" # Run once per a week on Friday morning (midnight UTC, 4 AM EST), to avoid Coverity's submission limits env: - QT_VERSION: 5.12.12 + QT_VERSION: 6.9.1 coverity_email: SirKnightlySCP@gmail.com coverity_token: ${{ secrets.COVERITY_TOKEN }} diff --git a/ci/linux/configure_cmake.sh b/ci/linux/configure_cmake.sh index 989e3ad42e2..5d53521a7b3 100755 --- a/ci/linux/configure_cmake.sh +++ b/ci/linux/configure_cmake.sh @@ -13,7 +13,7 @@ if [ "$COMPILER" = "clang-16" ]; then export CXX=clang++-16 fi -LD_LIBRARY_PATH=$Qt5_DIR/lib:$LD_LIBRARY_PATH +LD_LIBRARY_PATH=$Qt6_DIR/lib:$LD_LIBRARY_PATH if [ "$RUNNER_OS" = "macOS" ]; then CXXFLAGS="-mtune=generic -pipe -Wno-unknown-pragmas" CFLAGS="-mtune=generic -pipe -Wno-unknown-pragmas" diff --git a/code/math/bitarray.h b/code/math/bitarray.h index 2885c855de9..e1df1a489ec 100644 --- a/code/math/bitarray.h +++ b/code/math/bitarray.h @@ -19,7 +19,7 @@ * A 0 (1) bit corresponds to the Boolean value false (true), respectively. We can look at a stream of bytes * as a stream of bits; each byte contains 8 bits, so any n bytes hold n*8 bits. And the operation to * manipulate this stream or bit array is so easy, just read or change the bit's state or make any Boolean - * operation on the whole bits array, like ‘AND’, ‘OR’, or ‘XOR’. + * operation on the whole bits array, like 'AND', 'OR', or 'XOR'. * * As each byte contains 8 bits, we need to divide the bit number by 8 to reach the byte that holds the bit. * Then, we can seek to the right bit in the reached byte by the remainder of dividing the bit number by 8. diff --git a/qtfred/CMakeLists.txt b/qtfred/CMakeLists.txt index 6d3211ee29f..24f0453d092 100644 --- a/qtfred/CMakeLists.txt +++ b/qtfred/CMakeLists.txt @@ -1,44 +1,34 @@ -# largely inspired from http://doc.qt.io/qt-5/cmake-manual.html -# Find includes in corresponding build directories +cmake_minimum_required(VERSION 3.16) -# Find the QtWidgets library +project(qtfred VERSION 1.0.0 LANGUAGES CXX) -SET(QT5_INSTALL_ROOT "" CACHE PATH -"The path to the Qt5 installation root. May be necessary on windows if the standard find_package fails to find the Qt installation.") - -list(APPEND CMAKE_PREFIX_PATH "${QT5_INSTALL_ROOT}") - -find_package(Qt5 COMPONENTS Widgets OpenGL REQUIRED) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(QT6_INSTALL_ROOT "" CACHE PATH +"The path to the Qt6 installation root. May be necessary on windows if the standard find_package fails to find the Qt installation.") +list(APPEND CMAKE_PREFIX_PATH "${QT6_INSTALL_ROOT}") +find_package(Qt6 REQUIRED COMPONENTS Core Widgets OpenGL) include(source_groups.cmake) - -# When building qtFRED we need to install the runtime libraries since the Qt DLLs need them +qt_standard_project_setup() +add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x050F00) INCLUDE(InstallRequiredSystemLibraries) - -qt5_wrap_ui(QTFRED_UI_GENERATED ${files_UI}) - +qt6_wrap_ui(QTFRED_UI_GENERATED ${files_UI}) source_group("UI\\Generated" FILES ${QTFRED_UI_GENERATED}) - -add_executable(qtfred ${EXE_GUI_TYPE} - ${source_files} +qt_add_executable(qtfred + ${source_files} ${QTFRED_UI_GENERATED} ) set_target_properties(qtfred PROPERTIES AUTORCC TRUE - AUTOMOC TRUE) - + AUTOUIC FALSE) set_target_properties(qtfred PROPERTIES OUTPUT_NAME "qtfred_${FSO_BINARY_SUFFIX}") -target_compile_features(qtfred PUBLIC cxx_std_17) - target_compile_definitions(qtfred PRIVATE "$<$:PDB_DEBUGGING=1>") # Undefine emit since it conflicts with our code target_compile_definitions(qtfred PRIVATE QT_NO_EMIT) -# Define the deprecated attribute so that usage of such functions causes a warning -target_compile_definitions(qtfred PRIVATE QT_DEPRECATED_WARNINGS) - target_compile_definitions(qtfred PUBLIC USING_THIRD_PARTY_LIBS FRED) target_include_directories(qtfred PUBLIC @@ -46,17 +36,9 @@ target_include_directories(qtfred PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ) -if(WIN32) - # We handle the main method ourself since the prebuilt version conflicts with our setup - set_property(TARGET qtfred PROPERTY QT5_NO_LINK_QTMAIN ON) -endif() - set(CMAKE_MAP_IMPORTED_CONFIG_FASTDEBUG Release Debug) -target_link_libraries(qtfred - PUBLIC - code - Qt5::Widgets Qt5::OpenGL) +target_link_libraries(qtfred PUBLIC code Qt6::Widgets Qt6::OpenGL) include(CreateLaunchers) create_target_launcher(qtfred @@ -70,73 +52,27 @@ INSTALL( BUNDLE DESTINATION ${BINARY_DESTINATION} COMPONENT "qtFRED" ) -COPY_FILES_TO_TARGET(qtfred) - enable_clang_tidy(qtfred) - -if (WIN32) - set(additional_dlls +COPY_FILES_TO_TARGET(qtfred) +if(WIN32) +# Retrieve the absolute path to qmake and then use that path to find +# the windeployqt executable +get_target_property(QMAKE_EXE Qt6::qmake IMPORTED_LOCATION) +get_filename_component(QT_BIN_DIR ${QMAKE_EXE} DIRECTORY) + +find_program(WINDEPLOYQT_ENV_SETUP qtenv2.bat HINTS "${QT_BIN_DIR}") +find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${QT_BIN_DIR}") + +# Run windeployqt immediately after build +add_custom_command(TARGET qtfred + POST_BUILD + COMMAND Qt6::windeployqt --dir "${CMAKE_CURRENT_BINARY_DIR}/windeployqt" "$/$" ) - - include(util) - list_target_dependencies(qtfred qtfred_deps) - - get_target_property (QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION) - execute_process(COMMAND ${QT_QMAKE_EXECUTABLE} -query QT_INSTALL_BINS OUTPUT_VARIABLE QT_INSTALL_BINS OUTPUT_STRIP_TRAILING_WHITESPACE) - - set(file_paths) - foreach(dep ${qtfred_deps}) - if ("${dep}" MATCHES "(^|;)Qt5::[A-Za-z0-9_]") - set(file_paths ${file_paths} "$") - endif ("${dep}" MATCHES "(^|;)Qt5::[A-Za-z0-9_]") - endforeach(dep) - - foreach(dll ${additional_dlls}) - set(file_paths ${file_paths} "${QT_INSTALL_BINS}/${dll}") - endforeach(dll) - - foreach(path ${file_paths}) - add_custom_command(TARGET qtfred - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${path}" "$" - VERBATIM) - endforeach(path) - - install(FILES ${file_paths} +install( + DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}/windeployqt/" DESTINATION ${BINARY_DESTINATION} - COMPONENT "qtFRED" - ) - - # Windows requires that the qwindows DLL is copied as well - execute_process(COMMAND ${QT_QMAKE_EXECUTABLE} -query QT_INSTALL_PLUGINS OUTPUT_VARIABLE QT_INSTALL_PLUGINS OUTPUT_STRIP_TRAILING_WHITESPACE) - set(qwindows_path "${QT_INSTALL_PLUGINS}/platforms/qwindows$<$:d>.dll") - - add_custom_command(TARGET qtfred - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${qwindows_path}" "$/platforms/qwindows$<$:d>.dll" - VERBATIM) - - install(FILES ${qwindows_path} - DESTINATION ${BINARY_DESTINATION}/platforms - COMPONENT "qtFRED" - ) -elseif(FSO_BUILD_APPIMAGE) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/AppRun.in" "${CMAKE_CURRENT_BINARY_DIR}/AppRun.gen" @ONLY) - file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/AppRun-$" - INPUT "${CMAKE_CURRENT_BINARY_DIR}/AppRun.gen") - - install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/AppRun-$" DESTINATION "." RENAME "AppRun" - COMPONENT "qtFRED") - - configure_file("${CMAKE_CURRENT_LIST_DIR}/cmake/AppImage.desktop.in" "${CMAKE_CURRENT_BINARY_DIR}/AppImage.desktop.gen" @ONLY) - file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/AppImage-$.desktop" - INPUT "${CMAKE_CURRENT_BINARY_DIR}/AppImage.desktop.gen") - - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/AppImage-$.desktop" DESTINATION "." RENAME "qtfred.desktop" - COMPONENT "qtFRED") - - install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/fred_icon.png" DESTINATION "." - COMPONENT "qtFRED") + ) endif() if (FSO_INSTALL_DEBUG_FILES) @@ -145,4 +81,4 @@ if (FSO_INSTALL_DEBUG_FILES) DESTINATION ${BINARY_DESTINATION} OPTIONAL) endif() -endif() +endif() \ No newline at end of file diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp index b92ccc10371..1d370341225 100644 --- a/qtfred/src/mission/FredRenderer.cpp +++ b/qtfred/src/mission/FredRenderer.cpp @@ -1000,16 +1000,19 @@ void FredRenderer::render_models(int cur_object_index, } void FredRenderer::render_frame(int cur_object_index, - subsys_to_render& Render_subsys, - bool box_marking, - const Marking_box& marking_box, - bool Bg_bitmap_dialog) { + subsys_to_render& Render_subsys, + bool box_marking, + const Marking_box& marking_box, + bool Bg_bitmap_dialog, + qreal scale) +{ // Make sure our OpenGL context is used for rendering gr_use_viewport(_targetView); - + uint32_t width = _targetView->getSize().first * scale; + uint32_t height = _targetView->getSize().second * scale; // Resize the rendering window in case the previous size was different - gr_screen_resize(_targetView->getSize().first, _targetView->getSize().second); + gr_screen_resize(width, height); char buf[256]; int x, y, w, h, inst; diff --git a/qtfred/src/mission/FredRenderer.h b/qtfred/src/mission/FredRenderer.h index fd7e4814e84..d04b317538c 100644 --- a/qtfred/src/mission/FredRenderer.h +++ b/qtfred/src/mission/FredRenderer.h @@ -78,10 +78,11 @@ class FredRenderer: public QObject { void render_one_model_htl(object* objp, int cur_object_index, bool Bg_bitmap_dialog); void render_models(int cur_object_index, bool Bg_bitmap_dialog); void render_frame(int cur_object_index, - subsys_to_render& Render_subsys, - bool box_marking, - const Marking_box& marking_box, - bool Bg_bitmap_dialog); + subsys_to_render& Render_subsys, + bool box_marking, + const Marking_box& marking_box, + bool Bg_bitmap_dialog, + qreal scale); signals: void scheduleUpdate(); diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index eb9739258c4..4f23d89e892 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -1,1348 +1,1348 @@ -#include "FredApplication.h" -#include "BackgroundEditorDialogModel.h" - -#include "graphics/light.h" -#include "math/bitarray.h" -#include "mission/missionparse.h" -#include "nebula/neb.h" -#include "nebula/neblightning.h" -#include "starfield/nebula.h" -#include "lighting/lighting_profiles.h" - -// TODO move this to common for both FREDs. -const static float delta = .00001f; -const static float default_nebula_range = 3000.0f; - -extern void parse_one_background(background_t* background); - -namespace fso::fred::dialogs { -BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) - : AbstractDialogModel(parent, viewport) -{ - auto& bg = getActiveBackground(); - auto& bm_list = bg.bitmaps; - if (!bm_list.empty()) { - _selectedBitmapIndex = 0; - } - - auto& sun_list = bg.suns; - if (!sun_list.empty()) { - _selectedSunIndex = 0; - } -} - -bool BackgroundEditorDialogModel::apply() -{ - // override dumb values with reasonable ones - // this is what original FRED does but it was a text edit field using atoi - // ours is a limited spinbox so this probably isn't necessary anymore?? - // Does this mean range can never be 0????????? - if (Neb2_awacs <= 0.00000001f) { - Neb2_awacs = 3000.0f; - } - return true; -} - -void BackgroundEditorDialogModel::reject() -{ - // do nothing -} - -void BackgroundEditorDialogModel::refreshBackgroundPreview() -{ - stars_load_background(Cur_background); // rebuild instances from Backgrounds[] - stars_set_background_model(The_mission.skybox_model, nullptr, The_mission.skybox_flags); // rebuild skybox - stars_set_background_orientation(&The_mission.skybox_orientation); - // TODO make this actually show the stars in the background - _editor->missionChanged(); -} - -background_t& BackgroundEditorDialogModel::getActiveBackground() -{ - if (!SCP_vector_inbounds(Backgrounds, Cur_background)) { - // Fall back to first background if Cur_background isn’t set - Cur_background = 0; - } - return Backgrounds[Cur_background]; -} - -starfield_list_entry* BackgroundEditorDialogModel::getActiveBitmap() const -{ - auto& bg = getActiveBackground(); - auto& list = bg.bitmaps; - if (!SCP_vector_inbounds(list, _selectedBitmapIndex)) { - return nullptr; - } - return &list[_selectedBitmapIndex]; -} - -starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const -{ - auto& bg = getActiveBackground(); - auto& list = bg.suns; - if (!SCP_vector_inbounds(list, _selectedSunIndex)) { - return nullptr; - } - return &list[_selectedSunIndex]; -} - -SCP_vector BackgroundEditorDialogModel::getBackgroundNames() -{ - - SCP_vector out; - out.reserve(Backgrounds.size()); - for (size_t i = 0; i < Backgrounds.size(); ++i) - out.emplace_back("Background " + std::to_string(i + 1)); - return out; -} - -void BackgroundEditorDialogModel::setActiveBackgroundIndex(int idx) -{ - if (!SCP_vector_inbounds(Backgrounds, idx)) - return; - - Cur_background = idx; - - // Reseed selections for the new background - _selectedBitmapIndex = Backgrounds[idx].bitmaps.empty() ? -1 : 0; - _selectedSunIndex = Backgrounds[idx].suns.empty() ? -1 : 0; - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getActiveBackgroundIndex() -{ - return Cur_background < 0 ? 0 : Cur_background; -} - - -void BackgroundEditorDialogModel::addBackground() -{ - const int newIndex = static_cast(Backgrounds.size()); - stars_add_blank_background(/*creating_in_fred=*/true); - set_modified(); - - // select it - setActiveBackgroundIndex(newIndex); -} - -void BackgroundEditorDialogModel::removeActiveBackground() -{ - if (Backgrounds.size() <= 1 || Cur_background < 0) { - return; // keep at least one background - } - - const int oldIdx = Cur_background; - Backgrounds.erase(Backgrounds.begin() + oldIdx); - - // clamp selection to the new valid range - const int newIdx = std::min(oldIdx, static_cast(Backgrounds.size()) - 1); - set_modified(); - - setActiveBackgroundIndex(newIdx); - - // Ensure the swap index is still valid - if (!SCP_vector_inbounds(Backgrounds, _swapIndex)) { - _swapIndex = 0; - } -} - -int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) -{ - // Normalize the filepath to use the current platform's directory separator - SCP_string path = fs2Path; - std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); - - try { - read_file_text(path.c_str()); - reset_parse(); - - if (!skip_to_start_of_string("#Background bitmaps")) { - return 0; // no background section - } - - // Enter the section and skip the header fields - required_string("#Background bitmaps"); - required_string("$Num stars:"); - int tmp; - stuff_int(&tmp); - required_string("$Ambient light level:"); - stuff_int(&tmp); - - // Count how many explicit "$Bitmap List:" blocks this file has - char* saved = Mp; - int count = 0; - while (skip_to_string("$Bitmap List:")) { - ++count; - } - Mp = saved; - - // Retail-style missions may have 0 "$Bitmap List:" entries but still one background. - return (count > 0) ? count : 1; - } catch (...) { - return 0; // parse error - } -} - -bool BackgroundEditorDialogModel::importBackgroundFromMission(const SCP_string& fs2Path, int whichIndex) -{ - // Replace the CURRENT background with one parsed from another mission file. - if (Cur_background < 0) - return false; - - // Normalize the filepath to use the current platform's directory separator - SCP_string path = fs2Path; - std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); - - try { - read_file_text(path.c_str()); - reset_parse(); - - if (!skip_to_start_of_string("#Background bitmaps")) { - return false; // file has no background section - } - - required_string("#Background bitmaps"); - required_string("$Num stars:"); - int tmp; - stuff_int(&tmp); - required_string("$Ambient light level:"); - stuff_int(&tmp); - - // Count "$Bitmap List:" occurrences - char* saved = Mp; - int count = 0; - while (skip_to_string("$Bitmap List:")) { - ++count; - } - Mp = saved; - - // If multiple lists exist, skip to the requested one. - // If zero lists exist (retail), parse_one_background will handle the single background. - if (count > 0) { - const int target = std::max(0, std::min(whichIndex, count - 1)); - for (int i = 0; i < target + 1; ++i) { - skip_to_string("$Bitmap List:"); - } - } - - // Parse into the current slot - parse_one_background(&Backgrounds[Cur_background]); - } catch (...) { - return false; - } - - set_modified(); - // Rebuild instances & repaint so the import is visible immediately - stars_load_background(Cur_background); - if (_viewport) - _viewport->needsUpdate(); - return true; -} - -void BackgroundEditorDialogModel::swapBackgrounds() -{ - if (Cur_background < 0 || _swapIndex < 0 || _swapIndex >= static_cast(Backgrounds.size()) || _swapIndex == Cur_background) { - return; - } - - stars_swap_backgrounds(Cur_background, _swapIndex); - set_modified(); - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getSwapWithIndex() const -{ - return _swapIndex; -} - -void BackgroundEditorDialogModel::setSwapWithIndex(int idx) -{ - if (!SCP_vector_inbounds(Backgrounds, idx)) { - return; - } - - _swapIndex = idx; -} - -bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() -{ - const auto& bg = getActiveBackground(); - return bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; -} - -void BackgroundEditorDialogModel::setSaveAnglesCorrectFlag(bool on) -{ - auto& bg = getActiveBackground(); - const bool before = bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; - if (before == on) - return; - - if (on) - bg.flags.set(Starfield::Background_Flags::Corrected_angles_in_mission_file); - else - bg.flags.remove(Starfield::Background_Flags::Corrected_angles_in_mission_file); - - set_modified(); -} - -SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() -{ - SCP_vector out; - const int count = stars_get_num_entries(/*is_a_sun=*/false, /*bitmap_count=*/true); - out.reserve(count); - for (int i = 0; i < count; ++i) { - if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/false)) { - out.emplace_back(name); - } - } - return out; -} - -SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() -{ - SCP_vector out; - const auto& vec = getActiveBackground().bitmaps; - out.reserve(vec.size()); - for (const auto& sle : vec) { - out.emplace_back(sle.filename); - } - return out; -} - -void BackgroundEditorDialogModel::setSelectedBitmapIndex(int index) -{ - const auto& bg = getActiveBackground(); - const auto& list = bg.bitmaps; - if (!SCP_vector_inbounds(list, index)) { - _selectedBitmapIndex = -1; - return; - } - _selectedBitmapIndex = index; -} - -int BackgroundEditorDialogModel::getSelectedBitmapIndex() const -{ - return _selectedBitmapIndex; -} - -void BackgroundEditorDialogModel::addMissionBitmapByName(const SCP_string& name) -{ - if (name.empty()) - return; - - // Must exist in tables - if (stars_find_bitmap(name.c_str()) < 0) - return; - - starfield_list_entry sle{}; - std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); - sle.ang.p = 0.0f; - sle.ang.b = 0.0f; - sle.ang.h = 0.0f; - sle.scale_x = 1.0f; - sle.scale_y = 1.0f; - sle.div_x = 1; - sle.div_y = 1; - - auto& list = getActiveBackground().bitmaps; - list.push_back(sle); - - _selectedBitmapIndex = static_cast(list.size()) - 1; - - set_modified(); - refreshBackgroundPreview(); -} - -void BackgroundEditorDialogModel::removeMissionBitmap() -{ - auto& list = getActiveBackground().bitmaps; - - // Make sure we have an active bitmap - if (getActiveBitmap() == nullptr) { - return; - } - - list.erase(list.begin() + _selectedBitmapIndex); - - // choose a sensible new selection - if (list.empty()) { - _selectedBitmapIndex = -1; - } else { - _selectedBitmapIndex = std::min(_selectedBitmapIndex, static_cast(list.size()) - 1); - } - - set_modified(); - refreshBackgroundPreview(); -} - -SCP_string BackgroundEditorDialogModel::getBitmapName() const -{ - auto bm = getActiveBitmap(); - if (bm == nullptr) { - return ""; - } - - return bm->filename; -} - -void BackgroundEditorDialogModel::setBitmapName(const SCP_string& name) -{ - if (name.empty()) - return; - - // Must exist in tables - if (stars_find_bitmap(name.c_str()) < 0) - return; - - auto bm = getActiveBitmap(); - if (bm != nullptr) { - strcpy_s(bm->filename, name.c_str()); - set_modified(); - refreshBackgroundPreview(); - } -} - -int BackgroundEditorDialogModel::getBitmapPitch() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return fl2ir(fl_degrees(bm->ang.p) + delta); -} - -void BackgroundEditorDialogModel::setBitmapPitch(int deg) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(bm->ang.p, fl_radians(deg)); - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getBitmapBank() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return fl2ir(fl_degrees(bm->ang.b) + delta); -} - -void BackgroundEditorDialogModel::setBitmapBank(int deg) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(bm->ang.b, fl_radians(deg)); - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getBitmapHeading() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return fl2ir(fl_degrees(bm->ang.h) + delta); -} - -void BackgroundEditorDialogModel::setBitmapHeading(int deg) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(bm->ang.h, fl_radians(deg)); - - refreshBackgroundPreview(); -} - -float BackgroundEditorDialogModel::getBitmapScaleX() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return bm->scale_x; -} - -void BackgroundEditorDialogModel::setBitmapScaleX(float v) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); - modify(bm->scale_x, v); - - refreshBackgroundPreview(); -} - -float BackgroundEditorDialogModel::getBitmapScaleY() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return bm->scale_y; -} - -void BackgroundEditorDialogModel::setBitmapScaleY(float v) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); - modify(bm->scale_y, v); - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getBitmapDivX() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return bm->div_x; -} - -void BackgroundEditorDialogModel::setBitmapDivX(int v) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); - modify(bm->div_x, v); - - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getBitmapDivY() const -{ - auto* bm = getActiveBitmap(); - if (!bm) - return 0; - - return bm->div_y; -} - -void BackgroundEditorDialogModel::setBitmapDivY(int v) -{ - auto* bm = getActiveBitmap(); - if (!bm) - return; - - CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); - modify(bm->div_y, v); - - refreshBackgroundPreview(); -} - -SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() -{ - SCP_vector out; - const int count = stars_get_num_entries(/*is_a_sun=*/true, /*bitmap_count=*/true); - out.reserve(count); - for (int i = 0; i < count; ++i) { - if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/true)) { // table order - out.emplace_back(name); - } - } - return out; -} - -SCP_vector BackgroundEditorDialogModel::getMissionSunNames() -{ - SCP_vector out; - const auto& vec = getActiveBackground().suns; - out.reserve(vec.size()); - for (const auto& sle : vec) - out.emplace_back(sle.filename); - return out; -} - -void BackgroundEditorDialogModel::setSelectedSunIndex(int index) -{ - const auto& list = getActiveBackground().suns; - if (!SCP_vector_inbounds(list, index)) { - _selectedSunIndex = -1; - return; - } - _selectedSunIndex = index; -} - -int BackgroundEditorDialogModel::getSelectedSunIndex() const -{ - return _selectedSunIndex; -} - -void BackgroundEditorDialogModel::addMissionSunByName(const SCP_string& name) -{ - if (name.empty()) - return; - - if (stars_find_sun(name.c_str()) < 0) - return; // must exist in sun table - - starfield_list_entry sle{}; - std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); - sle.ang.p = sle.ang.b = sle.ang.h = 0.0f; - sle.scale_x = 1.0f; - sle.scale_y = 1.0f; - sle.div_x = 1; - sle.div_y = 1; - - auto& list = getActiveBackground().suns; - list.push_back(sle); - _selectedSunIndex = static_cast(list.size()) - 1; - - set_modified(); - refreshBackgroundPreview(); -} - -void BackgroundEditorDialogModel::removeMissionSun() -{ - auto& list = getActiveBackground().suns; - if (getActiveSun() == nullptr) - return; - - list.erase(list.begin() + _selectedSunIndex); - if (list.empty()) - _selectedSunIndex = -1; - else - _selectedSunIndex = std::min(_selectedSunIndex, static_cast(list.size()) - 1); - - set_modified(); - refreshBackgroundPreview(); -} - -SCP_string BackgroundEditorDialogModel::getSunName() const -{ - auto* s = getActiveSun(); - if (!s) - return ""; - - return s->filename; -} - -void BackgroundEditorDialogModel::setSunName(const SCP_string& name) -{ - if (name.empty()) - return; - - if (stars_find_sun(name.c_str()) < 0) - return; - - auto* s = getActiveSun(); - if (!s) - return; - - strcpy_s(s->filename, name.c_str()); - set_modified(); - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getSunPitch() const -{ - auto* s = getActiveSun(); - if (!s) - return 0; - - return fl2ir(fl_degrees(s->ang.p) + delta); -} - -void BackgroundEditorDialogModel::setSunPitch(int deg) -{ - auto* s = getActiveSun(); - if (!s) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(s->ang.p, fl_radians(deg)); - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getSunHeading() const -{ - auto* s = getActiveSun(); - if (!s) - return 0; - - return fl2ir(fl_degrees(s->ang.h) + delta); -} - -void BackgroundEditorDialogModel::setSunHeading(int deg) -{ - auto* s = getActiveSun(); - if (!s) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(s->ang.h, fl_radians(deg)); - refreshBackgroundPreview(); -} - -float BackgroundEditorDialogModel::getSunScale() const -{ - auto* s = getActiveSun(); - if (!s) - return 0; - - return s->scale_x; // suns store scale in X; Y remains 1.0 -} - -void BackgroundEditorDialogModel::setSunScale(float v) -{ - auto* s = getActiveSun(); - if (!s) - return; - - CLAMP(v, getSunScaleLimit().first, getSunScaleLimit().second); - modify(s->scale_x, v); - refreshBackgroundPreview(); -} - -SCP_vector BackgroundEditorDialogModel::getLightningNames() -{ - SCP_vector out; - out.emplace_back(""); // legacy default - for (const auto& st : Storm_types) { - out.emplace_back(st.name); - } - return out; -} - -SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() -{ - SCP_vector out; - out.emplace_back(""); // matches legacy combo where index 0 = none - for (const auto& neb : Neb2_bitmap_filenames) { - out.emplace_back(neb); - } - return out; -} - -SCP_vector BackgroundEditorDialogModel::getPoofNames() -{ - SCP_vector out; - out.reserve(Poof_info.size()); - for (const auto& p : Poof_info) { - out.emplace_back(p.name); - } - return out; -} - -bool BackgroundEditorDialogModel::getFullNebulaEnabled() -{ - return The_mission.flags[Mission::Mission_Flags::Fullneb]; -} - -void BackgroundEditorDialogModel::setFullNebulaEnabled(bool enabled) -{ - const bool currentlyEnabled = getFullNebulaEnabled(); - if (enabled == currentlyEnabled) { - return; - } - - if (enabled) { - The_mission.flags.set(Mission::Mission_Flags::Fullneb); - - // Set defaults if needed - if (Neb2_awacs <= 0.0f) { - modify(Neb2_awacs, default_nebula_range); - } - } else { - // Disable full nebula - The_mission.flags.remove(Mission::Mission_Flags::Fullneb); - modify(Neb2_awacs, -1.0f); - } - - set_modified(); -} - -float BackgroundEditorDialogModel::getFullNebulaRange() -{ - // May be -1 if full nebula is disabled - return Neb2_awacs; -} - -void BackgroundEditorDialogModel::setFullNebulaRange(float range) -{ - modify(Neb2_awacs, range); -} - -SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() -{ - return (Neb2_texture_name[0] != '\0') ? SCP_string(Neb2_texture_name) : SCP_string(""); -} - -void BackgroundEditorDialogModel::setNebulaFullPattern(const SCP_string& name) -{ - if (lcase_equal(name, "")) { - strcpy_s(Neb2_texture_name, ""); - } else { - strcpy_s(Neb2_texture_name, name.c_str()); - } - - set_modified(); -} - -SCP_string BackgroundEditorDialogModel::getLightning() -{ - // Return "" when engine stores "none" or empty - if (Mission_parse_storm_name[0] == '\0') - return ""; - SCP_string s = Mission_parse_storm_name; - if (lcase_equal(s, "none")) - return ""; - return s; -} - -void BackgroundEditorDialogModel::setLightning(const SCP_string& name) -{ - // Engine convention is the literal "none" for no storm - if (lcase_equal(name, "")) { - strcpy_s(Mission_parse_storm_name, "none"); - } else { - strcpy_s(Mission_parse_storm_name, name.c_str()); - } - set_modified(); -} - -SCP_vector BackgroundEditorDialogModel::getSelectedPoofs() -{ - SCP_vector out; - for (size_t i = 0; i < Poof_info.size(); ++i) { - if (get_bit(Neb2_poof_flags.get(), i)) - out.emplace_back(Poof_info[i].name); - } - return out; -} - -void BackgroundEditorDialogModel::setSelectedPoofs(const SCP_vector& names) -{ - // Clear all, then set matching names - clear_all_bits(Neb2_poof_flags.get(), Poof_info.size()); - for (const auto& want : names) { - for (size_t i = 0; i < Poof_info.size(); ++i) { - if (!stricmp(Poof_info[i].name, want.c_str())) { - set_bit(Neb2_poof_flags.get(), i); - break; - } - } - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getShipTrailsToggled() -{ - return The_mission.flags[Mission::Mission_Flags::Toggle_ship_trails]; -} - -void BackgroundEditorDialogModel::setShipTrailsToggled(bool on) -{ - The_mission.flags.set(Mission::Mission_Flags::Toggle_ship_trails, on); - set_modified(); -} - -float BackgroundEditorDialogModel::getFogNearMultiplier() -{ - return Neb2_fog_near_mult; -} - -void BackgroundEditorDialogModel::setFogNearMultiplier(float v) -{ - modify(Neb2_fog_near_mult, v); -} - -float BackgroundEditorDialogModel::getFogFarMultiplier() -{ - return Neb2_fog_far_mult; -} - -void BackgroundEditorDialogModel::setFogFarMultiplier(float v) -{ - modify(Neb2_fog_far_mult, v); -} - -bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() -{ - return The_mission.flags[Mission::Mission_Flags::Fullneb_background_bitmaps]; -} - -void BackgroundEditorDialogModel::setDisplayBackgroundBitmaps(bool on) -{ - The_mission.flags.set(Mission::Mission_Flags::Fullneb_background_bitmaps, on); - set_modified(); -} - -bool BackgroundEditorDialogModel::getFogPaletteOverride() -{ - return The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]; -} - -void BackgroundEditorDialogModel::setFogPaletteOverride(bool on) -{ - The_mission.flags.set(Mission::Mission_Flags::Neb2_fog_color_override, on); - set_modified(); -} - -int BackgroundEditorDialogModel::getFogR() -{ - return Neb2_fog_color[0]; -} - -void BackgroundEditorDialogModel::setFogR(int r) -{ - CLAMP(r, 0, 255) - const auto v = static_cast(r); - modify(Neb2_fog_color[0], v); -} - -int BackgroundEditorDialogModel::getFogG() -{ - return Neb2_fog_color[1]; -} - -void BackgroundEditorDialogModel::setFogG(int g) -{ - CLAMP(g, 0, 255) - const auto v = static_cast(g); - modify(Neb2_fog_color[1], v); -} - -int BackgroundEditorDialogModel::getFogB() -{ - return Neb2_fog_color[2]; -} - -void BackgroundEditorDialogModel::setFogB(int b) -{ - CLAMP(b, 0, 255) - const auto v = static_cast(b); - modify(Neb2_fog_color[2], v); -} - -SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() -{ - SCP_vector out; - out.emplace_back(""); - for (auto& neb : Nebula_filenames) { - out.emplace_back(neb); - } - return out; -} - -SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() -{ - SCP_vector out; - out.reserve(NUM_NEBULA_COLORS); - for (auto& color : Nebula_colors) { - out.emplace_back(color); - } - return out; -} - -SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() -{ - if (Nebula_index < 0) - return ""; - - if (Nebula_index >= 0 && Nebula_index < NUM_NEBULAS) { - return Nebula_filenames[Nebula_index]; - } - - return SCP_string{}; -} - -void BackgroundEditorDialogModel::setOldNebulaPattern(const SCP_string& name) -{ - int newIndex = -1; - if (!name.empty() && stricmp(name.c_str(), "") != 0) { - for (int i = 0; i < NUM_NEBULAS; ++i) { - if (!stricmp(Nebula_filenames[i], name.c_str())) { - newIndex = i; - break; - } - } - } - - modify(Nebula_index, newIndex); -} - -SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() -{ - if (Mission_palette >= 0 && Mission_palette < NUM_NEBULA_COLORS) { - return Nebula_colors[Mission_palette]; - } - return SCP_string{}; -} - -void BackgroundEditorDialogModel::setOldNebulaColorName(const SCP_string& name) -{ - if (name.empty()) - return; - for (int i = 0; i < NUM_NEBULA_COLORS; ++i) { - if (!stricmp(Nebula_colors[i], name.c_str())) { - modify(Mission_palette, i); - return; - } - } - // name not found: ignore -} - -int BackgroundEditorDialogModel::getOldNebulaPitch() -{ - return Nebula_pitch; -} - -void BackgroundEditorDialogModel::setOldNebulaPitch(int deg) -{ - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - if (Nebula_pitch != deg) { - Nebula_pitch = deg; - modify(Nebula_pitch, deg); - } -} - -int BackgroundEditorDialogModel::getOldNebulaBank() -{ - return Nebula_bank; -} - -void BackgroundEditorDialogModel::setOldNebulaBank(int deg) -{ - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - if (Nebula_bank != deg) { - Nebula_bank = deg; - modify(Nebula_bank, deg); - } -} - -int BackgroundEditorDialogModel::getOldNebulaHeading() -{ - return Nebula_heading; -} - -void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) -{ - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - if (Nebula_heading != deg) { - Nebula_heading = deg; - modify(Nebula_heading, deg); - } -} - -int BackgroundEditorDialogModel::getAmbientR() -{ - return The_mission.ambient_light_level & 0xff; -} - -void BackgroundEditorDialogModel::setAmbientR(int r) -{ - CLAMP(r, 1, 255); - - const int g = getAmbientG(), b = getAmbientB(); - const int newCol = (r) | (g << 8) | (b << 16); - - modify(The_mission.ambient_light_level, newCol); - - gr_set_ambient_light(r, g, b); - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getAmbientG() -{ - return (The_mission.ambient_light_level >> 8) & 0xff; -} - -void BackgroundEditorDialogModel::setAmbientG(int g) -{ - CLAMP(g, 1, 255); - - const int r = getAmbientR(), b = getAmbientB(); - const int newCol = (r) | (g << 8) | (b << 16); - - modify(The_mission.ambient_light_level, newCol); - - gr_set_ambient_light(r, g, b); - refreshBackgroundPreview(); -} - -int BackgroundEditorDialogModel::getAmbientB() -{ - return (The_mission.ambient_light_level >> 16) & 0xff; -} - -void BackgroundEditorDialogModel::setAmbientB(int b) -{ - CLAMP(b, 1, 255); - - const int r = getAmbientR(), g = getAmbientG(); - const int newCol = (r) | (g << 8) | (b << 16); - - modify(The_mission.ambient_light_level, newCol); - - gr_set_ambient_light(r, g, b); - refreshBackgroundPreview(); -} - -SCP_string BackgroundEditorDialogModel::getSkyboxModelName() -{ - return The_mission.skybox_model; -} -void BackgroundEditorDialogModel::setSkyboxModelName(const SCP_string& name) -{ - // empty string = no skybox - if (std::strncmp(The_mission.skybox_model, name.c_str(), NAME_LENGTH) != 0) { - std::memset(The_mission.skybox_model, 0, sizeof(The_mission.skybox_model)); - std::strncpy(The_mission.skybox_model, name.c_str(), NAME_LENGTH - 1); - } - - set_modified(); - refreshBackgroundPreview(); -} - -bool BackgroundEditorDialogModel::getSkyboxNoLighting() -{ - return (The_mission.skybox_flags & MR_NO_LIGHTING) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxNoLighting(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_NO_LIGHTING; - } else { - The_mission.skybox_flags &= ~MR_NO_LIGHTING; - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getSkyboxAllTransparent() -{ - return (The_mission.skybox_flags & MR_ALL_XPARENT) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxAllTransparent(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_ALL_XPARENT; - } else { - The_mission.skybox_flags &= ~MR_ALL_XPARENT; - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getSkyboxNoZbuffer() -{ - return (The_mission.skybox_flags & MR_NO_ZBUFFER) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxNoZbuffer(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_NO_ZBUFFER; - } else { - The_mission.skybox_flags &= ~MR_NO_ZBUFFER; - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getSkyboxNoCull() -{ - return (The_mission.skybox_flags & MR_NO_CULL) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxNoCull(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_NO_CULL; - } else { - The_mission.skybox_flags &= ~MR_NO_CULL; - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getSkyboxNoGlowmaps() -{ - return (The_mission.skybox_flags & MR_NO_GLOWMAPS) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxNoGlowmaps(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_NO_GLOWMAPS; - } else { - The_mission.skybox_flags &= ~MR_NO_GLOWMAPS; - } - - set_modified(); -} - -bool BackgroundEditorDialogModel::getSkyboxForceClamp() -{ - return (The_mission.skybox_flags & MR_FORCE_CLAMP) != 0; -} - -void BackgroundEditorDialogModel::setSkyboxForceClamp(bool on) -{ - if (on) { - The_mission.skybox_flags |= MR_FORCE_CLAMP; - } else { - The_mission.skybox_flags &= ~MR_FORCE_CLAMP; - } - - set_modified(); -} - -int BackgroundEditorDialogModel::getSkyboxPitch() -{ - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - int d = static_cast(fl2ir(fl_degrees(a.p))); - d = (d % 360 + 360) % 360; // wrap to [0, 359] - return d; -} - -void BackgroundEditorDialogModel::setSkyboxPitch(int deg) -{ - CLAMP(deg, 0, 359); - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - const int cur = static_cast(fl2ir(fl_degrees(a.p))); - if (cur != deg) { - a.p = fl_radians(static_cast(deg)); - vm_angles_2_matrix(&The_mission.skybox_orientation, &a); - set_modified(); - refreshBackgroundPreview(); - } -} - -int BackgroundEditorDialogModel::getSkyboxBank() -{ - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - int d = static_cast(fl2ir(fl_degrees(a.b))); - d = (d % 360 + 360) % 360; // wrap to [0, 359] - return d; -} - -void BackgroundEditorDialogModel::setSkyboxBank(int deg) -{ - CLAMP(deg, 0, 359); - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - const int cur = static_cast(fl2ir(fl_degrees(a.b))); - if (cur != deg) { - a.b = fl_radians(static_cast(deg)); - vm_angles_2_matrix(&The_mission.skybox_orientation, &a); - set_modified(); - refreshBackgroundPreview(); - } -} - -int BackgroundEditorDialogModel::getSkyboxHeading() -{ - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - int d = static_cast(fl2ir(fl_degrees(a.h))); - d = (d % 360 + 360) % 360; // wrap to [0, 359] - return d; -} - -void BackgroundEditorDialogModel::setSkyboxHeading(int deg) -{ - CLAMP(deg, 0, 359); - angles a; - vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); - const int cur = static_cast(fl2ir(fl_degrees(a.h))); - if (cur != deg) { - a.h = fl_radians(static_cast(deg)); - vm_angles_2_matrix(&The_mission.skybox_orientation, &a); - set_modified(); - refreshBackgroundPreview(); - } -} - -SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() -{ - SCP_vector out; - auto profiles = lighting_profiles::list_profiles(); // returns a vector of names - out.reserve(profiles.size()); - for (const auto& p : profiles) - out.emplace_back(p.c_str()); - return out; -} - -int BackgroundEditorDialogModel::getNumStars() -{ - return Num_stars; -} - -void BackgroundEditorDialogModel::setNumStars(int n) -{ - CLAMP(n, getStarsLimit().first, getStarsLimit().second); - modify(Num_stars, n); - refreshBackgroundPreview(); -} - -bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() -{ - return The_mission.flags[Mission::Mission_Flags::Subspace]; -} - -void BackgroundEditorDialogModel::setTakesPlaceInSubspace(bool on) -{ - auto before = The_mission.flags[Mission::Mission_Flags::Subspace]; - if (before == on) - return; - - The_mission.flags.set(Mission::Mission_Flags::Subspace, on); - - set_modified(); -} - -SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() -{ - return {The_mission.envmap_name}; -} - -void BackgroundEditorDialogModel::setEnvironmentMapName(const SCP_string& name) -{ - if (name == The_mission.envmap_name) - return; - - strcpy_s(The_mission.envmap_name, name.c_str()); - - set_modified(); -} - -SCP_string BackgroundEditorDialogModel::getLightingProfileName() -{ - return The_mission.lighting_profile_name; -} - -void BackgroundEditorDialogModel::setLightingProfileName(const SCP_string& name) -{ - modify(The_mission.lighting_profile_name, name); -} - +#include "FredApplication.h" +#include "BackgroundEditorDialogModel.h" + +#include "graphics/light.h" +#include "math/bitarray.h" +#include "mission/missionparse.h" +#include "nebula/neb.h" +#include "nebula/neblightning.h" +#include "starfield/nebula.h" +#include "lighting/lighting_profiles.h" + +// TODO move this to common for both FREDs. +const static float delta = .00001f; +const static float default_nebula_range = 3000.0f; + +extern void parse_one_background(background_t* background); + +namespace fso::fred::dialogs { +BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + auto& bg = getActiveBackground(); + auto& bm_list = bg.bitmaps; + if (!bm_list.empty()) { + _selectedBitmapIndex = 0; + } + + auto& sun_list = bg.suns; + if (!sun_list.empty()) { + _selectedSunIndex = 0; + } +} + +bool BackgroundEditorDialogModel::apply() +{ + // override dumb values with reasonable ones + // this is what original FRED does but it was a text edit field using atoi + // ours is a limited spinbox so this probably isn't necessary anymore?? + // Does this mean range can never be 0????????? + if (Neb2_awacs <= 0.00000001f) { + Neb2_awacs = 3000.0f; + } + return true; +} + +void BackgroundEditorDialogModel::reject() +{ + // do nothing +} + +void BackgroundEditorDialogModel::refreshBackgroundPreview() +{ + stars_load_background(Cur_background); // rebuild instances from Backgrounds[] + stars_set_background_model(The_mission.skybox_model, nullptr, The_mission.skybox_flags); // rebuild skybox + stars_set_background_orientation(&The_mission.skybox_orientation); + // TODO make this actually show the stars in the background + _editor->missionChanged(); +} + +background_t& BackgroundEditorDialogModel::getActiveBackground() +{ + if (!SCP_vector_inbounds(Backgrounds, Cur_background)) { + // Fall back to first background if Cur_background isn't set + Cur_background = 0; + } + return Backgrounds[Cur_background]; +} + +starfield_list_entry* BackgroundEditorDialogModel::getActiveBitmap() const +{ + auto& bg = getActiveBackground(); + auto& list = bg.bitmaps; + if (!SCP_vector_inbounds(list, _selectedBitmapIndex)) { + return nullptr; + } + return &list[_selectedBitmapIndex]; +} + +starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const +{ + auto& bg = getActiveBackground(); + auto& list = bg.suns; + if (!SCP_vector_inbounds(list, _selectedSunIndex)) { + return nullptr; + } + return &list[_selectedSunIndex]; +} + +SCP_vector BackgroundEditorDialogModel::getBackgroundNames() +{ + + SCP_vector out; + out.reserve(Backgrounds.size()); + for (size_t i = 0; i < Backgrounds.size(); ++i) + out.emplace_back("Background " + std::to_string(i + 1)); + return out; +} + +void BackgroundEditorDialogModel::setActiveBackgroundIndex(int idx) +{ + if (!SCP_vector_inbounds(Backgrounds, idx)) + return; + + Cur_background = idx; + + // Reseed selections for the new background + _selectedBitmapIndex = Backgrounds[idx].bitmaps.empty() ? -1 : 0; + _selectedSunIndex = Backgrounds[idx].suns.empty() ? -1 : 0; + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getActiveBackgroundIndex() +{ + return Cur_background < 0 ? 0 : Cur_background; +} + + +void BackgroundEditorDialogModel::addBackground() +{ + const int newIndex = static_cast(Backgrounds.size()); + stars_add_blank_background(/*creating_in_fred=*/true); + set_modified(); + + // select it + setActiveBackgroundIndex(newIndex); +} + +void BackgroundEditorDialogModel::removeActiveBackground() +{ + if (Backgrounds.size() <= 1 || Cur_background < 0) { + return; // keep at least one background + } + + const int oldIdx = Cur_background; + Backgrounds.erase(Backgrounds.begin() + oldIdx); + + // clamp selection to the new valid range + const int newIdx = std::min(oldIdx, static_cast(Backgrounds.size()) - 1); + set_modified(); + + setActiveBackgroundIndex(newIdx); + + // Ensure the swap index is still valid + if (!SCP_vector_inbounds(Backgrounds, _swapIndex)) { + _swapIndex = 0; + } +} + +int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) +{ + // Normalize the filepath to use the current platform's directory separator + SCP_string path = fs2Path; + std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); + + try { + read_file_text(path.c_str()); + reset_parse(); + + if (!skip_to_start_of_string("#Background bitmaps")) { + return 0; // no background section + } + + // Enter the section and skip the header fields + required_string("#Background bitmaps"); + required_string("$Num stars:"); + int tmp; + stuff_int(&tmp); + required_string("$Ambient light level:"); + stuff_int(&tmp); + + // Count how many explicit "$Bitmap List:" blocks this file has + char* saved = Mp; + int count = 0; + while (skip_to_string("$Bitmap List:")) { + ++count; + } + Mp = saved; + + // Retail-style missions may have 0 "$Bitmap List:" entries but still one background. + return (count > 0) ? count : 1; + } catch (...) { + return 0; // parse error + } +} + +bool BackgroundEditorDialogModel::importBackgroundFromMission(const SCP_string& fs2Path, int whichIndex) +{ + // Replace the CURRENT background with one parsed from another mission file. + if (Cur_background < 0) + return false; + + // Normalize the filepath to use the current platform's directory separator + SCP_string path = fs2Path; + std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); + + try { + read_file_text(path.c_str()); + reset_parse(); + + if (!skip_to_start_of_string("#Background bitmaps")) { + return false; // file has no background section + } + + required_string("#Background bitmaps"); + required_string("$Num stars:"); + int tmp; + stuff_int(&tmp); + required_string("$Ambient light level:"); + stuff_int(&tmp); + + // Count "$Bitmap List:" occurrences + char* saved = Mp; + int count = 0; + while (skip_to_string("$Bitmap List:")) { + ++count; + } + Mp = saved; + + // If multiple lists exist, skip to the requested one. + // If zero lists exist (retail), parse_one_background will handle the single background. + if (count > 0) { + const int target = std::max(0, std::min(whichIndex, count - 1)); + for (int i = 0; i < target + 1; ++i) { + skip_to_string("$Bitmap List:"); + } + } + + // Parse into the current slot + parse_one_background(&Backgrounds[Cur_background]); + } catch (...) { + return false; + } + + set_modified(); + // Rebuild instances & repaint so the import is visible immediately + stars_load_background(Cur_background); + if (_viewport) + _viewport->needsUpdate(); + return true; +} + +void BackgroundEditorDialogModel::swapBackgrounds() +{ + if (Cur_background < 0 || _swapIndex < 0 || _swapIndex >= static_cast(Backgrounds.size()) || _swapIndex == Cur_background) { + return; + } + + stars_swap_backgrounds(Cur_background, _swapIndex); + set_modified(); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getSwapWithIndex() const +{ + return _swapIndex; +} + +void BackgroundEditorDialogModel::setSwapWithIndex(int idx) +{ + if (!SCP_vector_inbounds(Backgrounds, idx)) { + return; + } + + _swapIndex = idx; +} + +bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() +{ + const auto& bg = getActiveBackground(); + return bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; +} + +void BackgroundEditorDialogModel::setSaveAnglesCorrectFlag(bool on) +{ + auto& bg = getActiveBackground(); + const bool before = bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; + if (before == on) + return; + + if (on) + bg.flags.set(Starfield::Background_Flags::Corrected_angles_in_mission_file); + else + bg.flags.remove(Starfield::Background_Flags::Corrected_angles_in_mission_file); + + set_modified(); +} + +SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() +{ + SCP_vector out; + const int count = stars_get_num_entries(/*is_a_sun=*/false, /*bitmap_count=*/true); + out.reserve(count); + for (int i = 0; i < count; ++i) { + if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/false)) { + out.emplace_back(name); + } + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() +{ + SCP_vector out; + const auto& vec = getActiveBackground().bitmaps; + out.reserve(vec.size()); + for (const auto& sle : vec) { + out.emplace_back(sle.filename); + } + return out; +} + +void BackgroundEditorDialogModel::setSelectedBitmapIndex(int index) +{ + const auto& bg = getActiveBackground(); + const auto& list = bg.bitmaps; + if (!SCP_vector_inbounds(list, index)) { + _selectedBitmapIndex = -1; + return; + } + _selectedBitmapIndex = index; +} + +int BackgroundEditorDialogModel::getSelectedBitmapIndex() const +{ + return _selectedBitmapIndex; +} + +void BackgroundEditorDialogModel::addMissionBitmapByName(const SCP_string& name) +{ + if (name.empty()) + return; + + // Must exist in tables + if (stars_find_bitmap(name.c_str()) < 0) + return; + + starfield_list_entry sle{}; + std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); + sle.ang.p = 0.0f; + sle.ang.b = 0.0f; + sle.ang.h = 0.0f; + sle.scale_x = 1.0f; + sle.scale_y = 1.0f; + sle.div_x = 1; + sle.div_y = 1; + + auto& list = getActiveBackground().bitmaps; + list.push_back(sle); + + _selectedBitmapIndex = static_cast(list.size()) - 1; + + set_modified(); + refreshBackgroundPreview(); +} + +void BackgroundEditorDialogModel::removeMissionBitmap() +{ + auto& list = getActiveBackground().bitmaps; + + // Make sure we have an active bitmap + if (getActiveBitmap() == nullptr) { + return; + } + + list.erase(list.begin() + _selectedBitmapIndex); + + // choose a sensible new selection + if (list.empty()) { + _selectedBitmapIndex = -1; + } else { + _selectedBitmapIndex = std::min(_selectedBitmapIndex, static_cast(list.size()) - 1); + } + + set_modified(); + refreshBackgroundPreview(); +} + +SCP_string BackgroundEditorDialogModel::getBitmapName() const +{ + auto bm = getActiveBitmap(); + if (bm == nullptr) { + return ""; + } + + return bm->filename; +} + +void BackgroundEditorDialogModel::setBitmapName(const SCP_string& name) +{ + if (name.empty()) + return; + + // Must exist in tables + if (stars_find_bitmap(name.c_str()) < 0) + return; + + auto bm = getActiveBitmap(); + if (bm != nullptr) { + strcpy_s(bm->filename, name.c_str()); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getBitmapPitch() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return fl2ir(fl_degrees(bm->ang.p) + delta); +} + +void BackgroundEditorDialogModel::setBitmapPitch(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.p, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapBank() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return fl2ir(fl_degrees(bm->ang.b) + delta); +} + +void BackgroundEditorDialogModel::setBitmapBank(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.b, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapHeading() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return fl2ir(fl_degrees(bm->ang.h) + delta); +} + +void BackgroundEditorDialogModel::setBitmapHeading(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.h, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getBitmapScaleX() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return bm->scale_x; +} + +void BackgroundEditorDialogModel::setBitmapScaleX(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); + modify(bm->scale_x, v); + + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getBitmapScaleY() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return bm->scale_y; +} + +void BackgroundEditorDialogModel::setBitmapScaleY(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); + modify(bm->scale_y, v); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapDivX() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return bm->div_x; +} + +void BackgroundEditorDialogModel::setBitmapDivX(int v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); + modify(bm->div_x, v); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapDivY() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return bm->div_y; +} + +void BackgroundEditorDialogModel::setBitmapDivY(int v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); + modify(bm->div_y, v); + + refreshBackgroundPreview(); +} + +SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() +{ + SCP_vector out; + const int count = stars_get_num_entries(/*is_a_sun=*/true, /*bitmap_count=*/true); + out.reserve(count); + for (int i = 0; i < count; ++i) { + if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/true)) { // table order + out.emplace_back(name); + } + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getMissionSunNames() +{ + SCP_vector out; + const auto& vec = getActiveBackground().suns; + out.reserve(vec.size()); + for (const auto& sle : vec) + out.emplace_back(sle.filename); + return out; +} + +void BackgroundEditorDialogModel::setSelectedSunIndex(int index) +{ + const auto& list = getActiveBackground().suns; + if (!SCP_vector_inbounds(list, index)) { + _selectedSunIndex = -1; + return; + } + _selectedSunIndex = index; +} + +int BackgroundEditorDialogModel::getSelectedSunIndex() const +{ + return _selectedSunIndex; +} + +void BackgroundEditorDialogModel::addMissionSunByName(const SCP_string& name) +{ + if (name.empty()) + return; + + if (stars_find_sun(name.c_str()) < 0) + return; // must exist in sun table + + starfield_list_entry sle{}; + std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); + sle.ang.p = sle.ang.b = sle.ang.h = 0.0f; + sle.scale_x = 1.0f; + sle.scale_y = 1.0f; + sle.div_x = 1; + sle.div_y = 1; + + auto& list = getActiveBackground().suns; + list.push_back(sle); + _selectedSunIndex = static_cast(list.size()) - 1; + + set_modified(); + refreshBackgroundPreview(); +} + +void BackgroundEditorDialogModel::removeMissionSun() +{ + auto& list = getActiveBackground().suns; + if (getActiveSun() == nullptr) + return; + + list.erase(list.begin() + _selectedSunIndex); + if (list.empty()) + _selectedSunIndex = -1; + else + _selectedSunIndex = std::min(_selectedSunIndex, static_cast(list.size()) - 1); + + set_modified(); + refreshBackgroundPreview(); +} + +SCP_string BackgroundEditorDialogModel::getSunName() const +{ + auto* s = getActiveSun(); + if (!s) + return ""; + + return s->filename; +} + +void BackgroundEditorDialogModel::setSunName(const SCP_string& name) +{ + if (name.empty()) + return; + + if (stars_find_sun(name.c_str()) < 0) + return; + + auto* s = getActiveSun(); + if (!s) + return; + + strcpy_s(s->filename, name.c_str()); + set_modified(); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getSunPitch() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return fl2ir(fl_degrees(s->ang.p) + delta); +} + +void BackgroundEditorDialogModel::setSunPitch(int deg) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(s->ang.p, fl_radians(deg)); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getSunHeading() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return fl2ir(fl_degrees(s->ang.h) + delta); +} + +void BackgroundEditorDialogModel::setSunHeading(int deg) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(s->ang.h, fl_radians(deg)); + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getSunScale() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return s->scale_x; // suns store scale in X; Y remains 1.0 +} + +void BackgroundEditorDialogModel::setSunScale(float v) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(v, getSunScaleLimit().first, getSunScaleLimit().second); + modify(s->scale_x, v); + refreshBackgroundPreview(); +} + +SCP_vector BackgroundEditorDialogModel::getLightningNames() +{ + SCP_vector out; + out.emplace_back(""); // legacy default + for (const auto& st : Storm_types) { + out.emplace_back(st.name); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() +{ + SCP_vector out; + out.emplace_back(""); // matches legacy combo where index 0 = none + for (const auto& neb : Neb2_bitmap_filenames) { + out.emplace_back(neb); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getPoofNames() +{ + SCP_vector out; + out.reserve(Poof_info.size()); + for (const auto& p : Poof_info) { + out.emplace_back(p.name); + } + return out; +} + +bool BackgroundEditorDialogModel::getFullNebulaEnabled() +{ + return The_mission.flags[Mission::Mission_Flags::Fullneb]; +} + +void BackgroundEditorDialogModel::setFullNebulaEnabled(bool enabled) +{ + const bool currentlyEnabled = getFullNebulaEnabled(); + if (enabled == currentlyEnabled) { + return; + } + + if (enabled) { + The_mission.flags.set(Mission::Mission_Flags::Fullneb); + + // Set defaults if needed + if (Neb2_awacs <= 0.0f) { + modify(Neb2_awacs, default_nebula_range); + } + } else { + // Disable full nebula + The_mission.flags.remove(Mission::Mission_Flags::Fullneb); + modify(Neb2_awacs, -1.0f); + } + + set_modified(); +} + +float BackgroundEditorDialogModel::getFullNebulaRange() +{ + // May be -1 if full nebula is disabled + return Neb2_awacs; +} + +void BackgroundEditorDialogModel::setFullNebulaRange(float range) +{ + modify(Neb2_awacs, range); +} + +SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() +{ + return (Neb2_texture_name[0] != '\0') ? SCP_string(Neb2_texture_name) : SCP_string(""); +} + +void BackgroundEditorDialogModel::setNebulaFullPattern(const SCP_string& name) +{ + if (lcase_equal(name, "")) { + strcpy_s(Neb2_texture_name, ""); + } else { + strcpy_s(Neb2_texture_name, name.c_str()); + } + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getLightning() +{ + // Return "" when engine stores "none" or empty + if (Mission_parse_storm_name[0] == '\0') + return ""; + SCP_string s = Mission_parse_storm_name; + if (lcase_equal(s, "none")) + return ""; + return s; +} + +void BackgroundEditorDialogModel::setLightning(const SCP_string& name) +{ + // Engine convention is the literal "none" for no storm + if (lcase_equal(name, "")) { + strcpy_s(Mission_parse_storm_name, "none"); + } else { + strcpy_s(Mission_parse_storm_name, name.c_str()); + } + set_modified(); +} + +SCP_vector BackgroundEditorDialogModel::getSelectedPoofs() +{ + SCP_vector out; + for (size_t i = 0; i < Poof_info.size(); ++i) { + if (get_bit(Neb2_poof_flags.get(), i)) + out.emplace_back(Poof_info[i].name); + } + return out; +} + +void BackgroundEditorDialogModel::setSelectedPoofs(const SCP_vector& names) +{ + // Clear all, then set matching names + clear_all_bits(Neb2_poof_flags.get(), Poof_info.size()); + for (const auto& want : names) { + for (size_t i = 0; i < Poof_info.size(); ++i) { + if (!stricmp(Poof_info[i].name, want.c_str())) { + set_bit(Neb2_poof_flags.get(), i); + break; + } + } + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getShipTrailsToggled() +{ + return The_mission.flags[Mission::Mission_Flags::Toggle_ship_trails]; +} + +void BackgroundEditorDialogModel::setShipTrailsToggled(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Toggle_ship_trails, on); + set_modified(); +} + +float BackgroundEditorDialogModel::getFogNearMultiplier() +{ + return Neb2_fog_near_mult; +} + +void BackgroundEditorDialogModel::setFogNearMultiplier(float v) +{ + modify(Neb2_fog_near_mult, v); +} + +float BackgroundEditorDialogModel::getFogFarMultiplier() +{ + return Neb2_fog_far_mult; +} + +void BackgroundEditorDialogModel::setFogFarMultiplier(float v) +{ + modify(Neb2_fog_far_mult, v); +} + +bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() +{ + return The_mission.flags[Mission::Mission_Flags::Fullneb_background_bitmaps]; +} + +void BackgroundEditorDialogModel::setDisplayBackgroundBitmaps(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Fullneb_background_bitmaps, on); + set_modified(); +} + +bool BackgroundEditorDialogModel::getFogPaletteOverride() +{ + return The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]; +} + +void BackgroundEditorDialogModel::setFogPaletteOverride(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Neb2_fog_color_override, on); + set_modified(); +} + +int BackgroundEditorDialogModel::getFogR() +{ + return Neb2_fog_color[0]; +} + +void BackgroundEditorDialogModel::setFogR(int r) +{ + CLAMP(r, 0, 255) + const auto v = static_cast(r); + modify(Neb2_fog_color[0], v); +} + +int BackgroundEditorDialogModel::getFogG() +{ + return Neb2_fog_color[1]; +} + +void BackgroundEditorDialogModel::setFogG(int g) +{ + CLAMP(g, 0, 255) + const auto v = static_cast(g); + modify(Neb2_fog_color[1], v); +} + +int BackgroundEditorDialogModel::getFogB() +{ + return Neb2_fog_color[2]; +} + +void BackgroundEditorDialogModel::setFogB(int b) +{ + CLAMP(b, 0, 255) + const auto v = static_cast(b); + modify(Neb2_fog_color[2], v); +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() +{ + SCP_vector out; + out.emplace_back(""); + for (auto& neb : Nebula_filenames) { + out.emplace_back(neb); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() +{ + SCP_vector out; + out.reserve(NUM_NEBULA_COLORS); + for (auto& color : Nebula_colors) { + out.emplace_back(color); + } + return out; +} + +SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() +{ + if (Nebula_index < 0) + return ""; + + if (Nebula_index >= 0 && Nebula_index < NUM_NEBULAS) { + return Nebula_filenames[Nebula_index]; + } + + return SCP_string{}; +} + +void BackgroundEditorDialogModel::setOldNebulaPattern(const SCP_string& name) +{ + int newIndex = -1; + if (!name.empty() && stricmp(name.c_str(), "") != 0) { + for (int i = 0; i < NUM_NEBULAS; ++i) { + if (!stricmp(Nebula_filenames[i], name.c_str())) { + newIndex = i; + break; + } + } + } + + modify(Nebula_index, newIndex); +} + +SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() +{ + if (Mission_palette >= 0 && Mission_palette < NUM_NEBULA_COLORS) { + return Nebula_colors[Mission_palette]; + } + return SCP_string{}; +} + +void BackgroundEditorDialogModel::setOldNebulaColorName(const SCP_string& name) +{ + if (name.empty()) + return; + for (int i = 0; i < NUM_NEBULA_COLORS; ++i) { + if (!stricmp(Nebula_colors[i], name.c_str())) { + modify(Mission_palette, i); + return; + } + } + // name not found: ignore +} + +int BackgroundEditorDialogModel::getOldNebulaPitch() +{ + return Nebula_pitch; +} + +void BackgroundEditorDialogModel::setOldNebulaPitch(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_pitch != deg) { + Nebula_pitch = deg; + modify(Nebula_pitch, deg); + } +} + +int BackgroundEditorDialogModel::getOldNebulaBank() +{ + return Nebula_bank; +} + +void BackgroundEditorDialogModel::setOldNebulaBank(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_bank != deg) { + Nebula_bank = deg; + modify(Nebula_bank, deg); + } +} + +int BackgroundEditorDialogModel::getOldNebulaHeading() +{ + return Nebula_heading; +} + +void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_heading != deg) { + Nebula_heading = deg; + modify(Nebula_heading, deg); + } +} + +int BackgroundEditorDialogModel::getAmbientR() +{ + return The_mission.ambient_light_level & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientR(int r) +{ + CLAMP(r, 1, 255); + + const int g = getAmbientG(), b = getAmbientB(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getAmbientG() +{ + return (The_mission.ambient_light_level >> 8) & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientG(int g) +{ + CLAMP(g, 1, 255); + + const int r = getAmbientR(), b = getAmbientB(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getAmbientB() +{ + return (The_mission.ambient_light_level >> 16) & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientB(int b) +{ + CLAMP(b, 1, 255); + + const int r = getAmbientR(), g = getAmbientG(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + +SCP_string BackgroundEditorDialogModel::getSkyboxModelName() +{ + return The_mission.skybox_model; +} +void BackgroundEditorDialogModel::setSkyboxModelName(const SCP_string& name) +{ + // empty string = no skybox + if (std::strncmp(The_mission.skybox_model, name.c_str(), NAME_LENGTH) != 0) { + std::memset(The_mission.skybox_model, 0, sizeof(The_mission.skybox_model)); + std::strncpy(The_mission.skybox_model, name.c_str(), NAME_LENGTH - 1); + } + + set_modified(); + refreshBackgroundPreview(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoLighting() +{ + return (The_mission.skybox_flags & MR_NO_LIGHTING) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoLighting(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_LIGHTING; + } else { + The_mission.skybox_flags &= ~MR_NO_LIGHTING; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxAllTransparent() +{ + return (The_mission.skybox_flags & MR_ALL_XPARENT) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxAllTransparent(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_ALL_XPARENT; + } else { + The_mission.skybox_flags &= ~MR_ALL_XPARENT; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoZbuffer() +{ + return (The_mission.skybox_flags & MR_NO_ZBUFFER) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoZbuffer(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_ZBUFFER; + } else { + The_mission.skybox_flags &= ~MR_NO_ZBUFFER; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoCull() +{ + return (The_mission.skybox_flags & MR_NO_CULL) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoCull(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_CULL; + } else { + The_mission.skybox_flags &= ~MR_NO_CULL; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoGlowmaps() +{ + return (The_mission.skybox_flags & MR_NO_GLOWMAPS) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoGlowmaps(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_GLOWMAPS; + } else { + The_mission.skybox_flags &= ~MR_NO_GLOWMAPS; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxForceClamp() +{ + return (The_mission.skybox_flags & MR_FORCE_CLAMP) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxForceClamp(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_FORCE_CLAMP; + } else { + The_mission.skybox_flags &= ~MR_FORCE_CLAMP; + } + + set_modified(); +} + +int BackgroundEditorDialogModel::getSkyboxPitch() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.p))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + return d; +} + +void BackgroundEditorDialogModel::setSkyboxPitch(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.p))); + if (cur != deg) { + a.p = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getSkyboxBank() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.b))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + return d; +} + +void BackgroundEditorDialogModel::setSkyboxBank(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.b))); + if (cur != deg) { + a.b = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getSkyboxHeading() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.h))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + return d; +} + +void BackgroundEditorDialogModel::setSkyboxHeading(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.h))); + if (cur != deg) { + a.h = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + +SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() +{ + SCP_vector out; + auto profiles = lighting_profiles::list_profiles(); // returns a vector of names + out.reserve(profiles.size()); + for (const auto& p : profiles) + out.emplace_back(p.c_str()); + return out; +} + +int BackgroundEditorDialogModel::getNumStars() +{ + return Num_stars; +} + +void BackgroundEditorDialogModel::setNumStars(int n) +{ + CLAMP(n, getStarsLimit().first, getStarsLimit().second); + modify(Num_stars, n); + refreshBackgroundPreview(); +} + +bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() +{ + return The_mission.flags[Mission::Mission_Flags::Subspace]; +} + +void BackgroundEditorDialogModel::setTakesPlaceInSubspace(bool on) +{ + auto before = The_mission.flags[Mission::Mission_Flags::Subspace]; + if (before == on) + return; + + The_mission.flags.set(Mission::Mission_Flags::Subspace, on); + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() +{ + return {The_mission.envmap_name}; +} + +void BackgroundEditorDialogModel::setEnvironmentMapName(const SCP_string& name) +{ + if (name == The_mission.envmap_name) + return; + + strcpy_s(The_mission.envmap_name, name.c_str()); + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getLightingProfileName() +{ + return The_mission.lighting_profile_name; +} + +void BackgroundEditorDialogModel::setLightingProfileName(const SCP_string& name) +{ + modify(The_mission.lighting_profile_name, name); +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp index f02f91c6255..936d15315c2 100644 --- a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp +++ b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp @@ -1,145 +1,145 @@ -#include -#include "mission/dialogs/FictionViewerDialogModel.h" - -namespace fso::fred::dialogs { - -FictionViewerDialogModel::FictionViewerDialogModel(QObject* parent, EditorViewport* viewport) : - AbstractDialogModel(parent, viewport) { - - initializeData(); -} - -bool FictionViewerDialogModel::apply() { - // if the story file for the current stage is empty, treat as no fiction viewer stage - // currently we only support one stage, so just check the first one - const auto& stage = _fictionViewerStages.at(0); - const bool empty = stage.story_filename[0] == '\0'; - - if (empty) { - _fictionViewerStages.clear(); - Mission_music[SCORE_FICTION_VIEWER] = -1; - } else { - // Keep whatever you’ve edited in _fictionViewerStages - Mission_music[SCORE_FICTION_VIEWER] = _fictionMusic; // -1 for none is valid - } - - // Commit working copy to mission - Fiction_viewer_stages = _fictionViewerStages; - return true; -} - -void FictionViewerDialogModel::reject() { - // nothing to do -} - -void FictionViewerDialogModel::initializeData() { - - _fictionViewerStages = Fiction_viewer_stages; - - // make sure we have at least one stage - if (_fictionViewerStages.empty()) { - fiction_viewer_stage stage; - memset(&stage, 0, sizeof(fiction_viewer_stage)); - stage.formula = Locked_sexp_true; - - _fictionViewerStages.push_back(stage); - } - - _musicOptions.emplace_back("None", -1); - for (int i = 0; i < static_cast(Spooled_music.size()); ++i) { - _musicOptions.emplace_back(Spooled_music[i].name, i); - } - - // music is managed through the mission - _fictionMusic = Mission_music[SCORE_FICTION_VIEWER]; -} - -const SCP_vector>& FictionViewerDialogModel::getMusicOptions() -{ - return _musicOptions; -} - -SCP_string FictionViewerDialogModel::getStoryFile() const -{ - return _fictionViewerStages[_fictionViewerStageIndex].story_filename; -} - -void FictionViewerDialogModel::setStoryFile(const SCP_string& storyFile) -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - if (strcmp(stage.story_filename, storyFile.c_str()) != 0) { - strcpy_s(stage.story_filename, storyFile.c_str()); - set_modified(); - } -} - -SCP_string FictionViewerDialogModel::getFontFile() const -{ - return _fictionViewerStages[_fictionViewerStageIndex].font_filename; -} - -void FictionViewerDialogModel::setFontFile(const SCP_string& fontFile) -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - if (stricmp(stage.font_filename, fontFile.c_str()) != 0) { - strcpy_s(stage.font_filename, fontFile.c_str()); - set_modified(); - } -} - -SCP_string FictionViewerDialogModel::getVoiceFile() const -{ - return _fictionViewerStages[_fictionViewerStageIndex].voice_filename; -} - -void FictionViewerDialogModel::setVoiceFile(const SCP_string& voiceFile) -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - if (stricmp(stage.voice_filename, voiceFile.c_str()) != 0) { - strcpy_s(stage.voice_filename, voiceFile.c_str()); - set_modified(); - } -} - -int FictionViewerDialogModel::getFictionMusic() const -{ - // TODO research how music is set for multiple fiction viewer stages so we - // can return the correct index when multiple stages is fully supported - return _fictionMusic; -} - -void FictionViewerDialogModel::setFictionMusic(int fictionMusic) { - bool valid = fictionMusic == -1 || SCP_vector_inbounds(Spooled_music, fictionMusic); - Assertion(valid, - "Fiction music index out of bounds: %d (max %d)", - fictionMusic, - static_cast(Spooled_music.size())); - - modify(_fictionMusic, fictionMusic); -} - -int FictionViewerDialogModel::getMaxStoryFileLength() const -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - return sizeof(stage.story_filename) - 1; -} - -int FictionViewerDialogModel::getMaxFontFileLength() const -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - return sizeof(stage.font_filename) - 1; -} - -int FictionViewerDialogModel::getMaxVoiceFileLength() const -{ - auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - - return sizeof(stage.voice_filename) - 1; -} - -} // namespace fso::fred::dialogs +#include +#include "mission/dialogs/FictionViewerDialogModel.h" + +namespace fso::fred::dialogs { + +FictionViewerDialogModel::FictionViewerDialogModel(QObject* parent, EditorViewport* viewport) : + AbstractDialogModel(parent, viewport) { + + initializeData(); +} + +bool FictionViewerDialogModel::apply() { + // if the story file for the current stage is empty, treat as no fiction viewer stage + // currently we only support one stage, so just check the first one + const auto& stage = _fictionViewerStages.at(0); + const bool empty = stage.story_filename[0] == '\0'; + + if (empty) { + _fictionViewerStages.clear(); + Mission_music[SCORE_FICTION_VIEWER] = -1; + } else { + // Keep whatever you've edited in _fictionViewerStages + Mission_music[SCORE_FICTION_VIEWER] = _fictionMusic; // -1 for none is valid + } + + // Commit working copy to mission + Fiction_viewer_stages = _fictionViewerStages; + return true; +} + +void FictionViewerDialogModel::reject() { + // nothing to do +} + +void FictionViewerDialogModel::initializeData() { + + _fictionViewerStages = Fiction_viewer_stages; + + // make sure we have at least one stage + if (_fictionViewerStages.empty()) { + fiction_viewer_stage stage; + memset(&stage, 0, sizeof(fiction_viewer_stage)); + stage.formula = Locked_sexp_true; + + _fictionViewerStages.push_back(stage); + } + + _musicOptions.emplace_back("None", -1); + for (int i = 0; i < static_cast(Spooled_music.size()); ++i) { + _musicOptions.emplace_back(Spooled_music[i].name, i); + } + + // music is managed through the mission + _fictionMusic = Mission_music[SCORE_FICTION_VIEWER]; +} + +const SCP_vector>& FictionViewerDialogModel::getMusicOptions() +{ + return _musicOptions; +} + +SCP_string FictionViewerDialogModel::getStoryFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].story_filename; +} + +void FictionViewerDialogModel::setStoryFile(const SCP_string& storyFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + if (strcmp(stage.story_filename, storyFile.c_str()) != 0) { + strcpy_s(stage.story_filename, storyFile.c_str()); + set_modified(); + } +} + +SCP_string FictionViewerDialogModel::getFontFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].font_filename; +} + +void FictionViewerDialogModel::setFontFile(const SCP_string& fontFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + if (stricmp(stage.font_filename, fontFile.c_str()) != 0) { + strcpy_s(stage.font_filename, fontFile.c_str()); + set_modified(); + } +} + +SCP_string FictionViewerDialogModel::getVoiceFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].voice_filename; +} + +void FictionViewerDialogModel::setVoiceFile(const SCP_string& voiceFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + if (stricmp(stage.voice_filename, voiceFile.c_str()) != 0) { + strcpy_s(stage.voice_filename, voiceFile.c_str()); + set_modified(); + } +} + +int FictionViewerDialogModel::getFictionMusic() const +{ + // TODO research how music is set for multiple fiction viewer stages so we + // can return the correct index when multiple stages is fully supported + return _fictionMusic; +} + +void FictionViewerDialogModel::setFictionMusic(int fictionMusic) { + bool valid = fictionMusic == -1 || SCP_vector_inbounds(Spooled_music, fictionMusic); + Assertion(valid, + "Fiction music index out of bounds: %d (max %d)", + fictionMusic, + static_cast(Spooled_music.size())); + + modify(_fictionMusic, fictionMusic); +} + +int FictionViewerDialogModel::getMaxStoryFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.story_filename) - 1; +} + +int FictionViewerDialogModel::getMaxFontFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.font_filename) - 1; +} + +int FictionViewerDialogModel::getMaxVoiceFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.voice_filename) - 1; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp index 022bad69e72..5c7acbcb54d 100644 --- a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -1,1500 +1,1500 @@ -#include "MissionEventsDialogModel.h" - -#include -#include - -namespace fso::fred::dialogs { - -MissionEventsDialogModel::MissionEventsDialogModel(QObject* parent, fso::fred::EditorViewport* viewport, IEventTreeOps& tree_ops) - : AbstractDialogModel(parent, viewport), m_event_tree_ops(tree_ops) -{ - initializeData(); -} - -bool MissionEventsDialogModel::apply() -{ - SCP_vector> names; - - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - for (auto& event : Mission_events) { - free_sexp2(event.formula); - event.result = 0; // use this as a processed flag - } - - // rename all sexp references to old events - for (int i = 0; i < static_cast(m_events.size()); i++) { - if (m_sig[i] >= 0) { - names.emplace_back(Mission_events[m_sig[i]].name, m_events[i].name); - Mission_events[m_sig[i]].result = 1; - } - } - - // invalidate all sexp references to deleted events. - for (const auto& event : Mission_events) { - if (!event.result) { - SCP_string buf = "<" + event.name + ">"; - - // force it to not be too long - if (SCP_truncate(buf, NAME_LENGTH - 1)) - buf.back() = '>'; - - names.emplace_back(event.name, buf); - } - } - - // copy all dialog events to the mission - Mission_events.clear(); - for (const auto& dialog_event : m_events) { - Mission_events.push_back(dialog_event); - Mission_events.back().formula = m_event_tree_ops.save_tree(dialog_event.formula); - } - - // now update all sexp references - for (const auto& name_pair : names) - update_sexp_references(name_pair.first.c_str(), name_pair.second.c_str(), OPF_EVENT_NAME); - - for (int i = Num_builtin_messages; i < Num_messages; i++) { - if (Messages[i].avi_info.name) - free(Messages[i].avi_info.name); - - if (Messages[i].wave_info.name) - free(Messages[i].wave_info.name); - } - - Num_messages = static_cast(m_messages.size()) + Num_builtin_messages; - Messages.resize(Num_messages); - for (int i = 0; i < static_cast(m_messages.size()); i++) - Messages[i + Num_builtin_messages] = m_messages[i]; - - applyAnnotations(); - - // Only fire the signal after the changes have been applied to make sure the other parts of the code see the updated - // state - if (query_modified()) { - _editor->missionChanged(); - } - return true; -} - -void MissionEventsDialogModel::reject() -{ - // Nothing to do here -} - -void MissionEventsDialogModel::initializeData() -{ - initializeMessages(); - initializeHeadAniList(); - initializeWaveList(); - initializePersonaList(); - - initializeTeamList(); - initializeEvents(); -} - -void MissionEventsDialogModel::initializeEvents() -{ - m_events.clear(); - m_sig.clear(); - m_cur_event = -1; - for (auto i = 0; i < static_cast(Mission_events.size()); i++) { - m_events.push_back(Mission_events[i]); - m_sig.push_back(i); - - if (m_events[i].name.empty()) { - m_events[i].name = ""; - } - - m_events[i].formula = m_event_tree_ops.load_sub_tree(Mission_events[i].formula, false, "do-nothing"); - - // we must check for the case of the repeat count being 0. This would happen if the repeat - // count is not specified in a mission - if (m_events[i].repeat_count <= 0) { - m_events[i].repeat_count = 1; - } - } - - m_event_tree_ops.post_load(); - - m_event_tree_ops.clear(); - for (auto& event : m_events) { - // set the proper bitmap - NodeImage image; - if (event.chain_delay >= 0) { - image = NodeImage::CHAIN; - if (!event.objective_text.empty()) { - image = NodeImage::CHAIN_DIRECTIVE; - } - } else { - image = NodeImage::ROOT; - if (!event.objective_text.empty()) { - image = NodeImage::ROOT_DIRECTIVE; - } - } - - m_event_tree_ops.add_sub_tree(event.name, image, event.formula); - } - - initializeEventAnnotations(); -} - -int MissionEventsDialogModel::findFormulaByOriginalEventIndex(int orig) const -{ - for (int cur = 0; cur < static_cast(m_sig.size()); ++cur) - if (m_sig[cur] == orig) - return m_events[cur].formula; - return -1; -} - -void MissionEventsDialogModel::initializeEventAnnotations() -{ - m_event_annotations = Event_annotations; // copy - - for (auto& ea : m_event_annotations) { - ea.handle = nullptr; - if (ea.path.empty()) - continue; - - const int origIdx = ea.path.front(); - const int formula = findFormulaByOriginalEventIndex(origIdx); - if (formula < 0) - continue; - - IEventTreeOps::Handle h = m_event_tree_ops.get_root_by_formula(formula); - if (!h) - continue; - - // walk children - auto it = ea.path.begin(); - ++it; // skip event index - for (; it != ea.path.end() && h; ++it) { - const int child = *it; - if (child < 0 || child >= m_event_tree_ops.child_count(h)) { - h = nullptr; - break; - } - h = m_event_tree_ops.child_at(h, child); - } - - ea.handle = h; - if (h) { - const bool hasColor = (ea.r != 255) || (ea.g != 255) || (ea.b != 255); - m_event_tree_ops.set_node_note(h, ea.comment); - m_event_tree_ops.set_node_bg_color(h, ea.r, ea.g, ea.b, hasColor); - } - } -} - -// Build the path for a handle (root formula -> orig index; then child indices) -SCP_list MissionEventsDialogModel::buildPathForHandle(IEventTreeOps::Handle h) const -{ - SCP_list path; - if (!h) - return path; - - const int rootFormula = m_event_tree_ops.root_formula_of(h); - if (rootFormula < 0) - return path; - - // Find the *current* index of this root in m_events - int curIdx = -1; - for (int i = 0; i < static_cast(m_events.size()); ++i) { - if (m_events[i].formula == rootFormula) { - curIdx = i; - break; - } - } - if (curIdx < 0) - return path; - - // persist the current index - path.push_back(curIdx); - - // Collect child indices from node up to root, then reverse - std::vector rev; - IEventTreeOps::Handle cur = h; - for (;;) { - IEventTreeOps::Handle parent = m_event_tree_ops.parent_of(cur); - if (!parent) - break; - rev.push_back(m_event_tree_ops.index_in_parent(cur)); - cur = parent; - } - for (auto it = rev.rbegin(); it != rev.rend(); ++it) - path.push_back(*it); - - return path; -} - -bool MissionEventsDialogModel::isDefaultAnnotation(const event_annotation& ea) -{ - const bool noNote = ea.comment.empty(); - const bool noColor = (ea.r == 255 && ea.g == 255 && ea.b == 255); - return noNote && noColor; -} - -IEventTreeOps::Handle MissionEventsDialogModel::resolveHandleFromPath(const SCP_list& path) const -{ - if (path.empty()) - return nullptr; - const int origEvt = path.front(); - const int formula = findFormulaByOriginalEventIndex(origEvt); - if (formula < 0) - return nullptr; - - auto h = m_event_tree_ops.get_root_by_formula(formula); - auto it = path.begin(); - ++it; // skip event index - for (; it != path.end() && h; ++it) { - const int childIdx = *it; - if (childIdx < 0 || childIdx >= m_event_tree_ops.child_count(h)) - return nullptr; - h = m_event_tree_ops.child_at(h, childIdx); - } - return h; -} - -event_annotation& MissionEventsDialogModel::ensureAnnotationByPath(const SCP_list& path) -{ - for (auto& ea : m_event_annotations) - if (ea.path == path) - return ea; - event_annotation ea{}; - ea.path = path; - m_event_annotations.push_back(ea); - return m_event_annotations.back(); -} - -void MissionEventsDialogModel::initializeTeamList() -{ - m_team_list.clear(); - m_team_list.emplace_back("", -1); - for (auto& team : Mission_event_teams_tvt) { - m_team_list.emplace_back(team.first, team.second); - } -} - -mission_event MissionEventsDialogModel::makeDefaultEvent() -{ - mission_event e{}; - e.name = "Event name"; - e.formula = -1; - // Seems like most initializers are handled by the sexp_tree widget... This is so messy - - return e; -} - -void MissionEventsDialogModel::applyAnnotations() -{ - // Recompute paths from whatever we currently have - for (auto& ea : m_event_annotations) { - // Prefer live handle if still valid - if (ea.handle && m_event_tree_ops.is_handle_valid(ea.handle)) { - ea.path = buildPathForHandle(ea.handle); - } else { - // If we lost the handle, try to resolve from the old path - auto h = resolveHandleFromPath(ea.path); - if (h) { - ea.handle = h; // refresh cache for the rest of the session - ea.path = buildPathForHandle(h); // normalize - } else { - // Node is gone; mark default so we prune - ea.comment.clear(); - ea.r = ea.g = ea.b = 255; - ea.handle = nullptr; - ea.path.clear(); - } - } - // Drop the handle - ea.handle = nullptr; - } - - // Prune defaults - m_event_annotations.erase(std::remove_if(m_event_annotations.begin(), m_event_annotations.end(), [](const event_annotation& ea) { return isDefaultAnnotation(ea); }), m_event_annotations.end()); - - // Apply - Event_annotations = m_event_annotations; -} - - -void MissionEventsDialogModel::initializeMessages() -{ - int num_messages = Num_messages - Num_builtin_messages; - m_messages.clear(); - m_messages.reserve(num_messages); - for (auto i = 0; i < num_messages; i++) { - auto msg = Messages[i + Num_builtin_messages]; - m_messages.push_back(msg); - if (m_messages[i].avi_info.name) { - m_messages[i].avi_info.name = strdup(m_messages[i].avi_info.name); - } - if (m_messages[i].wave_info.name) { - m_messages[i].wave_info.name = strdup(m_messages[i].wave_info.name); - } - } - - if (Num_messages > Num_builtin_messages) { - setCurrentlySelectedMessage(0); - } else { - setCurrentlySelectedMessage(-1); - } -} - -void MissionEventsDialogModel::initializeHeadAniList() -{ - m_head_ani_list.clear(); - m_head_ani_list.emplace_back(""); - - if (!Disable_hc_message_ani) { - m_head_ani_list.emplace_back("Head-TP2"); - m_head_ani_list.emplace_back("Head-TP3"); - m_head_ani_list.emplace_back("Head-TP4"); - m_head_ani_list.emplace_back("Head-TP5"); - m_head_ani_list.emplace_back("Head-TP6"); - m_head_ani_list.emplace_back("Head-TP7"); - m_head_ani_list.emplace_back("Head-TP8"); - m_head_ani_list.emplace_back("Head-VP1"); - m_head_ani_list.emplace_back("Head-VP2"); - m_head_ani_list.emplace_back("Head-CM1"); - m_head_ani_list.emplace_back("Head-CM2"); - m_head_ani_list.emplace_back("Head-CM3"); - m_head_ani_list.emplace_back("Head-CM4"); - m_head_ani_list.emplace_back("Head-CM5"); - m_head_ani_list.emplace_back("Head-VC"); - m_head_ani_list.emplace_back("Head-VC2"); - m_head_ani_list.emplace_back("Head-BSH"); - } - - for (auto& thisHead : Custom_head_anis) { - m_head_ani_list.emplace_back(thisHead); - } - - for (auto& msg : m_messages) { - if (msg.avi_info.name) { - auto it = std::find(m_head_ani_list.begin(), m_head_ani_list.end(), msg.avi_info.name); - if (it == m_head_ani_list.end()) { - m_head_ani_list.emplace_back(msg.avi_info.name); - } - } - } -} - -void MissionEventsDialogModel::initializeWaveList() -{ - m_wave_list.clear(); - m_wave_list.emplace_back(""); - - // Use the main Message vector so we also get the builtins? - for (auto i = 0; i < Num_messages; i++) { - if (Messages[i].wave_info.name) { - auto it = std::find(m_wave_list.begin(), m_wave_list.end(), Messages[i].wave_info.name); - if (it == m_wave_list.end()) { - m_wave_list.emplace_back(Messages[i].wave_info.name); - } - } - } -} - -void MissionEventsDialogModel::initializePersonaList() -{ - m_persona_list.clear(); - m_persona_list.emplace_back("", -1); - for (int i = 0; i < static_cast(Personas.size()); ++i) { - auto& persona = Personas[i]; - m_persona_list.emplace_back(persona.name, i); - } -} - -bool MissionEventsDialogModel::checkMessageNameConflict(const SCP_string& name) -{ - // Validate against builtin messages - for (auto i = 0; i < Num_builtin_messages; i++) { - if (!stricmp(name.c_str(), Messages[i].name)) { - _viewport->dialogProvider->showButtonDialog(DialogType::Warning, - "Invalid Message Name", - "Message name cannot be the same as a builtin message name!", - {DialogButton::Ok}); - return true; - break; - } - } - - // Validate against existing messages - for (auto i = 0; i < static_cast(m_messages.size()); i++) { - if ((i != m_cur_msg) && (!stricmp(name.c_str(), m_messages[i].name))) { - _viewport->dialogProvider->showButtonDialog(DialogType::Warning, - "Invalid Message Name", - "Message name cannot be the same another message!", - {DialogButton::Ok}); - return true; - break; - } - } - - return false; -} - -SCP_string MissionEventsDialogModel::makeUniqueMessageName(const SCP_string& base) const -{ - const int maxLen = NAME_LENGTH - 1; - - auto exists = [&](const SCP_string& cand) -> bool { - for (const auto& m : m_messages) { - if (m.name[0] != '\0' && stricmp(m.name, cand.c_str()) == 0) { - return true; - } - } - return false; - }; - - // Try base, then base + " 1", base + " 2", ... - for (int n = 0;; ++n) { - SCP_string suffix = (n == 0) ? "" : (" " + std::to_string(n)); - const size_t avail = (maxLen > static_cast(suffix.size())) - ? static_cast(maxLen - static_cast(suffix.size())) - : 0u; - SCP_string head = base.substr(0, avail); - SCP_string cand = head + suffix; - if (!exists(cand)) - return cand; - } -} - -bool MissionEventsDialogModel::eventIsValid() const -{ - return SCP_vector_inbounds(m_events, m_cur_event); -} - -bool MissionEventsDialogModel::messageIsValid() const -{ - return SCP_vector_inbounds(m_messages, m_cur_msg); -} - -void MissionEventsDialogModel::setCurrentlySelectedEvent(int event) -{ - m_cur_event = event; -} - -void MissionEventsDialogModel::setCurrentlySelectedEventByFormula(int formula) -{ - for (auto i = 0; i < static_cast(m_events.size()); i++) { - if (m_events[i].formula == formula) { - setCurrentlySelectedEvent(i); - return; - } - } -} - -int MissionEventsDialogModel::getCurrentlySelectedEvent() const -{ - return m_cur_event; -} - -SCP_vector& MissionEventsDialogModel::getEventList() -{ - return m_events; -} - -void MissionEventsDialogModel::deleteRootNode(int node) -{ - int i; - for (i = 0; i < static_cast(m_events.size()); i++) { - if (m_events[i].formula == node) { - break; - } - } - - Assertion(i < static_cast(m_events.size()), "Attempt to delete an invalid event!"); - m_events.erase(m_events.begin() + i); - m_sig.erase(m_sig.begin() + i); - - if (i >= static_cast(m_events.size())) // if we have deleted the last event, - i--; // i will be set to -1 which is what we want - - setCurrentlySelectedEvent(i); - set_modified(); -} - -void MissionEventsDialogModel::renameRootNode(int node, const SCP_string& name) -{ - int i; - for (i = 0; i < static_cast(m_events.size()); i++) { - if (m_events[i].formula == node) { - break; - } - } - Assertion(i < static_cast(m_events.size()), "Attempt to rename an invalid event!"); - m_events[i].name = name; - set_modified(); -} - -void MissionEventsDialogModel::changeRootNodeFormula(int old, int node) -{ - int i; - for (i = 0; i < static_cast(m_events.size()); i++) { - if (m_events[i].formula == old) { - break; - } - } - - Assertion(i < static_cast(m_events.size()), "Attempt to modify invalid event!"); - m_events[i].formula = node; - set_modified(); -} - -void MissionEventsDialogModel::reorderByRootFormulaOrder(const SCP_vector& newOrderedFormulas) -{ - // Basic sanity: must be a 1:1 permutation of current roots - if (newOrderedFormulas.size() != m_events.size()) - return; - - // Build the permuted arrays (O(n^2) is fine for event counts) - SCP_vector newEvents; - SCP_vector newSig; - newEvents.reserve(m_events.size()); - newSig.reserve(m_sig.size()); - - for (int formula : newOrderedFormulas) { - int oldIdx = -1; - for (int i = 0; i < static_cast(m_events.size()); ++i) { - if (m_events[i].formula == formula) { - oldIdx = i; - break; - } - } - if (oldIdx < 0) { - // Unknown formula; bail without mutating state - return; - } - newEvents.push_back(m_events[oldIdx]); - if (SCP_vector_inbounds(m_sig, oldIdx)) { - newSig.push_back(m_sig[oldIdx]); - } - } - - // Swap in the new order - m_events.swap(newEvents); - m_sig.swap(newSig); - - // Keep selection reasonable (select the first event after reorder) - setCurrentlySelectedEvent(m_events.empty() ? -1 : 0); - - // Rebuild applied annotations against new handles/order if needed - initializeEventAnnotations(); - - set_modified(); -} - -void MissionEventsDialogModel::setCurrentlySelectedMessage(int msg) -{ - m_cur_msg = msg; - - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; -} - -int MissionEventsDialogModel::getCurrentlySelectedMessage() const -{ - return m_cur_msg; -} - -const SCP_vector& MissionEventsDialogModel::getHeadAniList() -{ - return m_head_ani_list; -} - -const SCP_vector& MissionEventsDialogModel::getWaveList() -{ - return m_wave_list; -} - -const SCP_vector>& MissionEventsDialogModel::getPersonaList() -{ - return m_persona_list; -} - -const SCP_vector>& MissionEventsDialogModel::getTeamList() -{ - return m_team_list; -} - -void MissionEventsDialogModel::createEvent() -{ - m_events.emplace_back(makeDefaultEvent()); - m_sig.push_back(-1); - - auto& event = m_events.back(); - - const int after = event.formula; - event.formula = m_event_tree_ops.build_default_root(event.name, after); - m_event_tree_ops.select_root(event.formula); - - setCurrentlySelectedEventByFormula(event.formula); - set_modified(); -} - -void MissionEventsDialogModel::insertEvent() -{ - if (m_cur_event < 0 || m_events.empty()) { - createEvent(); - return; - } - - const int pos = m_cur_event; // Can shift during tree ops so save our position now - m_events.insert(m_events.begin() + pos, makeDefaultEvent()); - m_sig.insert(m_sig.begin() + pos, -1); - auto& event = m_events[pos]; - - // Place after the previous root if it exists and is valid; otherwise we’ll fix index explicitly - int after = (pos > 0 && m_events[pos - 1].formula >= 0) ? m_events[pos - 1].formula : -1; - - event.formula = m_event_tree_ops.build_default_root(event.name, after); - - if (pos == 0) { - m_event_tree_ops.ensure_top_level_index(event.formula, 0); - } - m_event_tree_ops.select_root(event.formula); - - setCurrentlySelectedEventByFormula(event.formula); - set_modified(); -} - -void MissionEventsDialogModel::deleteEvent() -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - - m_event_tree_ops.delete_event(); -} - -void MissionEventsDialogModel::renameEvent(int id, const SCP_string& name) -{ - // Find by formula id first; fallback to treating id as an index if you want. - int idx = -1; - for (int i = 0; i < static_cast(m_events.size()); ++i) { - if (m_events[i].formula == id) { - idx = i; - break; - } - } - if (idx == -1 && id >= 0 && id < static_cast(m_events.size())) - idx = id; - if (idx < 0) - return; - - // Normalize to engine expectations - SCP_string normalized = name.empty() ? SCP_string("") : name; - SCP_truncate(normalized, NAME_LENGTH - 1); - - modify(m_events[idx].name, normalized); -} - -int MissionEventsDialogModel::getFormula() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return -1; - } - return m_events[m_cur_event].formula; -} - -void MissionEventsDialogModel::setFormula(int node) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - - auto& event = m_events[m_cur_event]; - modify(event.formula, node); -} - -int MissionEventsDialogModel::getRepeatCount() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return 1; - } - return m_events[m_cur_event].repeat_count; -} - -void MissionEventsDialogModel::setRepeatCount(int count) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (count < -1) { - count = -1; - } - modify(event.repeat_count, count); -} - -int MissionEventsDialogModel::getTriggerCount() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return 0; - } - return m_events[m_cur_event].trigger_count; -} - -void MissionEventsDialogModel::setTriggerCount(int count) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (count < -1) { - count = -1; - } - modify(event.trigger_count, count); -} - -int MissionEventsDialogModel::getIntervalTime() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return 0; - } - return m_events[m_cur_event].interval; -} - -void MissionEventsDialogModel::setIntervalTime(int time) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (time < 0) { - time = 0; - } - modify(event.interval, time); -} - -bool MissionEventsDialogModel::getChained() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].chain_delay >= 0); -} - -void MissionEventsDialogModel::setChained(bool chained) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (chained) { - modify(event.chain_delay, 0); - } else { - modify(event.chain_delay, -1); - } -} - -int MissionEventsDialogModel::getChainDelay() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return -1; - } - return m_events[m_cur_event].chain_delay; -} - -void MissionEventsDialogModel::setChainDelay(int delay) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (delay < 0) { - delay = 0; - } - modify(event.chain_delay, delay); -} - -int MissionEventsDialogModel::getEventScore() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return 0; - } - return m_events[m_cur_event].score; -} - -void MissionEventsDialogModel::setEventScore(int score) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - modify(event.score, score); -} - -int MissionEventsDialogModel::getEventTeam() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return -1; - } - return m_events[m_cur_event].team; -} - -void MissionEventsDialogModel::setEventTeam(int team) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - - auto& event = m_events[m_cur_event]; - - if (team < -1 || team >= MAX_TVT_TEAMS) { - team = -1; - } - modify(event.team, team); -} - -SCP_string MissionEventsDialogModel::getEventDirectiveText() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return ""; - } - return m_events[m_cur_event].objective_text; -} - -void MissionEventsDialogModel::setEventDirectiveText(const SCP_string& text) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - modify(event.objective_text, text); - lcl_fred_replace_stuff(event.objective_text); -} - -SCP_string MissionEventsDialogModel::getEventDirectiveKeyText() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return ""; - } - return m_events[m_cur_event].objective_key_text; -} - -void MissionEventsDialogModel::setEventDirectiveKeyText(const SCP_string& text) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - modify(event.objective_key_text, text); - lcl_fred_replace_stuff(event.objective_key_text); -} - -bool MissionEventsDialogModel::getLogTrue() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_TRUE) != 0; -} - -void MissionEventsDialogModel::setLogTrue(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_SEXP_TRUE; - } else { - event.mission_log_flags &= ~MLF_SEXP_TRUE; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogFalse() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_FALSE) != 0; -} - -void MissionEventsDialogModel::setLogFalse(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_SEXP_FALSE; - } else { - event.mission_log_flags &= ~MLF_SEXP_FALSE; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogLogPrevious() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_STATE_CHANGE) != 0; -} - -void MissionEventsDialogModel::setLogLogPrevious(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_STATE_CHANGE; - } else { - event.mission_log_flags &= ~MLF_STATE_CHANGE; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogAlwaysFalse() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_KNOWN_FALSE) != 0; -} - -void MissionEventsDialogModel::setLogAlwaysFalse(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_SEXP_KNOWN_FALSE; - } else { - event.mission_log_flags &= ~MLF_SEXP_KNOWN_FALSE; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogFirstRepeat() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_REPEAT_ONLY) != 0; -} - -void MissionEventsDialogModel::setLogFirstRepeat(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_FIRST_REPEAT_ONLY; - } else { - event.mission_log_flags &= ~MLF_FIRST_REPEAT_ONLY; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogLastRepeat() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_LAST_REPEAT_ONLY) != 0; -} - -void MissionEventsDialogModel::setLogLastRepeat(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_LAST_REPEAT_ONLY; - } else { - event.mission_log_flags &= ~MLF_LAST_REPEAT_ONLY; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogFirstTrigger() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_TRIGGER_ONLY) != 0; -} - -void MissionEventsDialogModel::setLogFirstTrigger(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_FIRST_TRIGGER_ONLY; - } else { - event.mission_log_flags &= ~MLF_FIRST_TRIGGER_ONLY; - } - set_modified(); -} - -bool MissionEventsDialogModel::getLogLastTrigger() const -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return false; - } - return (m_events[m_cur_event].mission_log_flags & MLF_LAST_TRIGGER_ONLY) != 0; -} - -void MissionEventsDialogModel::setLogLastTrigger(bool log) -{ - if (!SCP_vector_inbounds(m_events, m_cur_event)) { - return; - } - auto& event = m_events[m_cur_event]; - if (log) { - event.mission_log_flags |= MLF_LAST_TRIGGER_ONLY; - } else { - event.mission_log_flags &= ~MLF_LAST_TRIGGER_ONLY; - } - set_modified(); -} - -void MissionEventsDialogModel::setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note) -{ - auto path = buildPathForHandle(h); - auto& ea = ensureAnnotationByPath(path); - ea.handle = h; - ea.comment = note; - m_event_tree_ops.set_node_note(h, note); - set_modified(); -} - -void MissionEventsDialogModel::setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color) -{ - auto path = buildPathForHandle(h); - auto& ea = ensureAnnotationByPath(path); - ea.handle = h; - if (has_color) { - ea.r = (ubyte)r; - ea.g = (ubyte)g; - ea.b = (ubyte)b; - } else { - ea.r = ea.g = ea.b = 255; - } - m_event_tree_ops.set_node_bg_color(h, r, g, b, has_color); - set_modified(); -} - -void MissionEventsDialogModel::createMessage() -{ - MMessage msg; - - const SCP_string base = ""; - const SCP_string unique = makeUniqueMessageName(base); - - strcpy_s(msg.name, unique.c_str()); - strcpy_s(msg.message, ""); - msg.avi_info.name = nullptr; - msg.wave_info.name = nullptr; - msg.persona_index = -1; - msg.multi_team = -1; - m_messages.push_back(msg); - auto id = static_cast(m_messages.size()) - 1; - - setCurrentlySelectedMessage(id); - - set_modified(); -} - -void MissionEventsDialogModel::insertMessage() -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - createMessage(); - return; - } - - MMessage msg; - const SCP_string base = ""; - const SCP_string unique = makeUniqueMessageName(base); - - strcpy_s(msg.name, unique.c_str()); - strcpy_s(msg.message, ""); - msg.avi_info.name = nullptr; - msg.wave_info.name = nullptr; - msg.persona_index = -1; - msg.multi_team = -1; - - // Insert at requested position - m_messages.insert(m_messages.begin() + m_cur_msg, msg); - - // Select the new message - setCurrentlySelectedMessage(m_cur_msg); - - set_modified(); -} - -void MissionEventsDialogModel::deleteMessage() -{ - // handle this case somewhat gracefully - Assertion(SCP_vector_inbounds(m_messages, m_cur_msg), - "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", - m_cur_msg, - static_cast(m_messages.size()) - 1); - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - if (m_messages[m_cur_msg].wave_info.name) { - free(m_messages[m_cur_msg].wave_info.name); - m_messages[m_cur_msg].wave_info.name = nullptr; - } - - SCP_string buf = "<" + SCP_string(m_messages[m_cur_msg].name) + ">"; - update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE); - update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE_OR_STRING); - - m_messages.erase(m_messages.begin() + m_cur_msg); - - if (m_cur_msg >= static_cast(m_messages.size())) { - m_cur_msg = static_cast(m_messages.size()) - 1; - } - - setCurrentlySelectedMessage(m_cur_msg); - - set_modified(); -} - -void MissionEventsDialogModel::moveMessageUp() -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg == 0) - return; - - std::swap(m_messages[m_cur_msg - 1], m_messages[m_cur_msg]); - setCurrentlySelectedMessage(m_cur_msg - 1); - set_modified(); -} - -void MissionEventsDialogModel::moveMessageDown() -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg >= static_cast(m_messages.size()) - 1) - return; - - std::swap(m_messages[m_cur_msg + 1], m_messages[m_cur_msg]); - setCurrentlySelectedMessage(m_cur_msg + 1); - set_modified(); -} - - -SCP_string MissionEventsDialogModel::getMessageName() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return ""; - } - return m_messages[m_cur_msg].name; -} - -void MissionEventsDialogModel::setMessageName(const SCP_string& name) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - - if (!checkMessageNameConflict(name)) { - strncpy(msg.name, name.c_str(), NAME_LENGTH - 1); - set_modified(); - } -} - -SCP_string MissionEventsDialogModel::getMessageText() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return ""; - } - return m_messages[m_cur_msg].message; -} - -void MissionEventsDialogModel::setMessageText(const SCP_string& text) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - - strncpy(msg.message, text.c_str(), MESSAGE_LENGTH - 1); - lcl_fred_replace_stuff(msg.message, MESSAGE_LENGTH - 1); - - set_modified(); -} - -SCP_string MissionEventsDialogModel::getMessageNote() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return ""; - } - return m_messages[m_cur_msg].note; -} - -void MissionEventsDialogModel::setMessageNote(const SCP_string& note) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - - modify(msg.note, note); -} - -SCP_string MissionEventsDialogModel::getMessageAni() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return ""; - } - auto& msg = m_messages[m_cur_msg]; - return msg.avi_info.name ? SCP_string(msg.avi_info.name) : SCP_string(""); -} - -void MissionEventsDialogModel::setMessageAni(const SCP_string& ani) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - const char* cur = msg.avi_info.name; - const SCP_string curStr = cur ? cur : ""; - - // Treat empty, "none", "" as no avi and store nullptr - const bool isNone = ani.empty() || lcase_equal(ani, "") || lcase_equal(ani, "none"); - if (isNone) { - if (cur != nullptr) { // only do work if changing something - free(msg.avi_info.name); - msg.avi_info.name = nullptr; - set_modified(); - } - return; - } - - // No change? bail - if (cur && curStr == ani) { - return; - } - - // Replace value - if (cur) - free(msg.avi_info.name); - msg.avi_info.name = strdup(ani.c_str()); - set_modified(); - - // Possibly add to list of known anis - auto it = std::find_if(m_head_ani_list.begin(), m_head_ani_list.end(), [&](const SCP_string& s) { - return lcase_equal(s, ani); - }); - if (it == m_head_ani_list.end()) { - m_head_ani_list.emplace_back(ani); - } -} - -SCP_string MissionEventsDialogModel::getMessageWave() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return ""; - } - auto& msg = m_messages[m_cur_msg]; - return msg.wave_info.name ? SCP_string(msg.wave_info.name) : SCP_string(""); -} - -void MissionEventsDialogModel::setMessageWave(const SCP_string& wave) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - auto& msg = m_messages[m_cur_msg]; - const char* cur = msg.wave_info.name; - const SCP_string curStr = cur ? cur : ""; - - // Treat empty, "none", "" as no avi and store nullptr - const bool isNone = wave.empty() || lcase_equal(wave, "") || lcase_equal(wave, "none"); - if (isNone) { - if (cur != nullptr) { // only do work if changing something - free(msg.wave_info.name); - msg.wave_info.name = nullptr; - set_modified(); - } - return; - } - - // No change? bail - if (cur && curStr == wave) { - return; - } - - // Replace value - if (cur) - free(msg.wave_info.name); - msg.wave_info.name = strdup(wave.c_str()); - set_modified(); - - autoSelectPersona(); -} - -int MissionEventsDialogModel::getMessagePersona() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return -1; - } - auto& msg = m_messages[m_cur_msg]; - if (SCP_vector_inbounds(Personas, msg.persona_index)) { - return msg.persona_index; - } - return -1; -} - -void MissionEventsDialogModel::setMessagePersona(int persona) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - - msg.persona_index = persona; - set_modified(); -} - -int MissionEventsDialogModel::getMessageTeam() const -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return -1; - } - auto& msg = m_messages[m_cur_msg]; - if (msg.multi_team < 0 || msg.multi_team >= MAX_TVT_TEAMS) { - return -1; - } - return msg.multi_team; -} - -void MissionEventsDialogModel::setMessageTeam(int team) -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - auto& msg = m_messages[m_cur_msg]; - - if (team >= MAX_TVT_TEAMS) { - msg.multi_team = -1; - } else { - msg.multi_team = team; - } - set_modified(); -} - -void MissionEventsDialogModel::autoSelectPersona() -{ - // I hate everything about this function outside of retail but someone will complain - // if I omit this "feature"... - - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - SCP_string wave_name = m_messages[m_cur_msg].wave_info.name ? m_messages[m_cur_msg].wave_info.name : ""; - SCP_string avi_name = m_messages[m_cur_msg].avi_info.name ? m_messages[m_cur_msg].avi_info.name : ""; - - if ((wave_name[0] >= '1') && (wave_name[0] <= '9') && (wave_name[1] == '_')) { - auto i = wave_name[0] - '1'; - if ((i < static_cast(Personas.size())) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) { - modify(m_messages[m_cur_msg].persona_index, i); - if (i == 0 || i == 1) { - avi_name = "HEAD-TP1"; - } else if (i == 2 || i == 3) { - avi_name = "HEAD-TP2"; - } else if (i == 4) { - avi_name = "HEAD-TP3"; - } else if (i == 5) { - avi_name = "HEAD-VP1"; - } - } - } else { - auto mask = 0; - if (!strnicmp(wave_name.c_str(), "S_", 2)) { - mask = PERSONA_FLAG_SUPPORT; - avi_name = "HEAD-CM1"; - } else if (!strnicmp(wave_name.c_str(), "L_", 2)) { - mask = PERSONA_FLAG_LARGE; - avi_name = "HEAD-CM1"; - } else if (!strnicmp(wave_name.c_str(), "TC_", 3)) { - mask = PERSONA_FLAG_COMMAND; - avi_name = "HEAD-CM1"; - } - - for (auto i = 0; i < (static_cast(Personas.size())); i++) { - if (Personas[i].flags & mask) { - modify(m_messages[m_cur_msg].persona_index, i); - } - } - } - - SCP_string original_avi_name = avi_name; - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - m_messages[m_cur_msg].avi_info.name = strdup(avi_name.c_str()); - - if (original_avi_name != avi_name) { - set_modified(); - } -} - -void MissionEventsDialogModel::playMessageWave() -{ - if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { - return; - } - - //audiostream_close_file(m_wave_id, false); - - auto& msg = m_messages[m_cur_msg]; - - if (msg.wave_info.name) { - m_wave_id = audiostream_open(msg.wave_info.name, ASF_VOICE); - if (m_wave_id >= 0) { - audiostream_play(m_wave_id, 1.0f, 0); - } - } -} - -const SCP_vector& MissionEventsDialogModel::getMessageList() const -{ - return m_messages; -} - -bool MissionEventsDialogModel::getMissionIsMultiTeam() -{ - return The_mission.game_type & MISSION_TYPE_MULTI_TEAMS; -} - -void MissionEventsDialogModel::setModified() { - set_modified(); -} - +#include "MissionEventsDialogModel.h" + +#include +#include + +namespace fso::fred::dialogs { + +MissionEventsDialogModel::MissionEventsDialogModel(QObject* parent, fso::fred::EditorViewport* viewport, IEventTreeOps& tree_ops) + : AbstractDialogModel(parent, viewport), m_event_tree_ops(tree_ops) +{ + initializeData(); +} + +bool MissionEventsDialogModel::apply() +{ + SCP_vector> names; + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + for (auto& event : Mission_events) { + free_sexp2(event.formula); + event.result = 0; // use this as a processed flag + } + + // rename all sexp references to old events + for (int i = 0; i < static_cast(m_events.size()); i++) { + if (m_sig[i] >= 0) { + names.emplace_back(Mission_events[m_sig[i]].name, m_events[i].name); + Mission_events[m_sig[i]].result = 1; + } + } + + // invalidate all sexp references to deleted events. + for (const auto& event : Mission_events) { + if (!event.result) { + SCP_string buf = "<" + event.name + ">"; + + // force it to not be too long + if (SCP_truncate(buf, NAME_LENGTH - 1)) + buf.back() = '>'; + + names.emplace_back(event.name, buf); + } + } + + // copy all dialog events to the mission + Mission_events.clear(); + for (const auto& dialog_event : m_events) { + Mission_events.push_back(dialog_event); + Mission_events.back().formula = m_event_tree_ops.save_tree(dialog_event.formula); + } + + // now update all sexp references + for (const auto& name_pair : names) + update_sexp_references(name_pair.first.c_str(), name_pair.second.c_str(), OPF_EVENT_NAME); + + for (int i = Num_builtin_messages; i < Num_messages; i++) { + if (Messages[i].avi_info.name) + free(Messages[i].avi_info.name); + + if (Messages[i].wave_info.name) + free(Messages[i].wave_info.name); + } + + Num_messages = static_cast(m_messages.size()) + Num_builtin_messages; + Messages.resize(Num_messages); + for (int i = 0; i < static_cast(m_messages.size()); i++) + Messages[i + Num_builtin_messages] = m_messages[i]; + + applyAnnotations(); + + // Only fire the signal after the changes have been applied to make sure the other parts of the code see the updated + // state + if (query_modified()) { + _editor->missionChanged(); + } + return true; +} + +void MissionEventsDialogModel::reject() +{ + // Nothing to do here +} + +void MissionEventsDialogModel::initializeData() +{ + initializeMessages(); + initializeHeadAniList(); + initializeWaveList(); + initializePersonaList(); + + initializeTeamList(); + initializeEvents(); +} + +void MissionEventsDialogModel::initializeEvents() +{ + m_events.clear(); + m_sig.clear(); + m_cur_event = -1; + for (auto i = 0; i < static_cast(Mission_events.size()); i++) { + m_events.push_back(Mission_events[i]); + m_sig.push_back(i); + + if (m_events[i].name.empty()) { + m_events[i].name = ""; + } + + m_events[i].formula = m_event_tree_ops.load_sub_tree(Mission_events[i].formula, false, "do-nothing"); + + // we must check for the case of the repeat count being 0. This would happen if the repeat + // count is not specified in a mission + if (m_events[i].repeat_count <= 0) { + m_events[i].repeat_count = 1; + } + } + + m_event_tree_ops.post_load(); + + m_event_tree_ops.clear(); + for (auto& event : m_events) { + // set the proper bitmap + NodeImage image; + if (event.chain_delay >= 0) { + image = NodeImage::CHAIN; + if (!event.objective_text.empty()) { + image = NodeImage::CHAIN_DIRECTIVE; + } + } else { + image = NodeImage::ROOT; + if (!event.objective_text.empty()) { + image = NodeImage::ROOT_DIRECTIVE; + } + } + + m_event_tree_ops.add_sub_tree(event.name, image, event.formula); + } + + initializeEventAnnotations(); +} + +int MissionEventsDialogModel::findFormulaByOriginalEventIndex(int orig) const +{ + for (int cur = 0; cur < static_cast(m_sig.size()); ++cur) + if (m_sig[cur] == orig) + return m_events[cur].formula; + return -1; +} + +void MissionEventsDialogModel::initializeEventAnnotations() +{ + m_event_annotations = Event_annotations; // copy + + for (auto& ea : m_event_annotations) { + ea.handle = nullptr; + if (ea.path.empty()) + continue; + + const int origIdx = ea.path.front(); + const int formula = findFormulaByOriginalEventIndex(origIdx); + if (formula < 0) + continue; + + IEventTreeOps::Handle h = m_event_tree_ops.get_root_by_formula(formula); + if (!h) + continue; + + // walk children + auto it = ea.path.begin(); + ++it; // skip event index + for (; it != ea.path.end() && h; ++it) { + const int child = *it; + if (child < 0 || child >= m_event_tree_ops.child_count(h)) { + h = nullptr; + break; + } + h = m_event_tree_ops.child_at(h, child); + } + + ea.handle = h; + if (h) { + const bool hasColor = (ea.r != 255) || (ea.g != 255) || (ea.b != 255); + m_event_tree_ops.set_node_note(h, ea.comment); + m_event_tree_ops.set_node_bg_color(h, ea.r, ea.g, ea.b, hasColor); + } + } +} + +// Build the path for a handle (root formula -> orig index; then child indices) +SCP_list MissionEventsDialogModel::buildPathForHandle(IEventTreeOps::Handle h) const +{ + SCP_list path; + if (!h) + return path; + + const int rootFormula = m_event_tree_ops.root_formula_of(h); + if (rootFormula < 0) + return path; + + // Find the *current* index of this root in m_events + int curIdx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == rootFormula) { + curIdx = i; + break; + } + } + if (curIdx < 0) + return path; + + // persist the current index + path.push_back(curIdx); + + // Collect child indices from node up to root, then reverse + std::vector rev; + IEventTreeOps::Handle cur = h; + for (;;) { + IEventTreeOps::Handle parent = m_event_tree_ops.parent_of(cur); + if (!parent) + break; + rev.push_back(m_event_tree_ops.index_in_parent(cur)); + cur = parent; + } + for (auto it = rev.rbegin(); it != rev.rend(); ++it) + path.push_back(*it); + + return path; +} + +bool MissionEventsDialogModel::isDefaultAnnotation(const event_annotation& ea) +{ + const bool noNote = ea.comment.empty(); + const bool noColor = (ea.r == 255 && ea.g == 255 && ea.b == 255); + return noNote && noColor; +} + +IEventTreeOps::Handle MissionEventsDialogModel::resolveHandleFromPath(const SCP_list& path) const +{ + if (path.empty()) + return nullptr; + const int origEvt = path.front(); + const int formula = findFormulaByOriginalEventIndex(origEvt); + if (formula < 0) + return nullptr; + + auto h = m_event_tree_ops.get_root_by_formula(formula); + auto it = path.begin(); + ++it; // skip event index + for (; it != path.end() && h; ++it) { + const int childIdx = *it; + if (childIdx < 0 || childIdx >= m_event_tree_ops.child_count(h)) + return nullptr; + h = m_event_tree_ops.child_at(h, childIdx); + } + return h; +} + +event_annotation& MissionEventsDialogModel::ensureAnnotationByPath(const SCP_list& path) +{ + for (auto& ea : m_event_annotations) + if (ea.path == path) + return ea; + event_annotation ea{}; + ea.path = path; + m_event_annotations.push_back(ea); + return m_event_annotations.back(); +} + +void MissionEventsDialogModel::initializeTeamList() +{ + m_team_list.clear(); + m_team_list.emplace_back("", -1); + for (auto& team : Mission_event_teams_tvt) { + m_team_list.emplace_back(team.first, team.second); + } +} + +mission_event MissionEventsDialogModel::makeDefaultEvent() +{ + mission_event e{}; + e.name = "Event name"; + e.formula = -1; + // Seems like most initializers are handled by the sexp_tree widget... This is so messy + + return e; +} + +void MissionEventsDialogModel::applyAnnotations() +{ + // Recompute paths from whatever we currently have + for (auto& ea : m_event_annotations) { + // Prefer live handle if still valid + if (ea.handle && m_event_tree_ops.is_handle_valid(ea.handle)) { + ea.path = buildPathForHandle(ea.handle); + } else { + // If we lost the handle, try to resolve from the old path + auto h = resolveHandleFromPath(ea.path); + if (h) { + ea.handle = h; // refresh cache for the rest of the session + ea.path = buildPathForHandle(h); // normalize + } else { + // Node is gone; mark default so we prune + ea.comment.clear(); + ea.r = ea.g = ea.b = 255; + ea.handle = nullptr; + ea.path.clear(); + } + } + // Drop the handle + ea.handle = nullptr; + } + + // Prune defaults + m_event_annotations.erase(std::remove_if(m_event_annotations.begin(), m_event_annotations.end(), [](const event_annotation& ea) { return isDefaultAnnotation(ea); }), m_event_annotations.end()); + + // Apply + Event_annotations = m_event_annotations; +} + + +void MissionEventsDialogModel::initializeMessages() +{ + int num_messages = Num_messages - Num_builtin_messages; + m_messages.clear(); + m_messages.reserve(num_messages); + for (auto i = 0; i < num_messages; i++) { + auto msg = Messages[i + Num_builtin_messages]; + m_messages.push_back(msg); + if (m_messages[i].avi_info.name) { + m_messages[i].avi_info.name = strdup(m_messages[i].avi_info.name); + } + if (m_messages[i].wave_info.name) { + m_messages[i].wave_info.name = strdup(m_messages[i].wave_info.name); + } + } + + if (Num_messages > Num_builtin_messages) { + setCurrentlySelectedMessage(0); + } else { + setCurrentlySelectedMessage(-1); + } +} + +void MissionEventsDialogModel::initializeHeadAniList() +{ + m_head_ani_list.clear(); + m_head_ani_list.emplace_back(""); + + if (!Disable_hc_message_ani) { + m_head_ani_list.emplace_back("Head-TP2"); + m_head_ani_list.emplace_back("Head-TP3"); + m_head_ani_list.emplace_back("Head-TP4"); + m_head_ani_list.emplace_back("Head-TP5"); + m_head_ani_list.emplace_back("Head-TP6"); + m_head_ani_list.emplace_back("Head-TP7"); + m_head_ani_list.emplace_back("Head-TP8"); + m_head_ani_list.emplace_back("Head-VP1"); + m_head_ani_list.emplace_back("Head-VP2"); + m_head_ani_list.emplace_back("Head-CM1"); + m_head_ani_list.emplace_back("Head-CM2"); + m_head_ani_list.emplace_back("Head-CM3"); + m_head_ani_list.emplace_back("Head-CM4"); + m_head_ani_list.emplace_back("Head-CM5"); + m_head_ani_list.emplace_back("Head-VC"); + m_head_ani_list.emplace_back("Head-VC2"); + m_head_ani_list.emplace_back("Head-BSH"); + } + + for (auto& thisHead : Custom_head_anis) { + m_head_ani_list.emplace_back(thisHead); + } + + for (auto& msg : m_messages) { + if (msg.avi_info.name) { + auto it = std::find(m_head_ani_list.begin(), m_head_ani_list.end(), msg.avi_info.name); + if (it == m_head_ani_list.end()) { + m_head_ani_list.emplace_back(msg.avi_info.name); + } + } + } +} + +void MissionEventsDialogModel::initializeWaveList() +{ + m_wave_list.clear(); + m_wave_list.emplace_back(""); + + // Use the main Message vector so we also get the builtins? + for (auto i = 0; i < Num_messages; i++) { + if (Messages[i].wave_info.name) { + auto it = std::find(m_wave_list.begin(), m_wave_list.end(), Messages[i].wave_info.name); + if (it == m_wave_list.end()) { + m_wave_list.emplace_back(Messages[i].wave_info.name); + } + } + } +} + +void MissionEventsDialogModel::initializePersonaList() +{ + m_persona_list.clear(); + m_persona_list.emplace_back("", -1); + for (int i = 0; i < static_cast(Personas.size()); ++i) { + auto& persona = Personas[i]; + m_persona_list.emplace_back(persona.name, i); + } +} + +bool MissionEventsDialogModel::checkMessageNameConflict(const SCP_string& name) +{ + // Validate against builtin messages + for (auto i = 0; i < Num_builtin_messages; i++) { + if (!stricmp(name.c_str(), Messages[i].name)) { + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, + "Invalid Message Name", + "Message name cannot be the same as a builtin message name!", + {DialogButton::Ok}); + return true; + break; + } + } + + // Validate against existing messages + for (auto i = 0; i < static_cast(m_messages.size()); i++) { + if ((i != m_cur_msg) && (!stricmp(name.c_str(), m_messages[i].name))) { + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, + "Invalid Message Name", + "Message name cannot be the same another message!", + {DialogButton::Ok}); + return true; + break; + } + } + + return false; +} + +SCP_string MissionEventsDialogModel::makeUniqueMessageName(const SCP_string& base) const +{ + const int maxLen = NAME_LENGTH - 1; + + auto exists = [&](const SCP_string& cand) -> bool { + for (const auto& m : m_messages) { + if (m.name[0] != '\0' && stricmp(m.name, cand.c_str()) == 0) { + return true; + } + } + return false; + }; + + // Try base, then base + " 1", base + " 2", ... + for (int n = 0;; ++n) { + SCP_string suffix = (n == 0) ? "" : (" " + std::to_string(n)); + const size_t avail = (maxLen > static_cast(suffix.size())) + ? static_cast(maxLen - static_cast(suffix.size())) + : 0u; + SCP_string head = base.substr(0, avail); + SCP_string cand = head + suffix; + if (!exists(cand)) + return cand; + } +} + +bool MissionEventsDialogModel::eventIsValid() const +{ + return SCP_vector_inbounds(m_events, m_cur_event); +} + +bool MissionEventsDialogModel::messageIsValid() const +{ + return SCP_vector_inbounds(m_messages, m_cur_msg); +} + +void MissionEventsDialogModel::setCurrentlySelectedEvent(int event) +{ + m_cur_event = event; +} + +void MissionEventsDialogModel::setCurrentlySelectedEventByFormula(int formula) +{ + for (auto i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == formula) { + setCurrentlySelectedEvent(i); + return; + } + } +} + +int MissionEventsDialogModel::getCurrentlySelectedEvent() const +{ + return m_cur_event; +} + +SCP_vector& MissionEventsDialogModel::getEventList() +{ + return m_events; +} + +void MissionEventsDialogModel::deleteRootNode(int node) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == node) { + break; + } + } + + Assertion(i < static_cast(m_events.size()), "Attempt to delete an invalid event!"); + m_events.erase(m_events.begin() + i); + m_sig.erase(m_sig.begin() + i); + + if (i >= static_cast(m_events.size())) // if we have deleted the last event, + i--; // i will be set to -1 which is what we want + + setCurrentlySelectedEvent(i); + set_modified(); +} + +void MissionEventsDialogModel::renameRootNode(int node, const SCP_string& name) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == node) { + break; + } + } + Assertion(i < static_cast(m_events.size()), "Attempt to rename an invalid event!"); + m_events[i].name = name; + set_modified(); +} + +void MissionEventsDialogModel::changeRootNodeFormula(int old, int node) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == old) { + break; + } + } + + Assertion(i < static_cast(m_events.size()), "Attempt to modify invalid event!"); + m_events[i].formula = node; + set_modified(); +} + +void MissionEventsDialogModel::reorderByRootFormulaOrder(const SCP_vector& newOrderedFormulas) +{ + // Basic sanity: must be a 1:1 permutation of current roots + if (newOrderedFormulas.size() != m_events.size()) + return; + + // Build the permuted arrays (O(n^2) is fine for event counts) + SCP_vector newEvents; + SCP_vector newSig; + newEvents.reserve(m_events.size()); + newSig.reserve(m_sig.size()); + + for (int formula : newOrderedFormulas) { + int oldIdx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == formula) { + oldIdx = i; + break; + } + } + if (oldIdx < 0) { + // Unknown formula; bail without mutating state + return; + } + newEvents.push_back(m_events[oldIdx]); + if (SCP_vector_inbounds(m_sig, oldIdx)) { + newSig.push_back(m_sig[oldIdx]); + } + } + + // Swap in the new order + m_events.swap(newEvents); + m_sig.swap(newSig); + + // Keep selection reasonable (select the first event after reorder) + setCurrentlySelectedEvent(m_events.empty() ? -1 : 0); + + // Rebuild applied annotations against new handles/order if needed + initializeEventAnnotations(); + + set_modified(); +} + +void MissionEventsDialogModel::setCurrentlySelectedMessage(int msg) +{ + m_cur_msg = msg; + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; +} + +int MissionEventsDialogModel::getCurrentlySelectedMessage() const +{ + return m_cur_msg; +} + +const SCP_vector& MissionEventsDialogModel::getHeadAniList() +{ + return m_head_ani_list; +} + +const SCP_vector& MissionEventsDialogModel::getWaveList() +{ + return m_wave_list; +} + +const SCP_vector>& MissionEventsDialogModel::getPersonaList() +{ + return m_persona_list; +} + +const SCP_vector>& MissionEventsDialogModel::getTeamList() +{ + return m_team_list; +} + +void MissionEventsDialogModel::createEvent() +{ + m_events.emplace_back(makeDefaultEvent()); + m_sig.push_back(-1); + + auto& event = m_events.back(); + + const int after = event.formula; + event.formula = m_event_tree_ops.build_default_root(event.name, after); + m_event_tree_ops.select_root(event.formula); + + setCurrentlySelectedEventByFormula(event.formula); + set_modified(); +} + +void MissionEventsDialogModel::insertEvent() +{ + if (m_cur_event < 0 || m_events.empty()) { + createEvent(); + return; + } + + const int pos = m_cur_event; // Can shift during tree ops so save our position now + m_events.insert(m_events.begin() + pos, makeDefaultEvent()); + m_sig.insert(m_sig.begin() + pos, -1); + auto& event = m_events[pos]; + + // Place after the previous root if it exists and is valid; otherwise we'll fix index explicitly + int after = (pos > 0 && m_events[pos - 1].formula >= 0) ? m_events[pos - 1].formula : -1; + + event.formula = m_event_tree_ops.build_default_root(event.name, after); + + if (pos == 0) { + m_event_tree_ops.ensure_top_level_index(event.formula, 0); + } + m_event_tree_ops.select_root(event.formula); + + setCurrentlySelectedEventByFormula(event.formula); + set_modified(); +} + +void MissionEventsDialogModel::deleteEvent() +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + m_event_tree_ops.delete_event(); +} + +void MissionEventsDialogModel::renameEvent(int id, const SCP_string& name) +{ + // Find by formula id first; fallback to treating id as an index if you want. + int idx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == id) { + idx = i; + break; + } + } + if (idx == -1 && id >= 0 && id < static_cast(m_events.size())) + idx = id; + if (idx < 0) + return; + + // Normalize to engine expectations + SCP_string normalized = name.empty() ? SCP_string("") : name; + SCP_truncate(normalized, NAME_LENGTH - 1); + + modify(m_events[idx].name, normalized); +} + +int MissionEventsDialogModel::getFormula() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].formula; +} + +void MissionEventsDialogModel::setFormula(int node) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + auto& event = m_events[m_cur_event]; + modify(event.formula, node); +} + +int MissionEventsDialogModel::getRepeatCount() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 1; + } + return m_events[m_cur_event].repeat_count; +} + +void MissionEventsDialogModel::setRepeatCount(int count) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (count < -1) { + count = -1; + } + modify(event.repeat_count, count); +} + +int MissionEventsDialogModel::getTriggerCount() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].trigger_count; +} + +void MissionEventsDialogModel::setTriggerCount(int count) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (count < -1) { + count = -1; + } + modify(event.trigger_count, count); +} + +int MissionEventsDialogModel::getIntervalTime() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].interval; +} + +void MissionEventsDialogModel::setIntervalTime(int time) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (time < 0) { + time = 0; + } + modify(event.interval, time); +} + +bool MissionEventsDialogModel::getChained() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].chain_delay >= 0); +} + +void MissionEventsDialogModel::setChained(bool chained) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (chained) { + modify(event.chain_delay, 0); + } else { + modify(event.chain_delay, -1); + } +} + +int MissionEventsDialogModel::getChainDelay() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].chain_delay; +} + +void MissionEventsDialogModel::setChainDelay(int delay) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (delay < 0) { + delay = 0; + } + modify(event.chain_delay, delay); +} + +int MissionEventsDialogModel::getEventScore() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].score; +} + +void MissionEventsDialogModel::setEventScore(int score) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.score, score); +} + +int MissionEventsDialogModel::getEventTeam() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].team; +} + +void MissionEventsDialogModel::setEventTeam(int team) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + auto& event = m_events[m_cur_event]; + + if (team < -1 || team >= MAX_TVT_TEAMS) { + team = -1; + } + modify(event.team, team); +} + +SCP_string MissionEventsDialogModel::getEventDirectiveText() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return ""; + } + return m_events[m_cur_event].objective_text; +} + +void MissionEventsDialogModel::setEventDirectiveText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.objective_text, text); + lcl_fred_replace_stuff(event.objective_text); +} + +SCP_string MissionEventsDialogModel::getEventDirectiveKeyText() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return ""; + } + return m_events[m_cur_event].objective_key_text; +} + +void MissionEventsDialogModel::setEventDirectiveKeyText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.objective_key_text, text); + lcl_fred_replace_stuff(event.objective_key_text); +} + +bool MissionEventsDialogModel::getLogTrue() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_TRUE) != 0; +} + +void MissionEventsDialogModel::setLogTrue(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_TRUE; + } else { + event.mission_log_flags &= ~MLF_SEXP_TRUE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFalse() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_FALSE) != 0; +} + +void MissionEventsDialogModel::setLogFalse(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_FALSE; + } else { + event.mission_log_flags &= ~MLF_SEXP_FALSE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLogPrevious() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_STATE_CHANGE) != 0; +} + +void MissionEventsDialogModel::setLogLogPrevious(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_STATE_CHANGE; + } else { + event.mission_log_flags &= ~MLF_STATE_CHANGE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogAlwaysFalse() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_KNOWN_FALSE) != 0; +} + +void MissionEventsDialogModel::setLogAlwaysFalse(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_KNOWN_FALSE; + } else { + event.mission_log_flags &= ~MLF_SEXP_KNOWN_FALSE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFirstRepeat() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_REPEAT_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogFirstRepeat(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_FIRST_REPEAT_ONLY; + } else { + event.mission_log_flags &= ~MLF_FIRST_REPEAT_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLastRepeat() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_LAST_REPEAT_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogLastRepeat(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_LAST_REPEAT_ONLY; + } else { + event.mission_log_flags &= ~MLF_LAST_REPEAT_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFirstTrigger() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_TRIGGER_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogFirstTrigger(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_FIRST_TRIGGER_ONLY; + } else { + event.mission_log_flags &= ~MLF_FIRST_TRIGGER_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLastTrigger() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_LAST_TRIGGER_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogLastTrigger(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_LAST_TRIGGER_ONLY; + } else { + event.mission_log_flags &= ~MLF_LAST_TRIGGER_ONLY; + } + set_modified(); +} + +void MissionEventsDialogModel::setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note) +{ + auto path = buildPathForHandle(h); + auto& ea = ensureAnnotationByPath(path); + ea.handle = h; + ea.comment = note; + m_event_tree_ops.set_node_note(h, note); + set_modified(); +} + +void MissionEventsDialogModel::setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color) +{ + auto path = buildPathForHandle(h); + auto& ea = ensureAnnotationByPath(path); + ea.handle = h; + if (has_color) { + ea.r = (ubyte)r; + ea.g = (ubyte)g; + ea.b = (ubyte)b; + } else { + ea.r = ea.g = ea.b = 255; + } + m_event_tree_ops.set_node_bg_color(h, r, g, b, has_color); + set_modified(); +} + +void MissionEventsDialogModel::createMessage() +{ + MMessage msg; + + const SCP_string base = ""; + const SCP_string unique = makeUniqueMessageName(base); + + strcpy_s(msg.name, unique.c_str()); + strcpy_s(msg.message, ""); + msg.avi_info.name = nullptr; + msg.wave_info.name = nullptr; + msg.persona_index = -1; + msg.multi_team = -1; + m_messages.push_back(msg); + auto id = static_cast(m_messages.size()) - 1; + + setCurrentlySelectedMessage(id); + + set_modified(); +} + +void MissionEventsDialogModel::insertMessage() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + createMessage(); + return; + } + + MMessage msg; + const SCP_string base = ""; + const SCP_string unique = makeUniqueMessageName(base); + + strcpy_s(msg.name, unique.c_str()); + strcpy_s(msg.message, ""); + msg.avi_info.name = nullptr; + msg.wave_info.name = nullptr; + msg.persona_index = -1; + msg.multi_team = -1; + + // Insert at requested position + m_messages.insert(m_messages.begin() + m_cur_msg, msg); + + // Select the new message + setCurrentlySelectedMessage(m_cur_msg); + + set_modified(); +} + +void MissionEventsDialogModel::deleteMessage() +{ + // handle this case somewhat gracefully + Assertion(SCP_vector_inbounds(m_messages, m_cur_msg), + "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", + m_cur_msg, + static_cast(m_messages.size()) - 1); + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + if (m_messages[m_cur_msg].avi_info.name) { + free(m_messages[m_cur_msg].avi_info.name); + m_messages[m_cur_msg].avi_info.name = nullptr; + } + if (m_messages[m_cur_msg].wave_info.name) { + free(m_messages[m_cur_msg].wave_info.name); + m_messages[m_cur_msg].wave_info.name = nullptr; + } + + SCP_string buf = "<" + SCP_string(m_messages[m_cur_msg].name) + ">"; + update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE); + update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE_OR_STRING); + + m_messages.erase(m_messages.begin() + m_cur_msg); + + if (m_cur_msg >= static_cast(m_messages.size())) { + m_cur_msg = static_cast(m_messages.size()) - 1; + } + + setCurrentlySelectedMessage(m_cur_msg); + + set_modified(); +} + +void MissionEventsDialogModel::moveMessageUp() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg == 0) + return; + + std::swap(m_messages[m_cur_msg - 1], m_messages[m_cur_msg]); + setCurrentlySelectedMessage(m_cur_msg - 1); + set_modified(); +} + +void MissionEventsDialogModel::moveMessageDown() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg >= static_cast(m_messages.size()) - 1) + return; + + std::swap(m_messages[m_cur_msg + 1], m_messages[m_cur_msg]); + setCurrentlySelectedMessage(m_cur_msg + 1); + set_modified(); +} + + +SCP_string MissionEventsDialogModel::getMessageName() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].name; +} + +void MissionEventsDialogModel::setMessageName(const SCP_string& name) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + if (!checkMessageNameConflict(name)) { + strncpy(msg.name, name.c_str(), NAME_LENGTH - 1); + set_modified(); + } +} + +SCP_string MissionEventsDialogModel::getMessageText() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].message; +} + +void MissionEventsDialogModel::setMessageText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + strncpy(msg.message, text.c_str(), MESSAGE_LENGTH - 1); + lcl_fred_replace_stuff(msg.message, MESSAGE_LENGTH - 1); + + set_modified(); +} + +SCP_string MissionEventsDialogModel::getMessageNote() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].note; +} + +void MissionEventsDialogModel::setMessageNote(const SCP_string& note) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + modify(msg.note, note); +} + +SCP_string MissionEventsDialogModel::getMessageAni() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + auto& msg = m_messages[m_cur_msg]; + return msg.avi_info.name ? SCP_string(msg.avi_info.name) : SCP_string(""); +} + +void MissionEventsDialogModel::setMessageAni(const SCP_string& ani) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + const char* cur = msg.avi_info.name; + const SCP_string curStr = cur ? cur : ""; + + // Treat empty, "none", "" as no avi and store nullptr + const bool isNone = ani.empty() || lcase_equal(ani, "") || lcase_equal(ani, "none"); + if (isNone) { + if (cur != nullptr) { // only do work if changing something + free(msg.avi_info.name); + msg.avi_info.name = nullptr; + set_modified(); + } + return; + } + + // No change? bail + if (cur && curStr == ani) { + return; + } + + // Replace value + if (cur) + free(msg.avi_info.name); + msg.avi_info.name = strdup(ani.c_str()); + set_modified(); + + // Possibly add to list of known anis + auto it = std::find_if(m_head_ani_list.begin(), m_head_ani_list.end(), [&](const SCP_string& s) { + return lcase_equal(s, ani); + }); + if (it == m_head_ani_list.end()) { + m_head_ani_list.emplace_back(ani); + } +} + +SCP_string MissionEventsDialogModel::getMessageWave() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + auto& msg = m_messages[m_cur_msg]; + return msg.wave_info.name ? SCP_string(msg.wave_info.name) : SCP_string(""); +} + +void MissionEventsDialogModel::setMessageWave(const SCP_string& wave) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + auto& msg = m_messages[m_cur_msg]; + const char* cur = msg.wave_info.name; + const SCP_string curStr = cur ? cur : ""; + + // Treat empty, "none", "" as no avi and store nullptr + const bool isNone = wave.empty() || lcase_equal(wave, "") || lcase_equal(wave, "none"); + if (isNone) { + if (cur != nullptr) { // only do work if changing something + free(msg.wave_info.name); + msg.wave_info.name = nullptr; + set_modified(); + } + return; + } + + // No change? bail + if (cur && curStr == wave) { + return; + } + + // Replace value + if (cur) + free(msg.wave_info.name); + msg.wave_info.name = strdup(wave.c_str()); + set_modified(); + + autoSelectPersona(); +} + +int MissionEventsDialogModel::getMessagePersona() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return -1; + } + auto& msg = m_messages[m_cur_msg]; + if (SCP_vector_inbounds(Personas, msg.persona_index)) { + return msg.persona_index; + } + return -1; +} + +void MissionEventsDialogModel::setMessagePersona(int persona) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + msg.persona_index = persona; + set_modified(); +} + +int MissionEventsDialogModel::getMessageTeam() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return -1; + } + auto& msg = m_messages[m_cur_msg]; + if (msg.multi_team < 0 || msg.multi_team >= MAX_TVT_TEAMS) { + return -1; + } + return msg.multi_team; +} + +void MissionEventsDialogModel::setMessageTeam(int team) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + if (team >= MAX_TVT_TEAMS) { + msg.multi_team = -1; + } else { + msg.multi_team = team; + } + set_modified(); +} + +void MissionEventsDialogModel::autoSelectPersona() +{ + // I hate everything about this function outside of retail but someone will complain + // if I omit this "feature"... + + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + SCP_string wave_name = m_messages[m_cur_msg].wave_info.name ? m_messages[m_cur_msg].wave_info.name : ""; + SCP_string avi_name = m_messages[m_cur_msg].avi_info.name ? m_messages[m_cur_msg].avi_info.name : ""; + + if ((wave_name[0] >= '1') && (wave_name[0] <= '9') && (wave_name[1] == '_')) { + auto i = wave_name[0] - '1'; + if ((i < static_cast(Personas.size())) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) { + modify(m_messages[m_cur_msg].persona_index, i); + if (i == 0 || i == 1) { + avi_name = "HEAD-TP1"; + } else if (i == 2 || i == 3) { + avi_name = "HEAD-TP2"; + } else if (i == 4) { + avi_name = "HEAD-TP3"; + } else if (i == 5) { + avi_name = "HEAD-VP1"; + } + } + } else { + auto mask = 0; + if (!strnicmp(wave_name.c_str(), "S_", 2)) { + mask = PERSONA_FLAG_SUPPORT; + avi_name = "HEAD-CM1"; + } else if (!strnicmp(wave_name.c_str(), "L_", 2)) { + mask = PERSONA_FLAG_LARGE; + avi_name = "HEAD-CM1"; + } else if (!strnicmp(wave_name.c_str(), "TC_", 3)) { + mask = PERSONA_FLAG_COMMAND; + avi_name = "HEAD-CM1"; + } + + for (auto i = 0; i < (static_cast(Personas.size())); i++) { + if (Personas[i].flags & mask) { + modify(m_messages[m_cur_msg].persona_index, i); + } + } + } + + SCP_string original_avi_name = avi_name; + if (m_messages[m_cur_msg].avi_info.name) { + free(m_messages[m_cur_msg].avi_info.name); + m_messages[m_cur_msg].avi_info.name = nullptr; + } + m_messages[m_cur_msg].avi_info.name = strdup(avi_name.c_str()); + + if (original_avi_name != avi_name) { + set_modified(); + } +} + +void MissionEventsDialogModel::playMessageWave() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + //audiostream_close_file(m_wave_id, false); + + auto& msg = m_messages[m_cur_msg]; + + if (msg.wave_info.name) { + m_wave_id = audiostream_open(msg.wave_info.name, ASF_VOICE); + if (m_wave_id >= 0) { + audiostream_play(m_wave_id, 1.0f, 0); + } + } +} + +const SCP_vector& MissionEventsDialogModel::getMessageList() const +{ + return m_messages; +} + +bool MissionEventsDialogModel::getMissionIsMultiTeam() +{ + return The_mission.game_type & MISSION_TYPE_MULTI_TEAMS; +} + +void MissionEventsDialogModel::setModified() { + set_modified(); +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp index 457580ffed5..490feb7f6e1 100644 --- a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp @@ -1,443 +1,443 @@ -#include "ObjectOrientEditorDialogModel.h" - -#include -#include -#include -#include - -namespace fso::fred::dialogs { - -ObjectOrientEditorDialogModel::ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport) - : AbstractDialogModel(parent, viewport) -{ - vm_vec_make(&_location, 0.f, 0.f, 0.f); - Assert(query_valid_object(_editor->currentObject)); - _position = Objects[_editor->currentObject].pos; - - angles ang{}; - vm_extract_angles_matrix(&ang, &Objects[_editor->currentObject].orient); - _orientationDeg.xyz.x = normalize_degrees(fl_degrees(ang.p)); - _orientationDeg.xyz.y = normalize_degrees(fl_degrees(ang.b)); - _orientationDeg.xyz.z = normalize_degrees(fl_degrees(ang.h)); - - initializeData(); -} - -void ObjectOrientEditorDialogModel::initializeData() -{ - char text[80]; - int type; - object* ptr; - - _pointToObjectList.clear(); - - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (_editor->getNumMarked() != 1 || OBJ_INDEX(ptr) != _editor->currentObject) { - switch (ptr->type) { - case OBJ_START: - case OBJ_SHIP: - _pointToObjectList.emplace_back(ObjectEntry(Ships[ptr->instance].ship_name, OBJ_INDEX(ptr))); - break; - case OBJ_WAYPOINT: { - int waypoint_num; - waypoint_list* wp_list = find_waypoint_list_with_instance(ptr->instance, &waypoint_num); - Assertion(wp_list != nullptr, "Waypoint list was nullptr!"); - sprintf(text, "%s:%d", wp_list->get_name(), waypoint_num + 1); - - _pointToObjectList.emplace_back(ObjectEntry(text, OBJ_INDEX(ptr))); - break; - } - case OBJ_POINT: - case OBJ_JUMP_NODE: - break; - default: - Assertion(false, "Unknown object type in Object Orient Dialog!"); // unknown object type - } - } - - ptr = GET_NEXT(ptr); - } - - type = Objects[_editor->currentObject].type; - if (_editor->getNumMarked() == 1 && (type == OBJ_WAYPOINT || type == OBJ_JUMP_NODE)) { - _orientationEnabledForType = false; - _selectedPointToObjectIndex = -1; - } else { - _selectedPointToObjectIndex = _pointToObjectList.empty() ? -1 : _pointToObjectList[0].objIndex; - } - - modelChanged(); -} - -void ObjectOrientEditorDialogModel::updateObject(object* ptr) -{ - if (ptr->type != OBJ_WAYPOINT && _pointTo) { - vec3d v; - matrix m; - - memset(&v, 0, sizeof(vec3d)); - if (_pointMode == PointToMode::Object) { - if (_selectedPointToObjectIndex >= 0) { - v = Objects[_selectedPointToObjectIndex].pos; - vm_vec_sub2(&v, &ptr->pos); - } - } else if (_pointMode == PointToMode::Location) { - vm_vec_sub(&v, &_location, &ptr->pos); - } else { - Assertion(false, "Unknown Point To mode in Object Orient Dialog!"); // neither radio button is checked. - } - - if (!v.xyz.x && !v.xyz.y && !v.xyz.z) { - return; // can't point to itself. - } - - vm_vector_2_matrix(&m, &v, nullptr, nullptr); - ptr->orient = m; - } -} - -// Also in objectorient.cpp in FRED. TODO Would be nice if this were somewhere common -float ObjectOrientEditorDialogModel::normalize_degrees(float deg) -{ - while (deg < -180.0f) - deg += 360.0f; - while (deg > 180.0f) - deg -= 360.0f; - // collapse negative zero - if (deg == -0.0f) - deg = 0.0f; - return deg; -} - -float ObjectOrientEditorDialogModel::round1(float v) -{ - return std::round(v * 10.0f) / 10.0f; -} - -ObjectOrientEditorDialogModel::ObjectEntry::ObjectEntry(SCP_string in_name, int in_objIndex) - : name(std::move(in_name)), objIndex(in_objIndex) -{ -} - -bool ObjectOrientEditorDialogModel::apply() -{ - object* origin_objp = nullptr; - - // Build translation delta and orientation matrix from UI values - vec3d delta = vmd_zero_vector; - matrix desired_orient = vmd_identity_matrix; - bool change_pos = false, change_orient = false; - - auto& obj = Objects[_editor->currentObject]; - - // ----- Position ----- - // If Relative: _position is a local space delta; unrotate into world - // If Absolute: delta = _position - obj.pos - { - const vec3d& refPos = (_setMode == SetMode::Relative) ? vmd_zero_vector : obj.pos; - if (!is_close(refPos.xyz.x, _position.xyz.x) || !is_close(refPos.xyz.y, _position.xyz.y) || - !is_close(refPos.xyz.z, _position.xyz.z)) { - - if (_setMode == SetMode::Relative) { - vm_vec_unrotate(&delta, &_position, &obj.orient); - } else { - vm_vec_sub(&delta, &_position, &refPos); - } - change_pos = true; - } - } - - // ----- Orientation ----- - { - angles object_ang{}; - vm_extract_angles_matrix(&object_ang, &obj.orient); - - vec3d refDeg = (_setMode == SetMode::Relative) ? vmd_zero_vector - : vec3d{{{normalize_degrees(fl_degrees(object_ang.p)), - normalize_degrees(fl_degrees(object_ang.b)), - normalize_degrees(fl_degrees(object_ang.h))}}}; - - if (!is_close(refDeg.xyz.x, normalize_degrees(_orientationDeg.xyz.x)) || - !is_close(refDeg.xyz.y, normalize_degrees(_orientationDeg.xyz.y)) || - !is_close(refDeg.xyz.z, normalize_degrees(_orientationDeg.xyz.z))) { - - angles ang{}; - ang.p = fl_radians(_orientationDeg.xyz.x); - ang.b = fl_radians(_orientationDeg.xyz.y); - ang.h = fl_radians(_orientationDeg.xyz.z); - - if (_setMode == SetMode::Relative) { - ang.p = object_ang.p + ang.p; - ang.b = object_ang.b + ang.b; - ang.h = object_ang.h + ang.h; - } - vm_angles_2_matrix(&desired_orient, &ang); - change_orient = true; - } - } - - // ----- Transform mode ----- - // If multiple marked and using Relative to Origin, move/rotate the origin first, then - // bring everyone else along by the origin’s delta rotation and position. - matrix origin_rotation = vmd_identity_matrix; - vec3d origin_prev_pos = vmd_zero_vector; - - const bool manyMarked = (_editor->getNumMarked() > 1); - const bool relativeToOrigin = (manyMarked && _transformMode == TransformMode::Relative); - - if (relativeToOrigin) { - origin_objp = &obj; - origin_prev_pos = origin_objp->pos; - matrix saved_orient = origin_objp->orient; - - // Move the origin first - if (change_pos) { - vm_vec_add2(&origin_objp->pos, &delta); - _editor->missionChanged(); - } - if (_pointTo) { - updateObject(origin_objp); - _editor->missionChanged(); - } else if (change_orient) { - origin_objp->orient = desired_orient; - _editor->missionChanged(); - } - - if (origin_objp->type != OBJ_WAYPOINT) { - vm_transpose(&saved_orient); - origin_rotation = saved_orient * origin_objp->orient; - } - } - - // Apply to all marked objects - for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (!ptr->flags[Object::Object_Flags::Marked]) - continue; - - // Skip the origin in the second pass - if (relativeToOrigin && ptr == origin_objp) - continue; - - if (relativeToOrigin) { - // Transform relative to new origin pose - vec3d relative_pos, transformed_pos; - vm_vec_sub(&relative_pos, &ptr->pos, &origin_prev_pos); - vm_vec_unrotate(&transformed_pos, &relative_pos, &origin_rotation); - vm_vec_add(&ptr->pos, &transformed_pos, &origin_objp->pos); - - ptr->orient = ptr->orient * origin_rotation; - _editor->missionChanged(); - } else { - // Independent transform of each marked object - if (change_pos) { - vm_vec_add2(&ptr->pos, &delta); - _editor->missionChanged(); - } - if (_pointTo) { - updateObject(ptr); - _editor->missionChanged(); - } else if (change_orient) { - ptr->orient = desired_orient; - _editor->missionChanged(); - } - } - } - - // Notify the engine about moved objects - if (change_pos || relativeToOrigin) { - for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { - if (ptr->flags[Object::Object_Flags::Marked]) { - object_moved(ptr); - } - } - } - - return true; -} - -void ObjectOrientEditorDialogModel::reject() { - // Do nothing -} - -void ObjectOrientEditorDialogModel::setPositionX(float x) -{ - if (!is_close(_position.xyz.x, x)) { - modify(_position.xyz.x, round1(x)); - } -} - -void ObjectOrientEditorDialogModel::setPositionY(float y) -{ - if (!is_close(_position.xyz.y, y)) { - modify(_position.xyz.y, round1(y)); - } -} -void ObjectOrientEditorDialogModel::setPositionZ(float z) -{ - if (!is_close(_position.xyz.z, z)) { - modify(_position.xyz.z, round1(z)); - } -} - -ObjectPosition ObjectOrientEditorDialogModel::getPosition() const -{ - return {_position.xyz.x, _position.xyz.y, _position.xyz.z}; -} - -void ObjectOrientEditorDialogModel::setOrientationP(float deg) -{ - float val = normalize_degrees(round1(deg)); - if (!is_close(_orientationDeg.xyz.x, deg)) { - modify(_orientationDeg.xyz.x, val); - } -} - -void ObjectOrientEditorDialogModel::setOrientationB(float deg) -{ - float val = normalize_degrees(round1(deg)); - if (!is_close(_orientationDeg.xyz.y, deg)) { - modify(_orientationDeg.xyz.y, val); - } -} - -void ObjectOrientEditorDialogModel::setOrientationH(float deg) -{ - float val = normalize_degrees(round1(deg)); - if (!is_close(_orientationDeg.xyz.z, deg)) { - modify(_orientationDeg.xyz.z, val); - } -} - -ObjectOrientation ObjectOrientEditorDialogModel::getOrientation() const -{ - return {normalize_degrees(_orientationDeg.xyz.x), - normalize_degrees(_orientationDeg.xyz.y), - normalize_degrees(_orientationDeg.xyz.z)}; -} - -void ObjectOrientEditorDialogModel::setSetMode(SetMode mode) -{ - if (_setMode == mode) { - return; - } - - // Current object pose for capturing baseline when entering Relative - const object& obj = Objects[_editor->currentObject]; - - angles objAng{}; - vm_extract_angles_matrix(&objAng, &obj.orient); - - vec3d objAngDeg; - objAngDeg.xyz.x = normalize_degrees(fl_degrees(objAng.p)); - objAngDeg.xyz.y = normalize_degrees(fl_degrees(objAng.b)); - objAngDeg.xyz.z = normalize_degrees(fl_degrees(objAng.h)); - - if (mode == SetMode::Relative) { - // Capture baseline once when switching to Relative - _rebaseRefPos = obj.pos; - _rebaseRefAnglesDeg = objAngDeg; - - // Absolute to Relative: subtract the captured baseline - _position.xyz.x -= _rebaseRefPos.xyz.x; - _position.xyz.y -= _rebaseRefPos.xyz.y; - _position.xyz.z -= _rebaseRefPos.xyz.z; - - _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x - _rebaseRefAnglesDeg.xyz.x); - _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y - _rebaseRefAnglesDeg.xyz.y); - _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z - _rebaseRefAnglesDeg.xyz.z); - } else { - // Relative to Absolute: add the same captured baseline - _position.xyz.x += _rebaseRefPos.xyz.x; - _position.xyz.y += _rebaseRefPos.xyz.y; - _position.xyz.z += _rebaseRefPos.xyz.z; - - _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x + _rebaseRefAnglesDeg.xyz.x); - _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y + _rebaseRefAnglesDeg.xyz.y); - _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z + _rebaseRefAnglesDeg.xyz.z); - } - - // Round to one decimal and normalize angles - _position.xyz.x = round1(_position.xyz.x); - _position.xyz.y = round1(_position.xyz.y); - _position.xyz.z = round1(_position.xyz.z); - - _orientationDeg.xyz.x = normalize_degrees(round1(_orientationDeg.xyz.x)); - _orientationDeg.xyz.y = normalize_degrees(round1(_orientationDeg.xyz.y)); - _orientationDeg.xyz.z = normalize_degrees(round1(_orientationDeg.xyz.z)); - - modify(_setMode, mode); -} - -ObjectOrientEditorDialogModel::SetMode ObjectOrientEditorDialogModel::getSetMode() const -{ - return _setMode; -} - -void ObjectOrientEditorDialogModel::setTransformMode(TransformMode mode) -{ - modify(_transformMode, mode); -} - -ObjectOrientEditorDialogModel::TransformMode ObjectOrientEditorDialogModel::getTransformMode() const -{ - return _transformMode; -} - -void ObjectOrientEditorDialogModel::setPointTo(bool point_to) -{ - modify(_pointTo, point_to); -} - -bool ObjectOrientEditorDialogModel::getPointTo() const -{ - return _pointTo; -} - -void ObjectOrientEditorDialogModel::setPointMode(ObjectOrientEditorDialogModel::PointToMode pointMode) -{ - modify(_pointMode, pointMode); -} - -ObjectOrientEditorDialogModel::PointToMode ObjectOrientEditorDialogModel::getPointMode() const -{ - return _pointMode; -} - -void ObjectOrientEditorDialogModel::setPointToObjectIndex(int selectedObjectNum) -{ - modify(_selectedPointToObjectIndex, selectedObjectNum); -} - -int ObjectOrientEditorDialogModel::getPointToObjectIndex() const -{ - return _selectedPointToObjectIndex; -} - -void ObjectOrientEditorDialogModel::setLocationX(float x) -{ - if (!is_close(_location.xyz.x, x)) { - modify(_location.xyz.x, round1(x)); - } -} - -void ObjectOrientEditorDialogModel::setLocationY(float y) -{ - if (!is_close(_location.xyz.y, y)) { - modify(_location.xyz.y, round1(y)); - } -} - -void ObjectOrientEditorDialogModel::setLocationZ(float z) -{ - if (!is_close(_location.xyz.z, z)) { - modify(_location.xyz.z, round1(z)); - } -} - -ObjectPosition ObjectOrientEditorDialogModel::getLocation() const -{ - return {_location.xyz.x, _location.xyz.y, _location.xyz.z}; -} - -} // namespace fso::fred::dialogs +#include "ObjectOrientEditorDialogModel.h" + +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +ObjectOrientEditorDialogModel::ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + vm_vec_make(&_location, 0.f, 0.f, 0.f); + Assert(query_valid_object(_editor->currentObject)); + _position = Objects[_editor->currentObject].pos; + + angles ang{}; + vm_extract_angles_matrix(&ang, &Objects[_editor->currentObject].orient); + _orientationDeg.xyz.x = normalize_degrees(fl_degrees(ang.p)); + _orientationDeg.xyz.y = normalize_degrees(fl_degrees(ang.b)); + _orientationDeg.xyz.z = normalize_degrees(fl_degrees(ang.h)); + + initializeData(); +} + +void ObjectOrientEditorDialogModel::initializeData() +{ + char text[80]; + int type; + object* ptr; + + _pointToObjectList.clear(); + + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (_editor->getNumMarked() != 1 || OBJ_INDEX(ptr) != _editor->currentObject) { + switch (ptr->type) { + case OBJ_START: + case OBJ_SHIP: + _pointToObjectList.emplace_back(ObjectEntry(Ships[ptr->instance].ship_name, OBJ_INDEX(ptr))); + break; + case OBJ_WAYPOINT: { + int waypoint_num; + waypoint_list* wp_list = find_waypoint_list_with_instance(ptr->instance, &waypoint_num); + Assertion(wp_list != nullptr, "Waypoint list was nullptr!"); + sprintf(text, "%s:%d", wp_list->get_name(), waypoint_num + 1); + + _pointToObjectList.emplace_back(ObjectEntry(text, OBJ_INDEX(ptr))); + break; + } + case OBJ_POINT: + case OBJ_JUMP_NODE: + break; + default: + Assertion(false, "Unknown object type in Object Orient Dialog!"); // unknown object type + } + } + + ptr = GET_NEXT(ptr); + } + + type = Objects[_editor->currentObject].type; + if (_editor->getNumMarked() == 1 && (type == OBJ_WAYPOINT || type == OBJ_JUMP_NODE)) { + _orientationEnabledForType = false; + _selectedPointToObjectIndex = -1; + } else { + _selectedPointToObjectIndex = _pointToObjectList.empty() ? -1 : _pointToObjectList[0].objIndex; + } + + modelChanged(); +} + +void ObjectOrientEditorDialogModel::updateObject(object* ptr) +{ + if (ptr->type != OBJ_WAYPOINT && _pointTo) { + vec3d v; + matrix m; + + memset(&v, 0, sizeof(vec3d)); + if (_pointMode == PointToMode::Object) { + if (_selectedPointToObjectIndex >= 0) { + v = Objects[_selectedPointToObjectIndex].pos; + vm_vec_sub2(&v, &ptr->pos); + } + } else if (_pointMode == PointToMode::Location) { + vm_vec_sub(&v, &_location, &ptr->pos); + } else { + Assertion(false, "Unknown Point To mode in Object Orient Dialog!"); // neither radio button is checked. + } + + if (!v.xyz.x && !v.xyz.y && !v.xyz.z) { + return; // can't point to itself. + } + + vm_vector_2_matrix(&m, &v, nullptr, nullptr); + ptr->orient = m; + } +} + +// Also in objectorient.cpp in FRED. TODO Would be nice if this were somewhere common +float ObjectOrientEditorDialogModel::normalize_degrees(float deg) +{ + while (deg < -180.0f) + deg += 360.0f; + while (deg > 180.0f) + deg -= 360.0f; + // collapse negative zero + if (deg == -0.0f) + deg = 0.0f; + return deg; +} + +float ObjectOrientEditorDialogModel::round1(float v) +{ + return std::round(v * 10.0f) / 10.0f; +} + +ObjectOrientEditorDialogModel::ObjectEntry::ObjectEntry(SCP_string in_name, int in_objIndex) + : name(std::move(in_name)), objIndex(in_objIndex) +{ +} + +bool ObjectOrientEditorDialogModel::apply() +{ + object* origin_objp = nullptr; + + // Build translation delta and orientation matrix from UI values + vec3d delta = vmd_zero_vector; + matrix desired_orient = vmd_identity_matrix; + bool change_pos = false, change_orient = false; + + auto& obj = Objects[_editor->currentObject]; + + // ----- Position ----- + // If Relative: _position is a local space delta; unrotate into world + // If Absolute: delta = _position - obj.pos + { + const vec3d& refPos = (_setMode == SetMode::Relative) ? vmd_zero_vector : obj.pos; + if (!is_close(refPos.xyz.x, _position.xyz.x) || !is_close(refPos.xyz.y, _position.xyz.y) || + !is_close(refPos.xyz.z, _position.xyz.z)) { + + if (_setMode == SetMode::Relative) { + vm_vec_unrotate(&delta, &_position, &obj.orient); + } else { + vm_vec_sub(&delta, &_position, &refPos); + } + change_pos = true; + } + } + + // ----- Orientation ----- + { + angles object_ang{}; + vm_extract_angles_matrix(&object_ang, &obj.orient); + + vec3d refDeg = (_setMode == SetMode::Relative) ? vmd_zero_vector + : vec3d{{{normalize_degrees(fl_degrees(object_ang.p)), + normalize_degrees(fl_degrees(object_ang.b)), + normalize_degrees(fl_degrees(object_ang.h))}}}; + + if (!is_close(refDeg.xyz.x, normalize_degrees(_orientationDeg.xyz.x)) || + !is_close(refDeg.xyz.y, normalize_degrees(_orientationDeg.xyz.y)) || + !is_close(refDeg.xyz.z, normalize_degrees(_orientationDeg.xyz.z))) { + + angles ang{}; + ang.p = fl_radians(_orientationDeg.xyz.x); + ang.b = fl_radians(_orientationDeg.xyz.y); + ang.h = fl_radians(_orientationDeg.xyz.z); + + if (_setMode == SetMode::Relative) { + ang.p = object_ang.p + ang.p; + ang.b = object_ang.b + ang.b; + ang.h = object_ang.h + ang.h; + } + vm_angles_2_matrix(&desired_orient, &ang); + change_orient = true; + } + } + + // ----- Transform mode ----- + // If multiple marked and using Relative to Origin, move/rotate the origin first, then + // bring everyone else along by the origin's delta rotation and position. + matrix origin_rotation = vmd_identity_matrix; + vec3d origin_prev_pos = vmd_zero_vector; + + const bool manyMarked = (_editor->getNumMarked() > 1); + const bool relativeToOrigin = (manyMarked && _transformMode == TransformMode::Relative); + + if (relativeToOrigin) { + origin_objp = &obj; + origin_prev_pos = origin_objp->pos; + matrix saved_orient = origin_objp->orient; + + // Move the origin first + if (change_pos) { + vm_vec_add2(&origin_objp->pos, &delta); + _editor->missionChanged(); + } + if (_pointTo) { + updateObject(origin_objp); + _editor->missionChanged(); + } else if (change_orient) { + origin_objp->orient = desired_orient; + _editor->missionChanged(); + } + + if (origin_objp->type != OBJ_WAYPOINT) { + vm_transpose(&saved_orient); + origin_rotation = saved_orient * origin_objp->orient; + } + } + + // Apply to all marked objects + for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (!ptr->flags[Object::Object_Flags::Marked]) + continue; + + // Skip the origin in the second pass + if (relativeToOrigin && ptr == origin_objp) + continue; + + if (relativeToOrigin) { + // Transform relative to new origin pose + vec3d relative_pos, transformed_pos; + vm_vec_sub(&relative_pos, &ptr->pos, &origin_prev_pos); + vm_vec_unrotate(&transformed_pos, &relative_pos, &origin_rotation); + vm_vec_add(&ptr->pos, &transformed_pos, &origin_objp->pos); + + ptr->orient = ptr->orient * origin_rotation; + _editor->missionChanged(); + } else { + // Independent transform of each marked object + if (change_pos) { + vm_vec_add2(&ptr->pos, &delta); + _editor->missionChanged(); + } + if (_pointTo) { + updateObject(ptr); + _editor->missionChanged(); + } else if (change_orient) { + ptr->orient = desired_orient; + _editor->missionChanged(); + } + } + } + + // Notify the engine about moved objects + if (change_pos || relativeToOrigin) { + for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->flags[Object::Object_Flags::Marked]) { + object_moved(ptr); + } + } + } + + return true; +} + +void ObjectOrientEditorDialogModel::reject() { + // Do nothing +} + +void ObjectOrientEditorDialogModel::setPositionX(float x) +{ + if (!is_close(_position.xyz.x, x)) { + modify(_position.xyz.x, round1(x)); + } +} + +void ObjectOrientEditorDialogModel::setPositionY(float y) +{ + if (!is_close(_position.xyz.y, y)) { + modify(_position.xyz.y, round1(y)); + } +} +void ObjectOrientEditorDialogModel::setPositionZ(float z) +{ + if (!is_close(_position.xyz.z, z)) { + modify(_position.xyz.z, round1(z)); + } +} + +ObjectPosition ObjectOrientEditorDialogModel::getPosition() const +{ + return {_position.xyz.x, _position.xyz.y, _position.xyz.z}; +} + +void ObjectOrientEditorDialogModel::setOrientationP(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.x, deg)) { + modify(_orientationDeg.xyz.x, val); + } +} + +void ObjectOrientEditorDialogModel::setOrientationB(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.y, deg)) { + modify(_orientationDeg.xyz.y, val); + } +} + +void ObjectOrientEditorDialogModel::setOrientationH(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.z, deg)) { + modify(_orientationDeg.xyz.z, val); + } +} + +ObjectOrientation ObjectOrientEditorDialogModel::getOrientation() const +{ + return {normalize_degrees(_orientationDeg.xyz.x), + normalize_degrees(_orientationDeg.xyz.y), + normalize_degrees(_orientationDeg.xyz.z)}; +} + +void ObjectOrientEditorDialogModel::setSetMode(SetMode mode) +{ + if (_setMode == mode) { + return; + } + + // Current object pose for capturing baseline when entering Relative + const object& obj = Objects[_editor->currentObject]; + + angles objAng{}; + vm_extract_angles_matrix(&objAng, &obj.orient); + + vec3d objAngDeg; + objAngDeg.xyz.x = normalize_degrees(fl_degrees(objAng.p)); + objAngDeg.xyz.y = normalize_degrees(fl_degrees(objAng.b)); + objAngDeg.xyz.z = normalize_degrees(fl_degrees(objAng.h)); + + if (mode == SetMode::Relative) { + // Capture baseline once when switching to Relative + _rebaseRefPos = obj.pos; + _rebaseRefAnglesDeg = objAngDeg; + + // Absolute to Relative: subtract the captured baseline + _position.xyz.x -= _rebaseRefPos.xyz.x; + _position.xyz.y -= _rebaseRefPos.xyz.y; + _position.xyz.z -= _rebaseRefPos.xyz.z; + + _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x - _rebaseRefAnglesDeg.xyz.x); + _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y - _rebaseRefAnglesDeg.xyz.y); + _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z - _rebaseRefAnglesDeg.xyz.z); + } else { + // Relative to Absolute: add the same captured baseline + _position.xyz.x += _rebaseRefPos.xyz.x; + _position.xyz.y += _rebaseRefPos.xyz.y; + _position.xyz.z += _rebaseRefPos.xyz.z; + + _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x + _rebaseRefAnglesDeg.xyz.x); + _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y + _rebaseRefAnglesDeg.xyz.y); + _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z + _rebaseRefAnglesDeg.xyz.z); + } + + // Round to one decimal and normalize angles + _position.xyz.x = round1(_position.xyz.x); + _position.xyz.y = round1(_position.xyz.y); + _position.xyz.z = round1(_position.xyz.z); + + _orientationDeg.xyz.x = normalize_degrees(round1(_orientationDeg.xyz.x)); + _orientationDeg.xyz.y = normalize_degrees(round1(_orientationDeg.xyz.y)); + _orientationDeg.xyz.z = normalize_degrees(round1(_orientationDeg.xyz.z)); + + modify(_setMode, mode); +} + +ObjectOrientEditorDialogModel::SetMode ObjectOrientEditorDialogModel::getSetMode() const +{ + return _setMode; +} + +void ObjectOrientEditorDialogModel::setTransformMode(TransformMode mode) +{ + modify(_transformMode, mode); +} + +ObjectOrientEditorDialogModel::TransformMode ObjectOrientEditorDialogModel::getTransformMode() const +{ + return _transformMode; +} + +void ObjectOrientEditorDialogModel::setPointTo(bool point_to) +{ + modify(_pointTo, point_to); +} + +bool ObjectOrientEditorDialogModel::getPointTo() const +{ + return _pointTo; +} + +void ObjectOrientEditorDialogModel::setPointMode(ObjectOrientEditorDialogModel::PointToMode pointMode) +{ + modify(_pointMode, pointMode); +} + +ObjectOrientEditorDialogModel::PointToMode ObjectOrientEditorDialogModel::getPointMode() const +{ + return _pointMode; +} + +void ObjectOrientEditorDialogModel::setPointToObjectIndex(int selectedObjectNum) +{ + modify(_selectedPointToObjectIndex, selectedObjectNum); +} + +int ObjectOrientEditorDialogModel::getPointToObjectIndex() const +{ + return _selectedPointToObjectIndex; +} + +void ObjectOrientEditorDialogModel::setLocationX(float x) +{ + if (!is_close(_location.xyz.x, x)) { + modify(_location.xyz.x, round1(x)); + } +} + +void ObjectOrientEditorDialogModel::setLocationY(float y) +{ + if (!is_close(_location.xyz.y, y)) { + modify(_location.xyz.y, round1(y)); + } +} + +void ObjectOrientEditorDialogModel::setLocationZ(float z) +{ + if (!is_close(_location.xyz.z, z)) { + modify(_location.xyz.z, round1(z)); + } +} + +ObjectPosition ObjectOrientEditorDialogModel::getLocation() const +{ + return {_location.xyz.x, _location.xyz.y, _location.xyz.z}; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 42cda670e8a..7c6f633de91 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -383,7 +383,8 @@ void FredView::connectActionToViewSetting(QAction* option, std::vector* ve void FredView::showContextMenu(const QPoint& globalPos) { auto localPos = ui->centralWidget->mapFromGlobal(globalPos); - auto obj = _viewport->select_object(localPos.x(), localPos.y()); + auto obj = + _viewport->select_object(localPos.x() * this->devicePixelRatio(), localPos.y() * this->devicePixelRatio()); if (obj >= 0) { fred->selectObject(obj); @@ -917,8 +918,9 @@ void FredView::handleObjectEditor(int objNum) { } } void FredView::mouseDoubleClickEvent(QMouseEvent* event) { - auto viewLocal = ui->centralWidget->mapFromGlobal(event->globalPos()); - auto obj = _viewport->select_object(viewLocal.x(), viewLocal.y()); + auto viewLocal = ui->centralWidget->mapFromGlobal(event->globalPosition()); + auto obj = + _viewport->select_object(viewLocal.x() * this->devicePixelRatio(), viewLocal.y() * this->devicePixelRatio()); if (obj >= 0) { handleObjectEditor(obj); diff --git a/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp b/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp index 31af12546c5..417f1b86e35 100644 --- a/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp @@ -1,335 +1,335 @@ -#include "ui/dialogs/AsteroidEditorDialog.h" -#include "ui/dialogs/General/CheckBoxListDialog.h" -#include "ui/util/SignalBlockers.h" - -#include - -#include "ui_AsteroidEditorDialog.h" -#include - -namespace fso::fred::dialogs { - -AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* viewport) : - QDialog(parent), - _viewport(viewport), - _editor(viewport->editor), - ui(new Ui::AsteroidEditorDialog()), - _model(new AsteroidEditorDialogModel(this, viewport)) -{ - this->setFocus(); - ui->setupUi(this); - - // set our internal values, update the UI - initializeUi(); - updateUi(); - - // setup validators for text input - _box_validator.setNotation(QDoubleValidator::StandardNotation); - _box_validator.setDecimals(1); - - ui->lineEdit_obox_minX->setValidator(&_box_validator); - ui->lineEdit_obox_minY->setValidator(&_box_validator); - ui->lineEdit_obox_minZ->setValidator(&_box_validator); - ui->lineEdit_obox_maxX->setValidator(&_box_validator); - ui->lineEdit_obox_maxY->setValidator(&_box_validator); - ui->lineEdit_obox_maxZ->setValidator(&_box_validator); - ui->lineEdit_ibox_minX->setValidator(&_box_validator); - ui->lineEdit_ibox_minY->setValidator(&_box_validator); - ui->lineEdit_ibox_minZ->setValidator(&_box_validator); - ui->lineEdit_ibox_maxX->setValidator(&_box_validator); - ui->lineEdit_ibox_maxY->setValidator(&_box_validator); - ui->lineEdit_ibox_maxZ->setValidator(&_box_validator); - - ui->lineEditAvgSpeed->setValidator(&_speed_validator); -} - -AsteroidEditorDialog::~AsteroidEditorDialog() = default; - -void AsteroidEditorDialog::accept() -{ - // If apply() returns true, close the dialog - if (_model->apply()) { - QDialog::accept(); - } - // else: validation failed, don’t close -} - -void AsteroidEditorDialog::reject() -{ - // Asks the user if they want to save changes, if any - // If they do, it runs _model->apply() and returns the success value - // If they don't, it runs _model->reject() and returns true - if (rejectOrCloseHandler(this, _model.get(), _viewport)) { - QDialog::reject(); // actually close - } - // else: do nothing, don't close -} - -void AsteroidEditorDialog::closeEvent(QCloseEvent* e) -{ - reject(); - e->ignore(); // Don't let the base class close the window -} - -void AsteroidEditorDialog::initializeUi() -{ - util::SignalBlockers blockers(this); // block signals while we set up the UI - - // Checkboxes - ui->enabled->setChecked(_model->getFieldEnabled()); - ui->innerBoxEnabled->setChecked(_model->getInnerBoxEnabled()); - ui->enhancedFieldEnabled->setChecked(_model->getEnhancedEnabled()); - - // Radio buttons for field type - ui->radioButtonActiveField->setChecked(_model->getFieldType() == FT_ACTIVE); - ui->radioButtonPassiveField->setChecked(_model->getFieldType() == FT_PASSIVE); - - // Radio buttons for debris genre - ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); - ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); - - // Spin box - ui->spinBoxNumber->setValue(_model->getNumAsteroids()); - - // Average speed - ui->lineEditAvgSpeed->setText(_model->getAvgSpeed()); - - // Outer box - ui->lineEdit_obox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_X)); - ui->lineEdit_obox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Y)); - ui->lineEdit_obox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Z)); - ui->lineEdit_obox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_X)); - ui->lineEdit_obox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Y)); - ui->lineEdit_obox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Z)); - - // Inner box - ui->lineEdit_ibox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_X)); - ui->lineEdit_ibox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Y)); - ui->lineEdit_ibox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Z)); - ui->lineEdit_ibox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_X)); - ui->lineEdit_ibox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Y)); - ui->lineEdit_ibox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Z)); - - // Housekeeping - ui->spinBoxNumber->setRange(1, MAX_ASTEROIDS); -} - -void AsteroidEditorDialog::updateUi() -{ - util::SignalBlockers blockers(this); // block signals while we update the UI - - bool overall_enabled = _model->getFieldEnabled(); - bool asteroids_enabled = overall_enabled && _model->getDebrisGenre() == DG_ASTEROID; - bool debris_enabled = overall_enabled && _model->getDebrisGenre() == DG_DEBRIS; - bool inner_box_enabled = _model->getInnerBoxEnabled(); - bool field_is_active = (_model->getFieldType() == FT_ACTIVE); - - // Checkboxes - ui->innerBoxEnabled->setEnabled(overall_enabled); - ui->enhancedFieldEnabled->setEnabled(overall_enabled); - - // Radio buttons for field type - ui->radioButtonActiveField->setEnabled(overall_enabled); - ui->radioButtonPassiveField->setEnabled(overall_enabled); - - // Radio buttons for debris genre - ui->radioButtonAsteroid->setEnabled(overall_enabled); - ui->radioButtonDebris->setEnabled(overall_enabled && !field_is_active); - - // Spin box - ui->spinBoxNumber->setEnabled(overall_enabled); - - // Average speed - ui->lineEditAvgSpeed->setEnabled(overall_enabled); - - // Outer box - ui->lineEdit_obox_minX->setEnabled(overall_enabled); - ui->lineEdit_obox_minY->setEnabled(overall_enabled); - ui->lineEdit_obox_minZ->setEnabled(overall_enabled); - ui->lineEdit_obox_maxX->setEnabled(overall_enabled); - ui->lineEdit_obox_maxY->setEnabled(overall_enabled); - ui->lineEdit_obox_maxZ->setEnabled(overall_enabled); - - // Inner box - ui->lineEdit_ibox_minX->setEnabled(overall_enabled && inner_box_enabled); - ui->lineEdit_ibox_minY->setEnabled(overall_enabled && inner_box_enabled); - ui->lineEdit_ibox_minZ->setEnabled(overall_enabled && inner_box_enabled); - ui->lineEdit_ibox_maxX->setEnabled(overall_enabled && inner_box_enabled); - ui->lineEdit_ibox_maxY->setEnabled(overall_enabled && inner_box_enabled); - ui->lineEdit_ibox_maxZ->setEnabled(overall_enabled && inner_box_enabled); - - // Push buttons for object types - ui->asteroidSelectButton->setEnabled(overall_enabled && asteroids_enabled); - ui->debrisSelectButton->setEnabled(overall_enabled && debris_enabled && !field_is_active); - - // Push buttons for ship targets - ui->shipSelectButton->setEnabled(overall_enabled && field_is_active); - - // Update the radio buttons as these do depend on the field type - ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); - ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); -} - -void AsteroidEditorDialog::on_okAndCancelButtons_accepted() -{ - accept(); -} - -void AsteroidEditorDialog::on_okAndCancelButtons_rejected() -{ - reject(); -} - -void AsteroidEditorDialog::on_enabled_toggled(bool enabled) -{ - _model->setFieldEnabled(enabled); - updateUi(); -} - -void AsteroidEditorDialog::on_innerBoxEnabled_toggled(bool enabled) -{ - _model->setInnerBoxEnabled(enabled); - updateUi(); -} - -void AsteroidEditorDialog::on_enhancedFieldEnabled_toggled(bool enabled) -{ - _model->setEnhancedEnabled(enabled); -} - -void AsteroidEditorDialog::on_radioButtonActiveField_toggled(bool checked) -{ - if (checked) { - _model->setFieldType(FT_ACTIVE); - _model->setDebrisGenre(DG_ASTEROID); // only allow asteroids in active fields - updateUi(); - } -} - -void AsteroidEditorDialog::on_radioButtonPassiveField_toggled(bool checked) -{ - if (checked) { - _model->setFieldType(FT_PASSIVE); - updateUi(); - } -} - -void AsteroidEditorDialog::on_radioButtonAsteroid_toggled(bool checked) -{ - if (checked) { - _model->setDebrisGenre(DG_ASTEROID); - updateUi(); - } -} - -void AsteroidEditorDialog::on_radioButtonDebris_toggled(bool checked) -{ - if (checked) { - _model->setDebrisGenre(DG_DEBRIS); - updateUi(); - } -} - -void AsteroidEditorDialog::on_spinBoxNumber_valueChanged(int num_asteroids) -{ - _model->setNumAsteroids(num_asteroids); -} - -void AsteroidEditorDialog::on_lineEditAvgSpeed_textEdited(const QString& text) -{ - _model->setAvgSpeed(text); -} - -void AsteroidEditorDialog::on_lineEdit_obox_minX_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_X); -} - -void AsteroidEditorDialog::on_lineEdit_obox_minY_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Y); -} - -void AsteroidEditorDialog::on_lineEdit_obox_minZ_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Z); -} - -void AsteroidEditorDialog::on_lineEdit_obox_maxX_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_X); -} - -void AsteroidEditorDialog::on_lineEdit_obox_maxY_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Y); -} - -void AsteroidEditorDialog::on_lineEdit_obox_maxZ_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Z); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_minX_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_X); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_minY_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Y); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_minZ_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Z); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_maxX_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_X); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_maxY_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Y); -} - -void AsteroidEditorDialog::on_lineEdit_ibox_maxZ_textEdited(const QString& text) -{ - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Z); -} - -void AsteroidEditorDialog::on_asteroidSelectButton_clicked() -{ - CheckBoxListDialog dlg(this); - dlg.setCaption("Select Asteroid Types"); - dlg.setOptions(_model->getAsteroidSelections()); - - if (dlg.exec() == QDialog::Accepted) { - _model->setAsteroidSelections(dlg.getCheckedStates()); - } -} - -void AsteroidEditorDialog::on_debrisSelectButton_clicked() -{ - CheckBoxListDialog dlg(this); - dlg.setCaption("Select Debris Types"); - dlg.setOptions(_model->getDebrisSelections()); - - if (dlg.exec() == QDialog::Accepted) { - _model->setDebrisSelections(dlg.getCheckedStates()); - } -} - -void AsteroidEditorDialog::on_shipSelectButton_clicked() -{ - CheckBoxListDialog dlg(this); - dlg.setCaption("Select Ship Debris Types"); - dlg.setOptions(_model->getShipSelections()); - if (dlg.exec() == QDialog::Accepted) { - _model->setShipSelections(dlg.getCheckedStates()); - } -} - -} // namespace fso::fred::dialogs +#include "ui/dialogs/AsteroidEditorDialog.h" +#include "ui/dialogs/General/CheckBoxListDialog.h" +#include "ui/util/SignalBlockers.h" + +#include + +#include "ui_AsteroidEditorDialog.h" +#include + +namespace fso::fred::dialogs { + +AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* viewport) : + QDialog(parent), + _viewport(viewport), + _editor(viewport->editor), + ui(new Ui::AsteroidEditorDialog()), + _model(new AsteroidEditorDialogModel(this, viewport)) +{ + this->setFocus(); + ui->setupUi(this); + + // set our internal values, update the UI + initializeUi(); + updateUi(); + + // setup validators for text input + _box_validator.setNotation(QDoubleValidator::StandardNotation); + _box_validator.setDecimals(1); + + ui->lineEdit_obox_minX->setValidator(&_box_validator); + ui->lineEdit_obox_minY->setValidator(&_box_validator); + ui->lineEdit_obox_minZ->setValidator(&_box_validator); + ui->lineEdit_obox_maxX->setValidator(&_box_validator); + ui->lineEdit_obox_maxY->setValidator(&_box_validator); + ui->lineEdit_obox_maxZ->setValidator(&_box_validator); + ui->lineEdit_ibox_minX->setValidator(&_box_validator); + ui->lineEdit_ibox_minY->setValidator(&_box_validator); + ui->lineEdit_ibox_minZ->setValidator(&_box_validator); + ui->lineEdit_ibox_maxX->setValidator(&_box_validator); + ui->lineEdit_ibox_maxY->setValidator(&_box_validator); + ui->lineEdit_ibox_maxZ->setValidator(&_box_validator); + + ui->lineEditAvgSpeed->setValidator(&_speed_validator); +} + +AsteroidEditorDialog::~AsteroidEditorDialog() = default; + +void AsteroidEditorDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don't close +} + +void AsteroidEditorDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +void AsteroidEditorDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void AsteroidEditorDialog::initializeUi() +{ + util::SignalBlockers blockers(this); // block signals while we set up the UI + + // Checkboxes + ui->enabled->setChecked(_model->getFieldEnabled()); + ui->innerBoxEnabled->setChecked(_model->getInnerBoxEnabled()); + ui->enhancedFieldEnabled->setChecked(_model->getEnhancedEnabled()); + + // Radio buttons for field type + ui->radioButtonActiveField->setChecked(_model->getFieldType() == FT_ACTIVE); + ui->radioButtonPassiveField->setChecked(_model->getFieldType() == FT_PASSIVE); + + // Radio buttons for debris genre + ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); + ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); + + // Spin box + ui->spinBoxNumber->setValue(_model->getNumAsteroids()); + + // Average speed + ui->lineEditAvgSpeed->setText(_model->getAvgSpeed()); + + // Outer box + ui->lineEdit_obox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_X)); + ui->lineEdit_obox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Y)); + ui->lineEdit_obox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Z)); + ui->lineEdit_obox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_X)); + ui->lineEdit_obox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Y)); + ui->lineEdit_obox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Z)); + + // Inner box + ui->lineEdit_ibox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_X)); + ui->lineEdit_ibox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Y)); + ui->lineEdit_ibox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Z)); + ui->lineEdit_ibox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_X)); + ui->lineEdit_ibox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Y)); + ui->lineEdit_ibox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Z)); + + // Housekeeping + ui->spinBoxNumber->setRange(1, MAX_ASTEROIDS); +} + +void AsteroidEditorDialog::updateUi() +{ + util::SignalBlockers blockers(this); // block signals while we update the UI + + bool overall_enabled = _model->getFieldEnabled(); + bool asteroids_enabled = overall_enabled && _model->getDebrisGenre() == DG_ASTEROID; + bool debris_enabled = overall_enabled && _model->getDebrisGenre() == DG_DEBRIS; + bool inner_box_enabled = _model->getInnerBoxEnabled(); + bool field_is_active = (_model->getFieldType() == FT_ACTIVE); + + // Checkboxes + ui->innerBoxEnabled->setEnabled(overall_enabled); + ui->enhancedFieldEnabled->setEnabled(overall_enabled); + + // Radio buttons for field type + ui->radioButtonActiveField->setEnabled(overall_enabled); + ui->radioButtonPassiveField->setEnabled(overall_enabled); + + // Radio buttons for debris genre + ui->radioButtonAsteroid->setEnabled(overall_enabled); + ui->radioButtonDebris->setEnabled(overall_enabled && !field_is_active); + + // Spin box + ui->spinBoxNumber->setEnabled(overall_enabled); + + // Average speed + ui->lineEditAvgSpeed->setEnabled(overall_enabled); + + // Outer box + ui->lineEdit_obox_minX->setEnabled(overall_enabled); + ui->lineEdit_obox_minY->setEnabled(overall_enabled); + ui->lineEdit_obox_minZ->setEnabled(overall_enabled); + ui->lineEdit_obox_maxX->setEnabled(overall_enabled); + ui->lineEdit_obox_maxY->setEnabled(overall_enabled); + ui->lineEdit_obox_maxZ->setEnabled(overall_enabled); + + // Inner box + ui->lineEdit_ibox_minX->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_minY->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_minZ->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxX->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxY->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxZ->setEnabled(overall_enabled && inner_box_enabled); + + // Push buttons for object types + ui->asteroidSelectButton->setEnabled(overall_enabled && asteroids_enabled); + ui->debrisSelectButton->setEnabled(overall_enabled && debris_enabled && !field_is_active); + + // Push buttons for ship targets + ui->shipSelectButton->setEnabled(overall_enabled && field_is_active); + + // Update the radio buttons as these do depend on the field type + ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); + ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); +} + +void AsteroidEditorDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void AsteroidEditorDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void AsteroidEditorDialog::on_enabled_toggled(bool enabled) +{ + _model->setFieldEnabled(enabled); + updateUi(); +} + +void AsteroidEditorDialog::on_innerBoxEnabled_toggled(bool enabled) +{ + _model->setInnerBoxEnabled(enabled); + updateUi(); +} + +void AsteroidEditorDialog::on_enhancedFieldEnabled_toggled(bool enabled) +{ + _model->setEnhancedEnabled(enabled); +} + +void AsteroidEditorDialog::on_radioButtonActiveField_toggled(bool checked) +{ + if (checked) { + _model->setFieldType(FT_ACTIVE); + _model->setDebrisGenre(DG_ASTEROID); // only allow asteroids in active fields + updateUi(); + } +} + +void AsteroidEditorDialog::on_radioButtonPassiveField_toggled(bool checked) +{ + if (checked) { + _model->setFieldType(FT_PASSIVE); + updateUi(); + } +} + +void AsteroidEditorDialog::on_radioButtonAsteroid_toggled(bool checked) +{ + if (checked) { + _model->setDebrisGenre(DG_ASTEROID); + updateUi(); + } +} + +void AsteroidEditorDialog::on_radioButtonDebris_toggled(bool checked) +{ + if (checked) { + _model->setDebrisGenre(DG_DEBRIS); + updateUi(); + } +} + +void AsteroidEditorDialog::on_spinBoxNumber_valueChanged(int num_asteroids) +{ + _model->setNumAsteroids(num_asteroids); +} + +void AsteroidEditorDialog::on_lineEditAvgSpeed_textEdited(const QString& text) +{ + _model->setAvgSpeed(text); +} + +void AsteroidEditorDialog::on_lineEdit_obox_minX_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_X); +} + +void AsteroidEditorDialog::on_lineEdit_obox_minY_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Y); +} + +void AsteroidEditorDialog::on_lineEdit_obox_minZ_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Z); +} + +void AsteroidEditorDialog::on_lineEdit_obox_maxX_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_X); +} + +void AsteroidEditorDialog::on_lineEdit_obox_maxY_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Y); +} + +void AsteroidEditorDialog::on_lineEdit_obox_maxZ_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Z); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_minX_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_X); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_minY_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Y); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_minZ_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Z); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_maxX_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_X); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_maxY_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Y); +} + +void AsteroidEditorDialog::on_lineEdit_ibox_maxZ_textEdited(const QString& text) +{ + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Z); +} + +void AsteroidEditorDialog::on_asteroidSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Asteroid Types"); + dlg.setOptions(_model->getAsteroidSelections()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setAsteroidSelections(dlg.getCheckedStates()); + } +} + +void AsteroidEditorDialog::on_debrisSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Debris Types"); + dlg.setOptions(_model->getDebrisSelections()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setDebrisSelections(dlg.getCheckedStates()); + } +} + +void AsteroidEditorDialog::on_shipSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Ship Debris Types"); + dlg.setOptions(_model->getShipSelections()); + if (dlg.exec() == QDialog::Accepted) { + _model->setShipSelections(dlg.getCheckedStates()); + } +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp index 3686f1e1d83..31cc1ba8ba7 100644 --- a/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/CampaignEditorDialog.cpp @@ -75,9 +75,9 @@ CampaignEditorDialog::CampaignEditorDialog(QWidget *_parent, EditorViewport *_vi menFileNew->addAction(campaignType, this, &CampaignEditorDialog::fileNew); menFile->addSeparator(); - menFile->addAction(tr("&Open..."), this, &CampaignEditorDialog::fileOpen, tr("Ctrl+O")); - menFile->addAction(tr("&Save"), this, &CampaignEditorDialog::fileSave, tr("Ctrl+S")); - menFile->addAction(tr("Save &as..."), this, &CampaignEditorDialog::fileSaveAs, tr("Ctrl+Shift+S")); + menFile->addAction(tr("&Open..."), tr("Ctrl+O"), this, &CampaignEditorDialog::fileOpen); + menFile->addAction(tr("&Save"), tr("Ctrl+S"), this, &CampaignEditorDialog::fileSave); + menFile->addAction(tr("Save &as..."), tr("Ctrl+Shift+S"),this, &CampaignEditorDialog::fileSaveAs); menFile->addAction(tr("Save &Copy as..."), this, &CampaignEditorDialog::fileSaveCopyAs); menFile->addSeparator(); menFile->addAction(tr("E&xit"), this, &QDialog::reject); @@ -332,8 +332,8 @@ void CampaignEditorDialog::mnLinkMenu(const QPoint &pos){ QMenu menu{ ui->lstMissions }; - QAction *to = menu.addAction(tr("Add branch to ") + mnName); - QAction *from = menu.addAction(tr("Add branch from ") + mnName); + QAction *to = menu.addAction(tr("Add branch to ") + *mnName); + QAction *from = menu.addAction(tr("Add branch from ") + *mnName); QAction *end = menu.addAction(tr("Add campaign end")); bool mnSel{ model->isCurMnSelected() }; to->setEnabled(mnSel); diff --git a/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp b/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp index 04fd9a1134f..1a9f2343296 100644 --- a/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp +++ b/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp @@ -30,7 +30,7 @@ void CommandBriefingDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void CommandBriefingDialog::reject() diff --git a/qtfred/src/ui/dialogs/FictionViewerDialog.cpp b/qtfred/src/ui/dialogs/FictionViewerDialog.cpp index f69944f18e1..9f7c692956f 100644 --- a/qtfred/src/ui/dialogs/FictionViewerDialog.cpp +++ b/qtfred/src/ui/dialogs/FictionViewerDialog.cpp @@ -39,7 +39,7 @@ void FictionViewerDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void FictionViewerDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp b/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp index 0055905b692..eb68aa137e1 100644 --- a/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp @@ -39,7 +39,7 @@ void MissionCutscenesDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void MissionCutscenesDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp index a848991023c..966d178c9dd 100644 --- a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp @@ -252,7 +252,7 @@ void MissionEventsDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void MissionEventsDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp b/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp index bc39397a0f2..d04fa91044e 100644 --- a/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp @@ -37,7 +37,7 @@ void MissionGoalsDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void MissionGoalsDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index 5de5794e363..243739d6c8e 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp @@ -37,7 +37,7 @@ void MissionSpecDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void MissionSpecDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp index 54795666750..f26bc5a1b76 100644 --- a/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp @@ -42,7 +42,7 @@ void CustomDataDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void CustomDataDialog::reject() @@ -104,7 +104,7 @@ void CustomDataDialog::buildView() auto* hdr = ui->stringsTableView->horizontalHeader(); hdr->setSectionsClickable(false); // no click/press behavior hdr->setSortIndicatorShown(false); // hide sort arrow - hdr->setHighlightSections(false); // don’t change look when selected + hdr->setHighlightSections(false); // don't change look when selected hdr->setSectionsMovable(false); // no drag-to-reorder columns hdr->setFocusPolicy(Qt::NoFocus); diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp index 91c19f87d43..8c844b2e977 100644 --- a/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp @@ -42,7 +42,7 @@ void CustomStringsDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void CustomStringsDialog::reject() @@ -104,7 +104,7 @@ void CustomStringsDialog::buildView() auto* hdr = ui->stringsTableView->horizontalHeader(); hdr->setSectionsClickable(false); // no click/press behavior hdr->setSortIndicatorShown(false); // hide sort arrow - hdr->setHighlightSections(false); // don’t change look when selected + hdr->setHighlightSections(false); // don't change look when selected hdr->setSectionsMovable(false); // no drag-to-reorder columns hdr->setFocusPolicy(Qt::NoFocus); diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp index 99527d691ec..c79c5cee1a2 100644 --- a/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp @@ -28,7 +28,7 @@ void CustomWingNamesDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void CustomWingNamesDialog::reject() diff --git a/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp index 52bf7d6be02..a7e016483f2 100644 --- a/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp @@ -34,7 +34,7 @@ void SoundEnvironmentDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close closeWave(); disableEnvPreview(); diff --git a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp index 72d4d2bccc6..625c12425da 100644 --- a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp @@ -32,7 +32,7 @@ void ObjectOrientEditorDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void ObjectOrientEditorDialog::reject() diff --git a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp index a9e4c3936a7..d3304456c58 100644 --- a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp @@ -27,7 +27,7 @@ void ReinforcementsDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don�t close } void ReinforcementsDialog::reject() diff --git a/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp b/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp index 27975d0fdfc..aa54e3e1d59 100644 --- a/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp +++ b/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp @@ -29,7 +29,7 @@ void ShieldSystemDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void ShieldSystemDialog::reject() diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp index 2aad1084859..5e81c21d402 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp @@ -25,7 +25,7 @@ void ShipAltShipClass::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void ShipAltShipClass::reject() diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index c4fb66507bb..97046fe6de2 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace fso::fred::dialogs { @@ -211,7 +212,8 @@ void ShipEditorDialog::updateColumnOne(bool overwrite) if (overwrite) { ui->callsignCombo->addItem(""); for (auto j = 0; j < Mission_callsign_count; j++) { - ui->callsignCombo->addItem(Mission_callsigns[j], QVariant(Mission_callsigns[j])); + SCP_string current = Mission_callsigns[j]; + ui->callsignCombo->addItem(Mission_callsigns[j], current.c_str()); } if (ui->callsignCombo->findText(QString(callsign.c_str()))) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 87be9cc36be..cfb5cce1753 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -75,7 +75,7 @@ void ShipWeaponsDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void ShipWeaponsDialog::reject() diff --git a/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp index bddd931f4ea..293de6f9e6a 100644 --- a/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp +++ b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp @@ -29,7 +29,7 @@ void VolumetricNebulaDialog::accept() if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don’t close + // else: validation failed, don't close } void VolumetricNebulaDialog::reject() diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp index efbd704f37b..8dc7839473b 100644 --- a/qtfred/src/ui/dialogs/WingEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -447,7 +447,7 @@ void WingEditorDialog::on_initialOrdersButton_clicked() return; } - // block for empty wings (matches old FRED behavior where goals apply to the wing’s ships) + // block for empty wings (matches old FRED behavior where goals apply to the wing's ships) if (Wings[wingIndex].wave_count <= 0) { QMessageBox::information(this, "Initial Orders", "This wing has no ships (wave_count == 0)."); return; diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index f2d28dd22ee..fceb44cd761 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -12,7 +12,7 @@ void bankTree::dragEnterEvent(QDragEnterEvent* event) } void bankTree::dropEvent(QDropEvent* event) { - auto item = indexAt(event->pos()); + auto item = indexAt(event->position().toPoint()); if (!item.isValid()) { return; } diff --git a/qtfred/src/ui/widgets/renderwidget.cpp b/qtfred/src/ui/widgets/renderwidget.cpp index 4c6d9295c6e..1da6521a11e 100644 --- a/qtfred/src/ui/widgets/renderwidget.cpp +++ b/qtfred/src/ui/widgets/renderwidget.cpp @@ -222,11 +222,12 @@ void RenderWidget::mousePressEvent(QMouseEvent* event) { waypoint_instance = Objects[fred->cur_waypoint->get_objnum()].instance; } - _markingBox.x1 = event->x(); - _markingBox.y1 = event->y(); + _markingBox.x1 = event->position().x() * _window->devicePixelRatio(); + _markingBox.y1 = event->position().y() * _window->devicePixelRatio(); _viewport->Dup_drag = 0; - _viewport->on_object = _viewport->select_object(event->x(), event->y()); + _viewport->on_object = _viewport->select_object(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio()); _viewport->button_down = 1; _viewport->drag_rotate_save_backup(); @@ -235,7 +236,10 @@ void RenderWidget::mousePressEvent(QMouseEvent* event) { if (!_viewport->Bg_bitmap_dialog) { if (_viewport->on_object == -1) { _viewport->Selection_lock = 0; // force off selection lock - _viewport->on_object = _viewport->create_object_on_grid(event->x(), event->y(), waypoint_instance); + _viewport->on_object = + _viewport->create_object_on_grid(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio(), + waypoint_instance); } else { _viewport->Dup_drag = 1; @@ -281,7 +285,8 @@ void RenderWidget::mousePressEvent(QMouseEvent* event) { _viewport->moved = 0; if (_viewport->Selection_lock) { if (_viewport->Editing_mode == CursorMode::Moving) { - _viewport->drag_objects(event->x(), event->y()); + _viewport->drag_objects(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio()); } else if (_viewport->Editing_mode == CursorMode::Rotating) { _viewport->drag_rotate_objects(0, 0); } @@ -307,12 +312,13 @@ void RenderWidget::mouseMoveEvent(QMouseEvent* event) { _lastMouse = event->pos(); // Update marking box - _markingBox.x2 = event->x(); - _markingBox.y2 = event->y(); + _markingBox.x2 = event->position().x() * _window->devicePixelRatio(); + _markingBox.y2 = event->position().y() * _window->devicePixelRatio(); // RT point - _viewport->Cursor_over = _viewport->select_object(event->x(), event->y()); + _viewport->Cursor_over = _viewport->select_object(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio()); updateCursor(); if (!event->buttons().testFlag(Qt::LeftButton)) { @@ -335,7 +341,8 @@ void RenderWidget::mouseMoveEvent(QMouseEvent* event) { if (_viewport->moved) { if (_viewport->on_object != -1 || _viewport->Selection_lock) { if (_viewport->Editing_mode == CursorMode::Moving) { - _viewport->drag_objects(event->x(), event->y()); + _viewport->drag_objects(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio()); } else if (_viewport->Editing_mode == CursorMode::Rotating) { _viewport->drag_rotate_objects(mouseDX.x(), mouseDX.y()); } @@ -356,8 +363,8 @@ void RenderWidget::mouseReleaseEvent(QMouseEvent* event) { return QWidget::mousePressEvent(event); } - _markingBox.x2 = event->x(); - _markingBox.y2 = event->y(); + _markingBox.x2 = event->position().x() * _window->devicePixelRatio(); + _markingBox.y2 = event->position().y() * _window->devicePixelRatio(); /* TODO: Investiage if this is still required @@ -373,7 +380,8 @@ void RenderWidget::mouseReleaseEvent(QMouseEvent* event) { if (_viewport->moved) { if ((_viewport->on_object != -1) || _viewport->Selection_lock) { if (_viewport->Editing_mode == CursorMode::Moving) { - _viewport->drag_objects(event->x(), event->y()); + _viewport->drag_objects(event->position().x() * _window->devicePixelRatio(), + event->position().y() * _window->devicePixelRatio()); } else if (_viewport->Editing_mode == CursorMode::Rotating) { _viewport->drag_rotate_objects(0, 0); } @@ -484,7 +492,8 @@ void RenderWidget::setCursorMode(CursorMode mode) { _cursorMode = mode; } void RenderWidget::renderFrame() { - _viewport->renderer->render_frame(fred->currentObject, fred->Render_subsys, _usingMarkingBox, _markingBox, false); + qreal scale = _window->devicePixelRatio(); + _viewport->renderer->render_frame(fred->currentObject, fred->Render_subsys, _usingMarkingBox, _markingBox, false, scale); } } // namespace fred } // namespace fso diff --git a/qtfred/src/ui/widgets/sexp_tree.cpp b/qtfred/src/ui/widgets/sexp_tree.cpp index 2a6975d9abd..398a4be716a 100644 --- a/qtfred/src/ui/widgets/sexp_tree.cpp +++ b/qtfred/src/ui/widgets/sexp_tree.cpp @@ -243,7 +243,7 @@ class NoteBadgeDelegate final : public QStyledItemDelegate { const QStyle* s = w ? w->style() : QApplication::style(); s->drawControl(QStyle::CE_ItemViewItem, &opt, p, w); - // if there’s a note, paint the badge directly after the text + // if there's a note, paint the badge directly after the text const QString note = index.data(sexp_tree::NoteRole).toString(); if (!note.isEmpty()) { // where Qt drew the text @@ -2882,14 +2882,14 @@ void sexp_tree::mouseMoveEvent(QMouseEvent* e) return; } - // “Dragging” – we just highlight potential drop target (a root under the cursor) + // "Dragging" - we just highlight potential drop target (a root under the cursor) s_dragging = true; if (auto* over = itemAt(e->pos())) { if (isRoot(over)) - setCurrentItem(over); // simple visual cue like OG’s SelectDropTarget + setCurrentItem(over); // simple visual cue like OG's SelectDropTarget } - // No QDrag payload; we’ll do the move on mouse release to keep logic simple. + // No QDrag payload; we'll do the move on mouse release to keep logic simple. QTreeWidget::mouseMoveEvent(e); } @@ -2899,7 +2899,7 @@ void sexp_tree::mouseReleaseEvent(QMouseEvent* e) auto* dropTarget = itemAt(e->pos()); if (dropTarget && isRoot(dropTarget) && dropTarget != s_dragSourceRoot) { // OG rule: if moving up, insert_before=true; if moving down, insert_after - // (so we “end up where we dropped”). :contentReference[oaicite:1]{index=1} + // (so we "end up where we dropped"). :contentReference[oaicite:1]{index=1} const int srcIdx = indexOfTopLevelItem(s_dragSourceRoot); const int dstIdx = indexOfTopLevelItem(dropTarget); const bool insert_before = (srcIdx > dstIdx); @@ -6040,7 +6040,7 @@ std::unique_ptr sexp_tree::buildContextMenu(QTreeWidgetItem* h) { std::unique_ptr popup_menu(new QMenu(tr("Edit SEXP tree"))); auto delete_act = - popup_menu->addAction(tr("&Delete Item"), this, [this]() { deleteActionHandler(); }, QKeySequence::Delete); + popup_menu->addAction(tr("&Delete Item"), QKeySequence::Delete, this, [this]() { deleteActionHandler(); }); auto edit_data_act = popup_menu->addAction(tr("&Edit Data"), this, [this]() { editDataActionHandler(); }); popup_menu->addAction(tr("Expand All"), this, [this]() { expand_branch(currentItem()); }); @@ -6051,10 +6051,10 @@ std::unique_ptr sexp_tree::buildContextMenu(QTreeWidgetItem* h) { edit_color_act->setEnabled(_interface->getFlags()[TreeFlags::AnnotationsAllowed]); popup_menu->addSection(tr("Copy operations")); - auto cut_act = popup_menu->addAction(tr("Cut"), this, [this]() { cutActionHandler(); }, QKeySequence::Cut); + auto cut_act = popup_menu->addAction(tr("Cut"), QKeySequence::Cut, this, [this]() { cutActionHandler(); }); cut_act->setEnabled(false); - auto copy_act = popup_menu->addAction(tr("Copy"), this, [this]() { copyActionHandler(); }, QKeySequence::Copy); - auto paste_act = popup_menu->addAction(tr("Paste"), this, [this]() { pasteActionHandler(); }, QKeySequence::Paste); //TODO match paste/add paste + auto copy_act = popup_menu->addAction(tr("Copy"), QKeySequence::Copy, this, [this]() { copyActionHandler(); }); + auto paste_act = popup_menu->addAction(tr("Paste"), QKeySequence::Paste, this, [this]() { pasteActionHandler(); }); paste_act->setEnabled(false); popup_menu->addSection(tr("Add")); @@ -7323,7 +7323,7 @@ void sexp_tree::startOperatorQuickSearch(QTreeWidgetItem* item, const QString& s return; // Only allow on editable positions (operator or data) that live beneath a parent - // (We’ll compute OPF from parent or root as necessary) + // (We'll compute OPF from parent or root as necessary) _opAll = validOperatorsForNode(nodeIdx); if (_opAll.isEmpty()) return;