Merge pull request #25239 from graelo/quicklook-clean-pixi
feat(macos): add quicklook extensions
This commit is contained in:
@@ -96,6 +96,13 @@ function run_codesign {
|
||||
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements entitlements.plist "$1"
|
||||
}
|
||||
|
||||
function run_codesign_extension {
|
||||
local target="$1"
|
||||
local entitlements_file="$2"
|
||||
echo "Signing extension $target with entitlements $entitlements_file"
|
||||
/usr/bin/codesign --options runtime -f -s ${SIGNING_KEY_ID} --timestamp --entitlements "$entitlements_file" "$target"
|
||||
}
|
||||
|
||||
IFS=$'\n'
|
||||
dylibs=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.dylib"))
|
||||
shared_objects=($(/usr/bin/find "${CONTAINING_FOLDER}/${APP_NAME}" -name "*.so"))
|
||||
@@ -108,13 +115,42 @@ signed_files=("${dylibs[@]}" "${shared_objects[@]}" "${bundles[@]}" "${executabl
|
||||
# This list of files is generated from:
|
||||
# file `find . -type f -perm +111 -print` | grep "Mach-O 64-bit executable" | sed 's/:.*//g'
|
||||
for exe in ${signed_files}; do
|
||||
run_codesign "${exe}"
|
||||
# Skip .appex executables as they will be signed separately with their bundles
|
||||
if [[ "$exe" != */Contents/PlugIns/*.appex/* ]]; then
|
||||
run_codesign "${exe}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Two additional files that must be signed that aren't caught by the above searches:
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/packages.txt"
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd"
|
||||
|
||||
# Sign new Swift QuickLook extensions (macOS 15.0+) with their specific entitlements
|
||||
# These must be signed before the app itself to avoid overriding the extension signatures
|
||||
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns" ]; then
|
||||
# Find the entitlements files relative to script location
|
||||
# Script is in package/scripts/, entitlements are in src/MacAppBundle/QuickLook/modern/
|
||||
SCRIPT_DIR="${0:A:h}" # zsh equivalent of dirname with full path resolution
|
||||
PREVIEW_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/PreviewExtension.entitlements"
|
||||
THUMBNAIL_ENTITLEMENTS="${SCRIPT_DIR}/../../src/MacAppBundle/QuickLook/modern/ThumbnailExtension.entitlements"
|
||||
|
||||
# Sign individual executables within .appex bundles first
|
||||
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension" ]; then
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension"
|
||||
fi
|
||||
if [ -f "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension" ]; then
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension"
|
||||
fi
|
||||
|
||||
# Then sign the .appex bundles themselves with extension-specific entitlements
|
||||
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" ] && [ -f "$THUMBNAIL_ENTITLEMENTS" ]; then
|
||||
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADThumbnailExtension.appex" "$THUMBNAIL_ENTITLEMENTS"
|
||||
fi
|
||||
if [ -d "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" ] && [ -f "$PREVIEW_ENTITLEMENTS" ]; then
|
||||
run_codesign_extension "${CONTAINING_FOLDER}/${APP_NAME}/Contents/PlugIns/FreeCADPreviewExtension.appex" "$PREVIEW_ENTITLEMENTS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Finally, sign the app itself (must be done last)
|
||||
run_codesign "${CONTAINING_FOLDER}/${APP_NAME}"
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
#include <QMimeData>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QPainter>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QScreen>
|
||||
@@ -1793,8 +1794,67 @@ void MainWindow::delayedStartup()
|
||||
);
|
||||
safeModePopup.exec();
|
||||
}
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
// Register QuickLook extensions on first launch
|
||||
registerQuickLookExtensions();
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
void MainWindow::registerQuickLookExtensions()
|
||||
{
|
||||
// Only check once per session
|
||||
static bool quickLookChecked = false;
|
||||
if (quickLookChecked) {
|
||||
return;
|
||||
}
|
||||
quickLookChecked = true;
|
||||
|
||||
// Get the path to FreeCAD.app/Contents/PlugIns
|
||||
QString appPath = QApplication::applicationDirPath();
|
||||
QString plugInsPath = appPath + "/../PlugIns";
|
||||
|
||||
QString thumbnailExt = plugInsPath + "/FreeCADThumbnailExtension.appex";
|
||||
QString previewExt = plugInsPath + "/FreeCADPreviewExtension.appex";
|
||||
|
||||
// Check if extensions exist before attempting registration
|
||||
if (!QFileInfo::exists(thumbnailExt) || !QFileInfo::exists(previewExt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if extensions are already registered with pluginkit
|
||||
QProcess checkProcess;
|
||||
checkProcess.start("pluginkit", QStringList() << "-m");
|
||||
checkProcess.waitForFinished();
|
||||
QString registeredPlugins = QString::fromUtf8(checkProcess.readAllStandardOutput());
|
||||
|
||||
const QString thumbnailId = QStringLiteral("org.freecad.FreeCAD.quicklook.thumbnail");
|
||||
const QString previewId = QStringLiteral("org.freecad.FreeCAD.quicklook.preview");
|
||||
|
||||
bool thumbnailRegistered = registeredPlugins.contains(thumbnailId);
|
||||
bool previewRegistered = registeredPlugins.contains(previewId);
|
||||
|
||||
if (thumbnailRegistered && previewRegistered) {
|
||||
Base::Console().log("QuickLook extensions already registered\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register and activate only the extensions that are not yet registered
|
||||
if (!thumbnailRegistered) {
|
||||
QProcess::execute("pluginkit", QStringList() << "-a" << thumbnailExt);
|
||||
QProcess::execute("pluginkit", QStringList() << "-e" << "use" << "-i" << thumbnailId);
|
||||
}
|
||||
|
||||
if (!previewRegistered) {
|
||||
QProcess::execute("pluginkit", QStringList() << "-a" << previewExt);
|
||||
QProcess::execute("pluginkit", QStringList() << "-e" << "use" << "-i" << previewId);
|
||||
}
|
||||
|
||||
Base::Console().log("QuickLook extensions registered successfully\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainWindow::appendRecentFile(const QString& filename)
|
||||
{
|
||||
auto recent = this->findChild<RecentFilesAction*>(QStringLiteral("recentFiles"));
|
||||
|
||||
@@ -370,6 +370,12 @@ private Q_SLOTS:
|
||||
* \internal
|
||||
*/
|
||||
void delayedStartup();
|
||||
#ifdef Q_OS_MAC
|
||||
/**
|
||||
* \internal
|
||||
*/
|
||||
void registerQuickLookExtensions();
|
||||
#endif
|
||||
/**
|
||||
* \internal
|
||||
*/
|
||||
|
||||
@@ -6,14 +6,7 @@
|
||||
#
|
||||
if(FREECAD_CREATE_MAC_APP OR (APPLE AND BUILD_WITH_CONDA))
|
||||
add_subdirectory(QuickLook)
|
||||
install(
|
||||
DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/QuickLook/QuicklookFCStd.qlgenerator"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook"
|
||||
)
|
||||
install(
|
||||
PROGRAMS "${PROJECT_BINARY_DIR}/src/MacAppBundle/QuickLook/QuicklookFCStd.framework/Versions/A/QuicklookFCStd"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook/QuicklookFCStd.qlgenerator/Contents/MacOS/"
|
||||
)
|
||||
# Installation handled by QuickLook/CMakeLists.txt
|
||||
endif()
|
||||
|
||||
|
||||
@@ -25,13 +18,21 @@ if(FREECAD_CREATE_MAC_APP)
|
||||
|
||||
set(PYTHON_DIR_BASENAME python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR})
|
||||
|
||||
if(PYTHON_LIBRARY MATCHES "(.*Python\\.framework).*")
|
||||
#framework
|
||||
set(PYTHON_DIR ${CMAKE_MATCH_1}/Versions/${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/lib/${PYTHON_DIR_BASENAME})
|
||||
if(Python3_LIBRARY_DIRS)
|
||||
# Get first library directory
|
||||
list(GET Python3_LIBRARY_DIRS 0 FIRST_PYTHON_LIB_DIR)
|
||||
if(FIRST_PYTHON_LIB_DIR MATCHES "(.*Python\\.framework).*")
|
||||
#framework
|
||||
set(PYTHON_DIR ${CMAKE_MATCH_1}/Versions/${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/lib/${PYTHON_DIR_BASENAME})
|
||||
else()
|
||||
#unix
|
||||
set(PYTHON_DIR ${FIRST_PYTHON_LIB_DIR}/${PYTHON_DIR_BASENAME})
|
||||
endif()
|
||||
else()
|
||||
#unix
|
||||
get_filename_component(PYTHON_DIR ${PYTHON_LIBRARY} PATH)
|
||||
set(PYTHON_DIR ${PYTHON_DIR}/${PYTHON_DIR_BASENAME})
|
||||
# Fallback: derive from Python3_EXECUTABLE
|
||||
get_filename_component(PYTHON_BIN_DIR ${Python3_EXECUTABLE} DIRECTORY)
|
||||
get_filename_component(PYTHON_PREFIX ${PYTHON_BIN_DIR} DIRECTORY)
|
||||
set(PYTHON_DIR ${PYTHON_PREFIX}/lib/${PYTHON_DIR_BASENAME})
|
||||
endif()
|
||||
|
||||
message(" PYTHON_DIR is ${PYTHON_DIR} --------------------ipatch--")
|
||||
@@ -51,7 +52,7 @@ if(HOMEBREW_PREFIX)
|
||||
|
||||
message(" PYTHON_DIR is ${PYTHON_DIR} --------------------ipatch--")
|
||||
|
||||
file(GLOB HOMEBREW_PTH_FILES "${HOMEBREW_PREFIX}/lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/freecad*.pth")
|
||||
file(GLOB HOMEBREW_PTH_FILES "${HOMEBREW_PREFIX}/lib/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/freecad*.pth")
|
||||
|
||||
message(STATUS "HOMEBREW_PTH_FILES are ${HOMEBREW_PTH_FILES} -----------------------ipatch--")
|
||||
|
||||
@@ -120,13 +121,13 @@ if(HOMEBREW_PREFIX)
|
||||
|
||||
if(IS_DIRECTORY "${CONTENT_ITEM}")
|
||||
# Install directory
|
||||
install(DIRECTORY "${CONTENT_ITEM}/"
|
||||
install(DIRECTORY "${CONTENT_ITEM}/"
|
||||
DESTINATION "${BUNDLE_SITE_PACKAGES_DIR}/${ITEM_NAME}"
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
message(STATUS " Installing directory: ${ITEM_NAME}")
|
||||
else()
|
||||
# Install file
|
||||
install(FILES "${CONTENT_ITEM}"
|
||||
install(FILES "${CONTENT_ITEM}"
|
||||
DESTINATION "${BUNDLE_SITE_PACKAGES_DIR}")
|
||||
message(STATUS " Installing file: ${ITEM_NAME}")
|
||||
endif()
|
||||
@@ -149,7 +150,15 @@ if(HOMEBREW_PREFIX)
|
||||
endforeach(PTH_FILE)
|
||||
endif()
|
||||
|
||||
set(QT_PLUGINS_DIR "${Qt5Core_DIR}/../../../plugins")
|
||||
set(QT_PLUGINS_DIR "${Qt6Core_DIR}/../../../plugins")
|
||||
# Fallback for Homebrew Qt6 layout
|
||||
if(NOT EXISTS "${QT_PLUGINS_DIR}")
|
||||
set(QT_PLUGINS_DIR "/opt/homebrew/share/qt/plugins")
|
||||
endif()
|
||||
set(QT_ASSISTANT_PATH "${Qt6Core_DIR}/../../../libexec/Assistant.app/Contents/MacOS/Assistant")
|
||||
set(QT_PLUGIN_SUBDIRS "platforms;imageformats;styles;iconengines")
|
||||
|
||||
|
||||
execute_process(COMMAND "xcode-select" "--print-path"
|
||||
OUTPUT_VARIABLE XCODE_PATH
|
||||
ERROR_QUIET
|
||||
@@ -159,22 +168,33 @@ string(STRIP ${XCODE_PATH} XCODE_PATH)
|
||||
set(XCTEST_PATH "${XCODE_PATH}/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework/Versions/Current")
|
||||
|
||||
# add qt assistant to bundle
|
||||
install(PROGRAMS "${Qt5Core_DIR}/../../../libexec/Assistant.app/Contents/MacOS/Assistant" DESTINATION ${CMAKE_INSTALL_PREFIX}/MacOS)
|
||||
if(EXISTS "${QT_ASSISTANT_PATH}")
|
||||
install(PROGRAMS "${QT_ASSISTANT_PATH}" DESTINATION ${CMAKE_INSTALL_PREFIX}/MacOS)
|
||||
else()
|
||||
message(WARNING "Qt Assistant not found at ${QT_ASSISTANT_PATH}, skipping installation")
|
||||
endif()
|
||||
|
||||
# Ensure the actual plugin files are installed instead of symlinks.
|
||||
file(GLOB _subdirs RELATIVE "${QT_PLUGINS_DIR}" "${QT_PLUGINS_DIR}/*")
|
||||
|
||||
foreach(_subdir ${_subdirs})
|
||||
# Install specific Qt plugin subdirectories that FreeCAD needs
|
||||
foreach(_subdir ${QT_PLUGIN_SUBDIRS})
|
||||
if(IS_DIRECTORY "${QT_PLUGINS_DIR}/${_subdir}")
|
||||
set(_resolved_files "")
|
||||
file(GLOB _plugin_files RELATIVE "${QT_PLUGINS_DIR}/${_subdir}" "${QT_PLUGINS_DIR}/${_subdir}/*")
|
||||
file(GLOB _plugin_files "${QT_PLUGINS_DIR}/${_subdir}/*.dylib")
|
||||
|
||||
foreach(_plugin_file ${_plugin_files})
|
||||
get_filename_component(_resolved_file "${QT_PLUGINS_DIR}/${_subdir}/${_plugin_file}" REALPATH)
|
||||
list(APPEND _resolved_files ${_resolved_file})
|
||||
if(EXISTS "${_plugin_file}" AND NOT IS_DIRECTORY "${_plugin_file}")
|
||||
get_filename_component(_resolved_file "${_plugin_file}" REALPATH)
|
||||
list(APPEND _resolved_files ${_resolved_file})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_LIBDIR}/qtplugins/${_subdir}")
|
||||
if(_resolved_files)
|
||||
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_LIBDIR}/qtplugins/${_subdir}")
|
||||
|
||||
# Also install platform plugins to standard Qt location for runtime loading
|
||||
if("${_subdir}" STREQUAL "platforms")
|
||||
install(FILES ${_resolved_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns/platforms")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -194,9 +214,65 @@ install(CODE
|
||||
${HOMEBREW_PREFIX}${MACPORTS_PREFIX}/lib
|
||||
${HOMEBREW_PREFIX}/opt
|
||||
${HOMEBREW_PREFIX}/opt/*/lib
|
||||
${Qt5Core_DIR}/../../..
|
||||
${Qt${FREECAD_QT_MAJOR_VERSION}Core_DIR}/../../..
|
||||
${XCTEST_PATH}
|
||||
)"
|
||||
)
|
||||
|
||||
# Fix Qt platform plugin dependencies after bundle relocation
|
||||
# This addresses the SIGKILL issue where Qt plugins reference framework paths
|
||||
# but the Qt libraries are installed as flat dylibs
|
||||
install(CODE "
|
||||
message(STATUS \"Fixing Qt platform plugin dependencies...\")
|
||||
|
||||
# Find platform plugins
|
||||
file(GLOB_RECURSE PLATFORM_PLUGINS \"${CMAKE_INSTALL_PREFIX}/PlugIns/platforms/*.dylib\")
|
||||
|
||||
foreach(PLUGIN \${PLATFORM_PLUGINS})
|
||||
message(STATUS \"Processing plugin: \${PLUGIN}\")
|
||||
|
||||
# Get current dependencies
|
||||
execute_process(
|
||||
COMMAND otool -L \"\${PLUGIN}\"
|
||||
OUTPUT_VARIABLE PLUGIN_DEPS
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
# Fix framework-style Qt dependencies to use flat dylib names
|
||||
if(FREECAD_QT_MAJOR_VERSION STREQUAL \"6\")
|
||||
set(QT_FRAMEWORKS \"QtCore;QtGui;QtWidgets;QtOpenGL;QtPrintSupport;QtSvg;QtNetwork\")
|
||||
else()
|
||||
set(QT_FRAMEWORKS \"QtCore;QtGui;QtWidgets;QtOpenGL;QtPrintSupport;QtSvg;QtNetwork\")
|
||||
endif()
|
||||
|
||||
foreach(FRAMEWORK \${QT_FRAMEWORKS})
|
||||
# Change @rpath/QtXXX.framework/Versions/A/QtXXX to @rpath/QtXXX
|
||||
execute_process(
|
||||
COMMAND install_name_tool -change
|
||||
\"@rpath/\${FRAMEWORK}.framework/Versions/A/\${FRAMEWORK}\"
|
||||
\"@rpath/\${FRAMEWORK}\"
|
||||
\"\${PLUGIN}\"
|
||||
OUTPUT_QUIET
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
# Also handle versioned framework paths for Qt5
|
||||
if(FREECAD_QT_MAJOR_VERSION STREQUAL \"5\")
|
||||
execute_process(
|
||||
COMMAND install_name_tool -change
|
||||
\"@rpath/\${FRAMEWORK}.framework/Versions/5/\${FRAMEWORK}\"
|
||||
\"@rpath/\${FRAMEWORK}\"
|
||||
\"\${PLUGIN}\"
|
||||
OUTPUT_QUIET
|
||||
ERROR_QUIET
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
message(STATUS \"Fixed dependencies for: \${PLUGIN}\")
|
||||
endforeach()
|
||||
|
||||
message(STATUS \"Qt platform plugin dependency fixing complete.\")
|
||||
")
|
||||
|
||||
endif(FREECAD_CREATE_MAC_APP)
|
||||
|
||||
@@ -1,36 +1,156 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#cmake_minimum_required(VERSION 3.23)
|
||||
#project(FCQuickLook)
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(FreeCADQuickLook)
|
||||
|
||||
add_library(
|
||||
QuicklookFCStd
|
||||
SHARED
|
||||
GeneratePreviewForURL.m
|
||||
GenerateThumbnailForURL.m
|
||||
main.c
|
||||
# Configuration options for QuickLook support
|
||||
option(FREECAD_QUICKLOOK_LEGACY_SUPPORT "Build legacy .qlgenerator for older macOS compatibility" ON)
|
||||
option(FREECAD_QUICKLOOK_MODERN_SUPPORT "Build modern .appex extensions for macOS 15.0+" ON)
|
||||
|
||||
|
||||
# Only build QuickLook extensions with Unix Makefiles or Ninja generator
|
||||
if(NOT CMAKE_GENERATOR MATCHES "Unix Makefiles|Ninja")
|
||||
message(STATUS "QuickLook extensions only supported with Unix Makefiles or Ninja generators (current: ${CMAKE_GENERATOR})")
|
||||
add_custom_target(FreeCADQuickLook
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "QuickLook extensions require Unix Makefiles or Ninja generator"
|
||||
COMMENT "QuickLook placeholder target"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Validate CMAKE_OSX_DEPLOYMENT_TARGET is set (or use system default)
|
||||
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
# If not explicitly set, query the system default
|
||||
execute_process(
|
||||
COMMAND xcrun --show-sdk-version
|
||||
OUTPUT_VARIABLE MACOS_SDK_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if(MACOS_SDK_VERSION)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET ${MACOS_SDK_VERSION})
|
||||
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET not set, using SDK version: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
else()
|
||||
message(WARNING "CMAKE_OSX_DEPLOYMENT_TARGET not set and could not determine SDK version. Defaulting to 15.0")
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Determine which QuickLook implementations to build
|
||||
set(BUILD_MODERN_EXTENSIONS OFF)
|
||||
set(BUILD_LEGACY_GENERATOR OFF)
|
||||
|
||||
if(FREECAD_QUICKLOOK_MODERN_SUPPORT)
|
||||
if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_GREATER_EQUAL "15.0")
|
||||
set(BUILD_MODERN_EXTENSIONS ON)
|
||||
message(STATUS "Building modern Swift .appex QuickLook extensions (macOS 15.0+)")
|
||||
else()
|
||||
message(STATUS "Modern QuickLook extensions require macOS 15.0+, current target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(FREECAD_QUICKLOOK_LEGACY_SUPPORT)
|
||||
set(BUILD_LEGACY_GENERATOR ON)
|
||||
message(STATUS "Building legacy .qlgenerator QuickLook support")
|
||||
endif()
|
||||
|
||||
if(NOT BUILD_MODERN_EXTENSIONS AND NOT BUILD_LEGACY_GENERATOR)
|
||||
message(WARNING "No QuickLook implementations enabled. Enable FREECAD_QUICKLOOK_MODERN_SUPPORT or FREECAD_QUICKLOOK_LEGACY_SUPPORT")
|
||||
add_custom_target(FreeCADQuickLook
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "No QuickLook implementations enabled"
|
||||
COMMENT "QuickLook disabled target"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Global configuration
|
||||
set(FREECAD_BUNDLE_ID "org.freecad.FreeCAD")
|
||||
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
|
||||
|
||||
# Detect target architecture
|
||||
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
|
||||
set(TARGET_ARCH "arm64")
|
||||
else()
|
||||
set(TARGET_ARCH "x86_64")
|
||||
endif()
|
||||
|
||||
# Build subdirectories conditionally
|
||||
set(QUICKLOOK_TARGETS "")
|
||||
|
||||
if(BUILD_MODERN_EXTENSIONS)
|
||||
add_subdirectory(modern)
|
||||
list(APPEND QUICKLOOK_TARGETS FreeCADModernQuickLook)
|
||||
endif()
|
||||
|
||||
if(BUILD_LEGACY_GENERATOR)
|
||||
add_subdirectory(legacy)
|
||||
list(APPEND QUICKLOOK_TARGETS FreeCADLegacyQuickLook)
|
||||
endif()
|
||||
|
||||
# Main target that coordinates all QuickLook implementations
|
||||
add_custom_target(FreeCADQuickLook ALL
|
||||
DEPENDS ${QUICKLOOK_TARGETS}
|
||||
COMMENT "FreeCAD QuickLook support integrated into main app"
|
||||
)
|
||||
|
||||
set_target_properties(
|
||||
QuicklookFCStd
|
||||
PROPERTIES
|
||||
FRAMEWORK TRUE
|
||||
MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/QuicklookFCStd.qlgenerator/Contents/Info.plist"
|
||||
#SUFFIX .qlgenerator
|
||||
# Ensure QuickLook is built after main FreeCAD app if it's available as a target
|
||||
if(TARGET FreeCAD)
|
||||
add_dependencies(FreeCADQuickLook FreeCAD)
|
||||
endif()
|
||||
|
||||
# Combined verification target
|
||||
set(VERIFY_COMMANDS "")
|
||||
if(BUILD_MODERN_EXTENSIONS)
|
||||
list(APPEND VERIFY_COMMANDS
|
||||
COMMAND ${CMAKE_COMMAND} --build . --target verify_modern_build
|
||||
)
|
||||
endif()
|
||||
if(BUILD_LEGACY_GENERATOR)
|
||||
list(APPEND VERIFY_COMMANDS
|
||||
COMMAND ${CMAKE_COMMAND} --build . --target verify_legacy_build
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_target(verify_build
|
||||
${VERIFY_COMMANDS}
|
||||
DEPENDS FreeCADQuickLook
|
||||
COMMENT "Verifying all QuickLook implementations"
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
QuicklookFCStd
|
||||
"-framework AppKit"
|
||||
"-framework ApplicationServices"
|
||||
"-framework CoreData"
|
||||
"-framework CoreFoundation"
|
||||
"-framework CoreServices"
|
||||
"-framework Foundation"
|
||||
"-framework QuickLook"
|
||||
# Combined test registration target
|
||||
set(REGISTRATION_COMMANDS "")
|
||||
if(BUILD_MODERN_EXTENSIONS)
|
||||
list(APPEND REGISTRATION_COMMANDS
|
||||
COMMAND ${CMAKE_COMMAND} --build . --target test_modern_registration
|
||||
)
|
||||
endif()
|
||||
if(BUILD_LEGACY_GENERATOR)
|
||||
list(APPEND REGISTRATION_COMMANDS
|
||||
COMMAND ${CMAKE_COMMAND} --build . --target test_legacy_registration
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_target(test_registration
|
||||
${REGISTRATION_COMMANDS}
|
||||
DEPENDS FreeCADQuickLook
|
||||
COMMENT "Testing all QuickLook registrations"
|
||||
)
|
||||
|
||||
set_target_properties(
|
||||
QuicklookFCStd
|
||||
PROPERTIES LINK_FLAGS "-Wl,-F/Library/Frameworks"
|
||||
)
|
||||
# Installation components
|
||||
if(BUILD_MODERN_EXTENSIONS)
|
||||
# Modern extensions install themselves via their CMakeLists.txt
|
||||
endif()
|
||||
|
||||
if(BUILD_LEGACY_GENERATOR)
|
||||
# Legacy generator installs itself via its CMakeLists.txt
|
||||
endif()
|
||||
|
||||
# Status summary
|
||||
message(STATUS "FreeCAD QuickLook Configuration Summary:")
|
||||
message(STATUS " Generator: ${CMAKE_GENERATOR}")
|
||||
message(STATUS " Target Architecture: ${TARGET_ARCH}")
|
||||
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
message(STATUS " Target FreeCAD.app: ${TARGET_APP_BUNDLE}")
|
||||
message(STATUS " Modern Extensions (.appex): ${BUILD_MODERN_EXTENSIONS}")
|
||||
message(STATUS " Legacy Generator (.qlgenerator): ${BUILD_LEGACY_GENERATOR}")
|
||||
|
||||
message(STATUS " Code Signing: Handled by official src/Tools/macos_sign_and_notarize.sh script")
|
||||
|
||||
97
src/MacAppBundle/QuickLook/legacy/CMakeLists.txt
Normal file
97
src/MacAppBundle/QuickLook/legacy/CMakeLists.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# Legacy Objective-C QuickLook Generator for older macOS compatibility
|
||||
# This file handles building .qlgenerator bundles using Objective-C/C
|
||||
|
||||
# Configuration options (inherited from parent)
|
||||
# CMAKE_OSX_DEPLOYMENT_TARGET
|
||||
|
||||
# Paths (inherited from parent)
|
||||
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
|
||||
set(LEGACY_QUICKLOOK_DIR "${TARGET_APP_BUNDLE}/Contents/Library/QuickLook")
|
||||
|
||||
# Build legacy QuickLook generator using traditional CMake approach
|
||||
add_library(
|
||||
QuicklookFCStd
|
||||
SHARED
|
||||
GeneratePreviewForURL.m
|
||||
GenerateThumbnailForURL.m
|
||||
main.c
|
||||
)
|
||||
|
||||
set_target_properties(
|
||||
QuicklookFCStd
|
||||
PROPERTIES
|
||||
FRAMEWORK TRUE
|
||||
MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/QuicklookFCStd.qlgenerator/Contents/Info.plist"
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
QuicklookFCStd
|
||||
"-framework AppKit"
|
||||
"-framework ApplicationServices"
|
||||
"-framework CoreData"
|
||||
"-framework CoreFoundation"
|
||||
"-framework CoreServices"
|
||||
"-framework Foundation"
|
||||
"-framework QuickLook"
|
||||
)
|
||||
|
||||
set_target_properties(
|
||||
QuicklookFCStd
|
||||
PROPERTIES LINK_FLAGS "-Wl,-F/Library/Frameworks"
|
||||
)
|
||||
|
||||
# Create bundle directory structure
|
||||
add_custom_target(create_legacy_bundle_dirs ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${LEGACY_QUICKLOOK_DIR}
|
||||
COMMENT "Creating legacy QuickLook directory structure"
|
||||
)
|
||||
|
||||
# Install legacy generator to the system QuickLook directory within the app bundle
|
||||
add_custom_target(embed_legacy_generator ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"$<TARGET_BUNDLE_DIR:QuicklookFCStd>"
|
||||
"${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator"
|
||||
DEPENDS
|
||||
QuicklookFCStd
|
||||
create_legacy_bundle_dirs
|
||||
COMMENT "Embedding legacy QuickLook generator in main FreeCAD.app"
|
||||
)
|
||||
|
||||
# Main legacy generator target
|
||||
add_custom_target(FreeCADLegacyQuickLook ALL
|
||||
DEPENDS embed_legacy_generator
|
||||
COMMENT "Legacy Objective-C QuickLook generator built"
|
||||
)
|
||||
|
||||
# Install target for legacy generator
|
||||
install(DIRECTORY ${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/Library/QuickLook"
|
||||
COMPONENT LegacyQuickLook
|
||||
USE_SOURCE_PERMISSIONS
|
||||
)
|
||||
|
||||
# Verification target
|
||||
add_custom_target(verify_legacy_build
|
||||
COMMAND test -f ${LEGACY_QUICKLOOK_DIR}/QuicklookFCStd.qlgenerator/Contents/MacOS/QuicklookFCStd
|
||||
COMMAND echo "Legacy generator successfully embedded in FreeCAD.app"
|
||||
DEPENDS FreeCADLegacyQuickLook
|
||||
COMMENT "Verifying legacy QuickLook generator integration"
|
||||
)
|
||||
|
||||
# Test registration target
|
||||
add_custom_target(test_legacy_registration
|
||||
COMMAND echo "Legacy QuickLook generator will be registered automatically by the system"
|
||||
COMMAND qlmanage -r || echo "QuickLook cache reset attempted"
|
||||
DEPENDS FreeCADLegacyQuickLook
|
||||
COMMENT "Testing legacy QuickLook generator registration"
|
||||
)
|
||||
|
||||
# Status messages
|
||||
message(STATUS "Legacy Objective-C QuickLook Generator Configuration:")
|
||||
message(STATUS " Target Library: QuicklookFCStd")
|
||||
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
message(STATUS " Legacy QuickLook Directory: ${LEGACY_QUICKLOOK_DIR}")
|
||||
message(STATUS " Bundle ID: org.freecad.qlgenerator.QuicklookFCStd")
|
||||
message(STATUS " Code Signing: Handled by official macos_sign_and_notarize.sh script")
|
||||
@@ -210,5 +210,4 @@ void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID)
|
||||
}
|
||||
/* If the requested type is incorrect, return NULL. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
220
src/MacAppBundle/QuickLook/modern/CMakeLists.txt
Normal file
220
src/MacAppBundle/QuickLook/modern/CMakeLists.txt
Normal file
@@ -0,0 +1,220 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
# Modern Swift QuickLook Extensions for macOS 15.0+
|
||||
# This file handles building .appex extensions using Swift
|
||||
|
||||
# Configuration options (inherited from parent)
|
||||
# CMAKE_OSX_DEPLOYMENT_TARGET
|
||||
|
||||
# Check for required tools
|
||||
find_program(SWIFTC_EXECUTABLE swiftc REQUIRED)
|
||||
find_program(PLUTIL_EXECUTABLE plutil REQUIRED)
|
||||
|
||||
# Find macOS SDK
|
||||
execute_process(
|
||||
COMMAND xcrun --show-sdk-path --sdk macosx
|
||||
OUTPUT_VARIABLE MACOS_SDK_PATH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE SDK_RESULT
|
||||
)
|
||||
|
||||
if(NOT SDK_RESULT EQUAL 0 OR NOT EXISTS "${MACOS_SDK_PATH}")
|
||||
message(FATAL_ERROR "macOS SDK not found. Please install Xcode Command Line Tools.")
|
||||
endif()
|
||||
|
||||
message(STATUS "Modern QuickLook: Using macOS SDK: ${MACOS_SDK_PATH}")
|
||||
|
||||
# Bundle identifiers (inherited from parent)
|
||||
set(FREECAD_BUNDLE_ID "org.freecad.FreeCAD")
|
||||
set(THUMBNAIL_BUNDLE_ID "${FREECAD_BUNDLE_ID}.quicklook.thumbnail")
|
||||
set(PREVIEW_BUNDLE_ID "${FREECAD_BUNDLE_ID}.quicklook.preview")
|
||||
|
||||
# Paths (inherited from parent)
|
||||
set(TARGET_APP_BUNDLE "${CMAKE_BINARY_DIR}/src/MacAppBundle/FreeCAD.app")
|
||||
set(EXTENSIONS_DIR "${TARGET_APP_BUNDLE}/Contents/PlugIns")
|
||||
|
||||
# Build directories
|
||||
set(BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
set(THUMBNAIL_EXT_DIR "${BUILD_DIR}/FreeCADThumbnailExtension.appex")
|
||||
set(PREVIEW_EXT_DIR "${BUILD_DIR}/FreeCADPreviewExtension.appex")
|
||||
|
||||
# Detect target architecture
|
||||
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
|
||||
set(TARGET_ARCH "arm64")
|
||||
else()
|
||||
set(TARGET_ARCH "x86_64")
|
||||
endif()
|
||||
|
||||
# Swift compilation flags
|
||||
set(SWIFT_FLAGS
|
||||
-target ${TARGET_ARCH}-apple-macosx${CMAKE_OSX_DEPLOYMENT_TARGET}
|
||||
-sdk ${MACOS_SDK_PATH}
|
||||
-O
|
||||
-whole-module-optimization
|
||||
-enable-library-evolution
|
||||
-swift-version 5
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Function to process Info.plist files
|
||||
function(process_plist input_plist output_plist bundle_id executable_name product_name principal_class)
|
||||
add_custom_command(
|
||||
OUTPUT ${output_plist}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${input_plist} ${output_plist}
|
||||
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleIdentifier -string ${bundle_id} ${output_plist}
|
||||
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleExecutable -string ${executable_name} ${output_plist}
|
||||
COMMAND ${PLUTIL_EXECUTABLE} -replace CFBundleName -string ${product_name} ${output_plist}
|
||||
COMMAND ${PLUTIL_EXECUTABLE} -replace NSExtension.NSExtensionPrincipalClass -string ${principal_class} ${output_plist}
|
||||
DEPENDS ${input_plist}
|
||||
COMMENT "Processing extension ${input_plist} -> ${output_plist}"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# Create bundle directory structure
|
||||
add_custom_target(create_modern_bundle_dirs ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${EXTENSIONS_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${THUMBNAIL_EXT_DIR}/Contents/MacOS
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${THUMBNAIL_EXT_DIR}/Contents/Resources
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${PREVIEW_EXT_DIR}/Contents/MacOS
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${PREVIEW_EXT_DIR}/Contents/Resources
|
||||
COMMENT "Creating modern extensions directory structure"
|
||||
)
|
||||
|
||||
# Process Info.plist files
|
||||
process_plist(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailExtensionInfo.plist"
|
||||
"${THUMBNAIL_EXT_DIR}/Contents/Info.plist"
|
||||
${THUMBNAIL_BUNDLE_ID}
|
||||
"FreeCADThumbnailExtension"
|
||||
"FreeCADThumbnailExtension"
|
||||
"FreeCADThumbnailExtension.ThumbnailProvider"
|
||||
)
|
||||
|
||||
process_plist(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/PreviewExtensionInfo.plist"
|
||||
"${PREVIEW_EXT_DIR}/Contents/Info.plist"
|
||||
${PREVIEW_BUNDLE_ID}
|
||||
"FreeCADPreviewExtension"
|
||||
"FreeCADPreviewExtension"
|
||||
"FreeCADPreviewExtension.PreviewProvider"
|
||||
)
|
||||
|
||||
# Compile Thumbnail Extension
|
||||
add_custom_target(compile_thumbnail_extension ALL
|
||||
COMMAND ${SWIFTC_EXECUTABLE}
|
||||
${SWIFT_FLAGS}
|
||||
-emit-executable
|
||||
-module-name FreeCADThumbnailExtension
|
||||
-parse-as-library
|
||||
-Xlinker -e -Xlinker _NSExtensionMain
|
||||
-Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
||||
-Xlinker -rpath -Xlinker @executable_path/../../../../Frameworks
|
||||
-framework Foundation
|
||||
-framework CoreGraphics
|
||||
-framework ImageIO
|
||||
-framework QuickLookThumbnailing
|
||||
-framework AppKit
|
||||
-lcompression
|
||||
-o ${THUMBNAIL_EXT_DIR}/Contents/MacOS/FreeCADThumbnailExtension
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailProvider.swift
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
|
||||
DEPENDS
|
||||
create_modern_bundle_dirs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ThumbnailProvider.swift
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
|
||||
${THUMBNAIL_EXT_DIR}/Contents/Info.plist
|
||||
COMMENT "Compiling Thumbnail Extension"
|
||||
)
|
||||
|
||||
# Compile Preview Extension
|
||||
add_custom_target(compile_preview_extension ALL
|
||||
COMMAND ${SWIFTC_EXECUTABLE}
|
||||
${SWIFT_FLAGS}
|
||||
-emit-executable
|
||||
-module-name FreeCADPreviewExtension
|
||||
-parse-as-library
|
||||
-Xlinker -e -Xlinker _NSExtensionMain
|
||||
-Xlinker -rpath -Xlinker @executable_path/../Frameworks
|
||||
-Xlinker -rpath -Xlinker @executable_path/../../../../Frameworks
|
||||
-framework Foundation
|
||||
-framework CoreGraphics
|
||||
-framework ImageIO
|
||||
-framework Quartz
|
||||
-framework Cocoa
|
||||
-framework UniformTypeIdentifiers
|
||||
-lcompression
|
||||
-o ${PREVIEW_EXT_DIR}/Contents/MacOS/FreeCADPreviewExtension
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PreviewProvider.swift
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
|
||||
DEPENDS
|
||||
create_modern_bundle_dirs
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PreviewProvider.swift
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ZipExtractor.swift
|
||||
${PREVIEW_EXT_DIR}/Contents/Info.plist
|
||||
COMMENT "Compiling Preview Extension"
|
||||
)
|
||||
|
||||
# Embed unsigned extensions in main FreeCAD.app
|
||||
add_custom_target(embed_modern_extensions ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${THUMBNAIL_EXT_DIR}
|
||||
${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${PREVIEW_EXT_DIR}
|
||||
${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
|
||||
DEPENDS
|
||||
compile_thumbnail_extension
|
||||
compile_preview_extension
|
||||
COMMENT "Embedding modern extensions in main FreeCAD.app"
|
||||
)
|
||||
|
||||
# Main modern extensions target
|
||||
add_custom_target(FreeCADModernQuickLook ALL
|
||||
DEPENDS embed_modern_extensions
|
||||
COMMENT "Modern Swift QuickLook extensions built"
|
||||
)
|
||||
|
||||
# Install targets for modern extensions
|
||||
install(DIRECTORY ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns"
|
||||
COMPONENT ModernQuickLook
|
||||
USE_SOURCE_PERMISSIONS
|
||||
)
|
||||
|
||||
install(DIRECTORY ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/PlugIns"
|
||||
COMPONENT ModernQuickLook
|
||||
USE_SOURCE_PERMISSIONS
|
||||
)
|
||||
|
||||
# Verification target
|
||||
add_custom_target(verify_modern_build
|
||||
COMMAND test -f ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex/Contents/MacOS/FreeCADThumbnailExtension
|
||||
COMMAND test -f ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex/Contents/MacOS/FreeCADPreviewExtension
|
||||
COMMAND echo "Modern extensions successfully embedded in FreeCAD.app"
|
||||
DEPENDS FreeCADModernQuickLook
|
||||
COMMENT "Verifying modern QuickLook extension integration"
|
||||
)
|
||||
|
||||
# Test registration target
|
||||
add_custom_target(test_modern_registration
|
||||
COMMAND pluginkit -a ${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex
|
||||
COMMAND pluginkit -a ${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex
|
||||
COMMAND pluginkit -e use -i ${THUMBNAIL_BUNDLE_ID}
|
||||
COMMAND pluginkit -e use -i ${PREVIEW_BUNDLE_ID}
|
||||
COMMAND pluginkit -m -v -i ${THUMBNAIL_BUNDLE_ID} || echo "Thumbnail extension registration status unknown"
|
||||
COMMAND pluginkit -m -v -i ${PREVIEW_BUNDLE_ID} || echo "Preview extension registration status unknown"
|
||||
DEPENDS FreeCADModernQuickLook
|
||||
COMMENT "Testing modern QuickLook extension registration"
|
||||
)
|
||||
|
||||
# Status messages
|
||||
message(STATUS "Modern Swift QuickLook Extensions Configuration:")
|
||||
message(STATUS " Swift Compiler: ${SWIFTC_EXECUTABLE}")
|
||||
message(STATUS " Target Architecture: ${TARGET_ARCH}")
|
||||
message(STATUS " Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
|
||||
message(STATUS " Thumbnail Bundle ID: ${THUMBNAIL_BUNDLE_ID}")
|
||||
message(STATUS " Preview Bundle ID: ${PREVIEW_BUNDLE_ID}")
|
||||
message(STATUS " Extensions Directory: ${EXTENSIONS_DIR}")
|
||||
message(STATUS " Code Signing: Handled by official macos_sign_and_notarize.sh script")
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.bookmarks.app-scope</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
48
src/MacAppBundle/QuickLook/modern/PreviewExtensionInfo.plist
Normal file
48
src/MacAppBundle/QuickLook/modern/PreviewExtensionInfo.plist
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>QuickLookPreviewExtension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>15.0</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>QLIsDataBasedPreview</key>
|
||||
<true/>
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<string>org.freecad.fcstd</string>
|
||||
</array>
|
||||
<key>QLSupportsSearchableItems</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.quicklook.preview</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PreviewProvider</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
77
src/MacAppBundle/QuickLook/modern/PreviewProvider.swift
Normal file
77
src/MacAppBundle/QuickLook/modern/PreviewProvider.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Cocoa
|
||||
import Quartz
|
||||
import UniformTypeIdentifiers
|
||||
import os.log
|
||||
|
||||
private let logger = Logger(
|
||||
subsystem: Bundle(for: PreviewProvider.self).bundleIdentifier
|
||||
?? "org.freecad.quicklook.fallback",
|
||||
category: "PreviewProvider"
|
||||
)
|
||||
|
||||
class PreviewProvider: QLPreviewProvider, QLPreviewingController {
|
||||
|
||||
func providePreview(for request: QLFilePreviewRequest) async throws
|
||||
-> QLPreviewReply
|
||||
{
|
||||
logger.debug(
|
||||
"--- PreviewProvider: providePreview CALLED for \(request.fileURL.lastPathComponent) ---"
|
||||
)
|
||||
|
||||
let fileURL = request.fileURL
|
||||
logger.info("Received file URL: \(fileURL.path)")
|
||||
|
||||
guard let image = try? SwiftZIPParser.extractThumbnail(from: fileURL) else {
|
||||
let errorMessage =
|
||||
"Failed to extract thumbnail from FreeCAD file: \(fileURL.lastPathComponent)"
|
||||
logger.error("\(errorMessage)")
|
||||
throw NSError(
|
||||
domain: Bundle(for: PreviewProvider.self).bundleIdentifier
|
||||
?? "org.freecad.quicklook.fallback",
|
||||
code: 1001,
|
||||
userInfo: [NSLocalizedDescriptionKey: errorMessage]
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Thumbnail extracted successfully. Image size: \(image.width)x\(image.height)"
|
||||
)
|
||||
|
||||
let imageSize = CGSize(
|
||||
width: CGFloat(image.width),
|
||||
height: CGFloat(image.height)
|
||||
)
|
||||
logger.debug(
|
||||
"Preview contextSize will be: \(imageSize.width)x\(imageSize.height)"
|
||||
)
|
||||
|
||||
// Ensure imageSize is valid and positive
|
||||
if imageSize.width <= 0 || imageSize.height <= 0 {
|
||||
let errorMessage =
|
||||
"Cannot create preview with zero or negative dimensions: \(imageSize) for file: \(fileURL.lastPathComponent)"
|
||||
logger.error("\(errorMessage)")
|
||||
throw NSError(
|
||||
domain: Bundle(for: PreviewProvider.self).bundleIdentifier
|
||||
?? "org.freecad.quicklook.fallback",
|
||||
code: 1003,
|
||||
userInfo: [NSLocalizedDescriptionKey: errorMessage]
|
||||
)
|
||||
}
|
||||
|
||||
let reply = QLPreviewReply(contextSize: imageSize, isBitmap: true) {
|
||||
context,
|
||||
_ in
|
||||
logger.info("Drawing block started. Drawing extracted thumbnail.")
|
||||
|
||||
// Draw the extracted thumbnail image
|
||||
context.draw(image, in: CGRect(origin: .zero, size: imageSize))
|
||||
logger.debug("Thumbnail image drawn in context.")
|
||||
|
||||
logger.info("Drawing block finished.")
|
||||
return
|
||||
}
|
||||
|
||||
logger.notice("QLPreviewReply created. Returning reply.")
|
||||
return reply
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.bookmarks.app-scope</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>QuickLookThumbnailExtension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>15.0</string>
|
||||
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>QLIsDataBasedThumbnail</key>
|
||||
<true/>
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<string>org.freecad.fcstd</string>
|
||||
</array>
|
||||
<key>QLSupportsSearchableItems</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.quicklook.thumbnail</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ThumbnailProvider</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
42
src/MacAppBundle/QuickLook/modern/ThumbnailProvider.swift
Normal file
42
src/MacAppBundle/QuickLook/modern/ThumbnailProvider.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import AppKit
|
||||
import QuickLookThumbnailing
|
||||
import os.log
|
||||
|
||||
private let logger = Logger(
|
||||
subsystem: Bundle(for: ThumbnailProvider.self).bundleIdentifier
|
||||
?? "org.freecad.quicklook.thumbnail.fallback",
|
||||
category: "ThumbnailProvider"
|
||||
)
|
||||
|
||||
class ThumbnailProvider: QLThumbnailProvider {
|
||||
|
||||
override func provideThumbnail(
|
||||
for request: QLFileThumbnailRequest,
|
||||
_ handler: @escaping (QLThumbnailReply?, Error?) -> Void
|
||||
) {
|
||||
|
||||
logger.debug(
|
||||
"Providing thumbnail for: \(request.fileURL.path)"
|
||||
)
|
||||
|
||||
guard
|
||||
let cgImage = try? SwiftZIPParser.extractThumbnail(
|
||||
from: request.fileURL, maxSize: request.maximumSize)
|
||||
else {
|
||||
logger.warning("No valid thumbnail found; returning empty reply.")
|
||||
handler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
let reply = QLThumbnailReply(
|
||||
contextSize: request.maximumSize,
|
||||
currentContextDrawing: { () -> Bool in
|
||||
let image = NSImage(cgImage: cgImage, size: request.maximumSize)
|
||||
image.draw(in: CGRect(origin: .zero, size: request.maximumSize))
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
handler(reply, nil)
|
||||
}
|
||||
}
|
||||
474
src/MacAppBundle/QuickLook/modern/ZipExtractor.swift
Normal file
474
src/MacAppBundle/QuickLook/modern/ZipExtractor.swift
Normal file
@@ -0,0 +1,474 @@
|
||||
//
|
||||
// ZipExtractor.swift
|
||||
// FreeCAD QuickLook Swift Implementation
|
||||
//
|
||||
// Pure Swift ZIP parser for extracting thumbnails from FreeCAD (.FCStd) files
|
||||
// This removes external dependencies while maintaining modern Swift APIs
|
||||
//
|
||||
// Created for integration with FreeCAD upstream
|
||||
//
|
||||
|
||||
import Compression
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import os.log
|
||||
|
||||
// MARK: - ZIP File Format Constants
|
||||
|
||||
private struct ZIPConstants {
|
||||
static let localFileSignature: UInt32 = 0x0403_4b50
|
||||
static let centralDirSignature: UInt32 = 0x0201_4b50
|
||||
static let endOfCentralDirSignature: UInt32 = 0x0605_4b50
|
||||
|
||||
static let compressionStored: UInt16 = 0
|
||||
static let compressionDeflate: UInt16 = 8
|
||||
}
|
||||
|
||||
// MARK: - ZIP Structures
|
||||
|
||||
private struct ZIPLocalFileHeader {
|
||||
let signature: UInt32
|
||||
let version: UInt16
|
||||
let flags: UInt16
|
||||
let compression: UInt16
|
||||
let modTime: UInt16
|
||||
let modDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let filenameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
|
||||
static let size = 30
|
||||
}
|
||||
|
||||
private struct ZIPCentralDirHeader {
|
||||
let signature: UInt32
|
||||
let versionMadeBy: UInt16
|
||||
let versionNeeded: UInt16
|
||||
let flags: UInt16
|
||||
let compression: UInt16
|
||||
let modTime: UInt16
|
||||
let modDate: UInt16
|
||||
let crc32: UInt32
|
||||
let compressedSize: UInt32
|
||||
let uncompressedSize: UInt32
|
||||
let filenameLength: UInt16
|
||||
let extraFieldLength: UInt16
|
||||
let commentLength: UInt16
|
||||
let diskNumber: UInt16
|
||||
let internalAttributes: UInt16
|
||||
let externalAttributes: UInt32
|
||||
let localHeaderOffset: UInt32
|
||||
|
||||
static let size = 46
|
||||
}
|
||||
|
||||
private struct ZIPEndOfCentralDir {
|
||||
let signature: UInt32
|
||||
let diskNumber: UInt16
|
||||
let centralDirDisk: UInt16
|
||||
let entriesOnDisk: UInt16
|
||||
let totalEntries: UInt16
|
||||
let centralDirSize: UInt32
|
||||
let centralDirOffset: UInt32
|
||||
let commentLength: UInt16
|
||||
|
||||
static let size = 22
|
||||
}
|
||||
|
||||
// MARK: - Error Types
|
||||
|
||||
enum ZIPParserError: Error, LocalizedError {
|
||||
case fileNotFound
|
||||
case invalidZipFile
|
||||
case corruptedZipFile
|
||||
case thumbnailNotFound
|
||||
case compressionUnsupported
|
||||
case decompressionFailed
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .fileNotFound:
|
||||
return "FCStd file not found"
|
||||
case .invalidZipFile:
|
||||
return "Invalid ZIP file format"
|
||||
case .corruptedZipFile:
|
||||
return "Corrupted ZIP file"
|
||||
case .thumbnailNotFound:
|
||||
return "No thumbnail found in FCStd file"
|
||||
case .compressionUnsupported:
|
||||
return "Unsupported compression method"
|
||||
case .decompressionFailed:
|
||||
return "Failed to decompress file data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Pure Swift ZIP Parser
|
||||
|
||||
struct SwiftZIPParser {
|
||||
private static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier ?? "org.freecad.quicklook",
|
||||
category: "SwiftZIPParser"
|
||||
)
|
||||
|
||||
/// Extract thumbnail from FCStd file
|
||||
static func extractThumbnail(from fileURL: URL, maxSize: CGSize? = nil) throws -> CGImage {
|
||||
logger.debug("Extracting thumbnail from file: \(fileURL.path)")
|
||||
logger.info("=== SwiftZIPParser.extractThumbnail called ===")
|
||||
logger.info("File URL: \(fileURL.path)")
|
||||
logger.info("File exists: \(FileManager.default.fileExists(atPath: fileURL.path))")
|
||||
do {
|
||||
let isReachable = try fileURL.checkResourceIsReachable()
|
||||
logger.info("File is readable: \(isReachable)")
|
||||
} catch {
|
||||
logger.info("File is readable: false (error: \(error.localizedDescription))")
|
||||
}
|
||||
|
||||
// Handle security scoped resources
|
||||
let didStartAccessing = fileURL.startAccessingSecurityScopedResource()
|
||||
logger.info("Started accessing security scoped resource: \(didStartAccessing)")
|
||||
defer {
|
||||
if didStartAccessing {
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
logger.debug("Stopped accessing security scoped resource")
|
||||
}
|
||||
}
|
||||
|
||||
// Read file data
|
||||
let zipData: Data
|
||||
do {
|
||||
zipData = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
logger.info("Successfully read \(zipData.count) bytes from file")
|
||||
} catch {
|
||||
logger.error(
|
||||
"Failed to read file data: \(error.localizedDescription)")
|
||||
logger.error("Failed to read file data: \(error.localizedDescription)")
|
||||
throw ZIPParserError.fileNotFound
|
||||
}
|
||||
|
||||
return try extractThumbnail(from: zipData, maxSize: maxSize)
|
||||
}
|
||||
|
||||
/// Extract thumbnail from ZIP data
|
||||
static func extractThumbnail(from zipData: Data, maxSize: CGSize? = nil) throws -> CGImage {
|
||||
logger.info("=== Processing ZIP data ===")
|
||||
logger.info("ZIP data size: \(zipData.count) bytes")
|
||||
|
||||
guard zipData.count >= ZIPLocalFileHeader.size else {
|
||||
logger.error("ZIP data too small")
|
||||
logger.error("ZIP data too small: \(zipData.count) < \(ZIPLocalFileHeader.size)")
|
||||
throw ZIPParserError.invalidZipFile
|
||||
}
|
||||
|
||||
// Verify ZIP signature
|
||||
let signature = zipData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
|
||||
logger.info("ZIP signature: 0x\(String(signature, radix: 16))")
|
||||
guard signature == ZIPConstants.localFileSignature else {
|
||||
logger.error("Invalid ZIP signature: 0x\(String(signature, radix: 16))")
|
||||
logger.error(
|
||||
"Invalid ZIP signature: 0x\(String(signature, radix: 16)), expected: 0x\(String(ZIPConstants.localFileSignature, radix: 16))"
|
||||
)
|
||||
throw ZIPParserError.invalidZipFile
|
||||
}
|
||||
|
||||
// Find end of central directory
|
||||
logger.info("Searching for end of central directory...")
|
||||
guard let endOfCentralDir = findEndOfCentralDirectory(in: zipData) else {
|
||||
logger.error("Could not find end of central directory")
|
||||
throw ZIPParserError.corruptedZipFile
|
||||
}
|
||||
logger.info("Found end of central directory with \(endOfCentralDir.totalEntries) entries")
|
||||
|
||||
// Look for thumbnail files
|
||||
let thumbnailPaths = ["thumbnails/Thumbnail.png", "Thumbnail.png"]
|
||||
logger.info("Searching for thumbnail files: \(thumbnailPaths)")
|
||||
|
||||
for thumbnailPath in thumbnailPaths {
|
||||
logger.info("Trying path: \(thumbnailPath)")
|
||||
if let thumbnailData = try? extractFile(
|
||||
from: zipData,
|
||||
endOfCentralDir: endOfCentralDir,
|
||||
filename: thumbnailPath
|
||||
) {
|
||||
logger.debug("Found thumbnail at path: \(thumbnailPath)")
|
||||
logger.info(
|
||||
"Found thumbnail at path: \(thumbnailPath), size: \(thumbnailData.count) bytes")
|
||||
|
||||
if let image = createImage(from: thumbnailData, maxSize: maxSize) {
|
||||
logger.debug("Successfully created CGImage from thumbnail data")
|
||||
logger.info("Successfully created CGImage from thumbnail data")
|
||||
return image
|
||||
} else {
|
||||
logger.warning(
|
||||
"Failed to create CGImage from thumbnail data at path: \(thumbnailPath)")
|
||||
}
|
||||
} else {
|
||||
logger.info("No thumbnail found at path: \(thumbnailPath)")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("No thumbnail found in FCStd file")
|
||||
logger.warning("No valid thumbnail found in FCStd file")
|
||||
throw ZIPParserError.thumbnailNotFound
|
||||
}
|
||||
|
||||
/// Validate if file is a valid FCStd (ZIP) file
|
||||
static func isValidFCStdFile(at url: URL) -> Bool {
|
||||
guard let headerData = try? Data(contentsOf: url, options: .uncached),
|
||||
headerData.count >= 4
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
let signature = headerData.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
|
||||
return signature == ZIPConstants.localFileSignature
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Extensions
|
||||
|
||||
extension SwiftZIPParser {
|
||||
|
||||
/// Find the end of central directory record
|
||||
private static func findEndOfCentralDirectory(in data: Data) -> ZIPEndOfCentralDir? {
|
||||
// Search backwards from the end of the file
|
||||
let minOffset = max(0, data.count - 65557) // Max comment length + EOCD size
|
||||
for offset in stride(from: data.count - ZIPEndOfCentralDir.size, through: minOffset, by: -1)
|
||||
{
|
||||
let signature = readUInt32(from: data, at: offset)
|
||||
if signature == ZIPConstants.endOfCentralDirSignature {
|
||||
return parseEndOfCentralDir(from: data, at: offset)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Parse end of central directory record
|
||||
private static func parseEndOfCentralDir(from data: Data, at offset: Int) -> ZIPEndOfCentralDir
|
||||
{
|
||||
return ZIPEndOfCentralDir(
|
||||
signature: readUInt32(from: data, at: offset),
|
||||
diskNumber: readUInt16(from: data, at: offset + 4),
|
||||
centralDirDisk: readUInt16(from: data, at: offset + 6),
|
||||
entriesOnDisk: readUInt16(from: data, at: offset + 8),
|
||||
totalEntries: readUInt16(from: data, at: offset + 10),
|
||||
centralDirSize: readUInt32(from: data, at: offset + 12),
|
||||
centralDirOffset: readUInt32(from: data, at: offset + 16),
|
||||
commentLength: readUInt16(from: data, at: offset + 20)
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract a specific file from the ZIP archive
|
||||
private static func extractFile(
|
||||
from zipData: Data,
|
||||
endOfCentralDir: ZIPEndOfCentralDir,
|
||||
filename: String
|
||||
) throws -> Data {
|
||||
logger.debug("Searching for file: \(filename)")
|
||||
|
||||
// Search through central directory
|
||||
var offset = Int(endOfCentralDir.centralDirOffset)
|
||||
for _ in 0..<endOfCentralDir.totalEntries {
|
||||
let centralHeader = parseCentralDirHeader(from: zipData, at: offset)
|
||||
|
||||
// Extract filename
|
||||
let filenameStart = offset + ZIPCentralDirHeader.size
|
||||
let filenameData = zipData.subdata(
|
||||
in: filenameStart..<(filenameStart + Int(centralHeader.filenameLength)))
|
||||
if let foundFilename = String(data: filenameData, encoding: .utf8),
|
||||
foundFilename == filename
|
||||
{
|
||||
logger.debug("Found matching file: \(foundFilename)")
|
||||
return try extractFileData(from: zipData, centralHeader: centralHeader)
|
||||
}
|
||||
|
||||
// Move to next entry
|
||||
offset +=
|
||||
ZIPCentralDirHeader.size + Int(centralHeader.filenameLength)
|
||||
+ Int(centralHeader.extraFieldLength) + Int(centralHeader.commentLength)
|
||||
}
|
||||
|
||||
throw ZIPParserError.thumbnailNotFound
|
||||
}
|
||||
|
||||
/// Parse central directory file header
|
||||
private static func parseCentralDirHeader(from data: Data, at offset: Int)
|
||||
-> ZIPCentralDirHeader
|
||||
{
|
||||
return ZIPCentralDirHeader(
|
||||
signature: readUInt32(from: data, at: offset),
|
||||
versionMadeBy: readUInt16(from: data, at: offset + 4),
|
||||
versionNeeded: readUInt16(from: data, at: offset + 6),
|
||||
flags: readUInt16(from: data, at: offset + 8),
|
||||
compression: readUInt16(from: data, at: offset + 10),
|
||||
modTime: readUInt16(from: data, at: offset + 12),
|
||||
modDate: readUInt16(from: data, at: offset + 14),
|
||||
crc32: readUInt32(from: data, at: offset + 16),
|
||||
compressedSize: readUInt32(from: data, at: offset + 20),
|
||||
uncompressedSize: readUInt32(from: data, at: offset + 24),
|
||||
filenameLength: readUInt16(from: data, at: offset + 28),
|
||||
extraFieldLength: readUInt16(from: data, at: offset + 30),
|
||||
commentLength: readUInt16(from: data, at: offset + 32),
|
||||
diskNumber: readUInt16(from: data, at: offset + 34),
|
||||
internalAttributes: readUInt16(from: data, at: offset + 36),
|
||||
externalAttributes: readUInt32(from: data, at: offset + 38),
|
||||
localHeaderOffset: readUInt32(from: data, at: offset + 42)
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract file data using central directory header
|
||||
private static func extractFileData(from zipData: Data, centralHeader: ZIPCentralDirHeader)
|
||||
throws -> Data
|
||||
{
|
||||
let localHeaderOffset = Int(centralHeader.localHeaderOffset)
|
||||
let localHeader = parseLocalFileHeader(from: zipData, at: localHeaderOffset)
|
||||
|
||||
guard localHeader.signature == ZIPConstants.localFileSignature else {
|
||||
throw ZIPParserError.corruptedZipFile
|
||||
}
|
||||
|
||||
let dataStart =
|
||||
localHeaderOffset + ZIPLocalFileHeader.size + Int(localHeader.filenameLength)
|
||||
+ Int(localHeader.extraFieldLength)
|
||||
let dataEnd = dataStart + Int(localHeader.compressedSize)
|
||||
|
||||
guard dataEnd <= zipData.count else {
|
||||
throw ZIPParserError.corruptedZipFile
|
||||
}
|
||||
|
||||
let compressedData = zipData.subdata(in: dataStart..<dataEnd)
|
||||
|
||||
switch localHeader.compression {
|
||||
case ZIPConstants.compressionStored:
|
||||
return compressedData
|
||||
case ZIPConstants.compressionDeflate:
|
||||
return try deflateDecompress(
|
||||
data: compressedData, expectedSize: Int(localHeader.uncompressedSize))
|
||||
default:
|
||||
throw ZIPParserError.compressionUnsupported
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse local file header
|
||||
private static func parseLocalFileHeader(from data: Data, at offset: Int) -> ZIPLocalFileHeader
|
||||
{
|
||||
return ZIPLocalFileHeader(
|
||||
signature: readUInt32(from: data, at: offset),
|
||||
version: readUInt16(from: data, at: offset + 4),
|
||||
flags: readUInt16(from: data, at: offset + 6),
|
||||
compression: readUInt16(from: data, at: offset + 8),
|
||||
modTime: readUInt16(from: data, at: offset + 10),
|
||||
modDate: readUInt16(from: data, at: offset + 12),
|
||||
crc32: readUInt32(from: data, at: offset + 14),
|
||||
compressedSize: readUInt32(from: data, at: offset + 18),
|
||||
uncompressedSize: readUInt32(from: data, at: offset + 22),
|
||||
filenameLength: readUInt16(from: data, at: offset + 26),
|
||||
extraFieldLength: readUInt16(from: data, at: offset + 28)
|
||||
)
|
||||
}
|
||||
|
||||
/// Decompress deflate-compressed data
|
||||
private static func deflateDecompress(data: Data, expectedSize: Int) throws -> Data {
|
||||
guard expectedSize > 0 else {
|
||||
throw ZIPParserError.decompressionFailed
|
||||
}
|
||||
|
||||
return try data.withUnsafeBytes { bytes in
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: expectedSize)
|
||||
defer { buffer.deallocate() }
|
||||
|
||||
let actualSize = compression_decode_buffer(
|
||||
buffer, expectedSize,
|
||||
bytes.bindMemory(to: UInt8.self).baseAddress!, data.count,
|
||||
nil, COMPRESSION_ZLIB
|
||||
)
|
||||
|
||||
guard actualSize == expectedSize else {
|
||||
throw ZIPParserError.decompressionFailed
|
||||
}
|
||||
|
||||
return Data(bytes: buffer, count: expectedSize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create CGImage from PNG data with optional size constraints
|
||||
private static func createImage(from pngData: Data, maxSize: CGSize? = nil) -> CGImage? {
|
||||
guard let dataProvider = CGDataProvider(data: pngData as CFData),
|
||||
let image = CGImage(
|
||||
pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: true,
|
||||
intent: .defaultIntent)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no max size specified, return original
|
||||
guard let maxSize = maxSize else {
|
||||
return image
|
||||
}
|
||||
|
||||
// Calculate scaled size
|
||||
let originalSize = CGSize(width: CGFloat(image.width), height: CGFloat(image.height))
|
||||
let scaledSize = scaleToFit(originalSize: originalSize, maxSize: maxSize)
|
||||
|
||||
// If no scaling needed, return original
|
||||
if scaledSize == originalSize {
|
||||
return image
|
||||
}
|
||||
|
||||
// Scale the image
|
||||
return scaleImage(image, to: scaledSize)
|
||||
}
|
||||
|
||||
/// Safely read UInt32 from Data at offset
|
||||
fileprivate static func readUInt32(from data: Data, at offset: Int) -> UInt32 {
|
||||
let subdata = data.subdata(in: offset..<(offset + 4))
|
||||
return subdata.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self) }
|
||||
}
|
||||
|
||||
/// Safely read UInt16 from Data at offset
|
||||
fileprivate static func readUInt16(from data: Data, at offset: Int) -> UInt16 {
|
||||
let subdata = data.subdata(in: offset..<(offset + 2))
|
||||
return subdata.withUnsafeBytes { $0.loadUnaligned(as: UInt16.self) }
|
||||
}
|
||||
|
||||
/// Calculate size that fits within maxSize while maintaining aspect ratio
|
||||
fileprivate static func scaleToFit(originalSize: CGSize, maxSize: CGSize) -> CGSize {
|
||||
let widthRatio = maxSize.width / originalSize.width
|
||||
let heightRatio = maxSize.height / originalSize.height
|
||||
let scaleFactor = min(widthRatio, heightRatio)
|
||||
|
||||
return CGSize(
|
||||
width: originalSize.width * scaleFactor,
|
||||
height: originalSize.height * scaleFactor
|
||||
)
|
||||
}
|
||||
|
||||
/// Scale CGImage to specified size
|
||||
fileprivate static func scaleImage(_ image: CGImage, to size: CGSize) -> CGImage? {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
|
||||
|
||||
guard
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: Int(size.width),
|
||||
height: Int(size.height),
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace,
|
||||
bitmapInfo: bitmapInfo.rawValue
|
||||
)
|
||||
else {
|
||||
logger.error("Failed to create CGContext for image scaling")
|
||||
return nil
|
||||
}
|
||||
|
||||
context.interpolationQuality = .high
|
||||
context.draw(image, in: CGRect(origin: .zero, size: size))
|
||||
|
||||
return context.makeImage()
|
||||
}
|
||||
}
|
||||
371
src/MacAppBundle/QuickLook/test_integration.sh
Executable file
371
src/MacAppBundle/QuickLook/test_integration.sh
Executable file
@@ -0,0 +1,371 @@
|
||||
#!/bin/bash
|
||||
|
||||
# FreeCAD QuickLook Extensions - Integration Test Script
|
||||
# This script tests the QuickLook extension integration in FreeCAD.app
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Look for FreeCAD.app in common install locations
|
||||
if [[ -d "${SCRIPT_DIR}/../../../FreeCAD.app" ]]; then
|
||||
FREECAD_APP="${SCRIPT_DIR}/../../../FreeCAD.app"
|
||||
elif [[ -d "${SCRIPT_DIR}/../../../../FreeCAD.app" ]]; then
|
||||
FREECAD_APP="${SCRIPT_DIR}/../../../../FreeCAD.app"
|
||||
else
|
||||
# Default to relative path from script
|
||||
FREECAD_APP="${SCRIPT_DIR}/../../../FreeCAD.app"
|
||||
fi
|
||||
|
||||
EXTENSIONS_DIR="${FREECAD_APP}/Contents/PlugIns"
|
||||
THUMBNAIL_EXT="${EXTENSIONS_DIR}/FreeCADThumbnailExtension.appex"
|
||||
PREVIEW_EXT="${EXTENSIONS_DIR}/FreeCADPreviewExtension.appex"
|
||||
|
||||
THUMBNAIL_BUNDLE_ID="org.freecad.FreeCAD.quicklook.thumbnail"
|
||||
PREVIEW_BUNDLE_ID="org.freecad.FreeCAD.quicklook.preview"
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
case $status in
|
||||
"OK")
|
||||
echo -e "${GREEN}✓${NC} $message"
|
||||
;;
|
||||
"FAIL")
|
||||
echo -e "${RED}✗${NC} $message"
|
||||
;;
|
||||
"WARN")
|
||||
echo -e "${YELLOW}⚠${NC} $message"
|
||||
;;
|
||||
"INFO")
|
||||
echo -e "${BLUE}ℹ${NC} $message"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main test function
|
||||
main() {
|
||||
echo "FreeCAD QuickLook Extensions - Integration Test"
|
||||
echo "=============================================="
|
||||
echo "Testing app at: $FREECAD_APP"
|
||||
echo
|
||||
|
||||
local total_tests=0
|
||||
local passed_tests=0
|
||||
|
||||
# Test 1: Check if FreeCAD.app exists
|
||||
((total_tests++))
|
||||
if [[ -d "$FREECAD_APP" ]]; then
|
||||
print_status "OK" "FreeCAD.app exists at: $FREECAD_APP"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "FreeCAD.app not found at: $FREECAD_APP"
|
||||
print_status "INFO" "Please build and install FreeCAD first with: make install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Check if main FreeCAD executable exists
|
||||
((total_tests++))
|
||||
if [[ -f "$FREECAD_APP/Contents/MacOS/FreeCAD" ]]; then
|
||||
print_status "OK" "FreeCAD executable exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "FreeCAD executable not found"
|
||||
fi
|
||||
|
||||
# Test 3: Check if PlugIns directory exists
|
||||
((total_tests++))
|
||||
if [[ -d "$EXTENSIONS_DIR" ]]; then
|
||||
print_status "OK" "Extensions directory exists: $EXTENSIONS_DIR"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Extensions directory not found: $EXTENSIONS_DIR"
|
||||
print_status "INFO" "QuickLook extensions may not have been built. Check cmake configuration."
|
||||
fi
|
||||
|
||||
# Test 4: Check if thumbnail extension exists
|
||||
((total_tests++))
|
||||
if [[ -d "$THUMBNAIL_EXT" ]]; then
|
||||
print_status "OK" "Thumbnail extension exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Thumbnail extension not found: $THUMBNAIL_EXT"
|
||||
fi
|
||||
|
||||
# Test 5: Check if preview extension exists
|
||||
((total_tests++))
|
||||
if [[ -d "$PREVIEW_EXT" ]]; then
|
||||
print_status "OK" "Preview extension exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Preview extension not found: $PREVIEW_EXT"
|
||||
fi
|
||||
|
||||
# Test 6: Check if thumbnail extension executable exists
|
||||
((total_tests++))
|
||||
if [[ -f "$THUMBNAIL_EXT/Contents/MacOS/FreeCADThumbnailExtension" ]]; then
|
||||
print_status "OK" "Thumbnail extension executable exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Thumbnail extension executable not found"
|
||||
fi
|
||||
|
||||
# Test 7: Check if preview extension executable exists
|
||||
((total_tests++))
|
||||
if [[ -f "$PREVIEW_EXT/Contents/MacOS/FreeCADPreviewExtension" ]]; then
|
||||
print_status "OK" "Preview extension executable exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Preview extension executable not found"
|
||||
fi
|
||||
|
||||
# Test 8: Check if thumbnail extension Info.plist exists
|
||||
((total_tests++))
|
||||
if [[ -f "$THUMBNAIL_EXT/Contents/Info.plist" ]]; then
|
||||
print_status "OK" "Thumbnail extension Info.plist exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Thumbnail extension Info.plist not found"
|
||||
fi
|
||||
|
||||
# Test 9: Check if preview extension Info.plist exists
|
||||
((total_tests++))
|
||||
if [[ -f "$PREVIEW_EXT/Contents/Info.plist" ]]; then
|
||||
print_status "OK" "Preview extension Info.plist exists"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Preview extension Info.plist not found"
|
||||
fi
|
||||
|
||||
# Test 10: Check thumbnail extension bundle ID
|
||||
((total_tests++))
|
||||
if [[ -f "$THUMBNAIL_EXT/Contents/Info.plist" ]]; then
|
||||
local bundle_id=$(plutil -extract CFBundleIdentifier raw "$THUMBNAIL_EXT/Contents/Info.plist" 2>/dev/null)
|
||||
if [[ "$bundle_id" == "$THUMBNAIL_BUNDLE_ID" ]]; then
|
||||
print_status "OK" "Thumbnail extension has correct bundle ID: $bundle_id"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Thumbnail extension bundle ID incorrect: $bundle_id (expected: $THUMBNAIL_BUNDLE_ID)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 11: Check preview extension bundle ID
|
||||
((total_tests++))
|
||||
if [[ -f "$PREVIEW_EXT/Contents/Info.plist" ]]; then
|
||||
local bundle_id=$(plutil -extract CFBundleIdentifier raw "$PREVIEW_EXT/Contents/Info.plist" 2>/dev/null)
|
||||
if [[ "$bundle_id" == "$PREVIEW_BUNDLE_ID" ]]; then
|
||||
print_status "OK" "Preview extension has correct bundle ID: $bundle_id"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "Preview extension bundle ID incorrect: $bundle_id (expected: $PREVIEW_BUNDLE_ID)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 12: Basic FreeCAD launch test
|
||||
((total_tests++))
|
||||
print_status "INFO" "Testing FreeCAD launch (--version)..."
|
||||
if timeout 10 "$FREECAD_APP/Contents/MacOS/FreeCAD" --version >/dev/null 2>&1; then
|
||||
print_status "OK" "FreeCAD launches successfully"
|
||||
((passed_tests++))
|
||||
else
|
||||
print_status "FAIL" "FreeCAD failed to launch or crashed"
|
||||
print_status "WARN" "This will prevent QuickLook registration from working"
|
||||
fi
|
||||
|
||||
# Extension signing tests (optional - don't count toward pass/fail)
|
||||
echo
|
||||
print_status "INFO" "Code Signing Status:"
|
||||
|
||||
if codesign -v "$THUMBNAIL_EXT" >/dev/null 2>&1; then
|
||||
print_status "OK" "Thumbnail extension is signed"
|
||||
else
|
||||
print_status "WARN" "Thumbnail extension is unsigned (normal for development builds)"
|
||||
fi
|
||||
|
||||
if codesign -v "$PREVIEW_EXT" >/dev/null 2>&1; then
|
||||
print_status "OK" "Preview extension is signed"
|
||||
else
|
||||
print_status "WARN" "Preview extension is unsigned (normal for development builds)"
|
||||
fi
|
||||
|
||||
# App signing status
|
||||
if codesign -v "$FREECAD_APP" >/dev/null 2>&1; then
|
||||
print_status "OK" "FreeCAD.app is signed"
|
||||
else
|
||||
print_status "WARN" "FreeCAD.app is unsigned (normal for development builds)"
|
||||
fi
|
||||
|
||||
# Optional tests (don't count toward pass/fail)
|
||||
echo
|
||||
print_status "INFO" "Additional Information:"
|
||||
|
||||
# Show signing details if available
|
||||
if command -v codesign >/dev/null 2>&1; then
|
||||
echo
|
||||
print_status "INFO" "Signing Details:"
|
||||
|
||||
echo " FreeCAD.app:"
|
||||
codesign -dv "$FREECAD_APP" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
|
||||
|
||||
echo " Thumbnail Extension:"
|
||||
codesign -dv "$THUMBNAIL_EXT" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
|
||||
|
||||
echo " Preview Extension:"
|
||||
codesign -dv "$PREVIEW_EXT" 2>&1 | grep -E "(Identifier|Authority|Signature)" | head -3 | sed 's/^/ /' || echo " No signature information"
|
||||
fi
|
||||
|
||||
# Show current registration status (if pluginkit is available)
|
||||
if command -v pluginkit >/dev/null 2>&1; then
|
||||
echo
|
||||
print_status "INFO" "Current Registration Status:"
|
||||
|
||||
if pluginkit -m -v -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1; then
|
||||
print_status "OK" "Thumbnail extension is registered with system"
|
||||
else
|
||||
print_status "WARN" "Thumbnail extension not registered (normal before first successful FreeCAD launch)"
|
||||
fi
|
||||
|
||||
if pluginkit -m -v -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1; then
|
||||
print_status "OK" "Preview extension is registered with system"
|
||||
else
|
||||
print_status "WARN" "Preview extension not registered (normal before first successful FreeCAD launch)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for gatekeeper issues
|
||||
echo
|
||||
print_status "INFO" "Security Status:"
|
||||
|
||||
if command -v spctl >/dev/null 2>&1; then
|
||||
if spctl -a -v "$FREECAD_APP" >/dev/null 2>&1; then
|
||||
print_status "OK" "FreeCAD.app passes Gatekeeper checks"
|
||||
else
|
||||
print_status "WARN" "FreeCAD.app rejected by Gatekeeper (normal for unsigned development builds)"
|
||||
print_status "INFO" "You may need to: sudo xattr -rd com.apple.quarantine '$FREECAD_APP'"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for quarantine attributes
|
||||
if xattr "$FREECAD_APP" 2>/dev/null | grep -q quarantine; then
|
||||
print_status "WARN" "FreeCAD.app has quarantine attributes"
|
||||
print_status "INFO" "Remove with: sudo xattr -rd com.apple.quarantine '$FREECAD_APP'"
|
||||
else
|
||||
print_status "OK" "No quarantine attributes found"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo
|
||||
echo "Test Results:"
|
||||
echo "============"
|
||||
echo "Passed: $passed_tests/$total_tests core tests"
|
||||
|
||||
if [[ $passed_tests -eq $total_tests ]]; then
|
||||
print_status "OK" "All core tests passed! QuickLook extensions are properly built and integrated."
|
||||
echo
|
||||
echo "Next Steps:"
|
||||
echo " 1. Ensure FreeCAD launches successfully to trigger extension registration"
|
||||
echo " 2. Test QuickLook functionality with .FCStd files in Finder"
|
||||
echo " 3. Look for system notification about Quick Look extensions being added"
|
||||
return 0
|
||||
else
|
||||
print_status "FAIL" "Some core tests failed. Please check the build configuration."
|
||||
echo
|
||||
echo "Troubleshooting:"
|
||||
echo " 1. Ensure you're using Unix Makefiles generator: cmake -G 'Unix Makefiles'"
|
||||
echo " 2. Check that FREECAD_CREATE_MAC_APP=ON in cmake configuration"
|
||||
echo " 3. Run 'make install' to build and install FreeCAD with QuickLook extensions"
|
||||
echo " 4. If FreeCAD crashes, try removing quarantine attributes or ad-hoc signing"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test registration functionality (optional)
|
||||
test_registration() {
|
||||
if [[ "$1" == "--test-registration" ]]; then
|
||||
echo
|
||||
print_status "INFO" "Testing extension registration..."
|
||||
|
||||
if command -v pluginkit >/dev/null 2>&1; then
|
||||
print_status "INFO" "Attempting to register extensions manually..."
|
||||
|
||||
# Try to register extensions
|
||||
if pluginkit -a "$THUMBNAIL_EXT" >/dev/null 2>&1; then
|
||||
print_status "OK" "Thumbnail extension registration command succeeded"
|
||||
else
|
||||
print_status "WARN" "Thumbnail registration command failed (may already be registered)"
|
||||
fi
|
||||
|
||||
if pluginkit -a "$PREVIEW_EXT" >/dev/null 2>&1; then
|
||||
print_status "OK" "Preview extension registration command succeeded"
|
||||
else
|
||||
print_status "WARN" "Preview registration command failed (may already be registered)"
|
||||
fi
|
||||
|
||||
# Try to enable extensions
|
||||
pluginkit -e use -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1 || print_status "WARN" "Thumbnail activation command failed"
|
||||
pluginkit -e use -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1 || print_status "WARN" "Preview activation command failed"
|
||||
|
||||
sleep 2 # Give system time to process
|
||||
|
||||
# Check final status
|
||||
if pluginkit -m -v -i "$THUMBNAIL_BUNDLE_ID" >/dev/null 2>&1; then
|
||||
print_status "OK" "Thumbnail extension successfully registered and active"
|
||||
else
|
||||
print_status "FAIL" "Thumbnail extension registration failed"
|
||||
fi
|
||||
|
||||
if pluginkit -m -v -i "$PREVIEW_BUNDLE_ID" >/dev/null 2>&1; then
|
||||
print_status "OK" "Preview extension successfully registered and active"
|
||||
else
|
||||
print_status "FAIL" "Preview extension registration failed"
|
||||
fi
|
||||
|
||||
echo
|
||||
print_status "INFO" "Try testing with a .FCStd file in Finder now"
|
||||
else
|
||||
print_status "WARN" "pluginkit not available for registration testing"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Show usage information
|
||||
show_usage() {
|
||||
echo "Usage: $0 [--test-registration] [--help]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --test-registration Also test extension registration with pluginkit"
|
||||
echo " --help Show this help message"
|
||||
echo
|
||||
echo "This script tests the QuickLook extension integration in FreeCAD.app."
|
||||
echo "Run this after building and installing FreeCAD: 'make install'"
|
||||
echo
|
||||
echo "The script will look for FreeCAD.app in common install locations relative to the script."
|
||||
}
|
||||
|
||||
# Handle command line arguments
|
||||
case "${1:-}" in
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
--test-registration)
|
||||
main
|
||||
test_registration "$1"
|
||||
;;
|
||||
"")
|
||||
main
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user