Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
BasedOnStyle: LLVM
|
||||
ColumnLimit: 80
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: ForContinuationAndIndentation
|
||||
AccessModifierOffset: -4
|
||||
# Member definitions keep the return type on its own line in .cpp files.
|
||||
# In-class getters stay on one line (AllDefinitions forces them to wrap).
|
||||
AlwaysBreakAfterReturnType: TopLevelDefinitions
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
BreakBeforeBraces: Linux
|
||||
SpaceAfterCStyleCast: true
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignOperands: DontAlign
|
||||
SpacesBeforeTrailingComments: 2
|
||||
WhitespaceSensitiveMacros: ['G_DEFINE_QUARK']
|
||||
@@ -0,0 +1,6 @@
|
||||
/.qtcreator
|
||||
/.flatpak/
|
||||
/build/
|
||||
/build-*/
|
||||
compile_commands.json
|
||||
.cache/
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "submodules/wuffs-mirror-release-c"]
|
||||
path = submodules/wuffs-mirror-release-c
|
||||
url = https://github.com/google/wuffs-mirror-release-c
|
||||
[submodule "submodules/liberty"]
|
||||
path = submodules/liberty
|
||||
url = https://git.janouch.name/p/liberty.git
|
||||
@@ -0,0 +1,2 @@
|
||||
# This is the list of dawn's significant contributors.
|
||||
Přemysl Eric Janouch <p@janouch.name>
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
project(Dawn
|
||||
VERSION 0.1.0
|
||||
DESCRIPTION "Colour-managed image browser"
|
||||
HOMEPAGE_URL "https://git.janouch.name/p/dawn"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
|
||||
set(DAWN_NAMESPACE "name.janouch.Dawn")
|
||||
set(CPACK_PACKAGE_VENDOR "The Dawn Authors")
|
||||
set(CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# A plain build-root path would collide with CMake-produced subdirs.
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic
|
||||
-Wno-unused-parameter -Wno-missing-field-initializers)
|
||||
endif()
|
||||
|
||||
if (WIN32 AND CMAKE_CROSSCOMPILING)
|
||||
set(DAWN_MSYS2_PREFIX "${PROJECT_BINARY_DIR}/ucrt64")
|
||||
list(APPEND CMAKE_PREFIX_PATH "${DAWN_MSYS2_PREFIX}")
|
||||
list(APPEND CMAKE_FIND_ROOT_PATH "${DAWN_MSYS2_PREFIX}")
|
||||
|
||||
# Relativize prefixes, and bar pkg-config from looking up host libraries
|
||||
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${PROJECT_BINARY_DIR}")
|
||||
set(DAWN_MSYS2_PCPATH
|
||||
"${DAWN_MSYS2_PREFIX}/share/pkgconfig:${DAWN_MSYS2_PREFIX}/lib/pkgconfig")
|
||||
set(ENV{PKG_CONFIG_PATH} "${DAWN_MSYS2_PCPATH}")
|
||||
set(ENV{PKG_CONFIG_LIBDIR} "${DAWN_MSYS2_PCPATH}")
|
||||
|
||||
# Qt tries to find qtpaths without an .exe file extension
|
||||
# (see Qt6CoreMacros.cmake). This ultimately means broken windeployqt.
|
||||
set(QT_NO_QTPATHS_DEPLOYMENT_WARNING ON)
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
# Homebrew targets the host system. With LSMinimumSystemVersion,
|
||||
# Launch Services explains the refusal; dyld would not.
|
||||
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
|
||||
execute_process(COMMAND sw_vers -productVersion
|
||||
OUTPUT_VARIABLE dawn_macos_version
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if (NOT dawn_macos_version MATCHES "^([0-9]+)")
|
||||
message(FATAL_ERROR "Cannot tell what macOS version this is")
|
||||
endif()
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "${CMAKE_MATCH_1}.0")
|
||||
endif()
|
||||
|
||||
# Build tree: bin/dn.app/Contents/MacOS vs bin/libdn.dylib.
|
||||
# Install: MacOS/dn, Frameworks/libdn.dylib (macdeployqt leaves
|
||||
# @rpath/libdn.dylib; Homebrew dylibs become @executable_path).
|
||||
set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../Frameworks")
|
||||
else()
|
||||
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
|
||||
endif()
|
||||
|
||||
# Dependency fetching is centralised here in the root CMakeLists.txt.
|
||||
set(CMAKE_MODULE_PATH
|
||||
"${PROJECT_SOURCE_DIR}/cmake"
|
||||
"${PROJECT_SOURCE_DIR}/submodules/liberty/cmake"
|
||||
)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Vulkan REQUIRED)
|
||||
# Maybe we need a newer version, just not to build.
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Gui OPTIONAL_COMPONENTS WaylandClient)
|
||||
find_program(AWK NAMES awk REQUIRED)
|
||||
include(lxdr-generate)
|
||||
find_program(GLSLANG_VALIDATOR
|
||||
NAMES glslangValidator glslang
|
||||
HINTS ${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}
|
||||
REQUIRED
|
||||
)
|
||||
include(embed-shader)
|
||||
set(DN_WITH_WAYLAND "${Qt6WaylandClient_FOUND}")
|
||||
if (DN_WITH_WAYLAND)
|
||||
pkg_check_modules(DN_WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
|
||||
# For QPlatformNativeInterface: Qt exposes no other way to the toplevel.
|
||||
# Debian's qt6-base-private-dev adds Qt6::GuiPrivate while loading Qt6Gui,
|
||||
# but deliberately ships no standalone Qt6GuiPrivateConfig.cmake.
|
||||
if (NOT TARGET Qt6::GuiPrivate)
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS GuiPrivate)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
pkg_check_modules(JPEG REQUIRED IMPORTED_TARGET libjpeg)
|
||||
pkg_check_modules(WEBP REQUIRED IMPORTED_TARGET libwebp)
|
||||
pkg_check_modules(WEBPDEMUX REQUIRED IMPORTED_TARGET libwebpdemux)
|
||||
pkg_check_modules(WEBPMUX REQUIRED IMPORTED_TARGET libwebpmux)
|
||||
pkg_check_modules(ZLIB REQUIRED IMPORTED_TARGET zlib)
|
||||
# Qt6 SVG cannot render our icons correctly, so we hard depend on resvg.
|
||||
pkg_check_modules(RESVG REQUIRED IMPORTED_TARGET resvg)
|
||||
|
||||
macro(dawn_optional_pkg stem doc module)
|
||||
pkg_check_modules(${stem} IMPORTED_TARGET ${module})
|
||||
option(DAWN_WITH_${stem} "${doc}" ${${stem}_FOUND})
|
||||
if(DAWN_WITH_${stem} AND NOT ${stem}_FOUND)
|
||||
message(FATAL_ERROR "DAWN_WITH_${stem}=ON but ${module} was not found")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
dawn_optional_pkg(LIBRSVG "SVG via librsvg" librsvg-2.0)
|
||||
dawn_optional_pkg(XCURSOR "Xcursor support" xcursor)
|
||||
dawn_optional_pkg(LIBHEIF "HEIF/AVIF support" libheif)
|
||||
dawn_optional_pkg(LIBJXL "JPEG XL support" libjxl)
|
||||
dawn_optional_pkg(OPENJPEG "JPEG 2000 support" libopenjp2)
|
||||
dawn_optional_pkg(LIBTIFF "TIFF support" libtiff-4)
|
||||
dawn_optional_pkg(GLYCIN "glycin fallback" glycin-2)
|
||||
dawn_optional_pkg(GDKPIXBUF "gdk-pixbuf fallback" gdk-pixbuf-2.0)
|
||||
dawn_optional_pkg(LIBRAW "LibRaw support" libraw)
|
||||
|
||||
# jxrlib ships no pkg-config file, and its headers include one another by
|
||||
# bare name, so the whole package directory has to go on the include path.
|
||||
find_path(JXRLIB_INCLUDE_DIR JXRGlue.h PATH_SUFFIXES jxrlib)
|
||||
find_library(JXRLIB_GLUE_LIBRARY jxrglue)
|
||||
find_library(JXRLIB_CORE_LIBRARY jpegxr)
|
||||
if (JXRLIB_INCLUDE_DIR AND JXRLIB_GLUE_LIBRARY AND JXRLIB_CORE_LIBRARY)
|
||||
set(JXRLIB_FOUND TRUE)
|
||||
endif()
|
||||
option(DAWN_WITH_JXRLIB "JPEG XR support" ${JXRLIB_FOUND})
|
||||
if (DAWN_WITH_JXRLIB AND NOT JXRLIB_FOUND)
|
||||
message(FATAL_ERROR "DAWN_WITH_JXRLIB=ON but jxrlib was not found")
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
option(DAWN_WITH_IMAGEIO "ImageIO fallback" ON)
|
||||
option(DAWN_WITH_CGPDF "Core Graphics PDF page rendering" ON)
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# The GPL fast float plugin improves performance a ton,
|
||||
# but distributions usually omit it.
|
||||
option(DAWN_BUNDLED_LCMS2
|
||||
"Use a fast bundled lcms2 instead of the system one" ON)
|
||||
set(DAWN_WITH_LCMS2_FAST_FLOAT FALSE)
|
||||
set(DAWN_WITH_LCMS2_THREADED FALSE)
|
||||
|
||||
add_library(dawn-lcms2 INTERFACE)
|
||||
set(DAWN_LCMS2_TARGET dawn-lcms2)
|
||||
if (NOT DAWN_BUNDLED_LCMS2)
|
||||
pkg_check_modules(LCMS2 REQUIRED IMPORTED_TARGET lcms2)
|
||||
target_link_libraries(dawn-lcms2 INTERFACE PkgConfig::LCMS2)
|
||||
|
||||
# Either lcms2 has been built to include the plugin, or it hasn't.
|
||||
find_path(LCMS2_FAST_FLOAT_INCLUDE_DIR lcms2_fast_float.h
|
||||
HINTS ${LCMS2_INCLUDE_DIRS}
|
||||
NO_DEFAULT_PATH)
|
||||
find_library(LCMS2_FAST_FLOAT_LIB NAMES lcms2_fast_float
|
||||
HINTS ${LCMS2_LIBRARY_DIRS}
|
||||
NO_DEFAULT_PATH)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Lcms2FastFloat
|
||||
REQUIRED_VARS LCMS2_FAST_FLOAT_LIB LCMS2_FAST_FLOAT_INCLUDE_DIR)
|
||||
if (Lcms2FastFloat_FOUND)
|
||||
set(DAWN_WITH_LCMS2_FAST_FLOAT TRUE)
|
||||
target_link_libraries(dawn-lcms2 INTERFACE
|
||||
"${LCMS2_FAST_FLOAT_LIB}")
|
||||
target_include_directories(dawn-lcms2 INTERFACE
|
||||
"${LCMS2_FAST_FLOAT_INCLUDE_DIR}")
|
||||
endif()
|
||||
else()
|
||||
set(LCMS2_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(LCMS2_BUILD_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(LCMS2_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
||||
set(LCMS2_WITH_FASTFLOAT ON CACHE BOOL "" FORCE)
|
||||
set(LCMS2_WITH_THREADED_PLUGIN ON CACHE BOOL "" FORCE)
|
||||
|
||||
# Bundled static lib; do not ship headers/.a with the app package.
|
||||
FetchContent_Declare(lcms2
|
||||
URL https://github.com/mm2/Little-CMS/archive/refs/tags/lcms2.19.1.tar.gz
|
||||
URL_HASH SHA256=267705e278e2f7c2fb886c259dadcbaeb2be52748bcbc71c79f08aacacb7a709
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(lcms2)
|
||||
|
||||
set(DAWN_WITH_LCMS2_FAST_FLOAT TRUE)
|
||||
set(DAWN_WITH_LCMS2_THREADED TRUE)
|
||||
set_target_properties(lcms2 lcms2_fast_float lcms2_threaded PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
# Upstream keeps plugin includes PRIVATE.
|
||||
target_include_directories(lcms2_fast_float PUBLIC
|
||||
$<BUILD_INTERFACE:${lcms2_SOURCE_DIR}/plugins/fast_float/include>)
|
||||
target_include_directories(lcms2_threaded PUBLIC
|
||||
$<BUILD_INTERFACE:${lcms2_SOURCE_DIR}/plugins/threaded/include>)
|
||||
|
||||
# FIXME: LibRaw, GdkPixbuf, Glycin may transitively link to system lcms2.
|
||||
# What gives? Clear in LINK_INTERFACE_LIBRARIES?
|
||||
target_link_libraries(dawn-lcms2 INTERFACE
|
||||
lcms2 lcms2_fast_float lcms2_threaded)
|
||||
|
||||
# Have CTest not fail.
|
||||
set_target_properties(testcms PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
endif()
|
||||
|
||||
option(DAWN_WITH_JPEG_QS "Bundled JPEG Quant Smooth" ON)
|
||||
if (DAWN_WITH_JPEG_QS)
|
||||
FetchContent_Declare(jpegqs
|
||||
URL https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20260122.tar.gz
|
||||
URL_HASH SHA256=7dcbaa7d994511a03dba845aa218a25f9c8e70c0bcb0800ff61ae3d92dd50c3d
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
PATCH_COMMAND
|
||||
${CMAKE_COMMAND} -E copy
|
||||
"${PROJECT_SOURCE_DIR}/cmake/jpegqs/CMakeLists.txt"
|
||||
<SOURCE_DIR>/CMakeLists.txt
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E copy_directory
|
||||
"${PROJECT_SOURCE_DIR}/cmake/jpegqs/include"
|
||||
<SOURCE_DIR>/include
|
||||
)
|
||||
FetchContent_MakeAvailable(jpegqs)
|
||||
if (NOT TARGET jpegqs)
|
||||
message(FATAL_ERROR "Bundled jpegqs did not create target 'jpegqs'")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
"${PROJECT_SOURCE_DIR}/dawn-config.h.in"
|
||||
"${PROJECT_BINARY_DIR}/dawn-config.h")
|
||||
|
||||
include_directories("${PROJECT_SOURCE_DIR}")
|
||||
include(CTest)
|
||||
|
||||
add_subdirectory(libdn)
|
||||
add_subdirectory(dnthumbd)
|
||||
add_subdirectory(dn)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
install(TARGETS dn libdn
|
||||
RUNTIME DESTINATION .)
|
||||
install(DIRECTORY "${DAWN_MSYS2_PREFIX}/bin/"
|
||||
DESTINATION .
|
||||
FILES_MATCHING
|
||||
PATTERN "*.dll"
|
||||
PATTERN "vulkan-1.dll" EXCLUDE)
|
||||
install(IMPORTED_RUNTIME_ARTIFACTS Qt6::QWindowsIntegrationPlugin
|
||||
RUNTIME DESTINATION platforms
|
||||
LIBRARY DESTINATION platforms)
|
||||
install(FILES
|
||||
"${DAWN_MSYS2_PREFIX}/share/mime/globs2"
|
||||
"${DAWN_MSYS2_PREFIX}/share/mime/subclasses"
|
||||
DESTINATION share/mime)
|
||||
|
||||
install(FILES
|
||||
"${DAWN_MSYS2_PREFIX}/bin/vk_swiftshader_icd.json"
|
||||
DESTINATION .)
|
||||
|
||||
# This installation is pretty big, with a ton of files, thus it's optional.
|
||||
install(FILES
|
||||
"${DAWN_MSYS2_PREFIX}/bin/wperl.exe"
|
||||
"${DAWN_MSYS2_PREFIX}/bin/exiftool"
|
||||
DESTINATION .
|
||||
OPTIONAL)
|
||||
install(DIRECTORY
|
||||
"${DAWN_MSYS2_PREFIX}/lib/perl5/"
|
||||
DESTINATION lib/perl5
|
||||
OPTIONAL)
|
||||
|
||||
# Prune unused libraries.
|
||||
install(SCRIPT "${PROJECT_SOURCE_DIR}/cmake/msys2-cleanup.cmake")
|
||||
elseif (APPLE)
|
||||
include(cmake/macos-install.cmake)
|
||||
else()
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS dn
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(TARGETS libdn
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
# Software centres want this, and Flatpak bundles only carry an icon
|
||||
# when an AppStream component points at one.
|
||||
configure_file(flatpak-metainfo.xml.in
|
||||
"${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml" @ONLY)
|
||||
install(FILES "${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml"
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
|
||||
|
||||
# Only meaningful once dn is in place, hence here rather than in dn/,
|
||||
# which is processed before the rules above. A staged install cannot run
|
||||
# the binary it just staged, so packages ship the updater to run later.
|
||||
string(CONFIGURE [[
|
||||
if (NOT "$ENV{DESTDIR}" STREQUAL "")
|
||||
message(STATUS "Staged install: run dn-update-desktop-files on target")
|
||||
else()
|
||||
message(STATUS "Updating desktop file media types")
|
||||
execute_process(
|
||||
COMMAND "@DN_DESKTOP_UPDATER@"
|
||||
RESULT_VARIABLE dn_desktop_result)
|
||||
if (NOT dn_desktop_result EQUAL 0)
|
||||
message(WARNING "dn-update-desktop-files: ${dn_desktop_result}")
|
||||
endif()
|
||||
endif()
|
||||
]] dn_desktop_hook @ONLY)
|
||||
if (DN_DESKTOP_UPDATER AND NOT CMAKE_CROSSCOMPILING)
|
||||
install(CODE "${dn_desktop_hook}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_GENERATOR ZIP)
|
||||
set(CPACK_PACKAGE_DIRECTORY "${PROJECT_BINARY_DIR}")
|
||||
set(CPACK_PACKAGE_FILE_NAME
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
include(CPack)
|
||||
|
||||
# CPack's WIX generator needs the real WiX toolset (Windows or wine).
|
||||
# We've already figured out msitools.
|
||||
if (WIN32 AND CMAKE_CROSSCOMPILING)
|
||||
set(DAWN_ICON_ICO "${PROJECT_BINARY_DIR}/dn/dn.ico")
|
||||
set(dawn_wxs "${PROJECT_BINARY_DIR}/dawn.wxs")
|
||||
configure_file(dawn.wxs.in "${dawn_wxs}" @ONLY)
|
||||
|
||||
set(dawn_msi
|
||||
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_PROCESSOR}.msi")
|
||||
add_custom_target(msi
|
||||
COMMAND "${PROJECT_SOURCE_DIR}/msys2-package-msi.sh"
|
||||
"${CMAKE_SYSTEM_PROCESSOR}" "${dawn_msi}" "${dawn_wxs}"
|
||||
"${PROJECT_DESCRIPTION}"
|
||||
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
|
||||
VERBATIM)
|
||||
endif()
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
Dawn
|
||||
====
|
||||
:compact-option:
|
||||
|
||||
'Dawn' is a cross-platform colour-managed image browser, with high standards for
|
||||
fidelity and responsiveness.
|
||||
|
||||
See link:docs/why.adoc[Why you need a better image viewer] if you feel lost.
|
||||
|
||||
image::docs/dn.webp["Screenshot of both the browser and the viewer"]
|
||||
|
||||
Design-wise, the project is an amalgamation of
|
||||
https://git.janouch.name/p/fiv[older efforts] and
|
||||
https://git.janouch.name/p/gallery[desires].
|
||||
Right now, we're close to our first milestone. Once we declare the viewer part
|
||||
done, we'll clean up after rapid development, and move on to tackling the next
|
||||
major goals.
|
||||
|
||||
The project comes with a maintenance guarantee.
|
||||
Also see link:docs/plan.adoc[Plan],
|
||||
and a detailed link:docs/rationale.adoc[Rationale].
|
||||
|
||||
Features
|
||||
--------
|
||||
We care about
|
||||
~~~~~~~~~~~~~
|
||||
- *Wide-gamut*, high-bit-depth SDR: 10-bit displays in Adobe RGB or Display P3.
|
||||
- *16-bit* internal precision in order to minimise quantisation errors.
|
||||
- *Gamma-correct* scaling: see below for a test that your iPhone fails.
|
||||
- *Good scaling*: GIMP's NoHalo is an awesome compromise for upscaling
|
||||
both photos and man-made images.
|
||||
- *Loading as much as we can* from any file format at the best possible
|
||||
quality: multi-page TIFF, 16-bit PNG, 12-bit JPEG, etc.;
|
||||
at least 65535 pixels on one side.
|
||||
- *Making good use of computing resources*: exercise the GPU and all CPU cores.
|
||||
- *Superb keyboard accessibility*: we recognise
|
||||
https://p.janouch.name/text/human-interface-guidelines.html[HIGs]
|
||||
and also have Vimium-style link hints.
|
||||
- *We never silently move or modify image files.*
|
||||
|
||||
Do it yourself
|
||||
~~~~~~~~~~~~~~
|
||||
- HDR: the project scope is huge as it is, but you're welcome to lend a hand.
|
||||
|
||||
Rejected
|
||||
~~~~~~~~
|
||||
- Image editing: GIMP works well enough already.
|
||||
- UI animations are seen as distracting, frustrating, and even irritating.
|
||||
|
||||
Packages
|
||||
--------
|
||||
- Latest https://janouch.name/cd/Dawn-latest-x86_64.flatpak[Linux Flatpak],
|
||||
https://aur.archlinux.org/packages/dn-git[Arch Linux AUR].
|
||||
|
||||
- Latest https://janouch.name/cd/Dawn-latest-Windows-x86_64.zip[Windows ZIP],
|
||||
https://janouch.name/cd/Dawn-latest-x86_64.msi[Windows installer];
|
||||
they work in Wine.
|
||||
|
||||
- macOS requires a fee for a signed package to be published. Maybe later.
|
||||
|
||||
On Windows, if you run the application with a high integrity level and you
|
||||
have no true Vulkan backend installed, it may not initialise. Manually rename
|
||||
'vk_swiftshader.dll' to 'vulkan-1.dll'. You will obtain software rendering.
|
||||
|
||||
Building
|
||||
--------
|
||||
See link:docs/building-from-source.adoc[Building from source].
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
See link:docs/configuring-your-system.adoc[Configuring your system].
|
||||
If everything goes beautifully, the following test image will exercise your
|
||||
display's true gamut, and you'll also be able to see the finer steps.
|
||||
The bottom row should look more like '`linear 188`', not '`128`'
|
||||
(a popular bug).
|
||||
|
||||
image::docs/p3-srgb-ramps.png[Test image: 10-bit RGB ramps, scaling test]
|
||||
|
||||
Note that 'Dawn' currently implements Bayer dithering for 8-bit swapchains,
|
||||
which you may need to turn off in settings to observe the difference.
|
||||
|
||||
Competition
|
||||
-----------
|
||||
The macOS 'Preview.app' is excellent at what it does, if limited in scope:
|
||||
it effortlessly handles wide gamuts, high bit depth, and gamma-correct scaling.
|
||||
|
||||
Electron-based viewers like
|
||||
_https://github.com/Auxx/light-matter['Light Matter']_
|
||||
are promising in that browser engines typically handle things well,
|
||||
at the cost of being rather heavyweight.
|
||||
|
||||
'digiKam' must be mentioned, as it covers a lot of the advanced features
|
||||
of this project. Sadly, it's obviously been designed by programmers,
|
||||
and the programmer behind 'Dawn' can't make heads or tails of the UI.
|
||||
|
||||
Resources
|
||||
---------
|
||||
- https://wolf.nereid.pl/posts/image-viewer/
|
||||
|
||||
Acknowledgments
|
||||
---------------
|
||||
- William R. Commander for
|
||||
https://github.com/libjpeg-turbo/libjpeg-turbo.git[libjpeg-turbo]
|
||||
- Marti Maria for
|
||||
https://github.com/mm2/little-cms[Little CMS]
|
||||
- Nigel Tao for
|
||||
https://github.com/google/wuffs[Wuffs]
|
||||
|
||||
Contributing and Support
|
||||
------------------------
|
||||
Use https://git.janouch.name/p/dawn to report any bugs, request features,
|
||||
or submit pull requests. `git send-email` is tolerated. If you want to discuss
|
||||
the project, feel free to join at ircs://irc.janouch.name, channel #dev.
|
||||
|
||||
Bitcoin donations are accepted at: 12r5uEWEgcHC46xd64tt3hHt9EUvYYDHe9
|
||||
|
||||
License
|
||||
-------
|
||||
This software is released under the terms of the MPL 2.0 licence, the text of
|
||||
which is included within the package along with the list of authors.
|
||||
|
||||
With Little CMS plugins bundled in, the binary package becomes GPL-licensed.
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# embed-shader.cmake: glslang SPIR-V compile + uint32_t header embed
|
||||
#
|
||||
# Copyright The Dawn Authors
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# dawn_embed_shader(<target> <src> <symbol> [glslang args...])
|
||||
#
|
||||
# <src> is relative to ${PROJECT_SOURCE_DIR}/shaders. Compiles it to
|
||||
# SPIR-V and writes <target BINARY_DIR>/generated/<stem>-spv.h (stem =
|
||||
# symbol with '_' → '-'). BINARY_DIR is the target's, so a call from
|
||||
# another listfile still lands next to that target. Targets defined
|
||||
# in different directories (libdn vs dn) each get their own
|
||||
# fullscreen-vert-spv.h.
|
||||
#
|
||||
|
||||
function(dawn_embed_shader target src symbol)
|
||||
if(NOT GLSLANG_VALIDATOR)
|
||||
message(FATAL_ERROR "dawn_embed_shader requires GLSLANG_VALIDATOR")
|
||||
endif()
|
||||
if(NOT TARGET "${target}")
|
||||
message(FATAL_ERROR
|
||||
"dawn_embed_shader: target '${target}' does not exist")
|
||||
endif()
|
||||
|
||||
string(REPLACE "_" "-" stem "${symbol}")
|
||||
get_target_property(bin_dir ${target} BINARY_DIR)
|
||||
set(gen_dir "${bin_dir}/generated")
|
||||
set(spv "${gen_dir}/${stem}.spv")
|
||||
set(hdr "${gen_dir}/${stem}-spv.h")
|
||||
set(shader_dir "${PROJECT_SOURCE_DIR}/shaders")
|
||||
set(src_abs "${shader_dir}/${src}")
|
||||
|
||||
file(MAKE_DIRECTORY "${gen_dir}")
|
||||
add_custom_command(
|
||||
OUTPUT "${spv}" "${hdr}"
|
||||
COMMAND "${GLSLANG_VALIDATOR}"
|
||||
-V "-I${shader_dir}" ${ARGN} "${src_abs}" -o "${spv}"
|
||||
COMMAND "${CMAKE_COMMAND}"
|
||||
"-DINPUT=${spv}" "-DOUTPUT=${hdr}" "-DSYMBOL=${symbol}"
|
||||
-P "${PROJECT_SOURCE_DIR}/cmake/embed-spirv.cmake"
|
||||
DEPENDS "${src_abs}" "${shader_dir}/common.glsl"
|
||||
"${PROJECT_SOURCE_DIR}/cmake/embed-spirv.cmake"
|
||||
COMMENT "SPIR-V ${stem}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
target_sources(${target} PRIVATE "${hdr}")
|
||||
target_include_directories(${target} PRIVATE "${gen_dir}")
|
||||
endfunction()
|
||||
@@ -0,0 +1,42 @@
|
||||
#
|
||||
# embed-spirv.cmake: embed a SPIR-V binary as a C++ uint32_t array header
|
||||
#
|
||||
# Copyright The Dawn Authors
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# Usage: cmake -DINPUT=foo.spv -DOUTPUT=foo-spv.h -DSYMBOL=foo_spv -P embed-spirv.cmake
|
||||
#
|
||||
|
||||
if(NOT INPUT OR NOT OUTPUT OR NOT SYMBOL)
|
||||
message(FATAL_ERROR "INPUT, OUTPUT, and SYMBOL required")
|
||||
endif()
|
||||
|
||||
file(READ "${INPUT}" spirv_hex HEX)
|
||||
string(LENGTH "${spirv_hex}" hex_len)
|
||||
math(EXPR word_count "${hex_len} / 8")
|
||||
|
||||
set(out "// Code generated from ${INPUT}. DO NOT EDIT.\n")
|
||||
string(APPEND out [[
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
]])
|
||||
string(APPEND out "static constexpr uint32_t ${SYMBOL}[] = {\n")
|
||||
|
||||
set(i 0)
|
||||
while(i LESS hex_len)
|
||||
string(SUBSTRING "${spirv_hex}" ${i} 8 word_hex)
|
||||
# SPIR-V is little-endian in the file; HEX read is byte order as stored.
|
||||
string(SUBSTRING "${word_hex}" 0 2 b0)
|
||||
string(SUBSTRING "${word_hex}" 2 2 b1)
|
||||
string(SUBSTRING "${word_hex}" 4 2 b2)
|
||||
string(SUBSTRING "${word_hex}" 6 2 b3)
|
||||
string(APPEND out "\t0x${b3}${b2}${b1}${b0}u,\n")
|
||||
math(EXPR i "${i} + 8")
|
||||
endwhile()
|
||||
|
||||
string(APPEND out "};\n")
|
||||
string(APPEND out "static constexpr uint32_t ${SYMBOL}_words = ${word_count}u;\n")
|
||||
|
||||
file(WRITE "${OUTPUT}" "${out}")
|
||||
@@ -0,0 +1,68 @@
|
||||
# Overlay for jpeg-quantsmooth (copied into the FetchContent source tree).
|
||||
# Port of fiv/subprojects/packagefiles/libjpegqs/meson.build.
|
||||
#
|
||||
# One static archive (not several link_with deps): the selector and SIMD
|
||||
# variants reference each other, and separate .a files lose do_quantsmooth
|
||||
# when linked into libdn.so (GNU ld single-pass archives).
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(jpegqs LANGUAGES C)
|
||||
|
||||
if(NOT TARGET PkgConfig::JPEG)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(JPEG REQUIRED IMPORTED_TARGET libjpeg)
|
||||
endif()
|
||||
|
||||
find_library(JPEGQS_LIBM m)
|
||||
|
||||
set(_jpegqs_defs WITH_LOG)
|
||||
set(_jpegqs_libs PkgConfig::JPEG)
|
||||
if(JPEGQS_LIBM)
|
||||
list(APPEND _jpegqs_libs "${JPEGQS_LIBM}")
|
||||
endif()
|
||||
|
||||
# Do not put the quantsmooth source dir on the include path: it ships a
|
||||
# jconfig.h that shadows the system jpeg-turbo one under multiarch layouts.
|
||||
|
||||
function(dn_jpegqs_object name)
|
||||
cmake_parse_arguments(A "" "" "DEFS;FLAGS" ${ARGN})
|
||||
add_library(jpegqs-${name} OBJECT libjpegqs.c)
|
||||
target_compile_definitions(jpegqs-${name} PRIVATE ${_jpegqs_defs} ${A_DEFS})
|
||||
if(A_FLAGS)
|
||||
target_compile_options(jpegqs-${name} PRIVATE ${A_FLAGS})
|
||||
endif()
|
||||
target_link_libraries(jpegqs-${name} PUBLIC ${_jpegqs_libs})
|
||||
set_target_properties(jpegqs-${name} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
endfunction()
|
||||
|
||||
set(_jpegqs_objects "")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")
|
||||
dn_jpegqs_object(avx512
|
||||
DEFS SIMD_SELECT SIMD_NAME=avx512 SIMD_AVX512
|
||||
FLAGS -mavx512f -mfma)
|
||||
dn_jpegqs_object(avx2
|
||||
DEFS SIMD_SELECT SIMD_NAME=avx2 SIMD_AVX2
|
||||
FLAGS -mavx2 -mfma)
|
||||
dn_jpegqs_object(sse2
|
||||
DEFS SIMD_SELECT SIMD_NAME=sse2 SIMD_SSE2
|
||||
FLAGS -msse2)
|
||||
dn_jpegqs_object(base
|
||||
DEFS SIMD_SELECT SIMD_NAME=base SIMD_BASE)
|
||||
dn_jpegqs_object(select DEFS SIMD_SELECT)
|
||||
set(_jpegqs_objects
|
||||
$<TARGET_OBJECTS:jpegqs-base>
|
||||
$<TARGET_OBJECTS:jpegqs-sse2>
|
||||
$<TARGET_OBJECTS:jpegqs-avx2>
|
||||
$<TARGET_OBJECTS:jpegqs-avx512>
|
||||
$<TARGET_OBJECTS:jpegqs-select>)
|
||||
else()
|
||||
dn_jpegqs_object(nosimd DEFS NO_SIMD)
|
||||
set(_jpegqs_objects $<TARGET_OBJECTS:jpegqs-nosimd>)
|
||||
endif()
|
||||
|
||||
add_library(jpegqs STATIC ${_jpegqs_objects})
|
||||
set_target_properties(jpegqs PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
target_link_libraries(jpegqs PUBLIC ${_jpegqs_libs})
|
||||
target_include_directories(jpegqs PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
@@ -0,0 +1,4 @@
|
||||
// This separate directory is necessary for Debian's multiarch with jpeg-turbo,
|
||||
// because its jpeglib.h cannot perform local inclusion of jconfig.h,
|
||||
// resulting in it being found within jpeg-quantsmooth and breaking the build.
|
||||
#include "../libjpegqs.h"
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# lxdr-generate.cmake: build-time LibertyXDR C++ header generation
|
||||
#
|
||||
# Copyright The Dawn Authors
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# lxdr_generate(<out_header> <namespace> <prefix_camel> <lxdr>...)
|
||||
#
|
||||
# Writes <out_header> (typically .../<name>.lxdr.hpp) from the listed
|
||||
# .lxdr files. Does not attach the header to any compile target.
|
||||
#
|
||||
|
||||
function(lxdr_generate out_header namespace prefix_camel)
|
||||
if(NOT AWK)
|
||||
message(FATAL_ERROR "lxdr_generate requires AWK")
|
||||
endif()
|
||||
if(NOT ARGN)
|
||||
message(FATAL_ERROR "lxdr_generate: no .lxdr inputs")
|
||||
endif()
|
||||
|
||||
set(lxdrgen "${PROJECT_SOURCE_DIR}/submodules/liberty/tools/lxdrgen.awk")
|
||||
set(lxdrgen_cpp "${PROJECT_SOURCE_DIR}/ipc/lxdrgen-cpp.awk")
|
||||
|
||||
set(lxdr_abs)
|
||||
foreach(f IN LISTS ARGN)
|
||||
if(NOT IS_ABSOLUTE "${f}")
|
||||
get_filename_component(f "${f}" ABSOLUTE)
|
||||
endif()
|
||||
list(APPEND lxdr_abs "${f}")
|
||||
endforeach()
|
||||
|
||||
get_filename_component(out_dir "${out_header}" DIRECTORY)
|
||||
get_filename_component(out_name "${out_header}" NAME)
|
||||
string(MAKE_C_IDENTIFIER "${out_name}" out_ident)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${out_header}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${out_dir}"
|
||||
COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C
|
||||
${AWK} -f "${lxdrgen}" -f "${lxdrgen_cpp}"
|
||||
-v "PrefixCamel=${prefix_camel}"
|
||||
-v "Namespace=${namespace}"
|
||||
${lxdr_abs}
|
||||
> "${out_header}"
|
||||
DEPENDS ${lxdr_abs} "${lxdrgen}" "${lxdrgen_cpp}"
|
||||
COMMENT "Generating ${out_name}"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(lxdr-${out_ident} DEPENDS "${out_header}")
|
||||
endfunction()
|
||||
@@ -0,0 +1,60 @@
|
||||
# macOS app bundle: Qt Frameworks/PlugIns + fiv-like Resources/share.
|
||||
# MACOSX_BUNDLE is set on dn next to qt_add_executable.
|
||||
|
||||
set(_dn_bundle "$<TARGET_BUNDLE_DIR_NAME:dn>")
|
||||
install(TARGETS dn BUNDLE DESTINATION .)
|
||||
#install(TARGETS dnthumbd RUNTIME DESTINATION "${_dn_bundle}/Contents/MacOS")
|
||||
install(TARGETS libdn LIBRARY DESTINATION "${_dn_bundle}/Contents/Frameworks")
|
||||
|
||||
set(_dn_mime_dir)
|
||||
pkg_get_variable(_dn_smi_datadir shared-mime-info datadir)
|
||||
if(_dn_smi_datadir AND EXISTS "${_dn_smi_datadir}/mime/globs2"
|
||||
AND EXISTS "${_dn_smi_datadir}/mime/subclasses")
|
||||
set(_dn_mime_dir "${_dn_smi_datadir}/mime")
|
||||
endif()
|
||||
if(NOT _dn_mime_dir)
|
||||
foreach(_dn_root IN ITEMS "$ENV{HOMEBREW_PREFIX}" /opt/homebrew /usr/local)
|
||||
if(_dn_root AND EXISTS "${_dn_root}/share/mime/globs2"
|
||||
AND EXISTS "${_dn_root}/share/mime/subclasses")
|
||||
set(_dn_mime_dir "${_dn_root}/share/mime")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
install(FILES
|
||||
"${_dn_mime_dir}/globs2"
|
||||
"${_dn_mime_dir}/subclasses"
|
||||
DESTINATION "${_dn_bundle}/Contents/Resources/share/mime")
|
||||
|
||||
# macdeployqt otherwise copies imageformats/virtualkeyboard/styles and
|
||||
# pulls QtSvg, QtPdf, QtQuick, QtWidgets, QtNetwork with them.
|
||||
install(IMPORTED_RUNTIME_ARTIFACTS Qt6::QCocoaIntegrationPlugin
|
||||
LIBRARY DESTINATION "${_dn_bundle}/Contents/PlugIns/platforms"
|
||||
RUNTIME DESTINATION "${_dn_bundle}/Contents/PlugIns/platforms")
|
||||
|
||||
# MoltenVK is an ICD; macdeployqt only copies the linked loader.
|
||||
# Homebrew lib/libMoltenVK.dylib is a Cellar symlink; install(FILES) would
|
||||
# copy the link and the relocated bundle would not find MoltenVK.
|
||||
find_library(DN_MOLTENVK_LIBRARY NAMES MoltenVK REQUIRED
|
||||
HINTS "$ENV{HOMEBREW_PREFIX}/lib" /opt/homebrew/lib /usr/local/lib)
|
||||
get_filename_component(DN_MOLTENVK_LIBRARY "${DN_MOLTENVK_LIBRARY}" REALPATH)
|
||||
install(FILES "${DN_MOLTENVK_LIBRARY}"
|
||||
DESTINATION "${_dn_bundle}/Contents/Frameworks")
|
||||
file(WRITE "${CMAKE_BINARY_DIR}/MoltenVK_icd.json" [[{
|
||||
"file_format_version": "1.0.0",
|
||||
"ICD": {
|
||||
"library_path": "../../../Frameworks/libMoltenVK.dylib",
|
||||
"api_version": "1.4.0",
|
||||
"is_portability_driver": true
|
||||
}
|
||||
}
|
||||
]])
|
||||
install(FILES "${CMAKE_BINARY_DIR}/MoltenVK_icd.json"
|
||||
DESTINATION "${_dn_bundle}/Contents/Resources/vulkan/icd.d")
|
||||
unset(_dn_bundle)
|
||||
|
||||
qt_generate_deploy_app_script(TARGET dn OUTPUT_SCRIPT _dn_deploy_dn
|
||||
NO_PLUGINS)
|
||||
install(SCRIPT "${_dn_deploy_dn}")
|
||||
# Homebrew Qt QTBUG-127075: run macdeployqt twice.
|
||||
install(SCRIPT "${_dn_deploy_dn}")
|
||||
@@ -0,0 +1,46 @@
|
||||
# To be run from cmake_install.cmake, eradicates all unreferenced libraries.
|
||||
# CMake 3.9.6 has a parsing bug with ENCODING UTF-8.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
# CPack runs this almost without any CMake variables at all
|
||||
# (cmStateSnapshot::SetDefaultDefinitions(), CMAKE_INSTALL_PREFIX, [DESTDIR])
|
||||
set(installdir "${CMAKE_INSTALL_PREFIX}")
|
||||
if(NOT installdir OR installdir MATCHES "^/usr(/|$)")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# The function is recursive and CMake has tragic scoping behaviour;
|
||||
# environment variables are truly global there, in the absence of a cache
|
||||
unset(ENV{seen})
|
||||
function(expand path)
|
||||
set(seen $ENV{seen})
|
||||
if(path IN_LIST seen OR NOT EXISTS "${path}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(ENV{seen} "$ENV{seen};${path}")
|
||||
file(STRINGS "${path}" strings REGEX "[.][Dd][Ll][Ll]$" ENCODING UTF-8)
|
||||
foreach(string ${strings})
|
||||
string(REGEX MATCH "[-.+_a-zA-Z0-9]+$" word "${string}")
|
||||
expand("${installdir}/${word}")
|
||||
|
||||
# Windows is case-insensitive.
|
||||
string(TOLOWER "${word}" lower)
|
||||
if(NOT word STREQUAL lower)
|
||||
expand("${installdir}/${lower}")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
file(GLOB roots LIST_DIRECTORIES false "${installdir}/*.[Ee][Xx][Ee]"
|
||||
"${installdir}/platforms/*.[Dd][Ll][Ll]"
|
||||
"${installdir}/vk_swiftshader.dll")
|
||||
foreach(binary ${roots})
|
||||
expand("${binary}")
|
||||
endforeach()
|
||||
|
||||
file(GLOB libraries LIST_DIRECTORIES false "${installdir}/*.[Dd][Ll][Ll]")
|
||||
list(REMOVE_ITEM libraries $ENV{seen})
|
||||
if(libraries)
|
||||
file(REMOVE ${libraries})
|
||||
endif()
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// dawn-config.h.in: build-time feature macros
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#define DAWN_NAME "@PROJECT_NAME@"
|
||||
#define DAWN_NAMESPACE "@DAWN_NAMESPACE@"
|
||||
#define DAWN_VERSION "@PROJECT_VERSION@"
|
||||
|
||||
#cmakedefine01 DAWN_WITH_LCMS2_FAST_FLOAT
|
||||
#cmakedefine01 DAWN_WITH_LCMS2_THREADED
|
||||
#cmakedefine01 DAWN_WITH_JPEG_QS
|
||||
#cmakedefine01 DAWN_WITH_LIBRAW
|
||||
#cmakedefine01 DAWN_WITH_LIBRSVG
|
||||
#cmakedefine01 DAWN_WITH_XCURSOR
|
||||
#cmakedefine01 DAWN_WITH_LIBHEIF
|
||||
#cmakedefine01 DAWN_WITH_LIBJXL
|
||||
#cmakedefine01 DAWN_WITH_OPENJPEG
|
||||
#cmakedefine01 DAWN_WITH_LIBTIFF
|
||||
#cmakedefine01 DAWN_WITH_JXRLIB
|
||||
#cmakedefine01 DAWN_WITH_GLYCIN
|
||||
#cmakedefine01 DAWN_WITH_GDKPIXBUF
|
||||
#cmakedefine01 DAWN_WITH_IMAGEIO
|
||||
#cmakedefine01 DAWN_WITH_CGPDF
|
||||
|
||||
#cmakedefine01 DN_WITH_WAYLAND
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
<?define FullName = "@PROJECT_NAME@ @PROJECT_VERSION@" ?>
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else?>
|
||||
<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif?>
|
||||
|
||||
<Product Id='*'
|
||||
Name='$(var.FullName)'
|
||||
UpgradeCode='a99226f0-c9a4-4cc8-8065-bb79dc3c3469'
|
||||
Language='1033'
|
||||
Codepage='1252'
|
||||
Version='@PROJECT_VERSION@'
|
||||
Manufacturer='@CPACK_PACKAGE_VENDOR@'>
|
||||
|
||||
<Package Id='*'
|
||||
Keywords='Installer,Image,Browser'
|
||||
Description='$(var.FullName) Installer'
|
||||
Manufacturer='@CPACK_PACKAGE_VENDOR@'
|
||||
InstallerVersion='200'
|
||||
Compressed='yes'
|
||||
Languages='1033'
|
||||
SummaryCodepage='1252' />
|
||||
|
||||
<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
|
||||
<Icon Id='dn.ico' SourceFile='@DAWN_ICON_ICO@' />
|
||||
<Property Id='ARPPRODUCTICON' Value='dn.ico' />
|
||||
<Property Id='ARPURLINFOABOUT' Value='@PROJECT_HOMEPAGE_URL@' />
|
||||
|
||||
<UIRef Id='WixUI_Minimal' />
|
||||
<!-- This isn't supported by msitools, but is necessary for WiX.
|
||||
<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
|
||||
-->
|
||||
|
||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||
<Directory Id='$(var.ProgramFilesFolder)'>
|
||||
<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='ProgramMenuFolder'>
|
||||
<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
|
||||
</Directory>
|
||||
|
||||
<Directory Id='DesktopFolder' />
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id='ProgramMenuDir'>
|
||||
<Component Id='ProgramMenuDir' Guid='*'>
|
||||
<Shortcut Id='ProgramsMenuShortcut'
|
||||
Name='@PROJECT_NAME@'
|
||||
Target='[INSTALLDIR]\dn.exe'
|
||||
WorkingDirectory='INSTALLDIR'
|
||||
Arguments='"%USERPROFILE%"'
|
||||
Icon='dn.ico' />
|
||||
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
|
||||
<RegistryValue Root='HKCU'
|
||||
Key='Software\[Manufacturer]\[ProductName]'
|
||||
Type='string'
|
||||
Value=''
|
||||
KeyPath='yes' />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<!-- FileAssociations is defined in the generated associations.wxs:
|
||||
it needs dn's actual "list supported media types" output. -->
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentGroupRef Id='CG.dn' />
|
||||
<ComponentRef Id='ProgramMenuDir' />
|
||||
<ComponentRef Id='FileAssociations' />
|
||||
</Feature>
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -0,0 +1,361 @@
|
||||
qt_add_executable(dn
|
||||
action.cpp
|
||||
app.cpp
|
||||
display-profile.cpp
|
||||
gpu.cpp
|
||||
kit-browser.cpp
|
||||
kit-chrome.cpp
|
||||
kit-cie-diagram.cpp
|
||||
kit.cpp
|
||||
kit-viewer.cpp
|
||||
main.cpp
|
||||
overlay.cpp
|
||||
renderer.cpp
|
||||
thumbnail-cache.cpp
|
||||
thumbnailer.cpp
|
||||
url.cpp
|
||||
window.cpp
|
||||
xdg.cpp
|
||||
icons.qrc
|
||||
)
|
||||
set_target_properties(dn PROPERTIES AUTOMOC ON AUTORCC ON)
|
||||
target_include_directories(dn PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
"${PROJECT_SOURCE_DIR}/libdn"
|
||||
)
|
||||
|
||||
# Widget geometry is integral device pixels, and the only way in is an explicit
|
||||
# conversion: a silent float truncation here is a layout bug. Scoped to dn, as
|
||||
# vendored dependencies do not build clean under it.
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(dn PRIVATE -Werror=float-conversion)
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(dn PRIVATE
|
||||
dawn::libdn
|
||||
Qt6::Gui
|
||||
PkgConfig::WEBP
|
||||
PkgConfig::WEBPDEMUX
|
||||
PkgConfig::WEBPMUX
|
||||
PkgConfig::RESVG
|
||||
Vulkan::Vulkan
|
||||
Threads::Threads
|
||||
)
|
||||
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||
add_custom_command(
|
||||
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/cmf-cie1931-2deg-1nm.h
|
||||
COMMAND ${AWK} -f ${CMAKE_CURRENT_SOURCE_DIR}/cmf-to-header.awk
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ciexyz31_1.csv
|
||||
${CMAKE_CURRENT_BINARY_DIR}/generated/cmf-cie1931-2deg-1nm.h
|
||||
DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmf-to-header.awk
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ciexyz31_1.csv
|
||||
COMMENT "Generate CIE 1931 2° 1 nm CMF header"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(dn-cmf-header
|
||||
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated/cmf-cie1931-2deg-1nm.h)
|
||||
add_dependencies(dn dn-cmf-header)
|
||||
|
||||
dawn_embed_shader(dn dn-overlay.vert dn_overlay_vert)
|
||||
dawn_embed_shader(dn dn-overlay.frag dn_overlay_frag)
|
||||
dawn_embed_shader(dn dn-thumb.frag dn_thumb_frag)
|
||||
dawn_embed_shader(dn dn-dither.frag dn_dither_frag)
|
||||
dawn_embed_shader(dn fullscreen.vert fullscreen_vert)
|
||||
|
||||
if (WIN32)
|
||||
target_sources(dn PRIVATE
|
||||
display-profile-windows.cpp
|
||||
assoc-windows.cpp
|
||||
)
|
||||
target_link_libraries(dn PRIVATE
|
||||
gdi32 shell32 shlwapi ole32 advapi32 user32)
|
||||
set_target_properties(dn PROPERTIES WIN32_EXECUTABLE $<NOT:$<CONFIG:Debug>>)
|
||||
elseif (APPLE)
|
||||
enable_language(OBJCXX)
|
||||
find_library(DN_APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
find_library(DN_COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
|
||||
find_library(DN_CORESERVICES_FRAMEWORK CoreServices REQUIRED)
|
||||
find_library(DN_UNIFORMTYPES_FRAMEWORK UniformTypeIdentifiers REQUIRED)
|
||||
target_sources(dn PRIVATE
|
||||
display-profile-macos.mm
|
||||
assoc-macos.mm
|
||||
app-menu-macos.mm
|
||||
window-appearance-macos.mm
|
||||
)
|
||||
target_link_libraries(dn PRIVATE
|
||||
"${DN_APPKIT_FRAMEWORK}"
|
||||
"${DN_COREGRAPHICS_FRAMEWORK}"
|
||||
"${DN_CORESERVICES_FRAMEWORK}"
|
||||
"${DN_UNIFORMTYPES_FRAMEWORK}")
|
||||
else()
|
||||
pkg_check_modules(DN_COLORD REQUIRED IMPORTED_TARGET colord)
|
||||
target_sources(dn PRIVATE
|
||||
display-profile-linux.cpp
|
||||
assoc-unix.cpp)
|
||||
target_link_libraries(dn PRIVATE PkgConfig::DN_COLORD)
|
||||
endif()
|
||||
|
||||
if (NOT APPLE)
|
||||
target_sources(dn PRIVATE instance.cpp)
|
||||
endif()
|
||||
|
||||
# Application icon: SVG → PNG/ICO (xT) on non-Apple; Swift squircle ICNS (xM)
|
||||
# on Darwin. Qt looks up IDI_ICON1 and nothing else.
|
||||
set(DN_ICON_SVG "${CMAKE_CURRENT_SOURCE_DIR}/dn.svg")
|
||||
if (APPLE)
|
||||
find_program(DN_SWIFT_EXECUTABLE swift REQUIRED)
|
||||
set(MACOSX_BUNDLE_ICON_FILE dn.icns)
|
||||
set(DN_ICON_ICNS "${CMAKE_CURRENT_BINARY_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||
add_custom_command(OUTPUT "${DN_ICON_ICNS}"
|
||||
COMMAND "${DN_SWIFT_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/gen-icon.swift" "${DN_ICON_ICNS}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/gen-icon.swift"
|
||||
COMMENT "Generating dn application icon" VERBATIM)
|
||||
set_source_files_properties("${DN_ICON_ICNS}" PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
target_sources(dn PRIVATE "${DN_ICON_ICNS}")
|
||||
|
||||
# Mirrors supported_media_types(), restricted to macOS types.
|
||||
# See /System/Library/CoreServices/CoreTypes.bundle/Contents/Info.plist
|
||||
set(utis
|
||||
com.apple.icns
|
||||
com.compuserve.gif
|
||||
com.microsoft.bmp
|
||||
com.truevision.tga-image
|
||||
org.webmproject.webp
|
||||
public.jpeg
|
||||
public.png
|
||||
public.svg-image
|
||||
)
|
||||
if (DAWN_WITH_LIBTIFF)
|
||||
list(APPEND utis public.tiff)
|
||||
endif()
|
||||
if (DAWN_WITH_LIBHEIF)
|
||||
list(APPEND utis public.avif public.heic public.heif)
|
||||
endif()
|
||||
if (DAWN_WITH_LIBJXL)
|
||||
list(APPEND utis public.jpeg-xl)
|
||||
endif()
|
||||
if (DAWN_WITH_OPENJPEG)
|
||||
list(APPEND utis public.jpeg-2000)
|
||||
endif()
|
||||
if (DAWN_WITH_LIBRAW)
|
||||
# The abstract type the vendor formats conform to.
|
||||
list(APPEND utis public.camera-raw-image)
|
||||
endif()
|
||||
if (DAWN_WITH_GDKPIXBUF)
|
||||
list(APPEND utis
|
||||
com.apple.quicktime-image
|
||||
com.microsoft.ico
|
||||
public.xbitmap-image
|
||||
)
|
||||
endif()
|
||||
if (DAWN_WITH_IMAGEIO)
|
||||
# Ask ImageIO through the very call the loader uses.
|
||||
# Hopefully it will match the system the program is run on as well.
|
||||
execute_process(
|
||||
COMMAND "${DN_SWIFT_EXECUTABLE}" -e [=[
|
||||
import ImageIO
|
||||
for type in CGImageSourceCopyTypeIdentifiers() as! [String] { print(type) }
|
||||
]=]
|
||||
OUTPUT_VARIABLE imageio_utis
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
separate_arguments(imageio_utis UNIX_COMMAND "${imageio_utis}")
|
||||
list(APPEND utis ${imageio_utis})
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES utis)
|
||||
list(SORT utis)
|
||||
|
||||
set(DN_MACOS_CONTENT_TYPES "")
|
||||
foreach (uti IN LISTS utis)
|
||||
string(APPEND DN_MACOS_CONTENT_TYPES
|
||||
"\n\t\t\t\t<string>${uti}</string>")
|
||||
endforeach()
|
||||
|
||||
# Still a template afterwards: CMake fills ${MACOSX_BUNDLE_*} at generate
|
||||
# time, and Qt copies it once more in between.
|
||||
set(plist "${CMAKE_CURRENT_BINARY_DIR}/Info.plist.in")
|
||||
configure_file(Info.plist.in "${plist}" @ONLY)
|
||||
|
||||
# XXX: I actually want to name the binary dn but the bundle Dawn.
|
||||
set_target_properties(dn PROPERTIES
|
||||
RUNTIME_OUTPUT_NAME "${PROJECT_NAME}"
|
||||
MACOSX_BUNDLE ON
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${DAWN_NAMESPACE}"
|
||||
MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}"
|
||||
MACOSX_BUNDLE_INFO_PLIST "${plist}"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
|
||||
MACOSX_BUNDLE_COPYRIGHT "${CPACK_PACKAGE_VENDOR}"
|
||||
)
|
||||
else()
|
||||
include(IconUtils)
|
||||
set(DN_ICON_BASE "${PROJECT_BINARY_DIR}/icons")
|
||||
set(DN_ICON_PNG_LIST)
|
||||
if (WIN32)
|
||||
set(DN_ICON_NAME dn)
|
||||
else()
|
||||
set(DN_ICON_NAME "${DAWN_NAMESPACE}")
|
||||
endif()
|
||||
foreach (DN_ICON_SIZE 16 32 48 256)
|
||||
icon_to_png("${DN_ICON_NAME}" "${DN_ICON_SVG}" ${DN_ICON_SIZE}
|
||||
"${DN_ICON_BASE}" DN_ICON_PNG)
|
||||
list(APPEND DN_ICON_PNG_LIST "${DN_ICON_PNG}")
|
||||
endforeach()
|
||||
|
||||
if (WIN32)
|
||||
list(REMOVE_ITEM DN_ICON_PNG_LIST "${DN_ICON_PNG}")
|
||||
set(DN_ICON_ICO "${CMAKE_CURRENT_BINARY_DIR}/dn.ico")
|
||||
icon_for_win32("${DN_ICON_ICO}" "${DN_ICON_PNG_LIST}" "${DN_ICON_PNG}")
|
||||
|
||||
set(DN_ICON_RC "${CMAKE_CURRENT_BINARY_DIR}/dn.rc")
|
||||
# Qt specifically looks up IDI_ICON1 and nothing else.
|
||||
add_custom_command(OUTPUT "${DN_ICON_RC}"
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "IDI_ICON1 ICON \"dn.ico\""
|
||||
> ${DN_ICON_RC} VERBATIM)
|
||||
set_property(SOURCE "${DN_ICON_RC}"
|
||||
APPEND PROPERTY OBJECT_DEPENDS ${DN_ICON_ICO})
|
||||
target_sources(dn PRIVATE "${DN_ICON_RC}")
|
||||
else()
|
||||
add_custom_target(icons ALL DEPENDS ${DN_ICON_PNG_LIST})
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(FILES "${DN_ICON_SVG}"
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
|
||||
RENAME "${DAWN_NAMESPACE}.svg")
|
||||
install(DIRECTORY "${DN_ICON_BASE}"
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}")
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.desktop"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.Browse.desktop"
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
||||
|
||||
# The desktop file's MimeType= cannot be filled in at build time:
|
||||
# it depends on gdk-pixbuf loaders installed on the target.
|
||||
# The parent list runs this after dn itself is installed.
|
||||
set(DN_DESKTOP_UPDATER
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dn-update-desktop-files")
|
||||
|
||||
# The parent list runs this script directly, so it needs to be
|
||||
# executable, which configure_file() does not make it.
|
||||
configure_file(dn-update-desktop-files.in
|
||||
"${DN_DESKTOP_UPDATER}" @ONLY
|
||||
FILE_PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
install(PROGRAMS "${DN_DESKTOP_UPDATER}"
|
||||
DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
set(DN_DESKTOP_UPDATER "${DN_DESKTOP_UPDATER}" PARENT_SCOPE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (DN_WITH_WAYLAND)
|
||||
pkg_get_variable(DN_WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||
if (NOT DN_WAYLAND_PROTOCOLS_DIR)
|
||||
message(FATAL_ERROR "wayland-protocols pkgdatadir not found")
|
||||
endif()
|
||||
|
||||
pkg_get_variable(DN_WAYLAND_SCANNER wayland-scanner wayland_scanner)
|
||||
if (NOT DN_WAYLAND_SCANNER)
|
||||
find_program(DN_WAYLAND_SCANNER wayland-scanner REQUIRED)
|
||||
endif()
|
||||
|
||||
set(DN_PROTOCOL_DIR "${CMAKE_CURRENT_BINARY_DIR}/protocols")
|
||||
file(MAKE_DIRECTORY "${DN_PROTOCOL_DIR}")
|
||||
set(DN_COLOR_XML
|
||||
"${DN_WAYLAND_PROTOCOLS_DIR}/staging/color-management/color-management-v1.xml")
|
||||
set(DN_COLOR_HEADER
|
||||
"${DN_PROTOCOL_DIR}/color-management-v1-client-protocol.h")
|
||||
set(DN_COLOR_SOURCE
|
||||
"${DN_PROTOCOL_DIR}/color-management-v1-protocol.c")
|
||||
set(DN_XDG_XML
|
||||
"${DN_WAYLAND_PROTOCOLS_DIR}/stable/xdg-shell/xdg-shell.xml")
|
||||
set(DN_XDG_HEADER "${DN_PROTOCOL_DIR}/xdg-shell-client-protocol.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${DN_COLOR_HEADER}" "${DN_COLOR_SOURCE}" "${DN_XDG_HEADER}"
|
||||
COMMAND "${DN_WAYLAND_SCANNER}" client-header
|
||||
"${DN_COLOR_XML}" "${DN_COLOR_HEADER}"
|
||||
COMMAND "${DN_WAYLAND_SCANNER}" private-code
|
||||
"${DN_COLOR_XML}" "${DN_COLOR_SOURCE}"
|
||||
COMMAND "${DN_WAYLAND_SCANNER}" client-header
|
||||
"${DN_XDG_XML}" "${DN_XDG_HEADER}"
|
||||
DEPENDS "${DN_COLOR_XML}" "${DN_XDG_XML}"
|
||||
COMMENT "wayland-scanner protocols for dn"
|
||||
VERBATIM)
|
||||
|
||||
target_sources(dn PRIVATE
|
||||
wayland-color-bridge.cpp
|
||||
wayland-window.cpp
|
||||
"${DN_COLOR_HEADER}"
|
||||
"${DN_COLOR_SOURCE}"
|
||||
"${DN_XDG_HEADER}"
|
||||
)
|
||||
target_include_directories(dn PRIVATE "${DN_PROTOCOL_DIR}")
|
||||
target_link_libraries(dn PRIVATE PkgConfig::DN_WAYLAND Qt6::GuiPrivate)
|
||||
endif()
|
||||
|
||||
# User guide: build tree at share/docs/dawnn (../share from bin/),
|
||||
# installed next to the application on each platform.
|
||||
set(DAWN_HELP_DIR "${PROJECT_BINARY_DIR}/share/doc/dawn")
|
||||
file(MAKE_DIRECTORY "${DAWN_HELP_DIR}")
|
||||
configure_file(
|
||||
"${PROJECT_SOURCE_DIR}/docs/dn.html.in"
|
||||
"${DAWN_HELP_DIR}/dn.html"
|
||||
@ONLY)
|
||||
configure_file(
|
||||
"${PROJECT_SOURCE_DIR}/docs/stylesheet.css"
|
||||
"${DAWN_HELP_DIR}/stylesheet.css"
|
||||
COPYONLY)
|
||||
|
||||
if (APPLE)
|
||||
set_source_files_properties(
|
||||
"${DAWN_HELP_DIR}/dn.html"
|
||||
"${DAWN_HELP_DIR}/stylesheet.css"
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/share/doc/dawn")
|
||||
target_sources(dn PRIVATE
|
||||
"${DAWN_HELP_DIR}/dn.html"
|
||||
"${DAWN_HELP_DIR}/stylesheet.css"
|
||||
)
|
||||
elseif (WIN32)
|
||||
install(FILES
|
||||
"${DAWN_HELP_DIR}/dn.html"
|
||||
"${DAWN_HELP_DIR}/stylesheet.css"
|
||||
DESTINATION share/doc/dawn)
|
||||
else()
|
||||
include(GNUInstallDirs)
|
||||
install(FILES
|
||||
"${DAWN_HELP_DIR}/dn.html"
|
||||
"${DAWN_HELP_DIR}/stylesheet.css"
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/dawn")
|
||||
|
||||
# This manual page generator should be good enough.
|
||||
set(ASCIIMAN "${PROJECT_SOURCE_DIR}/submodules/liberty/tools/asciiman.awk")
|
||||
foreach (page dn)
|
||||
set(page_output "${CMAKE_CURRENT_BINARY_DIR}/${page}.1")
|
||||
list(APPEND project_MAN_PAGES "${page_output}")
|
||||
add_custom_command(OUTPUT ${page_output}
|
||||
COMMAND env LC_ALL=C asciidoc-release-version=${PROJECT_VERSION}
|
||||
${AWK} -f ${ASCIIMAN} "${PROJECT_SOURCE_DIR}/docs/${page}.adoc"
|
||||
> ${page_output}
|
||||
DEPENDS "${PROJECT_SOURCE_DIR}/docs/${page}.adoc" ${ASCIIMAN}
|
||||
COMMENT "Generating man page for ${page}" VERBATIM)
|
||||
endforeach()
|
||||
|
||||
add_custom_target(docs ALL DEPENDS ${project_MAN_PAGES})
|
||||
|
||||
foreach (page ${project_MAN_PAGES})
|
||||
string(REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
|
||||
install(FILES "${page}"
|
||||
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if (BUILD_TESTING)
|
||||
add_test(NAME dn-help COMMAND $<TARGET_FILE:dn> --help)
|
||||
endif()
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!--
|
||||
This file is configured three times, by three different things:
|
||||
|
||||
1. dn/CMakeLists.txt fills in the content type list, with @ONLY,
|
||||
2. Qt's qt_add_executable finalizer copies the result through a second
|
||||
@ONLY pass (_qt_internal_copy_info_plist), by which point nothing
|
||||
is left for it to substitute,
|
||||
3. CMake substitutes the MACOSX_BUNDLE_ entries from target properties.
|
||||
|
||||
At-sign placeholders are therefore ours and Qt's, dollar-brace ones are
|
||||
CMake's. Naming a bundle key with at-signs would have Qt substitute it in
|
||||
Qt's own scope, where it is undefined, and the value would silently come
|
||||
out empty. All three passes rewrite this comment as well, so do not write
|
||||
either kind of placeholder here, not even to illustrate the point.
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@CMAKE_OSX_DEPLOYMENT_TARGET@</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.photography</string>
|
||||
|
||||
<!-- From Qt's own template. -->
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
|
||||
<!-- Locations macOS withholds until the user consents. The purpose
|
||||
string is what the consent dialog has to show; leaving one out gets
|
||||
the browser refused with no way for the user to say yes. -->
|
||||
<key>NSDesktopFolderUsageDescription</key>
|
||||
<string>Dawn can show images kept on your Desktop.</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string>Dawn can show images kept in your Documents folder.</string>
|
||||
<key>NSDownloadsFolderUsageDescription</key>
|
||||
<string>Dawn can show images kept in your Downloads folder.</string>
|
||||
<key>NSRemovableVolumesUsageDescription</key>
|
||||
<string>Dawn can show images kept on removable volumes.</string>
|
||||
<key>NSNetworkVolumesUsageDescription</key>
|
||||
<string>Dawn can show images kept on network volumes.</string>
|
||||
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Image</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>@DN_MACOS_CONTENT_TYPES@
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Folder</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.folder</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
//
|
||||
// action.cpp: shared action table (labels, keys, menus)
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "action.hpp"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QIODevice>
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QMimeData>
|
||||
#include <QUrl>
|
||||
#include <QtLogging>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
constexpr uint8_t kMenu = ActionInMenu;
|
||||
constexpr uint8_t kToggle = ActionInMenu | ActionToggle;
|
||||
constexpr unsigned kCtrl = unsigned(Qt::ControlModifier);
|
||||
constexpr unsigned kAlt = unsigned(Qt::AltModifier);
|
||||
constexpr unsigned kShift = unsigned(Qt::ShiftModifier);
|
||||
constexpr unsigned kMeta = unsigned(Qt::MetaModifier);
|
||||
|
||||
// clang-format off
|
||||
constexpr ActionDef kDefs[] = {
|
||||
{},
|
||||
{kMenu, {"_New Window"}, {}, {{Qt::Key_N, kCtrl}}, {}},
|
||||
{kMenu, {"_Close Window"}, {}, {{Qt::Key_W, kCtrl}, {Qt::Key_Q}}, {}},
|
||||
{0, {"_Minimise"}, {}, {}, {}},
|
||||
{kToggle, {"_Maximise", "Res_tore"}, {}, {}, {}},
|
||||
{kMenu, {"_Quit"}, {}, {{Qt::Key_Q, kCtrl}}, {}},
|
||||
{kToggle, {"Enter _Full Screen", "Exit _Full Screen"},
|
||||
{"view-fullscreen-symbolic", "view-restore-symbolic"}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_F, kCtrl | kMeta},
|
||||
#else
|
||||
{Qt::Key_F11},
|
||||
#endif
|
||||
}, {}},
|
||||
{kToggle, {"_Dark Mode"}, {"dark-mode-symbolic"}, {{Qt::Key_D}}, {}},
|
||||
{kMenu, {"_Hint"}, {}, {{Qt::Key_F}}, {}},
|
||||
{kMenu, {"_Back in History"}, {"curved-arrow-left-symbolic"}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_BracketLeft, kCtrl},
|
||||
#else
|
||||
{Qt::Key_Left, kAlt},
|
||||
#endif
|
||||
{Qt::Key_Backspace}}, {}},
|
||||
{kMenu, {"_Forward in History"}, {"curved-arrow-right-symbolic"}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_BracketRight, kCtrl},
|
||||
#else
|
||||
{Qt::Key_Right, kAlt},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"_Location..."}, {}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_G, kCtrl | kShift},
|
||||
#else
|
||||
{Qt::Key_L, kCtrl},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"_Contents"}, {}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_Question, kCtrl},
|
||||
#else
|
||||
{Qt::Key_F1},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"_About"}, {}, {}, {}},
|
||||
{kMenu, {"_Keyboard Shortcuts"}, {}, {
|
||||
#ifndef Q_OS_MACOS
|
||||
{Qt::Key_Question, kCtrl},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"_Settings..."}, {}, {{Qt::Key_Comma, kCtrl}}, {}},
|
||||
// TODO(p): Skip on macOS entirely, as it uses the global menu.
|
||||
{0, {"_Menu"}, {}, {{Qt::Key_F10}}, {}},
|
||||
// The context menu action is mostly for documentation only.
|
||||
{0, {"_Context Menu"}, {}, {{Qt::Key_Menu}, {Qt::Key_F10, kShift}}, {}},
|
||||
{0, {"_Cancel"}, {}, {{Qt::Key_Escape}}, {}},
|
||||
{0, {"_Next Pane"}, {}, {{Qt::Key_F6}}, {}},
|
||||
{0, {"_Previous Pane"}, {}, {{Qt::Key_F6, kShift}}, {}},
|
||||
|
||||
{kToggle, {"Show _Sidebar"}, {"sidebar-left-symbolic"},
|
||||
{{Qt::Key_F9}}, {}},
|
||||
{kMenu, {"_Previous Directory in Tree"}, {"go-previous-symbolic"},
|
||||
{{Qt::Key_BracketLeft}}, {}},
|
||||
{kMenu, {"_Next Directory in Tree"}, {"go-next-symbolic"},
|
||||
{{Qt::Key_BracketRight}}, {}},
|
||||
{kMenu, {"Parent _Directory"}, {"go-up-symbolic"}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_Up, kCtrl},
|
||||
#else
|
||||
{Qt::Key_Up, kAlt},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"_Home"}, {}, {
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_H, kCtrl | kShift},
|
||||
#else
|
||||
{Qt::Key_Home, kAlt},
|
||||
#endif
|
||||
}, {}},
|
||||
{kMenu, {"S_maller Thumbnails"}, {"minus-framed-symbolic"},
|
||||
{{Qt::Key_Minus}, {Qt::Key_Minus, kCtrl}}, {}},
|
||||
{kMenu, {"_Larger Thumbnails"}, {"plus-framed-symbolic"},
|
||||
{{Qt::Key_Plus}, {Qt::Key_Plus, kCtrl}}, {}},
|
||||
{kToggle, {"Tiled _View"}, {"blocks-symbolic"},
|
||||
{{Qt::Key_1}, {Qt::Key_1, kCtrl}}, {}},
|
||||
{kToggle, {"_Grid View"}, {"view-grid-symbolic"},
|
||||
{{Qt::Key_2}, {Qt::Key_2, kCtrl}}, {}},
|
||||
{kToggle, {"L_ist View"}, {"view-list-symbolic"},
|
||||
{{Qt::Key_3}, {Qt::Key_3, kCtrl}}, {}},
|
||||
{kToggle, {"Sho_w Filenames"}, {"font-symbolic"},
|
||||
{{Qt::Key_T}, {Qt::Key_T, kCtrl}}, {}},
|
||||
{kToggle, {"Hide _Unsupported Files"}, {"filter-symbolic"},
|
||||
{{Qt::Key_H}, {Qt::Key_H, kCtrl}}, {}},
|
||||
{kToggle, {"Sort Des_cending", "Sort As_cending"},
|
||||
{"view-sort-descending-symbolic", "view-sort-ascending-symbolic"},
|
||||
{{Qt::Key_C}}, {}},
|
||||
{kToggle, {"Sort by _Name"}, {}, {{Qt::Key_1, kCtrl | kAlt}}, {}},
|
||||
{kToggle, {"Sort by _Time"}, {}, {{Qt::Key_2, kCtrl | kAlt}}, {}},
|
||||
{kMenu, {"_Filter"}, {}, {{Qt::Key_F, kCtrl}, {Qt::Key_Slash}}, {}},
|
||||
{0, {"_Open"}, {}, {
|
||||
// In Finder, Return nonsensically renames items.
|
||||
{Qt::Key_Return}, {Qt::Key_Enter},
|
||||
#ifdef Q_OS_MACOS
|
||||
{Qt::Key_Down, kCtrl},
|
||||
#endif
|
||||
}, {}},
|
||||
|
||||
{kMenu, {"_Browse"}, {"blocks-symbolic"},
|
||||
{{Qt::Key_Return}, {Qt::Key_Enter}}, {}},
|
||||
{kMenu, {"_Previous File"}, {"go-previous-symbolic"},
|
||||
{{Qt::Key_Left}, {Qt::Key_Up}, {Qt::Key_PageUp}}, {}},
|
||||
{kMenu, {"_Next File"}, {"go-next-symbolic"},
|
||||
{{Qt::Key_Right}, {Qt::Key_Down}, {Qt::Key_PageDown}}, {}},
|
||||
{kMenu, {"Zoom _In"}, {"plus-framed-symbolic"},
|
||||
{{Qt::Key_Plus}, {Qt::Key_Plus, kCtrl}}, {}},
|
||||
{kMenu, {"Zoom _Out"}, {"minus-framed-symbolic"},
|
||||
{{Qt::Key_Minus}, {Qt::Key_Minus, kCtrl}}, {}},
|
||||
{kMenu, {"O_riginal Size"}, {"one-framed-symbolic"},
|
||||
{{Qt::Key_0, kCtrl}}, {}},
|
||||
{0, {"Zoom _Level"}, {}, {}, "1-9"},
|
||||
{kToggle, {"_Scale to Fit"}, {"zoom-fit-symbolic"}, {{Qt::Key_X}}, {}},
|
||||
{0, {"Fit to _Width"}, {}, {{Qt::Key_W}}, {}},
|
||||
{0, {"Fit to H_eight"}, {}, {{Qt::Key_H}}, {}},
|
||||
{kToggle, {"_Lock View"},
|
||||
{"padlock-open-symbolic", "padlock-closed-symbolic"},
|
||||
{{Qt::Key_L}}, {}},
|
||||
{kToggle, {"_Keep Zoom and Position"}, {"pin2-symbolic"},
|
||||
{{Qt::Key_K}}, {}},
|
||||
{kToggle, {"_Colour Management"}, {"color-symbolic"}, {{Qt::Key_C}}, {}},
|
||||
{kToggle, {"S_mooth Scaling"}, {"blend-tool-symbolic"}, {{Qt::Key_I}}, {}},
|
||||
{kToggle, {"Highlight _Transparency"},
|
||||
{"transparent-background-symbolic"}, {{Qt::Key_T}}, {}},
|
||||
{kToggle, {"Blend in Linear Light"}, {}, {}, {}},
|
||||
{kMenu, {"Rotate _Left"}, {"rotate-acw-symbolic"}, {{Qt::Key_Less}}, {}},
|
||||
{kMenu, {"_Mirror"}, {"flip-h-symbolic"}, {{Qt::Key_Equal}}, {}},
|
||||
{kMenu, {"Rotate _Right"}, {"rotate-cw-symbolic"},
|
||||
{{Qt::Key_Greater}}, {}},
|
||||
{kToggle, {"Show I_nformation"}, {"info-outline-symbolic"},
|
||||
{{Qt::Key_Return, kAlt}, {Qt::Key_Enter, kAlt}}, {}},
|
||||
{kMenu, {"_First Page"}, {"go-top-symbolic"}, {}, {}},
|
||||
{kMenu, {"Pr_evious Page"}, {"go-up-symbolic"},
|
||||
{{Qt::Key_BracketLeft}}, {}},
|
||||
{kMenu, {"_Next Page"}, {"go-down-symbolic"},
|
||||
{{Qt::Key_BracketRight}}, {}},
|
||||
{kMenu, {"La_st Page"}, {"go-bottom-symbolic"}, {}, {}},
|
||||
{kMenu, {"Re_wind"}, {"media-skip-backward-symbolic"}, {}, {}},
|
||||
{kMenu, {"Pre_vious Frame"}, {"media-seek-backward-symbolic"},
|
||||
{{Qt::Key_BraceLeft}}, {}},
|
||||
{kToggle, {"_Play", "_Pause"},
|
||||
{"media-playback-start-symbolic", "media-playback-pause-symbolic"},
|
||||
{{Qt::Key_Space}}, {}},
|
||||
{kMenu, {"Ne_xt Frame"}, {"media-seek-forward-symbolic"},
|
||||
{{Qt::Key_BraceRight}}, {}},
|
||||
{0, {"_Copy"}, {}, {{Qt::Key_C, kCtrl}, {Qt::Key_Insert, kCtrl}}, {}},
|
||||
{0, {"Move to _Trash"}, {}, {{Qt::Key_Delete}}, {}},
|
||||
{kMenu, {"_Reload"}, {"arrows-circle-symbolic"},
|
||||
{{Qt::Key_F5}, {Qt::Key_R}, {Qt::Key_R, kCtrl}}, {}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static_assert(size(kDefs) == size_t(Action::Count));
|
||||
|
||||
// Shift is frequently just the means of typing a punctuation character:
|
||||
// Ctrl+? arrives as Ctrl+Shift+? where the question mark sits above the
|
||||
// slash. Letters, digits and named keys stay strict.
|
||||
static constexpr bool
|
||||
shift_is_incidental(uint32_t key)
|
||||
{
|
||||
if (key <= uint32_t(Qt::Key_Space) || key >= 0x7f)
|
||||
return false;
|
||||
return !(key >= uint32_t(Qt::Key_0) && key <= uint32_t(Qt::Key_9)) &&
|
||||
!(key >= uint32_t(Qt::Key_A) && key <= uint32_t(Qt::Key_Z));
|
||||
}
|
||||
|
||||
static_assert(shift_is_incidental(uint32_t(Qt::Key_Question)));
|
||||
static_assert(!shift_is_incidental(uint32_t(Qt::Key_A)));
|
||||
static_assert(!shift_is_incidental(uint32_t(Qt::Key_Return)));
|
||||
|
||||
static constexpr Action
|
||||
match_exact(span<const Action> scope, uint32_t key, uint32_t mods)
|
||||
{
|
||||
for (Action action : scope) {
|
||||
const size_t i = size_t(action);
|
||||
if (i >= size(kDefs))
|
||||
continue;
|
||||
for (const Accel &a : kDefs[i].keys) {
|
||||
if (a.key && a.key == key && a.mods == mods)
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return Action::None;
|
||||
}
|
||||
|
||||
constexpr Action kWindowKeys[] = {
|
||||
Action::NewWindow,
|
||||
Action::CloseWindow,
|
||||
Action::Quit,
|
||||
Action::Fullscreen,
|
||||
Action::DarkMode,
|
||||
Action::Hint,
|
||||
Action::Back,
|
||||
Action::Forward,
|
||||
Action::Location,
|
||||
Action::Help,
|
||||
Action::About,
|
||||
Action::Shortcuts,
|
||||
Action::Settings,
|
||||
Action::Menu,
|
||||
Action::NextPane,
|
||||
Action::PrevPane,
|
||||
Action::Reload,
|
||||
};
|
||||
|
||||
constexpr Action kBrowserKeys[] = {
|
||||
Action::Sidebar,
|
||||
Action::DirPrev,
|
||||
Action::DirNext,
|
||||
Action::DirParent,
|
||||
Action::DirHome,
|
||||
Action::ThumbMinus,
|
||||
Action::ThumbPlus,
|
||||
Action::ViewTile,
|
||||
Action::ViewGrid,
|
||||
// TODO: Action::ViewList,
|
||||
Action::Filenames,
|
||||
Action::Filter,
|
||||
Action::SortDir,
|
||||
Action::SortName,
|
||||
Action::SortTime,
|
||||
Action::Search,
|
||||
Action::Activate,
|
||||
Action::Copy,
|
||||
Action::Trash,
|
||||
Action::Context,
|
||||
};
|
||||
|
||||
constexpr Action kViewerKeys[] = {
|
||||
Action::Browse,
|
||||
Action::PrevFile,
|
||||
Action::NextFile,
|
||||
Action::ZoomIn,
|
||||
Action::ZoomOut,
|
||||
Action::Zoom1,
|
||||
Action::Fit,
|
||||
Action::FitWidth,
|
||||
Action::FitHeight,
|
||||
Action::Lock,
|
||||
Action::Fixate,
|
||||
Action::ColorManagement,
|
||||
Action::Smooth,
|
||||
Action::Checkerboard,
|
||||
Action::RotateLeft,
|
||||
Action::Mirror,
|
||||
Action::RotateRight,
|
||||
Action::Information,
|
||||
Action::PageFirst,
|
||||
Action::PagePrevious,
|
||||
Action::PageNext,
|
||||
Action::PageLast,
|
||||
Action::FrameFirst,
|
||||
Action::FramePrevious,
|
||||
Action::PlayPause,
|
||||
Action::FrameNext,
|
||||
Action::Copy,
|
||||
Action::Trash,
|
||||
Action::Context,
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
const MenuNode kFileMenu{"_File", {
|
||||
Action::NewWindow,
|
||||
Action::CloseWindow,
|
||||
{},
|
||||
Action::Reload,
|
||||
{},
|
||||
Action::Settings,
|
||||
{},
|
||||
Action::Quit,
|
||||
}};
|
||||
|
||||
const MenuNode kHelpMenu{"_Help", {
|
||||
Action::Help,
|
||||
Action::Shortcuts,
|
||||
Action::About,
|
||||
}};
|
||||
|
||||
const MenuNode kBrowserMenu[] = {
|
||||
kFileMenu,
|
||||
{"_Go", {
|
||||
Action::Back,
|
||||
Action::Forward,
|
||||
Action::Location,
|
||||
{},
|
||||
Action::DirPrev,
|
||||
Action::DirNext,
|
||||
Action::DirParent,
|
||||
Action::DirHome,
|
||||
}},
|
||||
{"_View", {
|
||||
Action::Sidebar,
|
||||
{},
|
||||
Action::ThumbPlus,
|
||||
Action::ThumbMinus,
|
||||
{},
|
||||
Action::ViewTile,
|
||||
Action::ViewGrid,
|
||||
// TODO: Action::ViewList,
|
||||
{},
|
||||
Action::Filenames,
|
||||
Action::Filter,
|
||||
{},
|
||||
Action::SortDir,
|
||||
Action::SortName,
|
||||
Action::SortTime,
|
||||
{},
|
||||
Action::Search,
|
||||
Action::Hint,
|
||||
Action::DarkMode,
|
||||
Action::Fullscreen,
|
||||
}},
|
||||
kHelpMenu,
|
||||
};
|
||||
|
||||
const MenuNode kViewerMenu[] = {
|
||||
kFileMenu,
|
||||
{"_Go", {
|
||||
Action::Back,
|
||||
Action::Forward,
|
||||
Action::Location,
|
||||
{},
|
||||
Action::Browse,
|
||||
Action::PrevFile,
|
||||
Action::NextFile,
|
||||
}},
|
||||
{"_View", {
|
||||
Action::Information,
|
||||
{},
|
||||
Action::ZoomIn,
|
||||
Action::ZoomOut,
|
||||
Action::Zoom1,
|
||||
Action::Fit,
|
||||
Action::FitWidth,
|
||||
Action::FitHeight,
|
||||
{},
|
||||
Action::Lock,
|
||||
Action::Fixate,
|
||||
{},
|
||||
Action::ColorManagement,
|
||||
Action::Smooth,
|
||||
Action::Checkerboard,
|
||||
Action::BlendLinearLight,
|
||||
{},
|
||||
Action::Hint,
|
||||
Action::DarkMode,
|
||||
Action::Fullscreen,
|
||||
}},
|
||||
{"_Image", {
|
||||
Action::RotateLeft,
|
||||
Action::Mirror,
|
||||
Action::RotateRight,
|
||||
{},
|
||||
Action::PageFirst,
|
||||
Action::PagePrevious,
|
||||
Action::PageNext,
|
||||
Action::PageLast,
|
||||
{},
|
||||
Action::FrameFirst,
|
||||
Action::FramePrevious,
|
||||
Action::PlayPause,
|
||||
Action::FrameNext,
|
||||
}},
|
||||
kHelpMenu,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
const ActionDef &
|
||||
action_def(Action action)
|
||||
{
|
||||
const size_t i = size_t(action);
|
||||
if (i >= size(kDefs))
|
||||
return kDefs[0];
|
||||
return kDefs[i];
|
||||
}
|
||||
|
||||
Action
|
||||
match_key(span<const Action> scope, int key, unsigned mods)
|
||||
{
|
||||
const uint32_t k = uint32_t(key);
|
||||
const uint32_t m = uint32_t(mods);
|
||||
if (Action a = match_exact(scope, k, m); a != Action::None)
|
||||
return a;
|
||||
if ((m & uint32_t(Qt::ShiftModifier)) && shift_is_incidental(k))
|
||||
return match_exact(scope, k, m & ~uint32_t(Qt::ShiftModifier));
|
||||
return Action::None;
|
||||
}
|
||||
|
||||
QString
|
||||
accel_label(const Accel &a)
|
||||
{
|
||||
if (!a.key)
|
||||
return {};
|
||||
|
||||
QString s = QKeySequence(int(a.mods) | int(a.key))
|
||||
.toString(QKeySequence::NativeText);
|
||||
s.replace(QLatin1Char('-'), QChar(0x2212));
|
||||
return s;
|
||||
}
|
||||
|
||||
QString
|
||||
accel_label(const ActionDef &def)
|
||||
{
|
||||
if (def.accel) {
|
||||
QString s = QString::fromUtf8(def.accel);
|
||||
s.replace(QLatin1Char('-'), QChar(0x2212));
|
||||
return s;
|
||||
}
|
||||
return accel_label(def.keys[0]);
|
||||
}
|
||||
|
||||
QString
|
||||
menu_label(const char *label, int *mnemonic_index)
|
||||
{
|
||||
if (mnemonic_index)
|
||||
*mnemonic_index = -1;
|
||||
if (!label)
|
||||
return {};
|
||||
|
||||
QString s = QString::fromUtf8(label);
|
||||
// On macOS, the Alt/Option key modifies characters, so mnemonics are
|
||||
// unusable for window navigation. The remaining thing mnemonics could do
|
||||
// is act as unmodified accelerators in menus, though AppKit menu navigation
|
||||
// is instead done using type-ahead (which resets after about a second).
|
||||
#ifndef Q_OS_MACOS
|
||||
for (int i = 0; i + 1 < s.size(); i++) {
|
||||
if (s[i] == QLatin1Char('_')) {
|
||||
if (mnemonic_index)
|
||||
*mnemonic_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
s.remove(QLatin1Char('_'));
|
||||
return s;
|
||||
}
|
||||
|
||||
const char *
|
||||
action_label(const ActionDef &def, bool checked)
|
||||
{
|
||||
if (checked && (def.flags & ActionToggle) && def.label[1])
|
||||
return def.label[1];
|
||||
return def.label[0];
|
||||
}
|
||||
|
||||
const char *
|
||||
action_icon(const ActionDef &def, bool checked)
|
||||
{
|
||||
if (checked && (def.flags & ActionToggle) && def.icon[1])
|
||||
return def.icon[1];
|
||||
return def.icon[0];
|
||||
}
|
||||
|
||||
QString
|
||||
action_tip(const ActionDef &def, bool checked)
|
||||
{
|
||||
return menu_label(action_label(def, checked), nullptr);
|
||||
}
|
||||
|
||||
QString
|
||||
action_accel(const ActionDef &def)
|
||||
{
|
||||
return accel_label(def);
|
||||
}
|
||||
|
||||
span<const MenuNode>
|
||||
browser_menu()
|
||||
{
|
||||
return kBrowserMenu;
|
||||
}
|
||||
|
||||
span<const MenuNode>
|
||||
viewer_menu()
|
||||
{
|
||||
return kViewerMenu;
|
||||
}
|
||||
|
||||
span<const Action>
|
||||
window_keys()
|
||||
{
|
||||
return kWindowKeys;
|
||||
}
|
||||
|
||||
span<const Action>
|
||||
browser_keys()
|
||||
{
|
||||
return kBrowserKeys;
|
||||
}
|
||||
|
||||
span<const Action>
|
||||
viewer_keys()
|
||||
{
|
||||
return kViewerKeys;
|
||||
}
|
||||
|
||||
void
|
||||
copy_files(QMimeData *mime, span<const QUrl> urls, bool cut)
|
||||
{
|
||||
if (!mime)
|
||||
return;
|
||||
|
||||
mime->setUrls(QList<QUrl>(urls.begin(), urls.end()));
|
||||
#ifdef Q_OS_WIN
|
||||
// A little-endian DWORD: DROPEFFECT_MOVE or DROPEFFECT_COPY.
|
||||
mime->setData(
|
||||
QStringLiteral(
|
||||
"application/x-qt-windows-mime;value=\"Preferred DropEffect\""),
|
||||
cut ? QByteArrayLiteral("\x02\x00\x00\x00")
|
||||
: QByteArrayLiteral("\x01\x00\x00\x00"));
|
||||
#endif
|
||||
#if defined Q_OS_UNIX && !defined Q_OS_MACOS
|
||||
QByteArray gnome;
|
||||
gnome += cut ? "cut" : "copy";
|
||||
for (const QUrl &url : urls) {
|
||||
gnome += '\n';
|
||||
gnome += url.toEncoded();
|
||||
}
|
||||
mime->setData(QByteArrayLiteral("x-special/gnome-copied-files"), gnome);
|
||||
mime->setData(
|
||||
QByteArrayLiteral("application/x-kde-cutselection"), cut ? "1" : "0");
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
copy_files(span<const QUrl> urls, bool cut)
|
||||
{
|
||||
auto *mime = new QMimeData;
|
||||
copy_files(mime, urls, cut);
|
||||
QGuiApplication::clipboard()->setMimeData(mime);
|
||||
}
|
||||
|
||||
bool
|
||||
move_to_trash(const QString &abs_path)
|
||||
{
|
||||
if (abs_path.isEmpty() || !QFileInfo(abs_path).isFile())
|
||||
return false;
|
||||
|
||||
QFile file(abs_path);
|
||||
if (file.moveToTrash())
|
||||
return true;
|
||||
|
||||
qWarning(
|
||||
"%s: %s", qUtf8Printable(abs_path), qUtf8Printable(file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// action.hpp: shared action table (labels, keys, menus)
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
class QMimeData;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
enum class Action : uint8_t {
|
||||
None,
|
||||
|
||||
// Window
|
||||
NewWindow,
|
||||
CloseWindow,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Quit,
|
||||
Fullscreen,
|
||||
DarkMode,
|
||||
Hint,
|
||||
Back,
|
||||
Forward,
|
||||
Location,
|
||||
Help,
|
||||
About,
|
||||
Shortcuts,
|
||||
Settings,
|
||||
Menu,
|
||||
Context,
|
||||
Cancel,
|
||||
NextPane,
|
||||
PrevPane,
|
||||
|
||||
// Browser
|
||||
Sidebar,
|
||||
DirPrev,
|
||||
DirNext,
|
||||
DirParent,
|
||||
DirHome,
|
||||
ThumbMinus,
|
||||
ThumbPlus,
|
||||
ViewTile,
|
||||
ViewGrid,
|
||||
ViewList,
|
||||
Filenames,
|
||||
Filter,
|
||||
SortDir,
|
||||
SortName,
|
||||
SortTime,
|
||||
Search,
|
||||
Activate,
|
||||
|
||||
// Viewer
|
||||
Browse,
|
||||
PrevFile,
|
||||
NextFile,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Zoom1,
|
||||
ZoomLevel,
|
||||
Fit,
|
||||
FitWidth,
|
||||
FitHeight,
|
||||
Lock,
|
||||
Fixate,
|
||||
ColorManagement,
|
||||
Smooth,
|
||||
Checkerboard,
|
||||
BlendLinearLight,
|
||||
RotateLeft,
|
||||
Mirror,
|
||||
RotateRight,
|
||||
Information,
|
||||
PageFirst,
|
||||
PagePrevious,
|
||||
PageNext,
|
||||
PageLast,
|
||||
FrameFirst,
|
||||
FramePrevious,
|
||||
PlayPause,
|
||||
FrameNext,
|
||||
Copy,
|
||||
Trash,
|
||||
Reload,
|
||||
|
||||
Count,
|
||||
};
|
||||
|
||||
enum : uint8_t { ActionInMenu = 1, ActionToggle = 2 };
|
||||
|
||||
struct Accel {
|
||||
uint32_t key = 0;
|
||||
uint32_t mods = 0;
|
||||
};
|
||||
|
||||
struct ActionDef {
|
||||
uint8_t flags = 0;
|
||||
const char *label[2] = {};
|
||||
const char *icon[2] = {};
|
||||
Accel keys[3] = {};
|
||||
const char *accel = {};
|
||||
};
|
||||
|
||||
struct MenuNode {
|
||||
const char *title = nullptr;
|
||||
Action action = Action::None;
|
||||
std::vector<MenuNode> items = {};
|
||||
MenuNode() = default;
|
||||
MenuNode(Action a) : action(a) {}
|
||||
MenuNode(const char *t, std::initializer_list<MenuNode> xs)
|
||||
: title(t), items(xs)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct Actor {
|
||||
std::function<void(Action)> apply;
|
||||
std::function<bool(Action)> enabled;
|
||||
std::function<bool(Action)> checked;
|
||||
};
|
||||
|
||||
const ActionDef &action_def(Action);
|
||||
Action match_key(std::span<const Action> scope, int key, unsigned mods);
|
||||
QString accel_label(const Accel &);
|
||||
QString accel_label(const ActionDef &);
|
||||
QString menu_label(const char *label, int *mnemonic_index);
|
||||
const char *action_label(const ActionDef &, bool checked);
|
||||
const char *action_icon(const ActionDef &, bool checked);
|
||||
QString action_tip(const ActionDef &, bool checked);
|
||||
QString action_accel(const ActionDef &);
|
||||
|
||||
std::span<const MenuNode> browser_menu();
|
||||
std::span<const MenuNode> viewer_menu();
|
||||
std::span<const Action> window_keys();
|
||||
std::span<const Action> browser_keys();
|
||||
std::span<const Action> viewer_keys();
|
||||
|
||||
void copy_files(QMimeData *mime, std::span<const QUrl> urls, bool cut = false);
|
||||
void copy_files(std::span<const QUrl> urls, bool cut = false);
|
||||
bool move_to_trash(const QString &abs_path);
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// app-menu-macos.hpp: native macOS application menu
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
class App;
|
||||
|
||||
#ifdef __APPLE__
|
||||
void install_macos_app_menu(App *app);
|
||||
void sync_macos_app_menu(App *app);
|
||||
#else
|
||||
inline void
|
||||
install_macos_app_menu(App *)
|
||||
{
|
||||
}
|
||||
inline void
|
||||
sync_macos_app_menu(App *)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,393 @@
|
||||
//
|
||||
// app-menu-macos.mm: append to Qt Cocoa's QCocoaMenuLoader bar
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "app-menu-macos.hpp"
|
||||
|
||||
#include "action.hpp"
|
||||
#include "app.hpp"
|
||||
#include "url.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QString>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
static NSEventModifierFlags
|
||||
ns_mods(dn::Accel a)
|
||||
{
|
||||
NSEventModifierFlags f = 0;
|
||||
if (a.mods & Qt::ControlModifier)
|
||||
f |= NSEventModifierFlagCommand;
|
||||
if (a.mods & Qt::AltModifier)
|
||||
f |= NSEventModifierFlagOption;
|
||||
if (a.mods & Qt::ShiftModifier)
|
||||
f |= NSEventModifierFlagShift;
|
||||
if (a.mods & Qt::MetaModifier)
|
||||
f |= NSEventModifierFlagControl;
|
||||
return f;
|
||||
}
|
||||
|
||||
static NSString *
|
||||
ns_equiv(dn::Accel a, NSEventModifierFlags *mods)
|
||||
{
|
||||
*mods = ns_mods(a);
|
||||
const uint32_t k = a.key;
|
||||
if (!k)
|
||||
return @"";
|
||||
|
||||
if (k >= Qt::Key_A && k <= Qt::Key_Z) {
|
||||
unichar c = unichar('a' + (k - Qt::Key_A));
|
||||
if (*mods & NSEventModifierFlagShift) {
|
||||
c = unichar('A' + (k - Qt::Key_A));
|
||||
*mods &= ~NSEventModifierFlagShift;
|
||||
}
|
||||
return [NSString stringWithCharacters:&c length:1];
|
||||
}
|
||||
if (k >= Qt::Key_F1 && k <= Qt::Key_F12) {
|
||||
unichar c = unichar(NSF1FunctionKey + (k - Qt::Key_F1));
|
||||
return [NSString stringWithCharacters:&c length:1];
|
||||
}
|
||||
|
||||
unichar c = 0;
|
||||
switch (k) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
c = '\r';
|
||||
break;
|
||||
case Qt::Key_Escape:
|
||||
c = '\033';
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
c = 0x7f;
|
||||
break;
|
||||
case Qt::Key_Tab:
|
||||
c = '\t';
|
||||
break;
|
||||
case Qt::Key_Left:
|
||||
c = NSLeftArrowFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
c = NSRightArrowFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Up:
|
||||
c = NSUpArrowFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Down:
|
||||
c = NSDownArrowFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Home:
|
||||
c = NSHomeFunctionKey;
|
||||
break;
|
||||
case Qt::Key_End:
|
||||
c = NSEndFunctionKey;
|
||||
break;
|
||||
case Qt::Key_PageUp:
|
||||
c = NSPageUpFunctionKey;
|
||||
break;
|
||||
case Qt::Key_PageDown:
|
||||
c = NSPageDownFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Insert:
|
||||
c = NSInsertFunctionKey;
|
||||
break;
|
||||
case Qt::Key_Delete:
|
||||
c = NSDeleteFunctionKey;
|
||||
break;
|
||||
default:
|
||||
if (k >= 32 && k < 128)
|
||||
c = unichar(k);
|
||||
else
|
||||
return @"";
|
||||
}
|
||||
return [NSString stringWithCharacters:&c length:1];
|
||||
}
|
||||
|
||||
static const dn::MenuNode *
|
||||
find_section(span<const dn::MenuNode> tree, NSString *title)
|
||||
{
|
||||
const QString want = QString::fromNSString(title);
|
||||
for (const dn::MenuNode &n : tree) {
|
||||
if (dn::menu_label(n.title, nullptr) == want)
|
||||
return &n;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool
|
||||
skip_action(dn::Action a)
|
||||
{
|
||||
return a == dn::Action::Quit || a == dn::Action::About ||
|
||||
a == dn::Action::Settings;
|
||||
}
|
||||
|
||||
static void
|
||||
sync_hidden(NSMenu *main, id delegate, span<const dn::MenuNode> tree)
|
||||
{
|
||||
for (NSMenuItem *top in main.itemArray) {
|
||||
if (top.submenu.delegate != delegate)
|
||||
continue;
|
||||
top.hidden = find_section(tree, top.title) ? NO : YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Qt's Cocoa plugin builds the application menu, and owns the About and
|
||||
// Settings items that we want pointed at ourselves. It installs no public
|
||||
// header for its menu loader, so redeclare just what we ask of it: the class
|
||||
// name outlives item titles, which Qt both translates and has renamed.
|
||||
@protocol DnCocoaMenuLoader <NSObject>
|
||||
- (NSMenuItem *)aboutMenuItem;
|
||||
- (NSMenuItem *)preferencesMenuItem;
|
||||
@end
|
||||
|
||||
@interface DnMenuDelegate : NSObject <NSMenuDelegate>
|
||||
@property(nonatomic, assign) dn::App *app;
|
||||
@end
|
||||
|
||||
// Keep native key equivalents for their standard menu presentation, but let
|
||||
// Qt deliver the actual key event through Dawn's normal shortcut path.
|
||||
@interface DnMenu : NSMenu
|
||||
@end
|
||||
|
||||
@implementation DnMenu
|
||||
|
||||
- (BOOL)performKeyEquivalent:(NSEvent *)event
|
||||
{
|
||||
(void)event;
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation DnMenuDelegate
|
||||
|
||||
- (dn::Window *)window
|
||||
{
|
||||
return _app ? _app->key_window() : nullptr;
|
||||
}
|
||||
|
||||
- (const dn::Actor *)actor
|
||||
{
|
||||
if (dn::Window *w = [self window])
|
||||
return w->active_actor();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (void)invoke:(NSMenuItem *)sender
|
||||
{
|
||||
const dn::Action a = dn::Action(sender.tag);
|
||||
const dn::Actor *actor = [self actor];
|
||||
if (actor && actor->apply)
|
||||
actor->apply(a);
|
||||
else if (dn::Window *w = [self window]) {
|
||||
if (w->host().apply)
|
||||
w->host().apply(a);
|
||||
} else if (a == dn::Action::NewWindow && _app)
|
||||
_app->open(dn::path_to_url(QDir::currentPath()));
|
||||
}
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem *)item
|
||||
{
|
||||
const dn::Action tag = dn::Action(item.tag);
|
||||
if (tag == dn::Action::About || tag == dn::Action::Settings)
|
||||
return [self window] != nullptr;
|
||||
const dn::Actor *actor = [self actor];
|
||||
if (!actor || !actor->enabled)
|
||||
return YES;
|
||||
return actor->enabled(tag);
|
||||
}
|
||||
|
||||
- (void)menuNeedsUpdate:(NSMenu *)menu
|
||||
{
|
||||
dn::Window *w = [self window];
|
||||
const span<const dn::MenuNode> tree =
|
||||
w ? w->active_menu() : span<const dn::MenuNode>{};
|
||||
const dn::Actor *actor = w ? w->active_actor() : nullptr;
|
||||
sync_hidden([NSApp mainMenu], self, tree);
|
||||
|
||||
const dn::MenuNode *node = find_section(tree, menu.title);
|
||||
[menu removeAllItems];
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
bool pending_sep = false;
|
||||
bool any = false;
|
||||
for (const dn::MenuNode &n : node->items) {
|
||||
if (!n.title && n.action == dn::Action::None) {
|
||||
pending_sep = any;
|
||||
continue;
|
||||
}
|
||||
if (n.action == dn::Action::None || skip_action(n.action))
|
||||
continue;
|
||||
if (pending_sep) {
|
||||
[menu addItem:[NSMenuItem separatorItem]];
|
||||
pending_sep = false;
|
||||
}
|
||||
const dn::ActionDef &def = dn::action_def(n.action);
|
||||
const bool checked =
|
||||
actor && actor->checked && actor->checked(n.action);
|
||||
const QString title =
|
||||
dn::menu_label(dn::action_label(def, checked), nullptr);
|
||||
NSEventModifierFlags mods = 0;
|
||||
NSString *key = ns_equiv(def.keys[0], &mods);
|
||||
NSMenuItem *it = [[[NSMenuItem alloc] initWithTitle:title.toNSString()
|
||||
action:@selector(invoke:)
|
||||
keyEquivalent:key] autorelease];
|
||||
it.tag = NSInteger(n.action);
|
||||
it.target = self;
|
||||
it.keyEquivalentModifierMask = mods;
|
||||
const bool on =
|
||||
(def.flags & dn::ActionToggle) && !def.label[1] && checked;
|
||||
it.state = on ? NSControlStateValueOn : NSControlStateValueOff;
|
||||
[menu addItem:it];
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
static DnMenuDelegate *g_menu_delegate;
|
||||
|
||||
static bool
|
||||
has_menu(NSMenu *main, NSString *title)
|
||||
{
|
||||
for (NSMenuItem *it in main.itemArray) {
|
||||
if ([it.title isEqualToString:title])
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
add_top_menu(NSMenu *main, NSString *title, NSMenu *sub)
|
||||
{
|
||||
[main addItemWithTitle:title action:nil keyEquivalent:@""].submenu = sub;
|
||||
}
|
||||
|
||||
// Qt's application delegate installs a hidden Window menu of its own, but only
|
||||
// if nothing has claimed NSApp.windowsMenu by the time the run loop starts;
|
||||
// claiming it here, from App::init(), is what keeps that from happening.
|
||||
// AppKit owns the rest: it appends window titles, and runs the items.
|
||||
static void
|
||||
add_window_menu(NSMenu *main)
|
||||
{
|
||||
NSString *title = @"Window";
|
||||
if (has_menu(main, title))
|
||||
return;
|
||||
|
||||
NSMenu *sub = [[[NSMenu alloc] initWithTitle:title] autorelease];
|
||||
// With no target, AppKit handles these through the responder chain.
|
||||
[sub addItemWithTitle:@"Minimize"
|
||||
action:@selector(performMiniaturize:)
|
||||
keyEquivalent:@"m"];
|
||||
[sub addItemWithTitle:@"Zoom"
|
||||
action:@selector(performZoom:)
|
||||
keyEquivalent:@""];
|
||||
[sub addItem:[NSMenuItem separatorItem]];
|
||||
[sub addItemWithTitle:@"Bring All to Front"
|
||||
action:@selector(arrangeInFront:)
|
||||
keyEquivalent:@""];
|
||||
add_top_menu(main, title, sub);
|
||||
NSApp.windowsMenu = sub;
|
||||
}
|
||||
|
||||
// Qt hides and disables the items it has found nothing to merge into.
|
||||
static void
|
||||
claim_item(NSMenuItem *item, Action action, id target, NSString *key,
|
||||
NSEventModifierFlags mods)
|
||||
{
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
item.hidden = NO;
|
||||
item.enabled = YES;
|
||||
item.tag = NSInteger(action);
|
||||
item.target = target;
|
||||
item.action = @selector(invoke:);
|
||||
item.keyEquivalent = key;
|
||||
item.keyEquivalentModifierMask = mods;
|
||||
}
|
||||
|
||||
// macOS usually keeps About and Settings in the application menu.
|
||||
static void
|
||||
adopt_app_menu(id target)
|
||||
{
|
||||
id cls = NSClassFromString(@"QCocoaMenuLoader");
|
||||
if (![cls respondsToSelector:@selector(sharedMenuLoader)])
|
||||
return;
|
||||
|
||||
id<DnCocoaMenuLoader> loader =
|
||||
[cls performSelector:@selector(sharedMenuLoader)];
|
||||
if ([loader respondsToSelector:@selector(aboutMenuItem)])
|
||||
claim_item([loader aboutMenuItem], Action::About, target, @"", 0);
|
||||
if ([loader respondsToSelector:@selector(preferencesMenuItem)])
|
||||
claim_item([loader preferencesMenuItem], Action::Settings, target,
|
||||
@",", NSEventModifierFlagCommand);
|
||||
}
|
||||
|
||||
void
|
||||
sync_macos_app_menu(App *app)
|
||||
{
|
||||
NSMenu *main = [NSApp mainMenu];
|
||||
if (!main || !app || !g_menu_delegate)
|
||||
return;
|
||||
g_menu_delegate.app = app;
|
||||
|
||||
if (Window *w = app->key_window())
|
||||
sync_hidden(main, g_menu_delegate, w->active_menu());
|
||||
}
|
||||
|
||||
void
|
||||
install_macos_app_menu(App *app)
|
||||
{
|
||||
NSMenu *main = [NSApp mainMenu];
|
||||
if (!main || !app)
|
||||
return;
|
||||
|
||||
if (!g_menu_delegate)
|
||||
g_menu_delegate = [[DnMenuDelegate alloc] init];
|
||||
DnMenuDelegate *delegate = g_menu_delegate;
|
||||
delegate.app = app;
|
||||
|
||||
adopt_app_menu(delegate);
|
||||
vector<QString> titles;
|
||||
auto consider = [&](span<const MenuNode> tree) {
|
||||
for (const MenuNode &n : tree) {
|
||||
const QString t = menu_label(n.title, nullptr);
|
||||
if (t.isEmpty())
|
||||
continue;
|
||||
|
||||
if (find(titles.begin(), titles.end(), t) == titles.end())
|
||||
titles.push_back(t);
|
||||
}
|
||||
};
|
||||
consider(viewer_menu());
|
||||
consider(browser_menu());
|
||||
for (const QString &t : titles) {
|
||||
// Both trees end with Help, and macOS wants Window just before it.
|
||||
if (t == QStringLiteral("Help"))
|
||||
add_window_menu(main);
|
||||
|
||||
NSString *title = t.toNSString();
|
||||
if (!has_menu(main, title)) {
|
||||
NSMenu *sub = [[[DnMenu alloc] initWithTitle:title] autorelease];
|
||||
sub.delegate = delegate;
|
||||
add_top_menu(main, title, sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
+549
@@ -0,0 +1,549 @@
|
||||
//
|
||||
// app.cpp: process-wide Vulkan, GPU, and top-level windows
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include <dawn-config.h>
|
||||
|
||||
#include "app.hpp"
|
||||
|
||||
#include "libdn/vk-device.hpp"
|
||||
#include "url.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#if DN_WITH_WAYLAND
|
||||
#include "wayland-color-bridge.hpp"
|
||||
#include "wayland-window.hpp"
|
||||
#endif
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileOpenEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QMetaObject>
|
||||
#include <QUrl>
|
||||
#include <QVersionNumber>
|
||||
#include <QtLogging>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
#include "app-menu-macos.hpp"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
constexpr string_view kBookmarksKey = "dn/Bookmarks";
|
||||
constexpr string_view kDitheringKey = "dn/DisableDithering";
|
||||
constexpr string_view kFilenamesKey = "dn/BrowserShowFilenames";
|
||||
constexpr string_view kThumbnailKey = "dn/BrowserThumbnailSize";
|
||||
constexpr string_view kProfileKey = "dn/ICCProfileOverride";
|
||||
constexpr string_view kLoaderOrderKey = "dn/LoaderOrder";
|
||||
constexpr string_view kLoaderDisabledKey = "dn/LoaderDisabled";
|
||||
|
||||
static void
|
||||
config_warn(string_view key, const char *message)
|
||||
{
|
||||
qWarning("configuration %.*s: %s", int(key.size()), key.data(), message);
|
||||
}
|
||||
|
||||
static optional<string>
|
||||
setting(string_view key)
|
||||
{
|
||||
dawn::Error error;
|
||||
optional<string> value = dawn::config_get(key, &error);
|
||||
if (error)
|
||||
config_warn(key, error.message.c_str());
|
||||
return value;
|
||||
}
|
||||
|
||||
static void
|
||||
set_setting(string_view key, string_view value)
|
||||
{
|
||||
dawn::Error error;
|
||||
if (!dawn::config_set(key, value, &error))
|
||||
config_warn(key, error.message.c_str());
|
||||
}
|
||||
|
||||
static bool
|
||||
boolean_setting(string_view key, bool fallback)
|
||||
{
|
||||
const optional<string> value = setting(key);
|
||||
if (!value)
|
||||
return fallback;
|
||||
if (*value == "true" || *value == "1")
|
||||
return true;
|
||||
if (*value == "false" || *value == "0")
|
||||
return false;
|
||||
config_warn(key, "expected true, false, 1, or 0");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static void
|
||||
set_boolean_setting(string_view key, bool value)
|
||||
{
|
||||
set_setting(key, value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Answers 0 for anything the browser has no thumbnails for.
|
||||
static int
|
||||
parse_thumbnail_size(const string &value)
|
||||
{
|
||||
int size = 0;
|
||||
const auto parsed =
|
||||
from_chars(value.data(), value.data() + value.size(), size);
|
||||
if (parsed.ec != errc{} || parsed.ptr != value.data() + value.size())
|
||||
return 0;
|
||||
for (const int candidate : kThumbSizes) {
|
||||
if (candidate == size)
|
||||
return size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char
|
||||
bookmark_separator()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return ';';
|
||||
#else
|
||||
return ':';
|
||||
#endif
|
||||
}
|
||||
|
||||
static vector<string>
|
||||
split_list(const string &value, char separator)
|
||||
{
|
||||
vector<string> out;
|
||||
for (size_t offset = 0; offset <= value.size();) {
|
||||
const size_t end = value.find(separator, offset);
|
||||
const string item = value.substr(
|
||||
offset, end == string::npos ? string::npos : end - offset);
|
||||
if (!item.empty())
|
||||
out.push_back(item);
|
||||
if (end == string::npos)
|
||||
break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static string
|
||||
join_list(const vector<string> &items, char separator)
|
||||
{
|
||||
string value;
|
||||
for (const string &item : items) {
|
||||
if (!value.empty())
|
||||
value += separator;
|
||||
value += item;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Whatever the configuration does not mention is enabled, and comes last:
|
||||
// a loader added in a later version must not arrive switched off.
|
||||
static vector<SettingsDraft::Loader>
|
||||
merge_loaders(const vector<string> &order, const vector<string> &disabled)
|
||||
{
|
||||
const span<const dawn::Loader> known = dawn::loaders();
|
||||
vector<bool> used(known.size());
|
||||
vector<SettingsDraft::Loader> out;
|
||||
const auto add = [&](size_t i) {
|
||||
// A loader this build lacks can only be ignored, so do not offer it;
|
||||
// it drops out of the stored order until the build has it again.
|
||||
if (used[i] || !known[i].load)
|
||||
return;
|
||||
|
||||
used[i] = true;
|
||||
out.push_back({QString::fromUtf8(known[i].name),
|
||||
QString::fromUtf8(known[i].formats),
|
||||
find(disabled.begin(), disabled.end(), known[i].name) ==
|
||||
disabled.end()});
|
||||
};
|
||||
|
||||
for (const string &name : order) {
|
||||
for (size_t i = 0; i < known.size(); i++) {
|
||||
if (known[i].name == name)
|
||||
add(i);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < known.size(); i++)
|
||||
add(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Bookmarks are compared by path, so they are stored canonicalised: the
|
||||
// sidebar highlights the open directory by std::filesystem::equivalent,
|
||||
// and a trailing separator or an unresolved symlink must not disagree.
|
||||
static string
|
||||
canonical_dir(const string &path)
|
||||
{
|
||||
error_code ec;
|
||||
filesystem::path p = filesystem::weakly_canonical(path, ec);
|
||||
if (ec)
|
||||
p = path;
|
||||
while (p.filename().empty()) {
|
||||
const filesystem::path parent = p.parent_path();
|
||||
if (parent.empty() || parent == p)
|
||||
break;
|
||||
p = parent;
|
||||
}
|
||||
return p.string();
|
||||
}
|
||||
|
||||
void
|
||||
Settings::load_icc_override(const string &path)
|
||||
{
|
||||
this->icc_profile_override.clear();
|
||||
this->icc_profile_override_path.clear();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
QFile file(QString::fromUtf8(path.data(), qsizetype(path.size())));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
config_warn(kProfileKey, ("cannot open " + path).c_str());
|
||||
return;
|
||||
}
|
||||
const QByteArray bytes = file.readAll();
|
||||
if (bytes.isEmpty()) {
|
||||
config_warn(kProfileKey, "empty ICC profile");
|
||||
return;
|
||||
}
|
||||
this->icc_profile_override.assign(bytes.begin(), bytes.end());
|
||||
this->icc_profile_override_path = path;
|
||||
}
|
||||
|
||||
// An empty list is the default order to libdn, so turning everything off
|
||||
// gets everything back; there is nothing sensible to do with that anyway.
|
||||
void
|
||||
Settings::update_enabled_loaders()
|
||||
{
|
||||
auto names = make_shared<vector<string>>();
|
||||
for (const SettingsDraft::Loader &loader : this->loaders) {
|
||||
if (loader.enabled)
|
||||
names->push_back(loader.name.toStdString());
|
||||
}
|
||||
this->enabled_loaders = std::move(names);
|
||||
}
|
||||
|
||||
void
|
||||
Settings::load()
|
||||
{
|
||||
this->disable_dithering = boolean_setting(kDitheringKey, false);
|
||||
this->browser_show_filenames = boolean_setting(kFilenamesKey, true);
|
||||
if (const optional<string> value = setting(kThumbnailKey)) {
|
||||
if (const int size = parse_thumbnail_size(*value))
|
||||
this->browser_thumbnail_size = size;
|
||||
else
|
||||
config_warn(kThumbnailKey, "unsupported size");
|
||||
}
|
||||
|
||||
this->bookmarks.clear();
|
||||
if (const optional<string> value = setting(kBookmarksKey))
|
||||
this->bookmarks = split_list(*value, bookmark_separator());
|
||||
|
||||
this->loaders = merge_loaders(
|
||||
split_list(setting(kLoaderOrderKey).value_or(string()), ';'),
|
||||
split_list(setting(kLoaderDisabledKey).value_or(string()), ';'));
|
||||
update_enabled_loaders();
|
||||
|
||||
load_icc_override(setting(kProfileKey).value_or(string()));
|
||||
}
|
||||
|
||||
void
|
||||
Settings::save(const SettingsDraft &draft)
|
||||
{
|
||||
this->browser_thumbnail_size = draft.thumbnail_size;
|
||||
this->browser_show_filenames = draft.show_filenames;
|
||||
this->disable_dithering = draft.disable_dithering;
|
||||
set_setting(kThumbnailKey, to_string(draft.thumbnail_size));
|
||||
set_boolean_setting(kFilenamesKey, draft.show_filenames);
|
||||
set_boolean_setting(kDitheringKey, draft.disable_dithering);
|
||||
|
||||
this->loaders = draft.loaders;
|
||||
vector<string> order, disabled;
|
||||
for (const SettingsDraft::Loader &loader : draft.loaders) {
|
||||
order.push_back(loader.name.toStdString());
|
||||
if (!loader.enabled)
|
||||
disabled.push_back(order.back());
|
||||
}
|
||||
update_enabled_loaders();
|
||||
set_setting(kLoaderOrderKey, join_list(order, ';'));
|
||||
set_setting(kLoaderDisabledKey, join_list(disabled, ';'));
|
||||
|
||||
const string path = draft.icc_profile_path.toStdString();
|
||||
set_setting(kProfileKey, path);
|
||||
load_icc_override(path);
|
||||
notify();
|
||||
}
|
||||
|
||||
bool
|
||||
Settings::bookmarked(const string &path) const
|
||||
{
|
||||
const string want = canonical_dir(path);
|
||||
return find(this->bookmarks.begin(), this->bookmarks.end(), want) !=
|
||||
this->bookmarks.end();
|
||||
}
|
||||
|
||||
void
|
||||
Settings::toggle_bookmark(const string &path)
|
||||
{
|
||||
const string want = canonical_dir(path);
|
||||
const auto it =
|
||||
find(this->bookmarks.begin(), this->bookmarks.end(), want);
|
||||
if (it != this->bookmarks.end())
|
||||
this->bookmarks.erase(it);
|
||||
else
|
||||
this->bookmarks.push_back(want);
|
||||
notify();
|
||||
set_setting(
|
||||
kBookmarksKey, join_list(this->bookmarks, bookmark_separator()));
|
||||
}
|
||||
|
||||
void
|
||||
Settings::listen(void *key, function<void()> fn)
|
||||
{
|
||||
unlisten(key);
|
||||
this->listeners_.emplace_back(key, std::move(fn));
|
||||
}
|
||||
|
||||
void
|
||||
Settings::unlisten(const void *key)
|
||||
{
|
||||
erase_if(this->listeners_,
|
||||
[key](const auto &item) { return item.first == key; });
|
||||
}
|
||||
|
||||
void
|
||||
Settings::notify() const
|
||||
{
|
||||
// All mutations originate on the GUI thread, so unlike DisplayProfileWatch
|
||||
// this needs no queued hop. Copy: a callback may unlisten.
|
||||
const auto copy = this->listeners_;
|
||||
for (const auto &item : copy)
|
||||
if (item.second)
|
||||
item.second();
|
||||
}
|
||||
|
||||
// Finder delivers a document to open as a QFileOpenEvent, not an argument.
|
||||
bool
|
||||
App::event(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Quit) {
|
||||
shutdown();
|
||||
return true;
|
||||
}
|
||||
if (event->type() == QEvent::FileOpen) {
|
||||
open(url_normalized(((QFileOpenEvent *) event)->url()));
|
||||
return true;
|
||||
}
|
||||
return QGuiApplication::event(event);
|
||||
}
|
||||
|
||||
bool
|
||||
App::init()
|
||||
{
|
||||
this->settings.load();
|
||||
#if DN_WITH_WAYLAND
|
||||
// Vulkan content is a wl_subsurface. Qt's presentAboutToBeQueued waits on
|
||||
// wl_surface.frame and marks the window unexposed on timeout; that
|
||||
// callback is unreliable for desync subsurfaces under Sway. Disable the
|
||||
// wait and the timeout so presentation does not stall. Process-global;
|
||||
// must run before the first Wayland window.
|
||||
qputenv("QT_WAYLAND_FRAME_CALLBACK_TIMEOUT", "0");
|
||||
this->needs_csd = wayland_needs_csd();
|
||||
#endif
|
||||
// The Wayland close dance hides the shell before closing it, so Qt never
|
||||
// emits lastWindowClosed; App::close() counts windows itself.
|
||||
// See WaylandWindow::finish_close().
|
||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||
#ifdef Q_OS_MACOS
|
||||
// QNSView backs a VulkanSurface with QMetalLayer, which arbitrates
|
||||
// presentation between Qt's display cycle and a Qt render thread through
|
||||
// a display lock. We present the drawable ourselves from the GUI thread,
|
||||
// and Qt's side of that arbitration withholds expose and update requests.
|
||||
// Process-global; must run before the first window.
|
||||
qputenv("QT_MTL_NO_TRANSACTION", "1");
|
||||
// QCocoaVulkanInstance loadVulkanLibrary("vulkan") — not a dylib name.
|
||||
if (!qEnvironmentVariableIsSet("QT_VULKAN_LIB")) {
|
||||
if (void *sym = dlsym(RTLD_DEFAULT, "vkGetInstanceProcAddr")) {
|
||||
Dl_info info{};
|
||||
if (dladdr(sym, &info) && info.dli_fname && info.dli_fname[0])
|
||||
qputenv("QT_VULKAN_LIB", info.dli_fname);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
this->vulkan_instance.setApiVersion(QVersionNumber(1, 1));
|
||||
dawn::vk_add_bundled_driver_files();
|
||||
this->vulkan_instance.setExtensions(
|
||||
{VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME});
|
||||
if (!this->vulkan_instance.create()) {
|
||||
qWarning("Qt Vulkan instance creation failed: VkResult %d",
|
||||
int(this->vulkan_instance.errorCode()));
|
||||
return false;
|
||||
}
|
||||
#ifdef Q_OS_MACOS
|
||||
install_macos_app_menu(this);
|
||||
#endif
|
||||
this->display_profiles.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
// xdg-shell requestActivate accepts a foreign token only via
|
||||
// XDG_ACTIVATION_TOKEN; there is no public setter. show() consumes it.
|
||||
static void
|
||||
apply_activation_token(const QString &token)
|
||||
{
|
||||
if (!token.isEmpty())
|
||||
qputenv("XDG_ACTIVATION_TOKEN", token.toUtf8());
|
||||
}
|
||||
|
||||
// Windows has no token to carry: the process that asked for the window
|
||||
// hands over its foreground right first, and only then does this work.
|
||||
// See AllowSetForegroundWindow in main.cpp.
|
||||
static void
|
||||
raise_window(Window *window)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
window->requestActivate();
|
||||
#else
|
||||
(void) window;
|
||||
#endif
|
||||
}
|
||||
|
||||
OpenResult
|
||||
App::open(const QUrl &url, const QString &activation_token, BrowseSetup setup,
|
||||
bool browse)
|
||||
{
|
||||
// Until there is a VFS, this is where anything but file:// stops.
|
||||
const QString path = url_to_path(url);
|
||||
if (path.isEmpty()) {
|
||||
qWarning("%s: unsupported location",
|
||||
qUtf8Printable(url.toString(QUrl::PrettyDecoded)));
|
||||
return OpenResult::InvalidArgument;
|
||||
}
|
||||
|
||||
const QFileInfo info(path);
|
||||
if (!info.exists()) {
|
||||
qWarning("%s: not found", qUtf8Printable(path));
|
||||
return OpenResult::NotFound;
|
||||
}
|
||||
if (!info.isReadable()) {
|
||||
qWarning("%s: permission denied", qUtf8Printable(path));
|
||||
return OpenResult::PermissionDenied;
|
||||
}
|
||||
|
||||
// Windows compare and store this, so hand them a canonical form.
|
||||
const QUrl resolved = QUrl::fromLocalFile(info.absoluteFilePath());
|
||||
if (Window *target = this->default_window) {
|
||||
this->default_window = nullptr;
|
||||
if (target->current_url() == path_to_url(QDir::currentPath())) {
|
||||
target->open_any(resolved, browse);
|
||||
apply_activation_token(activation_token);
|
||||
raise_window(target);
|
||||
return OpenResult::Ok;
|
||||
}
|
||||
}
|
||||
|
||||
#if DN_WITH_WAYLAND
|
||||
if (QGuiApplication::platformName() == QStringLiteral("wayland")) {
|
||||
auto window = make_unique<WaylandWindow>(this);
|
||||
if (!window->initialize(resolved, setup, browse))
|
||||
return OpenResult::Internal;
|
||||
apply_activation_token(activation_token);
|
||||
window->show();
|
||||
this->windows_.push_back(std::move(window));
|
||||
return OpenResult::Ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto window = make_unique<Window>(this);
|
||||
if (!window->initialize(resolved, setup, browse))
|
||||
return OpenResult::Internal;
|
||||
apply_activation_token(activation_token);
|
||||
window->show();
|
||||
raise_window(window.get());
|
||||
this->windows_.push_back(std::move(window));
|
||||
#ifdef Q_OS_MACOS
|
||||
sync_macos_app_menu(this);
|
||||
#endif
|
||||
return OpenResult::Ok;
|
||||
}
|
||||
|
||||
void
|
||||
App::close(const QWindow *top)
|
||||
{
|
||||
erase_if(this->windows_, [top](const unique_ptr<QWindow> &window) {
|
||||
return window.get() == top;
|
||||
});
|
||||
if (this->windows_.empty())
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
|
||||
void
|
||||
App::close_later(const QWindow *top)
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
this, [this, top] { close(top); }, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void
|
||||
App::shutdown()
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this] {
|
||||
// Unmap first. The shell is a black SHM buffer; destroying the
|
||||
// Vulkan subsurface while it is still mapped is the black window.
|
||||
for (unique_ptr<QWindow> &w : this->windows_)
|
||||
w->hide();
|
||||
QGuiApplication::sync();
|
||||
this->windows_.clear();
|
||||
QCoreApplication::exit(0);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
// A Wayland shell carries the viewer as a child window; elsewhere the
|
||||
// top-level is the viewer itself. Nothing here has Q_OBJECT, so qobject_cast
|
||||
// and findChild() would fall back to QWindow and match anything.
|
||||
static Window *
|
||||
content_window(QObject *window)
|
||||
{
|
||||
if (auto *win = dynamic_cast<Window *>(window))
|
||||
return win;
|
||||
for (QObject *child : window->children())
|
||||
if (auto *win = dynamic_cast<Window *>(child))
|
||||
return win;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Window *
|
||||
App::key_window() const
|
||||
{
|
||||
if (QWindow *focus = QGuiApplication::focusWindow())
|
||||
if (Window *win = content_window(focus))
|
||||
return win;
|
||||
for (const unique_ptr<QWindow> &w : this->windows_)
|
||||
if (Window *win = content_window(w.get()))
|
||||
return win;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// app.hpp: process-wide Vulkan, GPU, and top-level windows
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "display-profile.hpp"
|
||||
#include "gpu.hpp"
|
||||
#include "kit-browser.hpp"
|
||||
#include "kit-chrome.hpp"
|
||||
#include "thumbnailer.hpp"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVulkanInstance>
|
||||
#include <QWindow>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
class Window;
|
||||
|
||||
enum class OpenResult : uint8_t {
|
||||
Ok,
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
InvalidArgument,
|
||||
Internal
|
||||
};
|
||||
|
||||
/// Process-wide user settings and change notification.
|
||||
/// Notification is coarse: any change notifies all listeners.
|
||||
class Settings
|
||||
{
|
||||
std::vector<std::pair<void *, std::function<void()>>> listeners_;
|
||||
|
||||
void notify() const;
|
||||
void update_enabled_loaders();
|
||||
// Reads the profile in, so that a path that cannot be used says so when
|
||||
// it is chosen rather than at the next repaint.
|
||||
void load_icc_override(const std::string &path);
|
||||
|
||||
public:
|
||||
std::vector<std::string> bookmarks;
|
||||
std::vector<unsigned char> icc_profile_override;
|
||||
std::string icc_profile_override_path;
|
||||
// All loaders this build has, in the configured order.
|
||||
std::vector<SettingsDraft::Loader> loaders;
|
||||
// The enabled ones, for OpenContext::loaders. Replaced rather than
|
||||
// rewritten, so that a decoding thread may hold on to an older list.
|
||||
std::shared_ptr<const std::vector<std::string>> enabled_loaders;
|
||||
bool disable_dithering = false;
|
||||
bool browser_show_filenames = true;
|
||||
int browser_thumbnail_size = 256;
|
||||
|
||||
void load();
|
||||
void save(const SettingsDraft &draft);
|
||||
[[nodiscard]] bool bookmarked(const std::string &path) const;
|
||||
void toggle_bookmark(const std::string &path);
|
||||
void listen(void *key, std::function<void()> fn);
|
||||
void unlisten(const void *key);
|
||||
};
|
||||
|
||||
class App : public QGuiApplication
|
||||
{
|
||||
std::vector<std::unique_ptr<QWindow>> windows_;
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
public:
|
||||
App(int &argc, char **argv) : QGuiApplication(argc, argv) {}
|
||||
|
||||
QVulkanInstance vulkan_instance;
|
||||
GpuContext gpu;
|
||||
Thumbnailer thumbnailer;
|
||||
DisplayProfileWatch display_profiles;
|
||||
Settings settings;
|
||||
// macOS may open us blank before passing us association-opened files.
|
||||
QPointer<Window> default_window;
|
||||
bool needs_csd = false;
|
||||
|
||||
bool init();
|
||||
OpenResult open(const QUrl &url, const QString &activation_token = {},
|
||||
BrowseSetup setup = {}, bool browse = false);
|
||||
void close(const QWindow *top);
|
||||
void close_later(const QWindow *top);
|
||||
// Not quit(): that name is taken by a static QCoreApplication slot,
|
||||
// which Qt's own quit paths go through, skipping the unmapping below.
|
||||
void shutdown();
|
||||
[[nodiscard]] Window *key_window() const;
|
||||
};
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,188 @@
|
||||
//
|
||||
// assoc-macos.mm: Launch Services Open With (UTI from the file, not MIME)
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "assoc.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
using namespace std;
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
static NSURL *
|
||||
file_url(const QString &path)
|
||||
{
|
||||
if (path.isEmpty())
|
||||
return nil;
|
||||
return [NSURL fileURLWithPath:path.toNSString()];
|
||||
}
|
||||
|
||||
static QString
|
||||
from_ns(NSString *s)
|
||||
{
|
||||
return s ? QString::fromNSString(s) : QString();
|
||||
}
|
||||
|
||||
static Handler
|
||||
app_from_url(NSURL *url)
|
||||
{
|
||||
Handler a;
|
||||
if (!url)
|
||||
return a;
|
||||
a.id = from_ns(url.path);
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:url];
|
||||
if (bundle) {
|
||||
NSString *bid = bundle.bundleIdentifier;
|
||||
if (bid.length)
|
||||
a.id = from_ns(bid);
|
||||
NSString *name =
|
||||
[bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
||||
if (!name)
|
||||
name = [bundle objectForInfoDictionaryKey:@"CFBundleName"];
|
||||
a.name = from_ns(name);
|
||||
a.icon = from_ns(bundle.bundlePath);
|
||||
}
|
||||
if (a.name.isEmpty())
|
||||
a.name = from_ns(url.lastPathComponent);
|
||||
return a;
|
||||
}
|
||||
|
||||
static NSString *
|
||||
uti_from_file(NSURL *url)
|
||||
{
|
||||
if (!url)
|
||||
return nil;
|
||||
NSString *uti = nil;
|
||||
[url getResourceValue:&uti forKey:NSURLTypeIdentifierKey error:nil];
|
||||
if (uti.length)
|
||||
return uti;
|
||||
|
||||
NSString *ext = url.pathExtension;
|
||||
if (ext.length)
|
||||
return [UTType typeWithFilenameExtension:ext].identifier;
|
||||
return nil;
|
||||
}
|
||||
|
||||
static NSURL *
|
||||
app_url_for_bundle_id(NSString *bid)
|
||||
{
|
||||
if (!bid.length)
|
||||
return nil;
|
||||
CFErrorRef err = nullptr;
|
||||
CFArrayRef urls = LSCopyApplicationURLsForBundleIdentifier(
|
||||
(__bridge CFStringRef) bid, &err);
|
||||
if (err)
|
||||
CFRelease(err);
|
||||
if (!urls)
|
||||
return nil;
|
||||
NSURL *url = nil;
|
||||
if (CFArrayGetCount(urls) > 0)
|
||||
url = (__bridge NSURL *) CFArrayGetValueAtIndex(urls, 0);
|
||||
NSURL *copy = url ? [url copy] : nil;
|
||||
CFRelease(urls);
|
||||
return copy;
|
||||
}
|
||||
|
||||
Handler
|
||||
default_for(const QString &path)
|
||||
{
|
||||
NSURL *url = file_url(path);
|
||||
if (!url)
|
||||
return {};
|
||||
CFErrorRef err = nullptr;
|
||||
CFURLRef app = LSCopyDefaultApplicationURLForURL(
|
||||
(__bridge CFURLRef) url, kLSRolesAll, &err);
|
||||
if (err)
|
||||
CFRelease(err);
|
||||
if (!app)
|
||||
return {};
|
||||
Handler a = app_from_url((__bridge NSURL *) app);
|
||||
CFRelease(app);
|
||||
return a;
|
||||
}
|
||||
vector<Handler>
|
||||
recommended_for(const QString &path)
|
||||
{
|
||||
NSURL *url = file_url(path);
|
||||
NSString *uti = uti_from_file(url);
|
||||
if (!uti)
|
||||
return {};
|
||||
CFArrayRef handlers = LSCopyAllRoleHandlersForContentType(
|
||||
(__bridge CFStringRef) uti, kLSRolesAll);
|
||||
if (!handlers)
|
||||
return {};
|
||||
|
||||
const Handler def = default_for(path);
|
||||
vector<Handler> out;
|
||||
const CFIndex n = CFArrayGetCount(handlers);
|
||||
for (CFIndex i = 0; i < n; ++i) {
|
||||
NSString *bid =
|
||||
(__bridge NSString *) CFArrayGetValueAtIndex(handlers, i);
|
||||
NSURL *app_url = app_url_for_bundle_id(bid);
|
||||
Handler a = app_from_url(app_url);
|
||||
if (a.id.isEmpty()) {
|
||||
a.id = from_ns(bid);
|
||||
a.name = from_ns(bid);
|
||||
}
|
||||
if (a.id.isEmpty())
|
||||
continue;
|
||||
if (!def.id.isEmpty() && a.id == def.id)
|
||||
continue;
|
||||
out.push_back(std::move(a));
|
||||
}
|
||||
CFRelease(handlers);
|
||||
return out;
|
||||
}
|
||||
vector<Handler>
|
||||
fallback_for(const QString &)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
bool
|
||||
launch(const Handler &app, const QString &path)
|
||||
{
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return false;
|
||||
NSURL *file = file_url(path);
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
NSURL *app_url = nil;
|
||||
NSString *ident = app.id.toNSString();
|
||||
NSBundle *bundle = [NSBundle bundleWithIdentifier:ident];
|
||||
if (bundle)
|
||||
app_url = bundle.bundleURL;
|
||||
if (!app_url)
|
||||
app_url = app_url_for_bundle_id(ident);
|
||||
if (!app_url)
|
||||
app_url = [NSURL fileURLWithPath:ident];
|
||||
if (!app_url)
|
||||
return false;
|
||||
|
||||
// The launch is asynchronous, so failures can only be reported later,
|
||||
// and the caller has nothing to do with them anyway.
|
||||
[[NSWorkspace sharedWorkspace] openURLs:@[ file ]
|
||||
withApplicationAtURL:app_url
|
||||
configuration:[NSWorkspaceOpenConfiguration
|
||||
configuration]
|
||||
completionHandler:nil];
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
set_last_used(const Handler &, const QString &)
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,791 @@
|
||||
//
|
||||
// assoc-unix.cpp: XDG MIME Applications Open With (filename types, no GIO)
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include <dawn-config.h>
|
||||
|
||||
#include "assoc.hpp"
|
||||
|
||||
#include "xdg.hpp"
|
||||
|
||||
#include <libdn.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#include <QProcess>
|
||||
#include <QSaveFile>
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr auto kSelfDesktop = QLatin1String(DAWN_NAMESPACE ".desktop");
|
||||
|
||||
string
|
||||
read_text_file(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
return file.readAll().toStdString();
|
||||
}
|
||||
|
||||
bool
|
||||
write_text_file(const QString &path, string_view text)
|
||||
{
|
||||
QFileInfo info(path);
|
||||
if (!QDir().mkpath(info.absolutePath()))
|
||||
return false;
|
||||
|
||||
// One file carries the user's associations for every application,
|
||||
// so it must never be left truncated.
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
if (file.write(text.data(), qsizetype(text.size())) !=
|
||||
qsizetype(text.size()))
|
||||
return false;
|
||||
return file.commit();
|
||||
}
|
||||
|
||||
bool
|
||||
parse_bool(string_view value)
|
||||
{
|
||||
const QString v = QString::fromUtf8(value.data(), qsizetype(value.size()))
|
||||
.trimmed()
|
||||
.toLower();
|
||||
return v == QLatin1String("true") || v == QLatin1String("1");
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
split_semicolons(string_view value)
|
||||
{
|
||||
vector<QString> out;
|
||||
const QString decoded =
|
||||
QString::fromUtf8(value.data(), qsizetype(value.size()));
|
||||
for (const QString &part : decoded.split(u';', Qt::SkipEmptyParts)) {
|
||||
const QString item = part.trimmed();
|
||||
if (!item.isEmpty())
|
||||
out.push_back(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString
|
||||
normalize_desktop_id(QString id)
|
||||
{
|
||||
id = id.trimmed();
|
||||
if (id.isEmpty())
|
||||
return {};
|
||||
|
||||
if (!id.endsWith(QLatin1String(".desktop")))
|
||||
id += QLatin1String(".desktop");
|
||||
return id;
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
current_desktops()
|
||||
{
|
||||
vector<QString> out;
|
||||
const QString env = qEnvironmentVariable("XDG_CURRENT_DESKTOP");
|
||||
for (const QString &part : env.split(u':', Qt::SkipEmptyParts)) {
|
||||
const QString desk = part.trimmed();
|
||||
if (!desk.isEmpty())
|
||||
out.push_back(desk);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
locale_candidates()
|
||||
{
|
||||
vector<QString> raw;
|
||||
const QString language = qEnvironmentVariable("LANGUAGE");
|
||||
if (!language.isEmpty()) {
|
||||
for (const QString &part : language.split(u':', Qt::SkipEmptyParts))
|
||||
raw.push_back(part);
|
||||
}
|
||||
|
||||
raw.push_back(qEnvironmentVariable("LC_ALL"));
|
||||
raw.push_back(qEnvironmentVariable("LC_MESSAGES"));
|
||||
raw.push_back(qEnvironmentVariable("LANG"));
|
||||
|
||||
vector<QString> out;
|
||||
auto add = [&](const QString &s) {
|
||||
if (!s.isEmpty() && find(out.begin(), out.end(), s) == out.end())
|
||||
out.push_back(s);
|
||||
};
|
||||
for (QString loc : raw) {
|
||||
loc = loc.trimmed();
|
||||
if (loc.isEmpty() || loc == QLatin1String("C") ||
|
||||
loc == QLatin1String("POSIX"))
|
||||
continue;
|
||||
const int dot = loc.indexOf(u'.');
|
||||
if (dot >= 0)
|
||||
loc = loc.left(dot);
|
||||
loc.replace(u'-', u'_');
|
||||
QString modifier;
|
||||
const int at = loc.indexOf(u'@');
|
||||
if (at >= 0) {
|
||||
modifier = loc.mid(at);
|
||||
loc = loc.left(at);
|
||||
}
|
||||
QString lang = loc;
|
||||
QString country;
|
||||
const int us = loc.indexOf(u'_');
|
||||
if (us >= 0) {
|
||||
country = loc.mid(us);
|
||||
lang = loc.left(us);
|
||||
}
|
||||
if (!country.isEmpty() && !modifier.isEmpty())
|
||||
add(lang + country + modifier);
|
||||
if (!country.isEmpty())
|
||||
add(lang + country);
|
||||
if (!modifier.isEmpty())
|
||||
add(lang + modifier);
|
||||
add(lang);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
using IniGroup = dawn::detail::IniGroup;
|
||||
using IniFile = dawn::detail::IniFile;
|
||||
|
||||
vector<QString>
|
||||
mimeapps_list_paths()
|
||||
{
|
||||
vector<QString> paths;
|
||||
const vector<QString> desktops = current_desktops();
|
||||
auto add_dir = [&](const QString &dir, bool applications) {
|
||||
const QString base = applications
|
||||
? QDir(dir).filePath(QStringLiteral("applications"))
|
||||
: dir;
|
||||
for (const QString &desk : desktops) {
|
||||
paths.push_back(QDir(base).filePath(
|
||||
desk.toLower() + QStringLiteral("-mimeapps.list")));
|
||||
}
|
||||
paths.push_back(QDir(base).filePath(QStringLiteral("mimeapps.list")));
|
||||
};
|
||||
for (const QString &dir : xdg_config_dirs())
|
||||
add_dir(dir, false);
|
||||
for (const QString &dir : xdg_data_dirs())
|
||||
add_dir(dir, true);
|
||||
return paths;
|
||||
}
|
||||
|
||||
struct AssocSets {
|
||||
vector<QString> defaults;
|
||||
vector<QString> added;
|
||||
unordered_set<QString> removed;
|
||||
};
|
||||
|
||||
void
|
||||
append_unique(vector<QString> &list, const QString &id)
|
||||
{
|
||||
if (id.isEmpty())
|
||||
return;
|
||||
if (find(list.begin(), list.end(), id) != list.end())
|
||||
return;
|
||||
list.push_back(id);
|
||||
}
|
||||
|
||||
void
|
||||
apply_mimeapps(AssocSets &acc, const IniFile &ini, const QString &type)
|
||||
{
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name == "Default Applications") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8)))
|
||||
append_unique(acc.defaults, normalize_desktop_id(id));
|
||||
} else if (group.name == "Added Associations") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(id);
|
||||
if (!acc.removed.contains(nid))
|
||||
append_unique(acc.added, nid);
|
||||
}
|
||||
} else if (group.name == "Removed Associations") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(id);
|
||||
if (find(acc.added.begin(), acc.added.end(), nid) ==
|
||||
acc.added.end())
|
||||
acc.removed.insert(nid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssocSets
|
||||
associations_for_type(const QString &type)
|
||||
{
|
||||
AssocSets acc;
|
||||
for (const QString &path : mimeapps_list_paths()) {
|
||||
if (!QFileInfo::exists(path))
|
||||
continue;
|
||||
apply_mimeapps(
|
||||
acc, dawn::detail::ini_parse(read_text_file(path)), type);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
cache_ids_for_type(const QString &type)
|
||||
{
|
||||
vector<QString> ids;
|
||||
for (const QString &dir : xdg_data_dirs()) {
|
||||
const QString path =
|
||||
QDir(dir).filePath(QStringLiteral("applications/mimeinfo.cache"));
|
||||
if (!QFileInfo::exists(path))
|
||||
continue;
|
||||
|
||||
const IniFile ini = dawn::detail::ini_parse(read_text_file(path));
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name != "MIME Cache")
|
||||
continue;
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8)))
|
||||
append_unique(ids, normalize_desktop_id(id));
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
QString
|
||||
desktop_path_for_id(const QString &id)
|
||||
{
|
||||
vector<QString> names;
|
||||
names.push_back(id);
|
||||
QString alt = id;
|
||||
for (int i = 0; i < alt.size(); ++i) {
|
||||
if (alt[i] == u'-') {
|
||||
alt[i] = u'/';
|
||||
names.push_back(alt);
|
||||
}
|
||||
}
|
||||
for (const QString &dir : xdg_data_dirs()) {
|
||||
for (const QString &name : names) {
|
||||
const QString path =
|
||||
QDir(dir).filePath(QStringLiteral("applications/") + name);
|
||||
if (QFileInfo::exists(path))
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct Desktop {
|
||||
QString id;
|
||||
QString path;
|
||||
QString name;
|
||||
QString icon;
|
||||
QString exec;
|
||||
QString try_exec;
|
||||
vector<QString> only_show_in;
|
||||
vector<QString> not_show_in;
|
||||
bool hidden = false;
|
||||
bool application = true;
|
||||
};
|
||||
|
||||
QString
|
||||
localized_value(const IniGroup &entry, const QString &key)
|
||||
{
|
||||
const QString prefix = key + QLatin1String("[");
|
||||
unordered_map<QString, QString> localized;
|
||||
QString fallback;
|
||||
for (const auto &kv : entry.keys) {
|
||||
const QString item_key = QString::fromStdString(kv.first);
|
||||
if (item_key == key) {
|
||||
if (fallback.isEmpty())
|
||||
fallback = QString::fromStdString(
|
||||
dawn::detail::desktop_unescape(kv.second));
|
||||
continue;
|
||||
}
|
||||
if (!item_key.startsWith(prefix) || !item_key.endsWith(u']'))
|
||||
continue;
|
||||
const QString loc = item_key.mid(
|
||||
prefix.size(), item_key.size() - prefix.size() - 1);
|
||||
localized.insert({loc, QString::fromStdString(
|
||||
dawn::detail::desktop_unescape(kv.second))});
|
||||
}
|
||||
for (const QString &loc : locale_candidates()) {
|
||||
const auto it = localized.find(loc);
|
||||
if (it != localized.end())
|
||||
return it->second;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
QString
|
||||
localized_name(const IniGroup &entry)
|
||||
{
|
||||
// GLib's display name, which users see next to ours in other file
|
||||
// managers, prefers this GNOME extension: "Mozilla Firefox" over
|
||||
// "Firefox". Unlike GLib, don't let an empty value blank the name out.
|
||||
const QString full =
|
||||
localized_value(entry, QStringLiteral("X-GNOME-FullName"));
|
||||
if (!full.isEmpty())
|
||||
return full;
|
||||
return localized_value(entry, QStringLiteral("Name"));
|
||||
}
|
||||
|
||||
bool
|
||||
try_exec_ok(const QString &try_exec)
|
||||
{
|
||||
if (try_exec.isEmpty())
|
||||
return true;
|
||||
|
||||
const QFileInfo info(try_exec);
|
||||
if (info.isAbsolute())
|
||||
return info.isFile() && info.isExecutable();
|
||||
|
||||
const QString path = qEnvironmentVariable("PATH");
|
||||
for (const QString &dir : path.split(u':', Qt::SkipEmptyParts)) {
|
||||
const QFileInfo cand(QDir(dir).filePath(try_exec));
|
||||
if (cand.isFile() && cand.isExecutable())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
shown_on_desktop(const Desktop &d)
|
||||
{
|
||||
const vector<QString> desks = current_desktops();
|
||||
if (!d.only_show_in.empty()) {
|
||||
bool ok = false;
|
||||
for (const QString &desk : desks) {
|
||||
if (find(d.only_show_in.begin(), d.only_show_in.end(), desk) !=
|
||||
d.only_show_in.end()) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok)
|
||||
return false;
|
||||
}
|
||||
for (const QString &desk : desks) {
|
||||
if (find(d.not_show_in.begin(), d.not_show_in.end(), desk) !=
|
||||
d.not_show_in.end())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Desktop
|
||||
load_desktop(const QString &id)
|
||||
{
|
||||
Desktop d;
|
||||
d.id = id;
|
||||
d.path = desktop_path_for_id(id);
|
||||
if (d.path.isEmpty())
|
||||
return d;
|
||||
|
||||
const IniFile ini = dawn::detail::ini_parse(read_text_file(d.path));
|
||||
const IniGroup *entry = nullptr;
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name == "Desktop Entry") {
|
||||
entry = &group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!entry)
|
||||
return d;
|
||||
|
||||
const QString type = QString::fromStdString(
|
||||
dawn::detail::ini_get(*entry, "Type"))
|
||||
.trimmed();
|
||||
d.application = type.isEmpty() || type == QLatin1String("Application");
|
||||
d.name = localized_name(*entry);
|
||||
d.icon = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "Icon")));
|
||||
d.exec = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "Exec")));
|
||||
d.try_exec = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "TryExec")));
|
||||
d.hidden = parse_bool(dawn::detail::ini_get(*entry, "Hidden"));
|
||||
d.only_show_in =
|
||||
split_semicolons(dawn::detail::ini_get(*entry, "OnlyShowIn"));
|
||||
d.not_show_in =
|
||||
split_semicolons(dawn::detail::ini_get(*entry, "NotShowIn"));
|
||||
return d;
|
||||
}
|
||||
|
||||
const Desktop *
|
||||
desktop_by_id(const QString &id)
|
||||
{
|
||||
static unordered_map<QString, Desktop> cache;
|
||||
if (id.isEmpty())
|
||||
return nullptr;
|
||||
auto it = cache.find(id);
|
||||
if (it == cache.end())
|
||||
it = cache.insert({id, load_desktop(id)}).first;
|
||||
if (it->second.path.isEmpty() || !it->second.application)
|
||||
return nullptr;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
bool
|
||||
listable(const Desktop &d)
|
||||
{
|
||||
if (d.id == kSelfDesktop)
|
||||
return false;
|
||||
if (d.hidden || d.exec.isEmpty() || !shown_on_desktop(d))
|
||||
return false;
|
||||
if (!try_exec_ok(d.try_exec))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Handler
|
||||
to_app(const Desktop &d)
|
||||
{
|
||||
Handler a;
|
||||
a.id = d.id;
|
||||
a.name = d.name.isEmpty() ? d.id : d.name;
|
||||
a.icon = d.icon;
|
||||
return a;
|
||||
}
|
||||
|
||||
QMimeDatabase &
|
||||
db()
|
||||
{
|
||||
static QMimeDatabase mime;
|
||||
return mime;
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
ancestor_types(const QString &type)
|
||||
{
|
||||
vector<QString> ancestors;
|
||||
const QMimeType mt = db().mimeTypeForName(type);
|
||||
if (!mt.isValid())
|
||||
return ancestors;
|
||||
for (const QString &a : mt.allAncestors())
|
||||
append_unique(ancestors, a);
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
void
|
||||
merge_assoc(AssocSets &into, const AssocSets &from)
|
||||
{
|
||||
for (const QString &id : from.defaults)
|
||||
append_unique(into.defaults, id);
|
||||
for (const QString &id : from.added)
|
||||
append_unique(into.added, id);
|
||||
into.removed.insert(from.removed.begin(), from.removed.end());
|
||||
for (const QString &id : into.added)
|
||||
into.removed.erase(id);
|
||||
}
|
||||
|
||||
bool
|
||||
usable_id(const QString &id, const unordered_set<QString> &removed)
|
||||
{
|
||||
if (id.isEmpty() || id == kSelfDesktop || removed.contains(id))
|
||||
return false;
|
||||
const Desktop *d = desktop_by_id(id);
|
||||
return d && listable(*d);
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
split_exec(const QString &exec)
|
||||
{
|
||||
vector<QString> args;
|
||||
QString cur;
|
||||
bool in_quote = false;
|
||||
for (int i = 0; i < exec.size(); ++i) {
|
||||
const QChar c = exec[i];
|
||||
if (in_quote) {
|
||||
if (c == u'\\' && i + 1 < exec.size()) {
|
||||
const QChar n = exec[++i];
|
||||
if (n == u's')
|
||||
cur += u' ';
|
||||
else if (n == u'n')
|
||||
cur += u'\n';
|
||||
else if (n == u't')
|
||||
cur += u'\t';
|
||||
else if (n == u'r')
|
||||
cur += u'\r';
|
||||
else
|
||||
cur += n;
|
||||
} else if (c == u'"') {
|
||||
in_quote = false;
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
} else if (c == u'"') {
|
||||
in_quote = true;
|
||||
} else if (c.isSpace()) {
|
||||
if (!cur.isEmpty()) {
|
||||
args.push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
} else if (c == u'\\' && i + 1 < exec.size()) {
|
||||
cur += exec[++i];
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
if (!cur.isEmpty())
|
||||
args.push_back(cur);
|
||||
return args;
|
||||
}
|
||||
|
||||
QStringList
|
||||
expand_exec(const Desktop &d, const QString &path)
|
||||
{
|
||||
const QString url = QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath())
|
||||
.toString(QUrl::FullyEncoded);
|
||||
const QString abs = QFileInfo(path).absoluteFilePath();
|
||||
QStringList out;
|
||||
bool saw_file = false;
|
||||
for (const QString &arg : split_exec(d.exec)) {
|
||||
if (arg == QLatin1String("%i")) {
|
||||
if (!d.icon.isEmpty()) {
|
||||
out.push_back(QStringLiteral("--icon"));
|
||||
out.push_back(d.icon);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
QString built;
|
||||
for (int i = 0; i < arg.size(); ++i) {
|
||||
if (arg[i] != u'%' || i + 1 >= arg.size()) {
|
||||
built += arg[i];
|
||||
continue;
|
||||
}
|
||||
const QChar code = arg[++i];
|
||||
switch (code.unicode()) {
|
||||
case u'f':
|
||||
case u'F':
|
||||
built += abs;
|
||||
saw_file = true;
|
||||
break;
|
||||
case u'u':
|
||||
case u'U':
|
||||
built += url;
|
||||
saw_file = true;
|
||||
break;
|
||||
case u'c':
|
||||
built += d.name;
|
||||
break;
|
||||
case u'k':
|
||||
built += d.path;
|
||||
break;
|
||||
case u'%':
|
||||
built += u'%';
|
||||
break;
|
||||
case u'i':
|
||||
case u'd':
|
||||
case u'D':
|
||||
case u'n':
|
||||
case u'N':
|
||||
case u'v':
|
||||
case u'm':
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.push_back(built);
|
||||
}
|
||||
if (!saw_file && !abs.isEmpty())
|
||||
out.push_back(abs);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString
|
||||
user_mimeapps_path()
|
||||
{
|
||||
const vector<QString> dirs = xdg_config_dirs();
|
||||
if (dirs.empty())
|
||||
return {};
|
||||
return QDir(dirs.front()).filePath(QStringLiteral("mimeapps.list"));
|
||||
}
|
||||
|
||||
QString
|
||||
prepend_id(const QString &value, const QString &id)
|
||||
{
|
||||
vector<QString> ids;
|
||||
append_unique(ids, id);
|
||||
for (const QString &existing :
|
||||
split_semicolons(value.toUtf8().toStdString()))
|
||||
append_unique(ids, normalize_desktop_id(existing));
|
||||
QString out;
|
||||
for (const QString &item : ids) {
|
||||
out += item;
|
||||
out += u';';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Handler
|
||||
default_for(const QString &path)
|
||||
{
|
||||
const QString type = db().mimeTypeForFile(path).name();
|
||||
const AssocSets acc = associations_for_type(type);
|
||||
for (const QString &id : acc.defaults) {
|
||||
if (!usable_id(id, acc.removed))
|
||||
continue;
|
||||
return to_app(*desktop_by_id(id));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
vector<Handler>
|
||||
recommended_for(const QString &path)
|
||||
{
|
||||
const QString type = db().mimeTypeForFile(path).name();
|
||||
const AssocSets acc = associations_for_type(type);
|
||||
const vector<QString> cache_ids = cache_ids_for_type(type);
|
||||
|
||||
const Handler def = default_for(path);
|
||||
vector<Handler> out;
|
||||
unordered_set<QString> seen;
|
||||
if (!def.id.isEmpty())
|
||||
seen.insert(def.id);
|
||||
auto push = [&](const QString &id) {
|
||||
if (seen.contains(id) || !usable_id(id, acc.removed))
|
||||
return;
|
||||
seen.insert(id);
|
||||
out.push_back(to_app(*desktop_by_id(id)));
|
||||
};
|
||||
for (const QString &id : acc.added)
|
||||
push(id);
|
||||
for (const QString &id : cache_ids)
|
||||
push(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
vector<Handler>
|
||||
fallback_for(const QString &path)
|
||||
{
|
||||
const QString type = db().mimeTypeForFile(path).name();
|
||||
const vector<QString> ancestors = ancestor_types(type);
|
||||
AssocSets acc = associations_for_type(type);
|
||||
for (const QString &type : ancestors)
|
||||
merge_assoc(acc, associations_for_type(type));
|
||||
|
||||
unordered_set<QString> seen;
|
||||
const Handler def = default_for(path);
|
||||
if (!def.id.isEmpty())
|
||||
seen.insert(def.id);
|
||||
for (const Handler &a : recommended_for(path))
|
||||
seen.insert(a.id);
|
||||
|
||||
vector<Handler> out;
|
||||
for (const QString &type : ancestors) {
|
||||
for (const QString &id : cache_ids_for_type(type)) {
|
||||
if (seen.contains(id) || !usable_id(id, acc.removed))
|
||||
continue;
|
||||
seen.insert(id);
|
||||
out.push_back(to_app(*desktop_by_id(id)));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool
|
||||
launch(const Handler &app, const QString &path)
|
||||
{
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return false;
|
||||
|
||||
const Desktop *d = desktop_by_id(app.id);
|
||||
if (!d || d->exec.isEmpty())
|
||||
return false;
|
||||
const QStringList args = expand_exec(*d, path);
|
||||
if (args.isEmpty())
|
||||
return false;
|
||||
return QProcess::startDetached(args.front(), args.mid(1));
|
||||
}
|
||||
|
||||
void
|
||||
set_last_used(const Handler &app, const QString &path)
|
||||
{
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return;
|
||||
|
||||
const QString id = normalize_desktop_id(app.id);
|
||||
if (id.isEmpty() || id == kSelfDesktop)
|
||||
return;
|
||||
|
||||
const QString type = db().mimeTypeForFile(path).name();
|
||||
const QString dest = user_mimeapps_path();
|
||||
if (dest.isEmpty())
|
||||
return;
|
||||
|
||||
IniFile ini = dawn::detail::ini_parse(read_text_file(dest));
|
||||
auto find_group = [&](string_view name) -> IniGroup * {
|
||||
for (IniGroup &group : ini.groups) {
|
||||
if (group.name == name)
|
||||
return &group;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
if (!find_group("Added Associations")) {
|
||||
IniGroup group;
|
||||
group.name = "Added Associations";
|
||||
ini.groups.push_back(std::move(group));
|
||||
}
|
||||
IniGroup *added = find_group("Added Associations");
|
||||
IniGroup *removed = find_group("Removed Associations");
|
||||
auto drop_id = [&](IniGroup &group, const QString &type) {
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
vector<QString> kept;
|
||||
for (const QString &existing :
|
||||
split_semicolons(dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(existing);
|
||||
if (nid != id)
|
||||
append_unique(kept, nid);
|
||||
}
|
||||
QString value;
|
||||
for (const QString &item : kept) {
|
||||
value += item;
|
||||
value += u';';
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
group.keys.erase(remove_if(group.keys.begin(), group.keys.end(),
|
||||
[&](const pair<string, string> &kv) {
|
||||
return kv.first == type_utf8;
|
||||
}),
|
||||
group.keys.end());
|
||||
} else {
|
||||
dawn::detail::ini_set(
|
||||
group, type_utf8, value.toUtf8().toStdString());
|
||||
}
|
||||
};
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
const QString previous = QString::fromStdString(
|
||||
dawn::detail::ini_get(*added, type_utf8));
|
||||
dawn::detail::ini_set(*added, type_utf8,
|
||||
prepend_id(previous, id).toUtf8().toStdString());
|
||||
if (removed)
|
||||
drop_id(*removed, type);
|
||||
write_text_file(dest, dawn::detail::ini_serialize(ini));
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,255 @@
|
||||
//
|
||||
// assoc-windows.cpp: IAssocHandler Open With (extension from the path)
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "assoc.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlguid.h>
|
||||
#include <shlobj.h>
|
||||
#include <shlwapi.h>
|
||||
#include <shobjidl.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
static void
|
||||
ensure_com()
|
||||
{
|
||||
static const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
(void) hr;
|
||||
}
|
||||
|
||||
static QString
|
||||
extension_of(const QString &path)
|
||||
{
|
||||
// Windows doesn't really work this way. We could use command-subkey verbs,
|
||||
// though it's a lot of code for little gain.
|
||||
if (QFileInfo(path).isDir())
|
||||
return {};
|
||||
|
||||
const QString suffix = QFileInfo(path).suffix();
|
||||
if (suffix.isEmpty())
|
||||
return {};
|
||||
|
||||
return QStringLiteral(".") + suffix.toLower();
|
||||
}
|
||||
|
||||
static QString
|
||||
from_wide(const wchar_t *s)
|
||||
{
|
||||
return s ? QString::fromWCharArray(s) : QString();
|
||||
}
|
||||
|
||||
static Handler
|
||||
app_from_handler(IAssocHandler *handler)
|
||||
{
|
||||
Handler a;
|
||||
if (!handler)
|
||||
return a;
|
||||
LPWSTR name = nullptr;
|
||||
if (SUCCEEDED(handler->GetName(&name)) && name) {
|
||||
a.id = from_wide(name);
|
||||
CoTaskMemFree(name);
|
||||
}
|
||||
LPWSTR ui = nullptr;
|
||||
if (SUCCEEDED(handler->GetUIName(&ui)) && ui) {
|
||||
a.name = from_wide(ui);
|
||||
CoTaskMemFree(ui);
|
||||
}
|
||||
LPWSTR icon = nullptr;
|
||||
int icon_index = 0;
|
||||
if (SUCCEEDED(handler->GetIconLocation(&icon, &icon_index)) && icon) {
|
||||
a.icon = from_wide(icon);
|
||||
if (icon_index)
|
||||
a.icon += QStringLiteral(",%1").arg(icon_index);
|
||||
CoTaskMemFree(icon);
|
||||
}
|
||||
if (a.name.isEmpty())
|
||||
a.name = a.id;
|
||||
return a;
|
||||
}
|
||||
static vector<Handler>
|
||||
enum_handlers(const QString &ext, ASSOC_FILTER filter)
|
||||
{
|
||||
vector<Handler> out;
|
||||
if (ext.isEmpty())
|
||||
return out;
|
||||
const wstring wext = ext.toStdWString();
|
||||
IEnumAssocHandlers *en = nullptr;
|
||||
if (FAILED(SHAssocEnumHandlers(wext.c_str(), filter, &en)) || !en)
|
||||
return out;
|
||||
IAssocHandler *handler = nullptr;
|
||||
ULONG got = 0;
|
||||
while (en->Next(1, &handler, &got) == S_OK && handler) {
|
||||
Handler a = app_from_handler(handler);
|
||||
handler->Release();
|
||||
handler = nullptr;
|
||||
if (!a.id.isEmpty())
|
||||
out.push_back(std::move(a));
|
||||
}
|
||||
en->Release();
|
||||
return out;
|
||||
}
|
||||
|
||||
static IAssocHandler *
|
||||
find_handler(const QString &ext, const QString &id)
|
||||
{
|
||||
if (ext.isEmpty() || id.isEmpty())
|
||||
return nullptr;
|
||||
const wstring wext = ext.toStdWString();
|
||||
IEnumAssocHandlers *en = nullptr;
|
||||
if (FAILED(SHAssocEnumHandlers(wext.c_str(), ASSOC_FILTER_NONE, &en)) ||
|
||||
!en)
|
||||
return nullptr;
|
||||
IAssocHandler *found = nullptr;
|
||||
IAssocHandler *handler = nullptr;
|
||||
ULONG got = 0;
|
||||
while (en->Next(1, &handler, &got) == S_OK && handler) {
|
||||
LPWSTR name = nullptr;
|
||||
if (SUCCEEDED(handler->GetName(&name)) && name) {
|
||||
const bool match = from_wide(name) == id;
|
||||
CoTaskMemFree(name);
|
||||
if (match) {
|
||||
found = handler;
|
||||
break;
|
||||
}
|
||||
}
|
||||
handler->Release();
|
||||
handler = nullptr;
|
||||
}
|
||||
en->Release();
|
||||
return found;
|
||||
}
|
||||
|
||||
Handler
|
||||
default_for(const QString &path)
|
||||
{
|
||||
ensure_com();
|
||||
if (QFileInfo(path).isDir()) {
|
||||
Handler a;
|
||||
a.id = QStringLiteral("explorer.exe");
|
||||
// It's not quite clear how to get the localised, OS-appropriate name.
|
||||
a.name = QStringLiteral("File Explorer");
|
||||
return a;
|
||||
}
|
||||
const QString ext = extension_of(path);
|
||||
if (ext.isEmpty())
|
||||
return {};
|
||||
const wstring wext = ext.toStdWString();
|
||||
wchar_t name[MAX_PATH] = {};
|
||||
DWORD name_n = MAX_PATH;
|
||||
Handler a;
|
||||
if (SUCCEEDED(AssocQueryStringW(
|
||||
0, ASSOCSTR_FRIENDLYAPPNAME, wext.c_str(), L"open", name, &name_n)))
|
||||
a.name = from_wide(name);
|
||||
wchar_t exe[MAX_PATH] = {};
|
||||
DWORD exe_n = MAX_PATH;
|
||||
if (SUCCEEDED(AssocQueryStringW(
|
||||
0, ASSOCSTR_EXECUTABLE, wext.c_str(), L"open", exe, &exe_n)))
|
||||
a.id = from_wide(exe);
|
||||
if (!a.id.isEmpty()) {
|
||||
if (a.name.isEmpty())
|
||||
a.name = a.id;
|
||||
return a;
|
||||
}
|
||||
const vector<Handler> rec = enum_handlers(ext, ASSOC_FILTER_RECOMMENDED);
|
||||
if (!rec.empty())
|
||||
return rec.front();
|
||||
return {};
|
||||
}
|
||||
vector<Handler>
|
||||
recommended_for(const QString &path)
|
||||
{
|
||||
ensure_com();
|
||||
const Handler def = default_for(path);
|
||||
vector<Handler> out;
|
||||
for (Handler &a :
|
||||
enum_handlers(extension_of(path), ASSOC_FILTER_RECOMMENDED)) {
|
||||
if (!def.id.isEmpty() && a.id == def.id)
|
||||
continue;
|
||||
out.push_back(std::move(a));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
vector<Handler>
|
||||
fallback_for(const QString &path)
|
||||
{
|
||||
ensure_com();
|
||||
const QString ext = extension_of(path);
|
||||
const vector<Handler> rec = enum_handlers(ext, ASSOC_FILTER_RECOMMENDED);
|
||||
unordered_set<QString> seen;
|
||||
for (const Handler &a : rec)
|
||||
seen.insert(a.id);
|
||||
vector<Handler> out;
|
||||
for (Handler &a : enum_handlers(ext, ASSOC_FILTER_NONE)) {
|
||||
if (seen.contains(a.id))
|
||||
continue;
|
||||
seen.insert(a.id);
|
||||
out.push_back(std::move(a));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool
|
||||
launch(const Handler &app, const QString &path)
|
||||
{
|
||||
ensure_com();
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return false;
|
||||
|
||||
if (QFileInfo(path).isDir()) {
|
||||
const QString abs = QFileInfo(path).absoluteFilePath();
|
||||
const wstring wpath = QDir::toNativeSeparators(abs).toStdWString();
|
||||
return reinterpret_cast<INT_PTR>(ShellExecuteW(nullptr, nullptr,
|
||||
wpath.c_str(), nullptr, nullptr, SW_SHOWNORMAL)) > 32;
|
||||
}
|
||||
|
||||
const QString ext = extension_of(path);
|
||||
IAssocHandler *handler = find_handler(ext, app.id);
|
||||
if (!handler)
|
||||
return false;
|
||||
|
||||
const QString abs = QFileInfo(path).absoluteFilePath();
|
||||
const wstring wpath = QDir::toNativeSeparators(abs).toStdWString();
|
||||
IShellItem *item = nullptr;
|
||||
HRESULT hr = SHCreateItemFromParsingName(
|
||||
wpath.c_str(), nullptr, IID_PPV_ARGS(&item));
|
||||
if (FAILED(hr) || !item) {
|
||||
handler->Release();
|
||||
return false;
|
||||
}
|
||||
IDataObject *data = nullptr;
|
||||
hr = item->BindToHandler(nullptr, BHID_DataObject, IID_PPV_ARGS(&data));
|
||||
item->Release();
|
||||
if (FAILED(hr) || !data) {
|
||||
handler->Release();
|
||||
return false;
|
||||
}
|
||||
hr = handler->Invoke(data);
|
||||
data->Release();
|
||||
handler->Release();
|
||||
return SUCCEEDED(hr);
|
||||
}
|
||||
|
||||
void
|
||||
set_last_used(const Handler &, const QString &)
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// assoc.hpp: native Open With handlers for a file path
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
// Open With entry. Not named App: that is the process object in app.hpp.
|
||||
struct Handler {
|
||||
QString id;
|
||||
QString name;
|
||||
QString icon;
|
||||
};
|
||||
|
||||
Handler default_for(const QString &path);
|
||||
std::vector<Handler> recommended_for(const QString &path);
|
||||
std::vector<Handler> fallback_for(const QString &path);
|
||||
bool launch(const Handler &app, const QString &path);
|
||||
void set_last_used(const Handler &app, const QString &path);
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,481 @@
|
||||
# the 1931 color matching functions at 1nm interval
|
||||
#
|
||||
# the data is from
|
||||
# Wyszecki & Stiles
|
||||
# Color Science, 2nd edition.
|
||||
# Table I(3.3.1) pp. 723-735.
|
||||
#
|
||||
# It was downloaded from http://www.cvrl.org/
|
||||
|
||||
Wavelength,x,y,z
|
||||
360,0.000129900000,0.000003917000,0.000606100000
|
||||
361,0.000145847000,0.000004393581,0.000680879200
|
||||
362,0.000163802100,0.000004929604,0.000765145600
|
||||
363,0.000184003700,0.000005532136,0.000860012400
|
||||
364,0.000206690200,0.000006208245,0.000966592800
|
||||
365,0.000232100000,0.000006965000,0.001086000000
|
||||
366,0.000260728000,0.000007813219,0.001220586000
|
||||
367,0.000293075000,0.000008767336,0.001372729000
|
||||
368,0.000329388000,0.000009839844,0.001543579000
|
||||
369,0.000369914000,0.000011043230,0.001734286000
|
||||
370,0.000414900000,0.000012390000,0.001946000000
|
||||
371,0.000464158700,0.000013886410,0.002177777000
|
||||
372,0.000518986000,0.000015557280,0.002435809000
|
||||
373,0.000581854000,0.000017442960,0.002731953000
|
||||
374,0.000655234700,0.000019583750,0.003078064000
|
||||
375,0.000741600000,0.000022020000,0.003486000000
|
||||
376,0.000845029600,0.000024839650,0.003975227000
|
||||
377,0.000964526800,0.000028041260,0.004540880000
|
||||
378,0.001094949000,0.000031531040,0.005158320000
|
||||
379,0.001231154000,0.000035215210,0.005802907000
|
||||
380,0.001368000000,0.000039000000,0.006450001000
|
||||
381,0.001502050000,0.000042826400,0.007083216000
|
||||
382,0.001642328000,0.000046914600,0.007745488000
|
||||
383,0.001802382000,0.000051589600,0.008501152000
|
||||
384,0.001995757000,0.000057176400,0.009414544000
|
||||
385,0.002236000000,0.000064000000,0.010549990000
|
||||
386,0.002535385000,0.000072344210,0.011965800000
|
||||
387,0.002892603000,0.000082212240,0.013655870000
|
||||
388,0.003300829000,0.000093508160,0.015588050000
|
||||
389,0.003753236000,0.000106136100,0.017730150000
|
||||
390,0.004243000000,0.000120000000,0.020050010000
|
||||
391,0.004762389000,0.000134984000,0.022511360000
|
||||
392,0.005330048000,0.000151492000,0.025202880000
|
||||
393,0.005978712000,0.000170208000,0.028279720000
|
||||
394,0.006741117000,0.000191816000,0.031897040000
|
||||
395,0.007650000000,0.000217000000,0.036210000000
|
||||
396,0.008751373000,0.000246906700,0.041437710000
|
||||
397,0.010028880000,0.000281240000,0.047503720000
|
||||
398,0.011421700000,0.000318520000,0.054119880000
|
||||
399,0.012869010000,0.000357266700,0.060998030000
|
||||
400,0.014310000000,0.000396000000,0.067850010000
|
||||
401,0.015704430000,0.000433714700,0.074486320000
|
||||
402,0.017147440000,0.000473024000,0.081361560000
|
||||
403,0.018781220000,0.000517876000,0.089153640000
|
||||
404,0.020748010000,0.000572218700,0.098540480000
|
||||
405,0.023190000000,0.000640000000,0.110200000000
|
||||
406,0.026207360000,0.000724560000,0.124613300000
|
||||
407,0.029782480000,0.000825500000,0.141701700000
|
||||
408,0.033880920000,0.000941160000,0.161303500000
|
||||
409,0.038468240000,0.001069880000,0.183256800000
|
||||
410,0.043510000000,0.001210000000,0.207400000000
|
||||
411,0.048995600000,0.001362091000,0.233692100000
|
||||
412,0.055022600000,0.001530752000,0.262611400000
|
||||
413,0.061718800000,0.001720368000,0.294774600000
|
||||
414,0.069212000000,0.001935323000,0.330798500000
|
||||
415,0.077630000000,0.002180000000,0.371300000000
|
||||
416,0.086958110000,0.002454800000,0.416209100000
|
||||
417,0.097176720000,0.002764000000,0.465464200000
|
||||
418,0.108406300000,0.003117800000,0.519694800000
|
||||
419,0.120767200000,0.003526400000,0.579530300000
|
||||
420,0.134380000000,0.004000000000,0.645600000000
|
||||
421,0.149358200000,0.004546240000,0.718483800000
|
||||
422,0.165395700000,0.005159320000,0.796713300000
|
||||
423,0.181983100000,0.005829280000,0.877845900000
|
||||
424,0.198611000000,0.006546160000,0.959439000000
|
||||
425,0.214770000000,0.007300000000,1.039050100000
|
||||
426,0.230186800000,0.008086507000,1.115367300000
|
||||
427,0.244879700000,0.008908720000,1.188497100000
|
||||
428,0.258777300000,0.009767680000,1.258123300000
|
||||
429,0.271807900000,0.010664430000,1.323929600000
|
||||
430,0.283900000000,0.011600000000,1.385600000000
|
||||
431,0.294943800000,0.012573170000,1.442635200000
|
||||
432,0.304896500000,0.013582720000,1.494803500000
|
||||
433,0.313787300000,0.014629680000,1.542190300000
|
||||
434,0.321645400000,0.015715090000,1.584880700000
|
||||
435,0.328500000000,0.016840000000,1.622960000000
|
||||
436,0.334351300000,0.018007360000,1.656404800000
|
||||
437,0.339210100000,0.019214480000,1.685295900000
|
||||
438,0.343121300000,0.020453920000,1.709874500000
|
||||
439,0.346129600000,0.021718240000,1.730382100000
|
||||
440,0.348280000000,0.023000000000,1.747060000000
|
||||
441,0.349599900000,0.024294610000,1.760044600000
|
||||
442,0.350147400000,0.025610240000,1.769623300000
|
||||
443,0.350013000000,0.026958570000,1.776263700000
|
||||
444,0.349287000000,0.028351250000,1.780433400000
|
||||
445,0.348060000000,0.029800000000,1.782600000000
|
||||
446,0.346373300000,0.031310830000,1.782968200000
|
||||
447,0.344262400000,0.032883680000,1.781699800000
|
||||
448,0.341808800000,0.034521120000,1.779198200000
|
||||
449,0.339094100000,0.036225710000,1.775867100000
|
||||
450,0.336200000000,0.038000000000,1.772110000000
|
||||
451,0.333197700000,0.039846670000,1.768258900000
|
||||
452,0.330041100000,0.041768000000,1.764039000000
|
||||
453,0.326635700000,0.043766000000,1.758943800000
|
||||
454,0.322886800000,0.045842670000,1.752466300000
|
||||
455,0.318700000000,0.048000000000,1.744100000000
|
||||
456,0.314025100000,0.050243680000,1.733559500000
|
||||
457,0.308884000000,0.052573040000,1.720858100000
|
||||
458,0.303290400000,0.054980560000,1.705936900000
|
||||
459,0.297257900000,0.057458720000,1.688737200000
|
||||
460,0.290800000000,0.060000000000,1.669200000000
|
||||
461,0.283970100000,0.062601970000,1.647528700000
|
||||
462,0.276721400000,0.065277520000,1.623412700000
|
||||
463,0.268917800000,0.068042080000,1.596022300000
|
||||
464,0.260422700000,0.070911090000,1.564528000000
|
||||
465,0.251100000000,0.073900000000,1.528100000000
|
||||
466,0.240847500000,0.077016000000,1.486111400000
|
||||
467,0.229851200000,0.080266400000,1.439521500000
|
||||
468,0.218407200000,0.083666800000,1.389879900000
|
||||
469,0.206811500000,0.087232800000,1.338736200000
|
||||
470,0.195360000000,0.090980000000,1.287640000000
|
||||
471,0.184213600000,0.094917550000,1.237422300000
|
||||
472,0.173327300000,0.099045840000,1.187824300000
|
||||
473,0.162688100000,0.103367400000,1.138761100000
|
||||
474,0.152283300000,0.107884600000,1.090148000000
|
||||
475,0.142100000000,0.112600000000,1.041900000000
|
||||
476,0.132178600000,0.117532000000,0.994197600000
|
||||
477,0.122569600000,0.122674400000,0.947347300000
|
||||
478,0.113275200000,0.127992800000,0.901453100000
|
||||
479,0.104297900000,0.133452800000,0.856619300000
|
||||
480,0.095640000000,0.139020000000,0.812950100000
|
||||
481,0.087299550000,0.144676400000,0.770517300000
|
||||
482,0.079308040000,0.150469300000,0.729444800000
|
||||
483,0.071717760000,0.156461900000,0.689913600000
|
||||
484,0.064580990000,0.162717700000,0.652104900000
|
||||
485,0.057950010000,0.169300000000,0.616200000000
|
||||
486,0.051862110000,0.176243100000,0.582328600000
|
||||
487,0.046281520000,0.183558100000,0.550416200000
|
||||
488,0.041150880000,0.191273500000,0.520337600000
|
||||
489,0.036412830000,0.199418000000,0.491967300000
|
||||
490,0.032010000000,0.208020000000,0.465180000000
|
||||
491,0.027917200000,0.217119900000,0.439924600000
|
||||
492,0.024144400000,0.226734500000,0.416183600000
|
||||
493,0.020687000000,0.236857100000,0.393882200000
|
||||
494,0.017540400000,0.247481200000,0.372945900000
|
||||
495,0.014700000000,0.258600000000,0.353300000000
|
||||
496,0.012161790000,0.270184900000,0.334857800000
|
||||
497,0.009919960000,0.282293900000,0.317552100000
|
||||
498,0.007967240000,0.295050500000,0.301337500000
|
||||
499,0.006296346000,0.308578000000,0.286168600000
|
||||
500,0.004900000000,0.323000000000,0.272000000000
|
||||
501,0.003777173000,0.338402100000,0.258817100000
|
||||
502,0.002945320000,0.354685800000,0.246483800000
|
||||
503,0.002424880000,0.371698600000,0.234771800000
|
||||
504,0.002236293000,0.389287500000,0.223453300000
|
||||
505,0.002400000000,0.407300000000,0.212300000000
|
||||
506,0.002925520000,0.425629900000,0.201169200000
|
||||
507,0.003836560000,0.444309600000,0.190119600000
|
||||
508,0.005174840000,0.463394400000,0.179225400000
|
||||
509,0.006982080000,0.482939500000,0.168560800000
|
||||
510,0.009300000000,0.503000000000,0.158200000000
|
||||
511,0.012149490000,0.523569300000,0.148138300000
|
||||
512,0.015535880000,0.544512000000,0.138375800000
|
||||
513,0.019477520000,0.565690000000,0.128994200000
|
||||
514,0.023992770000,0.586965300000,0.120075100000
|
||||
515,0.029100000000,0.608200000000,0.111700000000
|
||||
516,0.034814850000,0.629345600000,0.103904800000
|
||||
517,0.041120160000,0.650306800000,0.096667480000
|
||||
518,0.047985040000,0.670875200000,0.089982720000
|
||||
519,0.055378610000,0.690842400000,0.083845310000
|
||||
520,0.063270000000,0.710000000000,0.078249990000
|
||||
521,0.071635010000,0.728185200000,0.073208990000
|
||||
522,0.080462240000,0.745463600000,0.068678160000
|
||||
523,0.089739960000,0.761969400000,0.064567840000
|
||||
524,0.099456450000,0.777836800000,0.060788350000
|
||||
525,0.109600000000,0.793200000000,0.057250010000
|
||||
526,0.120167400000,0.808110400000,0.053904350000
|
||||
527,0.131114500000,0.822496200000,0.050746640000
|
||||
528,0.142367900000,0.836306800000,0.047752760000
|
||||
529,0.153854200000,0.849491600000,0.044898590000
|
||||
530,0.165500000000,0.862000000000,0.042160000000
|
||||
531,0.177257100000,0.873810800000,0.039507280000
|
||||
532,0.189140000000,0.884962400000,0.036935640000
|
||||
533,0.201169400000,0.895493600000,0.034458360000
|
||||
534,0.213365800000,0.905443200000,0.032088720000
|
||||
535,0.225749900000,0.914850100000,0.029840000000
|
||||
536,0.238320900000,0.923734800000,0.027711810000
|
||||
537,0.251066800000,0.932092400000,0.025694440000
|
||||
538,0.263992200000,0.939922600000,0.023787160000
|
||||
539,0.277101700000,0.947225200000,0.021989250000
|
||||
540,0.290400000000,0.954000000000,0.020300000000
|
||||
541,0.303891200000,0.960256100000,0.018718050000
|
||||
542,0.317572600000,0.966007400000,0.017240360000
|
||||
543,0.331438400000,0.971260600000,0.015863640000
|
||||
544,0.345482800000,0.976022500000,0.014584610000
|
||||
545,0.359700000000,0.980300000000,0.013400000000
|
||||
546,0.374083900000,0.984092400000,0.012307230000
|
||||
547,0.388639600000,0.987418200000,0.011301880000
|
||||
548,0.403378400000,0.990312800000,0.010377920000
|
||||
549,0.418311500000,0.992811600000,0.009529306000
|
||||
550,0.433449900000,0.994950100000,0.008749999000
|
||||
551,0.448795300000,0.996710800000,0.008035200000
|
||||
552,0.464336000000,0.998098300000,0.007381600000
|
||||
553,0.480064000000,0.999112000000,0.006785400000
|
||||
554,0.495971300000,0.999748200000,0.006242800000
|
||||
555,0.512050100000,1.000000000000,0.005749999000
|
||||
556,0.528295900000,0.999856700000,0.005303600000
|
||||
557,0.544691600000,0.999304600000,0.004899800000
|
||||
558,0.561209400000,0.998325500000,0.004534200000
|
||||
559,0.577821500000,0.996898700000,0.004202400000
|
||||
560,0.594500000000,0.995000000000,0.003900000000
|
||||
561,0.611220900000,0.992600500000,0.003623200000
|
||||
562,0.627975800000,0.989742600000,0.003370600000
|
||||
563,0.644760200000,0.986444400000,0.003141400000
|
||||
564,0.661569700000,0.982724100000,0.002934800000
|
||||
565,0.678400000000,0.978600000000,0.002749999000
|
||||
566,0.695239200000,0.974083700000,0.002585200000
|
||||
567,0.712058600000,0.969171200000,0.002438600000
|
||||
568,0.728828400000,0.963856800000,0.002309400000
|
||||
569,0.745518800000,0.958134900000,0.002196800000
|
||||
570,0.762100000000,0.952000000000,0.002100000000
|
||||
571,0.778543200000,0.945450400000,0.002017733000
|
||||
572,0.794825600000,0.938499200000,0.001948200000
|
||||
573,0.810926400000,0.931162800000,0.001889800000
|
||||
574,0.826824800000,0.923457600000,0.001840933000
|
||||
575,0.842500000000,0.915400000000,0.001800000000
|
||||
576,0.857932500000,0.907006400000,0.001766267000
|
||||
577,0.873081600000,0.898277200000,0.001737800000
|
||||
578,0.887894400000,0.889204800000,0.001711200000
|
||||
579,0.902318100000,0.879781600000,0.001683067000
|
||||
580,0.916300000000,0.870000000000,0.001650001000
|
||||
581,0.929799500000,0.859861300000,0.001610133000
|
||||
582,0.942798400000,0.849392000000,0.001564400000
|
||||
583,0.955277600000,0.838622000000,0.001513600000
|
||||
584,0.967217900000,0.827581300000,0.001458533000
|
||||
585,0.978600000000,0.816300000000,0.001400000000
|
||||
586,0.989385600000,0.804794700000,0.001336667000
|
||||
587,0.999548800000,0.793082000000,0.001270000000
|
||||
588,1.009089200000,0.781192000000,0.001205000000
|
||||
589,1.018006400000,0.769154700000,0.001146667000
|
||||
590,1.026300000000,0.757000000000,0.001100000000
|
||||
591,1.033982700000,0.744754100000,0.001068800000
|
||||
592,1.040986000000,0.732422400000,0.001049400000
|
||||
593,1.047188000000,0.720003600000,0.001035600000
|
||||
594,1.052466700000,0.707496500000,0.001021200000
|
||||
595,1.056700000000,0.694900000000,0.001000000000
|
||||
596,1.059794400000,0.682219200000,0.000968640000
|
||||
597,1.061799200000,0.669471600000,0.000929920000
|
||||
598,1.062806800000,0.656674400000,0.000886880000
|
||||
599,1.062909600000,0.643844800000,0.000842560000
|
||||
600,1.062200000000,0.631000000000,0.000800000000
|
||||
601,1.060735200000,0.618155500000,0.000760960000
|
||||
602,1.058443600000,0.605314400000,0.000723680000
|
||||
603,1.055224400000,0.592475600000,0.000685920000
|
||||
604,1.050976800000,0.579637900000,0.000645440000
|
||||
605,1.045600000000,0.566800000000,0.000600000000
|
||||
606,1.039036900000,0.553961100000,0.000547866700
|
||||
607,1.031360800000,0.541137200000,0.000491600000
|
||||
608,1.022666200000,0.528352800000,0.000435400000
|
||||
609,1.013047700000,0.515632300000,0.000383466700
|
||||
610,1.002600000000,0.503000000000,0.000340000000
|
||||
611,0.991367500000,0.490468800000,0.000307253300
|
||||
612,0.979331400000,0.478030400000,0.000283160000
|
||||
613,0.966491600000,0.465677600000,0.000265440000
|
||||
614,0.952847900000,0.453403200000,0.000251813300
|
||||
615,0.938400000000,0.441200000000,0.000240000000
|
||||
616,0.923194000000,0.429080000000,0.000229546700
|
||||
617,0.907244000000,0.417036000000,0.000220640000
|
||||
618,0.890502000000,0.405032000000,0.000211960000
|
||||
619,0.872920000000,0.393032000000,0.000202186700
|
||||
620,0.854449900000,0.381000000000,0.000190000000
|
||||
621,0.835084000000,0.368918400000,0.000174213300
|
||||
622,0.814946000000,0.356827200000,0.000155640000
|
||||
623,0.794186000000,0.344776800000,0.000135960000
|
||||
624,0.772954000000,0.332817600000,0.000116853300
|
||||
625,0.751400000000,0.321000000000,0.000100000000
|
||||
626,0.729583600000,0.309338100000,0.000086133330
|
||||
627,0.707588800000,0.297850400000,0.000074600000
|
||||
628,0.685602200000,0.286593600000,0.000065000000
|
||||
629,0.663810400000,0.275624500000,0.000056933330
|
||||
630,0.642400000000,0.265000000000,0.000049999990
|
||||
631,0.621514900000,0.254763200000,0.000044160000
|
||||
632,0.601113800000,0.244889600000,0.000039480000
|
||||
633,0.581105200000,0.235334400000,0.000035720000
|
||||
634,0.561397700000,0.226052800000,0.000032640000
|
||||
635,0.541900000000,0.217000000000,0.000030000000
|
||||
636,0.522599500000,0.208161600000,0.000027653330
|
||||
637,0.503546400000,0.199548800000,0.000025560000
|
||||
638,0.484743600000,0.191155200000,0.000023640000
|
||||
639,0.466193900000,0.182974400000,0.000021813330
|
||||
640,0.447900000000,0.175000000000,0.000020000000
|
||||
641,0.429861300000,0.167223500000,0.000018133330
|
||||
642,0.412098000000,0.159646400000,0.000016200000
|
||||
643,0.394644000000,0.152277600000,0.000014200000
|
||||
644,0.377533300000,0.145125900000,0.000012133330
|
||||
645,0.360800000000,0.138200000000,0.000010000000
|
||||
646,0.344456300000,0.131500300000,0.000007733333
|
||||
647,0.328516800000,0.125024800000,0.000005400000
|
||||
648,0.313019200000,0.118779200000,0.000003200000
|
||||
649,0.298001100000,0.112769100000,0.000001333333
|
||||
650,0.283500000000,0.107000000000,0.000000000000
|
||||
651,0.269544800000,0.101476200000,0.000000000000
|
||||
652,0.256118400000,0.096188640000,0.000000000000
|
||||
653,0.243189600000,0.091122960000,0.000000000000
|
||||
654,0.230727200000,0.086264850000,0.000000000000
|
||||
655,0.218700000000,0.081600000000,0.000000000000
|
||||
656,0.207097100000,0.077120640000,0.000000000000
|
||||
657,0.195923200000,0.072825520000,0.000000000000
|
||||
658,0.185170800000,0.068710080000,0.000000000000
|
||||
659,0.174832300000,0.064769760000,0.000000000000
|
||||
660,0.164900000000,0.061000000000,0.000000000000
|
||||
661,0.155366700000,0.057396210000,0.000000000000
|
||||
662,0.146230000000,0.053955040000,0.000000000000
|
||||
663,0.137490000000,0.050673760000,0.000000000000
|
||||
664,0.129146700000,0.047549650000,0.000000000000
|
||||
665,0.121200000000,0.044580000000,0.000000000000
|
||||
666,0.113639700000,0.041758720000,0.000000000000
|
||||
667,0.106465000000,0.039084960000,0.000000000000
|
||||
668,0.099690440000,0.036563840000,0.000000000000
|
||||
669,0.093330610000,0.034200480000,0.000000000000
|
||||
670,0.087400000000,0.032000000000,0.000000000000
|
||||
671,0.081900960000,0.029962610000,0.000000000000
|
||||
672,0.076804280000,0.028076640000,0.000000000000
|
||||
673,0.072077120000,0.026329360000,0.000000000000
|
||||
674,0.067686640000,0.024708050000,0.000000000000
|
||||
675,0.063600000000,0.023200000000,0.000000000000
|
||||
676,0.059806850000,0.021800770000,0.000000000000
|
||||
677,0.056282160000,0.020501120000,0.000000000000
|
||||
678,0.052971040000,0.019281080000,0.000000000000
|
||||
679,0.049818610000,0.018120690000,0.000000000000
|
||||
680,0.046770000000,0.017000000000,0.000000000000
|
||||
681,0.043784050000,0.015903790000,0.000000000000
|
||||
682,0.040875360000,0.014837180000,0.000000000000
|
||||
683,0.038072640000,0.013810680000,0.000000000000
|
||||
684,0.035404610000,0.012834780000,0.000000000000
|
||||
685,0.032900000000,0.011920000000,0.000000000000
|
||||
686,0.030564190000,0.011068310000,0.000000000000
|
||||
687,0.028380560000,0.010273390000,0.000000000000
|
||||
688,0.026344840000,0.009533311000,0.000000000000
|
||||
689,0.024452750000,0.008846157000,0.000000000000
|
||||
690,0.022700000000,0.008210000000,0.000000000000
|
||||
691,0.021084290000,0.007623781000,0.000000000000
|
||||
692,0.019599880000,0.007085424000,0.000000000000
|
||||
693,0.018237320000,0.006591476000,0.000000000000
|
||||
694,0.016987170000,0.006138485000,0.000000000000
|
||||
695,0.015840000000,0.005723000000,0.000000000000
|
||||
696,0.014790640000,0.005343059000,0.000000000000
|
||||
697,0.013831320000,0.004995796000,0.000000000000
|
||||
698,0.012948680000,0.004676404000,0.000000000000
|
||||
699,0.012129200000,0.004380075000,0.000000000000
|
||||
700,0.011359160000,0.004102000000,0.000000000000
|
||||
701,0.010629350000,0.003838453000,0.000000000000
|
||||
702,0.009938846000,0.003589099000,0.000000000000
|
||||
703,0.009288422000,0.003354219000,0.000000000000
|
||||
704,0.008678854000,0.003134093000,0.000000000000
|
||||
705,0.008110916000,0.002929000000,0.000000000000
|
||||
706,0.007582388000,0.002738139000,0.000000000000
|
||||
707,0.007088746000,0.002559876000,0.000000000000
|
||||
708,0.006627313000,0.002393244000,0.000000000000
|
||||
709,0.006195408000,0.002237275000,0.000000000000
|
||||
710,0.005790346000,0.002091000000,0.000000000000
|
||||
711,0.005409826000,0.001953587000,0.000000000000
|
||||
712,0.005052583000,0.001824580000,0.000000000000
|
||||
713,0.004717512000,0.001703580000,0.000000000000
|
||||
714,0.004403507000,0.001590187000,0.000000000000
|
||||
715,0.004109457000,0.001484000000,0.000000000000
|
||||
716,0.003833913000,0.001384496000,0.000000000000
|
||||
717,0.003575748000,0.001291268000,0.000000000000
|
||||
718,0.003334342000,0.001204092000,0.000000000000
|
||||
719,0.003109075000,0.001122744000,0.000000000000
|
||||
720,0.002899327000,0.001047000000,0.000000000000
|
||||
721,0.002704348000,0.000976589600,0.000000000000
|
||||
722,0.002523020000,0.000911108800,0.000000000000
|
||||
723,0.002354168000,0.000850133200,0.000000000000
|
||||
724,0.002196616000,0.000793238400,0.000000000000
|
||||
725,0.002049190000,0.000740000000,0.000000000000
|
||||
726,0.001910960000,0.000690082700,0.000000000000
|
||||
727,0.001781438000,0.000643310000,0.000000000000
|
||||
728,0.001660110000,0.000599496000,0.000000000000
|
||||
729,0.001546459000,0.000558454700,0.000000000000
|
||||
730,0.001439971000,0.000520000000,0.000000000000
|
||||
731,0.001340042000,0.000483913600,0.000000000000
|
||||
732,0.001246275000,0.000450052800,0.000000000000
|
||||
733,0.001158471000,0.000418345200,0.000000000000
|
||||
734,0.001076430000,0.000388718400,0.000000000000
|
||||
735,0.000999949300,0.000361100000,0.000000000000
|
||||
736,0.000928735800,0.000335383500,0.000000000000
|
||||
737,0.000862433200,0.000311440400,0.000000000000
|
||||
738,0.000800750300,0.000289165600,0.000000000000
|
||||
739,0.000743396000,0.000268453900,0.000000000000
|
||||
740,0.000690078600,0.000249200000,0.000000000000
|
||||
741,0.000640515600,0.000231301900,0.000000000000
|
||||
742,0.000594502100,0.000214685600,0.000000000000
|
||||
743,0.000551864600,0.000199288400,0.000000000000
|
||||
744,0.000512429000,0.000185047500,0.000000000000
|
||||
745,0.000476021300,0.000171900000,0.000000000000
|
||||
746,0.000442453600,0.000159778100,0.000000000000
|
||||
747,0.000411511700,0.000148604400,0.000000000000
|
||||
748,0.000382981400,0.000138301600,0.000000000000
|
||||
749,0.000356649100,0.000128792500,0.000000000000
|
||||
750,0.000332301100,0.000120000000,0.000000000000
|
||||
751,0.000309758600,0.000111859500,0.000000000000
|
||||
752,0.000288887100,0.000104322400,0.000000000000
|
||||
753,0.000269539400,0.000097335600,0.000000000000
|
||||
754,0.000251568200,0.000090845870,0.000000000000
|
||||
755,0.000234826100,0.000084800000,0.000000000000
|
||||
756,0.000219171000,0.000079146670,0.000000000000
|
||||
757,0.000204525800,0.000073858000,0.000000000000
|
||||
758,0.000190840500,0.000068916000,0.000000000000
|
||||
759,0.000178065400,0.000064302670,0.000000000000
|
||||
760,0.000166150500,0.000060000000,0.000000000000
|
||||
761,0.000155023600,0.000055981870,0.000000000000
|
||||
762,0.000144621900,0.000052225600,0.000000000000
|
||||
763,0.000134909800,0.000048718400,0.000000000000
|
||||
764,0.000125852000,0.000045447470,0.000000000000
|
||||
765,0.000117413000,0.000042400000,0.000000000000
|
||||
766,0.000109551500,0.000039561040,0.000000000000
|
||||
767,0.000102224500,0.000036915120,0.000000000000
|
||||
768,0.000095394450,0.000034448680,0.000000000000
|
||||
769,0.000089023900,0.000032148160,0.000000000000
|
||||
770,0.000083075270,0.000030000000,0.000000000000
|
||||
771,0.000077512690,0.000027991250,0.000000000000
|
||||
772,0.000072313040,0.000026113560,0.000000000000
|
||||
773,0.000067457780,0.000024360240,0.000000000000
|
||||
774,0.000062928440,0.000022724610,0.000000000000
|
||||
775,0.000058706520,0.000021200000,0.000000000000
|
||||
776,0.000054770280,0.000019778550,0.000000000000
|
||||
777,0.000051099180,0.000018452850,0.000000000000
|
||||
778,0.000047676540,0.000017216870,0.000000000000
|
||||
779,0.000044485670,0.000016064590,0.000000000000
|
||||
780,0.000041509940,0.000014990000,0.000000000000
|
||||
781,0.000038733240,0.000013987280,0.000000000000
|
||||
782,0.000036142030,0.000013051550,0.000000000000
|
||||
783,0.000033723520,0.000012178180,0.000000000000
|
||||
784,0.000031464870,0.000011362540,0.000000000000
|
||||
785,0.000029353260,0.000010600000,0.000000000000
|
||||
786,0.000027375730,0.000009885877,0.000000000000
|
||||
787,0.000025524330,0.000009217304,0.000000000000
|
||||
788,0.000023793760,0.000008592362,0.000000000000
|
||||
789,0.000022178700,0.000008009133,0.000000000000
|
||||
790,0.000020673830,0.000007465700,0.000000000000
|
||||
791,0.000019272260,0.000006959567,0.000000000000
|
||||
792,0.000017966400,0.000006487995,0.000000000000
|
||||
793,0.000016749910,0.000006048699,0.000000000000
|
||||
794,0.000015616480,0.000005639396,0.000000000000
|
||||
795,0.000014559770,0.000005257800,0.000000000000
|
||||
796,0.000013573870,0.000004901771,0.000000000000
|
||||
797,0.000012654360,0.000004569720,0.000000000000
|
||||
798,0.000011797230,0.000004260194,0.000000000000
|
||||
799,0.000010998440,0.000003971739,0.000000000000
|
||||
800,0.000010253980,0.000003702900,0.000000000000
|
||||
801,0.000009559646,0.000003452163,0.000000000000
|
||||
802,0.000008912044,0.000003218302,0.000000000000
|
||||
803,0.000008308358,0.000003000300,0.000000000000
|
||||
804,0.000007745769,0.000002797139,0.000000000000
|
||||
805,0.000007221456,0.000002607800,0.000000000000
|
||||
806,0.000006732475,0.000002431220,0.000000000000
|
||||
807,0.000006276423,0.000002266531,0.000000000000
|
||||
808,0.000005851304,0.000002113013,0.000000000000
|
||||
809,0.000005455118,0.000001969943,0.000000000000
|
||||
810,0.000005085868,0.000001836600,0.000000000000
|
||||
811,0.000004741466,0.000001712230,0.000000000000
|
||||
812,0.000004420236,0.000001596228,0.000000000000
|
||||
813,0.000004120783,0.000001488090,0.000000000000
|
||||
814,0.000003841716,0.000001387314,0.000000000000
|
||||
815,0.000003581652,0.000001293400,0.000000000000
|
||||
816,0.000003339127,0.000001205820,0.000000000000
|
||||
817,0.000003112949,0.000001124143,0.000000000000
|
||||
818,0.000002902121,0.000001048009,0.000000000000
|
||||
819,0.000002705645,0.000000977058,0.000000000000
|
||||
820,0.000002522525,0.000000910930,0.000000000000
|
||||
821,0.000002351726,0.000000849251,0.000000000000
|
||||
822,0.000002192415,0.000000791721,0.000000000000
|
||||
823,0.000002043902,0.000000738090,0.000000000000
|
||||
824,0.000001905497,0.000000688110,0.000000000000
|
||||
825,0.000001776509,0.000000641530,0.000000000000
|
||||
826,0.000001656215,0.000000598090,0.000000000000
|
||||
827,0.000001544022,0.000000557575,0.000000000000
|
||||
828,0.000001439440,0.000000519808,0.000000000000
|
||||
829,0.000001341977,0.000000484612,0.000000000000
|
||||
830,0.000001251141,0.000000451810,0.000000000000
|
||||
|
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# cmf-to-header.awk: convert CIE 1931 CMF CSV (wavelength,X,Y,Z) to a C++ header
|
||||
#
|
||||
# Copyright The Dawn Authors
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
|
||||
BEGIN {
|
||||
if (ARGC < 3) {
|
||||
print "usage: awk -f cmf-to-header.awk in.csv out.h" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
infile = ARGV[1]
|
||||
outfile = ARGV[2]
|
||||
ARGV[2] = ""
|
||||
rows = 0
|
||||
}
|
||||
|
||||
{
|
||||
gsub(/\r/, "")
|
||||
}
|
||||
|
||||
/^#/ { next }
|
||||
/^Wavelength/ { next }
|
||||
NF == 0 { next }
|
||||
|
||||
{
|
||||
n = split($0, a, ",")
|
||||
if (n < 4)
|
||||
next
|
||||
if (rows == 0) {
|
||||
base = infile
|
||||
sub(/^.*\//, "", base)
|
||||
print "// Generated from " base > outfile
|
||||
print "constexpr double kCmf[][3] = {" > outfile
|
||||
}
|
||||
printf "\t{%s, %s, %s},\n", a[2], a[3], a[4] > outfile
|
||||
++rows
|
||||
}
|
||||
|
||||
END {
|
||||
if (rows == 0) {
|
||||
print "cmf-to-header.awk: no data rows" > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
print "};" > outfile
|
||||
print "constexpr int kCmfN = int(sizeof(kCmf) / sizeof(kCmf[0]));" > outfile
|
||||
printf "static_assert(kCmfN == %d);\n", rows > outfile
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
//
|
||||
// display-profile-linux.cpp: display ICC via a long-lived colord session
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "display-profile.hpp"
|
||||
|
||||
#include <colord.h>
|
||||
#include <gio/gio.h>
|
||||
#include <lcms2.h>
|
||||
|
||||
#include <QScreen>
|
||||
#include <QtLogging>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace fs = filesystem;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
optional<string>
|
||||
edid_md5_from_bytes(const vector<unsigned char> &edid)
|
||||
{
|
||||
if (edid.empty())
|
||||
return nullopt;
|
||||
g_autofree gchar *md5 =
|
||||
g_compute_checksum_for_data(G_CHECKSUM_MD5, edid.data(), edid.size());
|
||||
return md5 ? optional<string>(md5) : nullopt;
|
||||
}
|
||||
|
||||
optional<string>
|
||||
edid_md5_for_connector(const string &connector)
|
||||
{
|
||||
error_code error;
|
||||
for (const auto &entry : fs::directory_iterator("/sys/class/drm", error)) {
|
||||
if (!entry.is_directory())
|
||||
continue;
|
||||
const string dirname = entry.path().filename().string();
|
||||
const auto dash = dirname.find('-');
|
||||
if (dash == string::npos || dirname.substr(dash + 1) != connector)
|
||||
continue;
|
||||
ifstream input(entry.path() / "edid", ios::binary);
|
||||
if (!input)
|
||||
continue;
|
||||
vector<unsigned char> bytes(
|
||||
(istreambuf_iterator<char>(input)), istreambuf_iterator<char>());
|
||||
if (auto md5 = edid_md5_from_bytes(bytes))
|
||||
return md5;
|
||||
}
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
vector<unsigned char>
|
||||
profile_bytes(cmsHPROFILE profile)
|
||||
{
|
||||
cmsUInt32Number size = 0;
|
||||
if (!profile || !cmsSaveProfileToMem(profile, nullptr, &size) || !size)
|
||||
return {};
|
||||
vector<unsigned char> bytes(size);
|
||||
if (!cmsSaveProfileToMem(profile, bytes.data(), &size))
|
||||
return {};
|
||||
bytes.resize(size);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool
|
||||
display_device(CdDevice *device)
|
||||
{
|
||||
if (!device)
|
||||
return true;
|
||||
const CdDeviceKind kind = cd_device_get_kind(device);
|
||||
return kind == CD_DEVICE_KIND_UNKNOWN || kind == CD_DEVICE_KIND_DISPLAY;
|
||||
}
|
||||
|
||||
DisplayProfile
|
||||
load_from_client(CdClient *client, const QScreen *screen)
|
||||
{
|
||||
DisplayProfile result;
|
||||
if (!screen) {
|
||||
qWarning("display profile: Qt has not assigned a screen yet");
|
||||
return result;
|
||||
}
|
||||
if (!client || !cd_client_get_connected(client))
|
||||
return result;
|
||||
const string connector = screen->name().toStdString();
|
||||
if (connector.empty()) {
|
||||
qWarning("display profile: Qt screen has no connector name yet");
|
||||
return result;
|
||||
}
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
g_autoptr(GPtrArray) devices = cd_client_get_devices_by_kind_sync(
|
||||
client, CD_DEVICE_KIND_DISPLAY, nullptr, &error);
|
||||
if (!devices) {
|
||||
qWarning("colord: get display devices: %s",
|
||||
error ? error->message : "failed");
|
||||
return result;
|
||||
}
|
||||
|
||||
CdDevice *matched = nullptr;
|
||||
string method;
|
||||
for (guint i = 0; i < devices->len; ++i) {
|
||||
auto *device = static_cast<CdDevice *>(g_ptr_array_index(devices, i));
|
||||
if (!cd_device_connect_sync(device, nullptr, &error)) {
|
||||
g_clear_error(&error);
|
||||
continue;
|
||||
}
|
||||
const char *name =
|
||||
cd_device_get_metadata_item(device, CD_DEVICE_METADATA_XRANDR_NAME);
|
||||
if (name && connector == name) {
|
||||
matched = device;
|
||||
method = "XRANDR_name";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
if (auto edid_md5 = edid_md5_for_connector(connector)) {
|
||||
for (guint i = 0; i < devices->len; ++i) {
|
||||
auto *device =
|
||||
static_cast<CdDevice *>(g_ptr_array_index(devices, i));
|
||||
const char *md5 = cd_device_get_metadata_item(
|
||||
device, CD_DEVICE_METADATA_OUTPUT_EDID_MD5);
|
||||
if (md5 && *edid_md5 == md5) {
|
||||
matched = device;
|
||||
method = "OutputEdidMd5";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
qWarning("colord: no device for connector %s", connector.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
CdProfile *profile = cd_device_get_default_profile(matched);
|
||||
if (!profile || !cd_profile_connect_sync(profile, nullptr, &error)) {
|
||||
qWarning(
|
||||
"colord: display profile unavailable for %s", connector.c_str());
|
||||
return result;
|
||||
}
|
||||
CdIcc *icc =
|
||||
cd_profile_load_icc(profile, CD_ICC_LOAD_FLAGS_ALL, nullptr, &error);
|
||||
if (!icc) {
|
||||
qWarning("colord: load ICC: %s", error ? error->message : "failed");
|
||||
return result;
|
||||
}
|
||||
result.icc =
|
||||
profile_bytes(static_cast<cmsHPROFILE>(cd_icc_get_handle(icc)));
|
||||
g_object_unref(icc);
|
||||
if (result.icc.empty())
|
||||
return {};
|
||||
|
||||
result.source = "colord";
|
||||
const char *filename = cd_profile_get_filename(profile);
|
||||
const char *profile_id = cd_profile_get_id(profile);
|
||||
result.label = filename && *filename
|
||||
? filename
|
||||
: (profile_id && *profile_id ? profile_id : "colord");
|
||||
qInfo("ICC source: colord (connector=%s via %s, profile=%s)",
|
||||
connector.c_str(), method.c_str(), result.label.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
struct ColordSource final : DisplayProfileSource {
|
||||
CdClient *client = nullptr;
|
||||
guint name_watch = 0;
|
||||
bool signals_hooked = false;
|
||||
function<void()> on_change;
|
||||
|
||||
~ColordSource() override;
|
||||
void start(function<void()> fn) override;
|
||||
DisplayProfile load(QScreen *screen) override;
|
||||
void notify() const;
|
||||
void hook_signals();
|
||||
void watch_name();
|
||||
void connect_async();
|
||||
};
|
||||
|
||||
void
|
||||
on_device(CdClient *, CdDevice *device, gpointer data)
|
||||
{
|
||||
auto *src = static_cast<ColordSource *>(data);
|
||||
if (!display_device(device))
|
||||
return;
|
||||
src->notify();
|
||||
}
|
||||
|
||||
void
|
||||
on_changed(CdClient *, gpointer data)
|
||||
{
|
||||
static_cast<ColordSource *>(data)->notify();
|
||||
}
|
||||
|
||||
void
|
||||
on_profile(CdClient *, CdProfile *, gpointer data)
|
||||
{
|
||||
static_cast<ColordSource *>(data)->notify();
|
||||
}
|
||||
|
||||
void
|
||||
on_connect_ready(GObject *source, GAsyncResult *res, gpointer data)
|
||||
{
|
||||
auto *src = static_cast<ColordSource *>(data);
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!cd_client_connect_finish(CD_CLIENT(source), res, &error)) {
|
||||
qWarning("colord: connect: %s", error ? error->message : "failed");
|
||||
return;
|
||||
}
|
||||
src->hook_signals();
|
||||
src->notify();
|
||||
}
|
||||
|
||||
void
|
||||
on_name_appeared(GDBusConnection *, const gchar *, const gchar *, gpointer data)
|
||||
{
|
||||
auto *src = static_cast<ColordSource *>(data);
|
||||
if (!src->client || cd_client_get_connected(src->client))
|
||||
return;
|
||||
src->connect_async();
|
||||
}
|
||||
|
||||
void
|
||||
on_name_vanished(GDBusConnection *, const gchar *, gpointer)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
ColordSource::notify() const
|
||||
{
|
||||
if (this->on_change)
|
||||
this->on_change();
|
||||
}
|
||||
|
||||
void
|
||||
ColordSource::hook_signals()
|
||||
{
|
||||
if (this->signals_hooked || !this->client)
|
||||
return;
|
||||
this->signals_hooked = true;
|
||||
g_signal_connect(this->client, "device-added", G_CALLBACK(on_device), this);
|
||||
g_signal_connect(
|
||||
this->client, "device-removed", G_CALLBACK(on_device), this);
|
||||
g_signal_connect(
|
||||
this->client, "device-changed", G_CALLBACK(on_device), this);
|
||||
g_signal_connect(
|
||||
this->client, "profile-changed", G_CALLBACK(on_profile), this);
|
||||
g_signal_connect(this->client, "changed", G_CALLBACK(on_changed), this);
|
||||
}
|
||||
|
||||
void
|
||||
ColordSource::connect_async()
|
||||
{
|
||||
if (!this->client || cd_client_get_connected(this->client))
|
||||
return;
|
||||
cd_client_connect(this->client, nullptr, on_connect_ready, this);
|
||||
}
|
||||
|
||||
void
|
||||
ColordSource::watch_name()
|
||||
{
|
||||
if (this->name_watch)
|
||||
return;
|
||||
this->name_watch = g_bus_watch_name(G_BUS_TYPE_SYSTEM,
|
||||
"org.freedesktop.ColorManager", G_BUS_NAME_WATCHER_FLAGS_NONE,
|
||||
on_name_appeared, on_name_vanished, this, nullptr);
|
||||
}
|
||||
|
||||
ColordSource::~ColordSource()
|
||||
{
|
||||
if (this->name_watch)
|
||||
g_bus_unwatch_name(this->name_watch);
|
||||
if (this->client)
|
||||
g_object_unref(this->client);
|
||||
}
|
||||
|
||||
void
|
||||
ColordSource::start(function<void()> fn)
|
||||
{
|
||||
this->on_change = std::move(fn);
|
||||
if (this->client)
|
||||
return;
|
||||
this->client = cd_client_new();
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (cd_client_connect_sync(this->client, nullptr, &error)) {
|
||||
this->hook_signals();
|
||||
return;
|
||||
}
|
||||
qWarning("colord: connect: %s", error ? error->message : "failed");
|
||||
this->watch_name();
|
||||
}
|
||||
|
||||
DisplayProfile
|
||||
ColordSource::load(QScreen *screen)
|
||||
{
|
||||
return load_from_client(this->client, screen);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
unique_ptr<DisplayProfileSource>
|
||||
make_display_profile_source()
|
||||
{
|
||||
return make_unique<ColordSource>();
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// display-profile-macos.mm: display ICC via ColorSync
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "display-profile.hpp"
|
||||
|
||||
#include <QScreen>
|
||||
#include <QtGui/qscreen_platform.h>
|
||||
#include <QtLogging>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
DisplayProfile
|
||||
load_display_profile(QScreen *screen)
|
||||
{
|
||||
DisplayProfile result;
|
||||
if (!screen)
|
||||
return result;
|
||||
auto *native = screen->nativeInterface<QNativeInterface::QCocoaScreen>();
|
||||
NSScreen *native_screen = native ? native->nativeScreen() : nil;
|
||||
NSNumber *number = native_screen.deviceDescription[@"NSScreenNumber"];
|
||||
if (!number)
|
||||
return result;
|
||||
|
||||
CGColorSpaceRef color_space = CGDisplayCopyColorSpace(
|
||||
static_cast<CGDirectDisplayID>(number.unsignedIntValue));
|
||||
if (!color_space)
|
||||
return result;
|
||||
CFDataRef data = CGColorSpaceCopyICCData(color_space);
|
||||
CGColorSpaceRelease(color_space);
|
||||
if (!data)
|
||||
return result;
|
||||
const auto *bytes = CFDataGetBytePtr(data);
|
||||
const CFIndex size = CFDataGetLength(data);
|
||||
if (bytes && size > 0)
|
||||
result.icc.assign(bytes, bytes + size);
|
||||
CFRelease(data);
|
||||
if (result.icc.empty())
|
||||
return {};
|
||||
|
||||
result.source = "ColorSync";
|
||||
result.label = screen->name().toUtf8().toStdString();
|
||||
qInfo("ICC source: ColorSync (%s)", result.label.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
struct CocoaSource final : DisplayProfileSource {
|
||||
function<void()> on_change;
|
||||
id observer = nil;
|
||||
|
||||
~CocoaSource() override;
|
||||
void start(function<void()> fn) override;
|
||||
DisplayProfile load(QScreen *screen) override
|
||||
{
|
||||
return load_display_profile(screen);
|
||||
}
|
||||
};
|
||||
|
||||
CocoaSource::~CocoaSource()
|
||||
{
|
||||
if (!this->observer)
|
||||
return;
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:this->observer];
|
||||
[this->observer release];
|
||||
}
|
||||
|
||||
void
|
||||
CocoaSource::start(function<void()> fn)
|
||||
{
|
||||
this->on_change = std::move(fn);
|
||||
if (this->observer)
|
||||
return;
|
||||
this->observer = [[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:NSWindowDidChangeBackingPropertiesNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *) {
|
||||
this->on_change();
|
||||
}] retain];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
unique_ptr<DisplayProfileSource>
|
||||
make_display_profile_source()
|
||||
{
|
||||
return make_unique<CocoaSource>();
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// display-profile-windows.cpp: display ICC via Windows ICM
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "display-profile.hpp"
|
||||
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
#include <QScreen>
|
||||
#include <QWinEventNotifier>
|
||||
#include <QtGui/qscreen_platform.h>
|
||||
#include <QtLogging>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
// Monitor class GUID {4d36e96e-e325-11ce-bfc1-08002be10318}
|
||||
constexpr wchar_t kSystemClass[] =
|
||||
L"SYSTEM\\CurrentControlSet\\Control\\Class\\"
|
||||
L"{4d36e96e-e325-11ce-bfc1-08002be10318}";
|
||||
constexpr wchar_t kUserLeaf[] =
|
||||
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ICM\\"
|
||||
L"ProfileAssociations\\Display\\{4d36e96e-e325-11ce-bfc1-08002be10318}";
|
||||
constexpr wchar_t kUserParent[] =
|
||||
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ICM";
|
||||
|
||||
DisplayProfile
|
||||
load_display_profile(QScreen *screen)
|
||||
{
|
||||
DisplayProfile result;
|
||||
if (!screen)
|
||||
return result;
|
||||
auto *native = screen->nativeInterface<QNativeInterface::QWindowsScreen>();
|
||||
if (!native)
|
||||
return result;
|
||||
|
||||
MONITORINFOEXW monitor{};
|
||||
monitor.cbSize = sizeof(monitor);
|
||||
if (!GetMonitorInfoW(native->handle(), &monitor))
|
||||
return result;
|
||||
HDC dc = CreateDCW(L"DISPLAY", monitor.szDevice, nullptr, nullptr);
|
||||
if (!dc)
|
||||
return result;
|
||||
|
||||
DWORD length = 0;
|
||||
GetICMProfileW(dc, &length, nullptr);
|
||||
vector<wchar_t> path(length ? length : 1);
|
||||
const bool found = length && GetICMProfileW(dc, &length, path.data());
|
||||
DeleteDC(dc);
|
||||
if (!found)
|
||||
return result;
|
||||
|
||||
const QString filename = QString::fromWCharArray(path.data());
|
||||
QFile file(filename);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning("Windows ICM: cannot read %s", filename.toUtf8().constData());
|
||||
return result;
|
||||
}
|
||||
const QByteArray bytes = file.readAll();
|
||||
result.icc.assign(bytes.begin(), bytes.end());
|
||||
if (result.icc.empty())
|
||||
return {};
|
||||
result.source = "Windows ICM";
|
||||
result.label = filename.toUtf8().toStdString();
|
||||
qInfo("ICC source: Windows ICM (%s)", result.label.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Watch {
|
||||
HKEY key = nullptr;
|
||||
HANDLE event = nullptr;
|
||||
unique_ptr<QWinEventNotifier> notifier;
|
||||
|
||||
Watch() = default;
|
||||
Watch(const Watch &) = delete;
|
||||
Watch &operator=(const Watch &) = delete;
|
||||
~Watch() { this->close(); }
|
||||
void close();
|
||||
bool open(HKEY root, const wchar_t *path);
|
||||
bool arm();
|
||||
};
|
||||
|
||||
void
|
||||
Watch::close()
|
||||
{
|
||||
this->notifier.reset();
|
||||
if (this->event) {
|
||||
CloseHandle(this->event);
|
||||
this->event = nullptr;
|
||||
}
|
||||
if (this->key) {
|
||||
RegCloseKey(this->key);
|
||||
this->key = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Watch::open(HKEY root, const wchar_t *path)
|
||||
{
|
||||
if (RegOpenKeyExW(root, path, 0, KEY_NOTIFY | KEY_READ, &this->key) !=
|
||||
ERROR_SUCCESS)
|
||||
return false;
|
||||
this->event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
return this->event != nullptr;
|
||||
}
|
||||
|
||||
bool
|
||||
Watch::arm()
|
||||
{
|
||||
if (!this->key || !this->event)
|
||||
return false;
|
||||
return RegNotifyChangeKeyValue(this->key, TRUE,
|
||||
REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET |
|
||||
REG_NOTIFY_THREAD_AGNOSTIC,
|
||||
this->event, TRUE) == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
struct WcsSource final : DisplayProfileSource {
|
||||
function<void()> on_change;
|
||||
Watch system;
|
||||
Watch user;
|
||||
|
||||
void start(function<void()> fn) override;
|
||||
DisplayProfile load(QScreen *screen) override;
|
||||
bool bind(Watch &watch, HKEY root, const wchar_t *path);
|
||||
};
|
||||
|
||||
bool
|
||||
WcsSource::bind(Watch &watch, HKEY root, const wchar_t *path)
|
||||
{
|
||||
if (!watch.open(root, path) || !watch.arm()) {
|
||||
watch.close();
|
||||
return false;
|
||||
}
|
||||
watch.notifier = make_unique<QWinEventNotifier>(watch.event);
|
||||
QObject::connect(watch.notifier.get(), &QWinEventNotifier::activated,
|
||||
[this, &watch](HANDLE) {
|
||||
ResetEvent(watch.event);
|
||||
watch.arm();
|
||||
if (this->on_change)
|
||||
this->on_change();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
WcsSource::start(function<void()> fn)
|
||||
{
|
||||
this->on_change = std::move(fn);
|
||||
if (this->system.notifier || this->user.notifier)
|
||||
return;
|
||||
if (!this->bind(this->system, HKEY_LOCAL_MACHINE, kSystemClass))
|
||||
qWarning("Windows ICM: cannot watch system profile associations");
|
||||
if (!this->bind(this->user, HKEY_CURRENT_USER, kUserLeaf) &&
|
||||
!this->bind(this->user, HKEY_CURRENT_USER, kUserParent))
|
||||
qWarning("Windows ICM: cannot watch user profile associations");
|
||||
}
|
||||
|
||||
DisplayProfile
|
||||
WcsSource::load(QScreen *screen)
|
||||
{
|
||||
return load_display_profile(screen);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
unique_ptr<DisplayProfileSource>
|
||||
make_display_profile_source()
|
||||
{
|
||||
return make_unique<WcsSource>();
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// display-profile.cpp: process-wide display ICC watch
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "display-profile.hpp"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QMetaObject>
|
||||
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
DisplayProfileWatch::DisplayProfileWatch()
|
||||
: source_(make_display_profile_source())
|
||||
{
|
||||
}
|
||||
|
||||
DisplayProfileWatch::~DisplayProfileWatch() = default;
|
||||
|
||||
void
|
||||
DisplayProfileWatch::start()
|
||||
{
|
||||
this->source_->start([this] { this->notify(); });
|
||||
}
|
||||
|
||||
DisplayProfile
|
||||
DisplayProfileWatch::load(QScreen *screen)
|
||||
{
|
||||
return this->source_->load(screen);
|
||||
}
|
||||
|
||||
void
|
||||
DisplayProfileWatch::listen(void *key, function<void()> fn)
|
||||
{
|
||||
unlisten(key);
|
||||
this->listeners_.emplace_back(key, std::move(fn));
|
||||
}
|
||||
|
||||
void
|
||||
DisplayProfileWatch::unlisten(const void *key)
|
||||
{
|
||||
erase_if(this->listeners_,
|
||||
[key](const auto &item) { return item.first == key; });
|
||||
}
|
||||
|
||||
void
|
||||
DisplayProfileWatch::notify() const
|
||||
{
|
||||
QObject *app = QCoreApplication::instance();
|
||||
if (!app)
|
||||
return;
|
||||
QMetaObject::invokeMethod(
|
||||
app,
|
||||
[this] {
|
||||
const auto copy = this->listeners_;
|
||||
for (const auto &item : copy)
|
||||
if (item.second)
|
||||
item.second();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// display-profile.hpp: display ICC profile lookup
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class QScreen;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
struct DisplayProfile {
|
||||
std::vector<unsigned char> icc;
|
||||
std::string source;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
/// Platform lookup and change notification for display profiles.
|
||||
class DisplayProfileSource
|
||||
{
|
||||
public:
|
||||
virtual ~DisplayProfileSource() = default;
|
||||
virtual void start(std::function<void()> on_change) = 0;
|
||||
virtual DisplayProfile load(QScreen *screen) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<DisplayProfileSource> make_display_profile_source();
|
||||
|
||||
/// Process-wide display ICC lookup and change notification.
|
||||
class DisplayProfileWatch
|
||||
{
|
||||
public:
|
||||
DisplayProfileWatch();
|
||||
~DisplayProfileWatch();
|
||||
DisplayProfileWatch(const DisplayProfileWatch &) = delete;
|
||||
DisplayProfileWatch &operator=(const DisplayProfileWatch &) = delete;
|
||||
|
||||
void start();
|
||||
DisplayProfile load(QScreen *screen);
|
||||
void listen(void *key, std::function<void()> fn);
|
||||
void unlisten(const void *key);
|
||||
|
||||
private:
|
||||
void notify() const;
|
||||
|
||||
std::vector<std::pair<void *, std::function<void()>>> listeners_;
|
||||
std::unique_ptr<DisplayProfileSource> source_;
|
||||
};
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh -e
|
||||
# Point the installed desktop file's MimeType= at what dn can actually open.
|
||||
#
|
||||
# The list is only partly fixed at build time: glycin and gdk-pixbuf each
|
||||
# contribute whatever loaders are installed on this machine. Package managers
|
||||
# want to run this after unpacking, and again whenever those change.
|
||||
dn='@CMAKE_INSTALL_FULL_BINDIR@/dn'
|
||||
desktop='@CMAKE_INSTALL_FULL_DATADIR@/applications/@DAWN_NAMESPACE@.desktop'
|
||||
|
||||
types=$("$dn" --list-supported-media-types | tr '\n' ';')
|
||||
if [ -z "$types" ]
|
||||
then
|
||||
echo "$0: $dn reported no media types" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp=$desktop.$$
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
sed "s|^MimeType=.*|MimeType=$types|" "$desktop" >"$tmp"
|
||||
mv "$tmp" "$desktop"
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop stop-color="#ffff00" offset="0" />
|
||||
<stop stop-color="#ff0000" offset="1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0.5" flood-color="#000" result="flood" />
|
||||
<feComposite in="flood" in2="SourceGraphic" operator="in" result="sil" />
|
||||
<feGaussianBlur in="sil" stdDeviation="1.4" result="blur" />
|
||||
<feOffset in="blur" dx="0" dy="1.4" result="drop" />
|
||||
<feComposite in="SourceGraphic" in2="drop" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
<circle cx="24" cy="24" r="18" fill="url(#fill)" filter="url(#shadow)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 905 B |
@@ -0,0 +1,94 @@
|
||||
// gen-icon.swift: generate a program icon for dn in the Apple icon format
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// As an odd regression, AppKit may be necessary for JIT linking.
|
||||
import AppKit
|
||||
|
||||
// NSGraphicsContext mostly just weirdly wraps over Quartz,
|
||||
// so we do it all in Quartz directly.
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// Apple uses something that's close to a "quintic superellipse" in their icons,
|
||||
// but doesn't quite match. Either way, it looks better than rounded rectangles.
|
||||
func addSquircle(context: CGContext, bounds: CGRect) {
|
||||
context.move(to: CGPoint(x: bounds.maxX, y: bounds.midY))
|
||||
for theta in stride(from: 0.0, to: .pi * 2, by: .pi / 1e4) {
|
||||
let x = pow(abs(cos(theta)), 2 / 5.0) * bounds.width / 2
|
||||
* CGFloat(signOf: cos(theta), magnitudeOf: 1) + bounds.midX
|
||||
let y = pow(abs(sin(theta)), 2 / 5.0) * bounds.height / 2
|
||||
* CGFloat(signOf: sin(theta), magnitudeOf: 1) + bounds.midY
|
||||
context.addLine(to: CGPoint(x: x, y: y))
|
||||
}
|
||||
context.closePath()
|
||||
}
|
||||
|
||||
func drawIcon(scale: CGFloat) -> CGImage? {
|
||||
let size = CGSizeMake(1024, 1024)
|
||||
|
||||
let colorspace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CGContext(data: nil,
|
||||
width: Int(size.width * scale), height: Int(size.height * scale),
|
||||
bitsPerComponent: 8, bytesPerRow: 0, space: colorspace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
|
||||
let bounds = CGRectMake(100, 100, size.width - 200, size.height - 200)
|
||||
addSquircle(context: context, bounds: bounds)
|
||||
let squircle = context.path!
|
||||
|
||||
// Gradients don't draw shadows, so draw it separately.
|
||||
context.saveGState()
|
||||
context.setShadow(offset: CGSizeMake(0, -12).applying(context.ctm),
|
||||
blur: 28 * scale, color: CGColor(gray: 0, alpha: 0.375))
|
||||
context.setFillColor(CGColor(red: 1, green: 0x7ap-8, blue: 0, alpha: 1))
|
||||
context.fillPath()
|
||||
context.restoreGState()
|
||||
|
||||
// Quartz Y is up: start is the bottom of the squircle (orange),
|
||||
// end is the top (yellow #ffee00).
|
||||
context.saveGState()
|
||||
context.addPath(squircle)
|
||||
context.clip()
|
||||
context.drawLinearGradient(
|
||||
CGGradient(colorsSpace: colorspace, colors: [
|
||||
CGColor(red: 1, green: 0x00p-8, blue: 0, alpha: 1),
|
||||
CGColor(red: 1, green: 0xffp-8, blue: 0, alpha: 1)
|
||||
] as CFArray, locations: [0, 1])!,
|
||||
start: CGPointMake(0, 100), end: CGPointMake(0, size.height - 100),
|
||||
options: CGGradientDrawingOptions(rawValue: 0))
|
||||
context.restoreGState()
|
||||
|
||||
return context.makeImage()
|
||||
}
|
||||
|
||||
if CommandLine.arguments.count != 2 {
|
||||
print("Usage: \(CommandLine.arguments.first!) OUTPUT.icns")
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
|
||||
let filename = CommandLine.arguments[1]
|
||||
|
||||
let macOSSizes: Array<CGFloat> = [16, 32, 128, 256, 512]
|
||||
let icns = CGImageDestinationCreateWithURL(
|
||||
URL(fileURLWithPath: filename) as CFURL,
|
||||
UTType.icns.identifier as CFString, macOSSizes.count * 2, nil)!
|
||||
for size in macOSSizes {
|
||||
// nil properties seem to inherit the last used ones, so specifying both.
|
||||
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0)!, [
|
||||
kCGImagePropertyDPIWidth: 72,
|
||||
kCGImagePropertyDPIHeight: 72,
|
||||
] as CFDictionary)
|
||||
CGImageDestinationAddImage(icns, drawIcon(scale: size / 1024.0 * 2)!, [
|
||||
kCGImagePropertyDPIWidth: 144,
|
||||
kCGImagePropertyDPIHeight: 144,
|
||||
] as CFDictionary)
|
||||
}
|
||||
if !CGImageDestinationFinalize(icns) {
|
||||
print("ICNS finalization failed.")
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// gpu.cpp: shared Vulkan device and queue
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "gpu.hpp"
|
||||
#include "libdn/vk-device.hpp"
|
||||
|
||||
#include <QtLogging>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
bool
|
||||
GpuContext::init(VkInstance instance, VkSurfaceKHR surface,
|
||||
function<bool(VkPhysicalDevice, uint32_t)> supports_present)
|
||||
{
|
||||
destroy();
|
||||
this->instance_ = instance;
|
||||
if (!this->instance_ || !surface)
|
||||
return false;
|
||||
string err;
|
||||
if (!dawn::vk_create_graphics_device(this->instance_, surface,
|
||||
std::move(supports_present), {VK_KHR_SWAPCHAIN_EXTENSION_NAME},
|
||||
&this->phys_, &this->device_, &this->queue_, &this->queue_family_,
|
||||
&err)) {
|
||||
qWarning("%s", err.c_str());
|
||||
return false;
|
||||
}
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
vkGetPhysicalDeviceProperties(this->phys_, &properties);
|
||||
this->device_name_ = properties.deviceName;
|
||||
qInfo("device: %s", this->device_name_.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
GpuContext::destroy()
|
||||
{
|
||||
if (this->device_) {
|
||||
vkDeviceWaitIdle(this->device_);
|
||||
vkDestroyDevice(this->device_, nullptr);
|
||||
}
|
||||
this->instance_ = VK_NULL_HANDLE;
|
||||
this->phys_ = VK_NULL_HANDLE;
|
||||
this->device_ = VK_NULL_HANDLE;
|
||||
this->queue_ = VK_NULL_HANDLE;
|
||||
this->queue_family_ = 0;
|
||||
this->device_name_.clear();
|
||||
}
|
||||
|
||||
bool
|
||||
GpuContext::supports_present(VkSurfaceKHR surface) const
|
||||
{
|
||||
if (!this->phys_ || !surface)
|
||||
return false;
|
||||
VkBool32 present = VK_FALSE;
|
||||
vkGetPhysicalDeviceSurfaceSupportKHR(
|
||||
this->phys_, this->queue_family_, surface, &present);
|
||||
return present == VK_TRUE;
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// gpu.hpp: shared Vulkan device and queue
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
// The one VkQueue is used only from the Qt main thread. Worker threads
|
||||
// produce CPU pixels only.
|
||||
class GpuContext
|
||||
{
|
||||
VkInstance instance_ = VK_NULL_HANDLE; // borrowed from QVulkanInstance
|
||||
VkPhysicalDevice phys_ = VK_NULL_HANDLE;
|
||||
VkDevice device_ = VK_NULL_HANDLE;
|
||||
VkQueue queue_ = VK_NULL_HANDLE;
|
||||
uint32_t queue_family_ = 0;
|
||||
std::string device_name_;
|
||||
|
||||
public:
|
||||
GpuContext() = default;
|
||||
~GpuContext() { destroy(); }
|
||||
|
||||
GpuContext(const GpuContext &) = delete;
|
||||
GpuContext &operator=(const GpuContext &) = delete;
|
||||
|
||||
bool init(VkInstance instance, VkSurfaceKHR surface,
|
||||
std::function<bool(VkPhysicalDevice, uint32_t)> supports_present);
|
||||
void destroy();
|
||||
|
||||
// Later windows: present support on the chosen family. False if not ready.
|
||||
[[nodiscard]] bool supports_present(VkSurfaceKHR surface) const;
|
||||
|
||||
[[nodiscard]] VkInstance instance() const { return this->instance_; }
|
||||
[[nodiscard]] VkPhysicalDevice phys() const { return this->phys_; }
|
||||
[[nodiscard]] VkDevice device() const { return this->device_; }
|
||||
[[nodiscard]] VkQueue queue() const { return this->queue_; }
|
||||
[[nodiscard]] uint32_t queue_family() const { return this->queue_family_; }
|
||||
[[nodiscard]] const std::string &device_name() const
|
||||
{
|
||||
return this->device_name_;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,62 @@
|
||||
<RCC>
|
||||
<qresource prefix="/dn/icons">
|
||||
<file alias="arrows-circle-symbolic.svg">../icons/arrows-circle-symbolic.svg</file>
|
||||
<file alias="object-select-symbolic.svg">../icons/object-select-symbolic.svg</file>
|
||||
<file alias="padlock-closed-symbolic.svg">../icons/padlock-closed-symbolic.svg</file>
|
||||
<file alias="padlock-open-symbolic.svg">../icons/padlock-open-symbolic.svg</file>
|
||||
<file alias="pin2-symbolic.svg">../icons/pin2-symbolic.svg</file>
|
||||
<file alias="blend-tool-symbolic.svg">../icons/blend-tool-symbolic.svg</file>
|
||||
<file alias="transparent-background-symbolic.svg">../icons/transparent-background-symbolic.svg</file>
|
||||
<file alias="heal-symbolic.svg">../icons/heal-symbolic.svg</file>
|
||||
<file alias="info-outline-symbolic.svg">../icons/info-outline-symbolic.svg</file>
|
||||
<file alias="font-symbolic.svg">../icons/font-symbolic.svg</file>
|
||||
<file alias="filter-symbolic.svg">../icons/filter-symbolic.svg</file>
|
||||
<file alias="drive-optical-symbolic.svg">../icons/drive-optical-symbolic.svg</file>
|
||||
<file alias="drive-removable-media-symbolic.svg">../icons/drive-removable-media-symbolic.svg</file>
|
||||
<file alias="network-server-symbolic.svg">../icons/network-server-symbolic.svg</file>
|
||||
<file alias="drive-ssd-symbolic.svg">../icons/drive-ssd-symbolic.svg</file>
|
||||
<file alias="drive-harddisk-symbolic.svg">../icons/drive-harddisk-symbolic.svg</file>
|
||||
<file alias="blocks-symbolic.svg">../icons/blocks-symbolic.svg</file>
|
||||
<file alias="color-symbolic.svg">../icons/color-symbolic.svg</file>
|
||||
<file alias="computer-symbolic.svg">../icons/computer-symbolic.svg</file>
|
||||
<file alias="curved-arrow-left-symbolic.svg">../icons/curved-arrow-left-symbolic.svg</file>
|
||||
<file alias="curved-arrow-right-symbolic.svg">../icons/curved-arrow-right-symbolic.svg</file>
|
||||
<file alias="dark-mode-symbolic.svg">../icons/dark-mode-symbolic.svg</file>
|
||||
<file alias="disclose-arrow-down-symbolic.svg">../icons/disclose-arrow-down-symbolic.svg</file>
|
||||
<file alias="document-save-as-symbolic.svg">../icons/document-save-as-symbolic.svg</file>
|
||||
<file alias="dots-horizontal-symbolic.svg">../icons/dots-horizontal-symbolic.svg</file>
|
||||
<file alias="dot-large-symbolic.svg">../icons/dot-large-symbolic.svg</file>
|
||||
<file alias="flip-h-symbolic.svg">../icons/flip-h-symbolic.svg</file>
|
||||
<file alias="go-bottom-symbolic.svg">../icons/go-bottom-symbolic.svg</file>
|
||||
<file alias="go-down-symbolic.svg">../icons/go-down-symbolic.svg</file>
|
||||
<file alias="go-home-symbolic.svg">../icons/go-home-symbolic.svg</file>
|
||||
<file alias="go-next-symbolic.svg">../icons/go-next-symbolic.svg</file>
|
||||
<file alias="go-previous-symbolic.svg">../icons/go-previous-symbolic.svg</file>
|
||||
<file alias="go-top-symbolic.svg">../icons/go-top-symbolic.svg</file>
|
||||
<file alias="go-up-symbolic.svg">../icons/go-up-symbolic.svg</file>
|
||||
<file alias="image-missing-symbolic.svg">../icons/image-missing-symbolic.svg</file>
|
||||
<file alias="image-symbolic.svg">../icons/image-symbolic.svg</file>
|
||||
<file alias="folder-symbolic.svg">../icons/folder-symbolic.svg</file>
|
||||
<file alias="media-playback-pause-symbolic.svg">../icons/media-playback-pause-symbolic.svg</file>
|
||||
<file alias="media-playback-start-symbolic.svg">../icons/media-playback-start-symbolic.svg</file>
|
||||
<file alias="media-seek-backward-symbolic.svg">../icons/media-seek-backward-symbolic.svg</file>
|
||||
<file alias="media-seek-forward-symbolic.svg">../icons/media-seek-forward-symbolic.svg</file>
|
||||
<file alias="media-skip-backward-symbolic.svg">../icons/media-skip-backward-symbolic.svg</file>
|
||||
<file alias="minus-framed-symbolic.svg">../icons/minus-framed-symbolic.svg</file>
|
||||
<file alias="one-framed-symbolic.svg">../icons/one-framed-symbolic.svg</file>
|
||||
<file alias="open-menu-symbolic.svg">../icons/open-menu-symbolic.svg</file>
|
||||
<file alias="plus-framed-symbolic.svg">../icons/plus-framed-symbolic.svg</file>
|
||||
<file alias="printer-symbolic.svg">../icons/printer-symbolic.svg</file>
|
||||
<file alias="rotate-acw-symbolic.svg">../icons/rotate-acw-symbolic.svg</file>
|
||||
<file alias="rotate-cw-symbolic.svg">../icons/rotate-cw-symbolic.svg</file>
|
||||
<file alias="sidebar-left-symbolic.svg">../icons/sidebar-left-symbolic.svg</file>
|
||||
<file alias="view-fullscreen-symbolic.svg">../icons/view-fullscreen-symbolic.svg</file>
|
||||
<file alias="view-grid-symbolic.svg">../icons/view-grid-symbolic.svg</file>
|
||||
<file alias="view-list-symbolic.svg">../icons/view-list-symbolic.svg</file>
|
||||
<file alias="view-restore-symbolic.svg">../icons/view-restore-symbolic.svg</file>
|
||||
<file alias="view-sort-ascending-symbolic.svg">../icons/view-sort-ascending-symbolic.svg</file>
|
||||
<file alias="view-sort-descending-symbolic.svg">../icons/view-sort-descending-symbolic.svg</file>
|
||||
<file alias="x-symbolic.svg">../icons/x-symbolic.svg</file>
|
||||
<file alias="zoom-fit-symbolic.svg">../icons/zoom-fit-symbolic.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// instance.cpp: Qt adapter for dn single-instance IPC
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "instance.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "libdn/ipc-instance.hpp"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QtGlobal>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QWinEventNotifier>
|
||||
#else
|
||||
#include <QSocketNotifier>
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
// One event-loop watch on an dawn::ipc::Waitable. Qt watches sockets on Unix
|
||||
// and overlapped completion events on Windows; nothing below cares which.
|
||||
#ifdef Q_OS_WIN
|
||||
using Watch = QWinEventNotifier;
|
||||
|
||||
Watch *
|
||||
make_watch(dawn::ipc::Waitable w, bool, QObject *parent)
|
||||
{
|
||||
return new QWinEventNotifier((Qt::HANDLE) w, parent);
|
||||
}
|
||||
#else
|
||||
using Watch = QSocketNotifier;
|
||||
|
||||
Watch *
|
||||
make_watch(dawn::ipc::Waitable w, bool write, QObject *parent)
|
||||
{
|
||||
return new QSocketNotifier(qintptr(w),
|
||||
write ? QSocketNotifier::Write : QSocketNotifier::Read, parent);
|
||||
}
|
||||
#endif
|
||||
|
||||
dawn::ipc::instance::ErrorCode
|
||||
map_open_error(OpenResult r)
|
||||
{
|
||||
switch (r) {
|
||||
case OpenResult::NotFound:
|
||||
return dawn::ipc::instance::ErrorCode::NotFound;
|
||||
case OpenResult::PermissionDenied:
|
||||
return dawn::ipc::instance::ErrorCode::PermissionDenied;
|
||||
case OpenResult::InvalidArgument:
|
||||
return dawn::ipc::instance::ErrorCode::InvalidArgument;
|
||||
case OpenResult::Ok:
|
||||
case OpenResult::Internal:
|
||||
break;
|
||||
}
|
||||
return dawn::ipc::instance::ErrorCode::Internal;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct InstanceHost::Impl {
|
||||
Impl(dawn::ipc::Listener listener, App &app, const QString &session,
|
||||
InstanceHost *host);
|
||||
void watch_read(uint64_t id, dawn::ipc::Waitable w);
|
||||
void watch_write(uint64_t id, dawn::ipc::Waitable w, bool enable);
|
||||
void unwatch(uint64_t id);
|
||||
void on_request(dawn::ipc::instance::Call call,
|
||||
const dawn::ipc::instance::RequestView &req);
|
||||
|
||||
App &app_;
|
||||
InstanceHost *host_;
|
||||
unordered_map<uint64_t, Watch *> reads_;
|
||||
unordered_map<uint64_t, Watch *> writes_;
|
||||
unique_ptr<dawn::ipc::instance::Server> server_;
|
||||
};
|
||||
|
||||
InstanceHost::Impl::Impl(dawn::ipc::Listener listener, App &app,
|
||||
const QString &session, InstanceHost *host)
|
||||
: app_(app), host_(host)
|
||||
{
|
||||
dawn::ipc::instance::Server::Config cfg;
|
||||
cfg.session = session.toUtf8().toStdString();
|
||||
cfg.on_request = [this](dawn::ipc::instance::Call call,
|
||||
const dawn::ipc::instance::RequestView &req) {
|
||||
on_request(std::move(call), req);
|
||||
};
|
||||
cfg.watch_read = [this](uint64_t id, dawn::ipc::Waitable w) {
|
||||
watch_read(id, w);
|
||||
};
|
||||
cfg.unwatch = [this](uint64_t id) { unwatch(id); };
|
||||
cfg.watch_write = [this](uint64_t id, dawn::ipc::Waitable w, bool enable) {
|
||||
watch_write(id, w, enable);
|
||||
};
|
||||
this->server_ = make_unique<dawn::ipc::instance::Server>(
|
||||
std::move(listener), std::move(cfg));
|
||||
|
||||
auto *n = make_watch(this->server_->listen_waitable(), false, this->host_);
|
||||
QObject::connect(n, &Watch::activated, this->host_,
|
||||
[this] { this->server_->poll_listen(); });
|
||||
}
|
||||
|
||||
void
|
||||
InstanceHost::Impl::watch_read(uint64_t id, dawn::ipc::Waitable w)
|
||||
{
|
||||
if (this->reads_.contains(id))
|
||||
return;
|
||||
auto *n = make_watch(w, false, this->host_);
|
||||
QObject::connect(n, &Watch::activated, this->host_,
|
||||
[this, id] { this->server_->poll_read(id); });
|
||||
this->reads_[id] = n;
|
||||
}
|
||||
|
||||
void
|
||||
InstanceHost::Impl::watch_write(uint64_t id, dawn::ipc::Waitable w, bool enable)
|
||||
{
|
||||
const auto it = this->writes_.find(id);
|
||||
if (it != this->writes_.end()) {
|
||||
it->second->setEnabled(enable);
|
||||
return;
|
||||
}
|
||||
if (!enable)
|
||||
return;
|
||||
auto *n = make_watch(w, true, this->host_);
|
||||
QObject::connect(n, &Watch::activated, this->host_,
|
||||
[this, id] { this->server_->poll_write(id); });
|
||||
this->writes_[id] = n;
|
||||
}
|
||||
|
||||
void
|
||||
InstanceHost::Impl::unwatch(uint64_t id)
|
||||
{
|
||||
if (const auto it = this->reads_.find(id); it != this->reads_.end()) {
|
||||
it->second->setEnabled(false);
|
||||
it->second->deleteLater();
|
||||
this->reads_.erase(it);
|
||||
}
|
||||
if (const auto it = this->writes_.find(id); it != this->writes_.end()) {
|
||||
it->second->setEnabled(false);
|
||||
it->second->deleteLater();
|
||||
this->writes_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Opening runs to completion right here, so there is no window in which
|
||||
// a cancellation could arrive: an ID only becomes cancellable when it is
|
||||
// dispatched, which is this call.
|
||||
void
|
||||
InstanceHost::Impl::on_request(
|
||||
dawn::ipc::instance::Call call, const dawn::ipc::instance::RequestView &req)
|
||||
{
|
||||
const auto *open_body =
|
||||
get_if<dawn::ipc::instance::RequestBodyOpenView>(&req.body.value);
|
||||
if (!open_body) {
|
||||
call.fail(dawn::ipc::instance::ErrorCode::InvalidArgument, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// default_window exists so Finder's first document can replace the
|
||||
// window dn guessed at startup. A hand-off is not that: it is another
|
||||
// invocation with its own arguments, and gets its own window.
|
||||
this->app_.default_window.clear();
|
||||
|
||||
const QString token = QString::fromUtf8(open_body->open.activation_token);
|
||||
const bool browse = open_body->open.browse;
|
||||
for (const string_view url : open_body->open.urls) {
|
||||
const OpenResult r = this->app_.open(
|
||||
QUrl::fromEncoded(QByteArray(url.data(), qsizetype(url.size()))),
|
||||
token, {}, browse);
|
||||
if (r != OpenResult::Ok) {
|
||||
call.fail(map_open_error(r), {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
call.done();
|
||||
}
|
||||
|
||||
InstanceHost::InstanceHost(
|
||||
dawn::ipc::Listener listener, App &app, QString session, QObject *parent)
|
||||
: QObject(parent),
|
||||
impl_(make_unique<Impl>(std::move(listener), app, session, this))
|
||||
{
|
||||
}
|
||||
|
||||
InstanceHost::~InstanceHost() = default;
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// instance.hpp: Qt adapter for dn single-instance IPC
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libdn/ipc.hpp"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
class App;
|
||||
|
||||
class InstanceHost : public QObject
|
||||
{
|
||||
public:
|
||||
// Takes over the bound listener. Forwards Open requests to app.open.
|
||||
InstanceHost(dawn::ipc::Listener listener, App &app, QString session,
|
||||
QObject *parent = nullptr);
|
||||
~InstanceHost() override;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace dn
|
||||
+3091
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// kit-browser.hpp: directory browser
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kit-chrome.hpp"
|
||||
#include "kit.hpp"
|
||||
#include "overlay.hpp"
|
||||
#include "thumbnailer.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
#include <libdn.h>
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
constexpr float kBrowseSidebarPts = 192.f;
|
||||
|
||||
enum class SortField : uint8_t { Name, Time };
|
||||
enum class BrowserView : uint8_t { Tile, Grid };
|
||||
|
||||
struct BrowseSetup {
|
||||
SortField sort = SortField::Name;
|
||||
bool sort_desc = false;
|
||||
bool filter_files = true;
|
||||
};
|
||||
|
||||
struct Browser : Widget {
|
||||
struct File {
|
||||
std::string path;
|
||||
std::string name;
|
||||
int64_t mtime = 0;
|
||||
uint64_t size = 0;
|
||||
uint32_t image_w = 0;
|
||||
uint32_t image_h = 0;
|
||||
int ram_w = 0;
|
||||
int ram_h = 0;
|
||||
int ram_tier = -1;
|
||||
std::vector<uint16_t> ram;
|
||||
bool ram_interim = false;
|
||||
bool ram_pending = false;
|
||||
bool persistent_checked = false;
|
||||
bool generation_needed = false;
|
||||
Thumbnailer::Reservation reservation = 0;
|
||||
bool regen_failed = false;
|
||||
dawn::Transfer transfer = dawn::Transfer::Srgb;
|
||||
Sheet::Packed gpu;
|
||||
bool failed = false;
|
||||
Rect tile{};
|
||||
Rect cell{};
|
||||
QString cap_text;
|
||||
Rect cap{};
|
||||
};
|
||||
struct DirRow {
|
||||
std::string path;
|
||||
std::string name;
|
||||
std::string tip;
|
||||
const char *icon = nullptr;
|
||||
bool current = false;
|
||||
};
|
||||
struct PlaceItem {
|
||||
Button *button = nullptr;
|
||||
std::string path;
|
||||
};
|
||||
struct CachedSize {
|
||||
int64_t mtime = 0;
|
||||
uint64_t size = 0;
|
||||
uint32_t w = 0;
|
||||
uint32_t h = 0;
|
||||
};
|
||||
struct ThumbInflight {
|
||||
uint64_t generation = 0;
|
||||
int64_t mtime = 0;
|
||||
uint64_t size = 0;
|
||||
int tier = 0;
|
||||
Thumbnailer::Priority priority = Thumbnailer::Priority::Dimensions;
|
||||
bool regeneration = false;
|
||||
};
|
||||
|
||||
Kit &kit_;
|
||||
Thumbnailer &thumbnailer_;
|
||||
uint64_t thumbnail_client_ = 0;
|
||||
Page *page_ = nullptr;
|
||||
ScrollColumn *places_ = nullptr;
|
||||
std::vector<PlaceItem> place_items_;
|
||||
|
||||
// Enumeration below is std::filesystem; this is the identity above it.
|
||||
QUrl dir_url_;
|
||||
std::shared_ptr<dawn::Cmm> cmm_;
|
||||
std::shared_ptr<dawn::Profile> screen_profile_;
|
||||
std::shared_ptr<const std::vector<uint8_t>> screen_icc_;
|
||||
|
||||
bool show_names_ = false;
|
||||
// The toolbar search field, whose text narrows the listing; it is
|
||||
// deliberately per-window, and not part of BrowseSetup.
|
||||
Entry *search_ = nullptr;
|
||||
BrowseSetup setup_;
|
||||
BrowserView view_ = BrowserView::Tile;
|
||||
int thumb_size_ = 256;
|
||||
bool places_dirty_ = true;
|
||||
|
||||
Scroll scroll_;
|
||||
|
||||
struct GridRow {
|
||||
int first = 0;
|
||||
int count = 0;
|
||||
int y = 0;
|
||||
int h = 0;
|
||||
};
|
||||
std::vector<GridRow> rows_;
|
||||
int cursor_ = -1;
|
||||
float cursor_x_ = 0;
|
||||
bool cursor_x_dirty_ = false;
|
||||
int layout_cursor_ = -1;
|
||||
float layout_cell_x_ = 0;
|
||||
int layout_w_ = 0;
|
||||
int mid_file_ = -1;
|
||||
|
||||
bool can_prev_dir_ = false;
|
||||
bool can_next_dir_ = false;
|
||||
bool can_parent_dir_ = false;
|
||||
std::vector<File> files_;
|
||||
std::vector<DirRow> side_dirs_;
|
||||
std::unordered_map<std::string, CachedSize> size_cache_;
|
||||
std::unordered_map<std::string, ThumbInflight> thumb_inflight_;
|
||||
|
||||
struct HistEntry {
|
||||
QUrl url;
|
||||
float side_scroll = 0;
|
||||
};
|
||||
std::vector<HistEntry> hist_back_;
|
||||
std::vector<HistEntry> hist_forward_;
|
||||
|
||||
Sheet sheet_{Sheet::kSize, false};
|
||||
uint64_t thumb_gen_ = 0;
|
||||
|
||||
Browser(Kit &kit, Thumbnailer &thumbnailer);
|
||||
~Browser() override;
|
||||
|
||||
void measure(Kit &kit, int max_w, int max_h) override;
|
||||
void arrange(Kit &kit, Rect alloc) override;
|
||||
void paint(Kit &kit) const override;
|
||||
void prepare(Kit &kit) override;
|
||||
[[nodiscard]] bool focusable() const override;
|
||||
[[nodiscard]] Qt::CursorShape cursor() const override;
|
||||
[[nodiscard]] QString tip() const override;
|
||||
[[nodiscard]] Rect tip_anchor() const override { return {}; }
|
||||
|
||||
void init();
|
||||
void destroy();
|
||||
void set_host(float width_pts, float height_pts, float dpr);
|
||||
void open_dir(const QUrl &url, bool record = true);
|
||||
void rescan();
|
||||
bool hist_back();
|
||||
bool hist_forward();
|
||||
void hist_clear_forward();
|
||||
[[nodiscard]] bool hist_can_back() const;
|
||||
[[nodiscard]] bool hist_can_forward() const;
|
||||
void select_file(const QUrl &url);
|
||||
void file_gone(const QUrl &url);
|
||||
[[nodiscard]] QUrl file_url(int index) const;
|
||||
[[nodiscard]] BrowseSetup browse_setup() const { return this->setup_; }
|
||||
void set_screen_profile(
|
||||
std::shared_ptr<dawn::Cmm> cmm, std::shared_ptr<dawn::Profile> profile);
|
||||
void present(Page &ui);
|
||||
bool press(Kit &kit, float x, float y, Qt::MouseButton button) override;
|
||||
bool release(Kit &kit, float x, float y, Qt::MouseButton button) override;
|
||||
Scroll *scrollbar() override { return &this->scroll_; }
|
||||
bool motion(Kit &kit, float x, float y) override;
|
||||
bool scroll(Kit &kit, float x, float y, int delta) override;
|
||||
bool pan(Kit &kit, float x, float y, float dx, float dy) override;
|
||||
bool key(Kit &kit, const Key &ev) override;
|
||||
bool double_click(Kit &kit, float x, float y, Qt::MouseButton button,
|
||||
unsigned mods) override;
|
||||
[[nodiscard]] int wake_ms() const override;
|
||||
[[nodiscard]] bool thumbs_busy() const;
|
||||
};
|
||||
|
||||
std::unique_ptr<Page> make_browser_page(
|
||||
Kit &kit, const HostActions &host, Thumbnailer &thumbnailer, Browser **out);
|
||||
|
||||
} // namespace dn
|
||||
+1224
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
//
|
||||
// kit-chrome.hpp: non-generic widgets
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "action.hpp"
|
||||
#include "kit.hpp"
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
struct HostActions {
|
||||
std::function<void(Action)> apply;
|
||||
std::function<bool(Action)> enabled;
|
||||
std::function<void(QUrl url)> activate;
|
||||
std::function<void(QUrl url)> new_window;
|
||||
std::function<void(QUrl url)> trash;
|
||||
std::function<void(QUrl url)> launch_exiftool;
|
||||
std::function<std::vector<std::string>()> bookmarks;
|
||||
std::function<bool(const QUrl &url)> bookmarked;
|
||||
std::function<void(QUrl url)> toggle_bookmark;
|
||||
};
|
||||
|
||||
// The menu that a right click on a file opens: what this application knows
|
||||
// how to do with it, plus whatever the desktop can open it with.
|
||||
struct ContextMenu : Menu {
|
||||
std::function<void(const QUrl &url)> on_new_window;
|
||||
std::function<void(const QUrl &url)> on_trash;
|
||||
std::function<bool(const QUrl &url)> on_bookmarked;
|
||||
std::function<void(const QUrl &url)> on_toggle_bookmark;
|
||||
|
||||
void show(Kit &kit, const QUrl &url, Rect anchor, bool kbd);
|
||||
|
||||
private:
|
||||
void fill_items(Kit &kit, const QUrl &url);
|
||||
};
|
||||
|
||||
// The thumbnail sizes the browser has, smallest first. One table: what the
|
||||
// settings dialog offers must not drift from what the browser can show, nor
|
||||
// from what Settings::load() will accept back off disk.
|
||||
constexpr int kThumbSizes[] = {128, 256, 512, 1024};
|
||||
|
||||
// What the settings dialog edits: a copy it owns, handed back whole on Save.
|
||||
// Deliberately not Settings itself -- the dialog has no business knowing
|
||||
// where any of this is kept, or what listens for it to change.
|
||||
struct SettingsDraft {
|
||||
int thumbnail_size = 256;
|
||||
bool show_filenames = true;
|
||||
QString icc_profile_path;
|
||||
bool disable_dithering = false;
|
||||
|
||||
struct Loader {
|
||||
QString name;
|
||||
QString formats;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
std::vector<Loader> loaders;
|
||||
};
|
||||
|
||||
void dialog_about(Kit &kit, Dialog &dialog);
|
||||
void dialog_shortcuts(Kit &kit, Dialog &dialog, std::span<const MenuNode> tree,
|
||||
std::span<const Action> keys);
|
||||
void dialog_location(Kit &kit, Dialog &dialog,
|
||||
std::function<void(const QString &)> on_open);
|
||||
|
||||
// Takes the draft by value: the dialog edits its own copy, and Save is the
|
||||
// only way anything gets back out.
|
||||
void dialog_settings(Kit &kit, Dialog &dialog, SettingsDraft draft,
|
||||
std::function<void(const SettingsDraft &)> on_save);
|
||||
|
||||
struct Sidebar : Panel {
|
||||
Widget *content = nullptr;
|
||||
|
||||
explicit Sidebar(std::unique_ptr<Widget> child);
|
||||
bool key(Kit &kit, const Key &ev) override;
|
||||
};
|
||||
|
||||
struct Browser;
|
||||
struct Page;
|
||||
|
||||
struct Hint : Popup {
|
||||
Page *page = nullptr;
|
||||
|
||||
Hint();
|
||||
void open(Kit &kit);
|
||||
void close(Kit &kit) override;
|
||||
void place(Kit &kit) override;
|
||||
void prepare(Kit &kit) override;
|
||||
void paint(Kit &kit) const override;
|
||||
bool captures_keys() const override { return true; }
|
||||
bool key(Kit &kit, const Key &ev) override;
|
||||
bool press(Kit &kit, float x, float y, Qt::MouseButton button) override;
|
||||
bool release(Kit &kit, float x, float y, Qt::MouseButton button) override;
|
||||
bool motion(Kit &kit, float x, float y) override;
|
||||
|
||||
private:
|
||||
struct Target {
|
||||
QString label;
|
||||
Rect at{};
|
||||
Rect chip{};
|
||||
Widget *widget = nullptr;
|
||||
// TODO(p): Unwanted dependency.
|
||||
Browser *browser = nullptr;
|
||||
int file_i = -1;
|
||||
};
|
||||
|
||||
std::vector<Target> targets_;
|
||||
QString typed_;
|
||||
|
||||
void collect(Widget *scope);
|
||||
void assign_labels();
|
||||
void refresh_rects();
|
||||
void layout_chips(const Kit &kit);
|
||||
[[nodiscard]] bool matches(const Target &t) const;
|
||||
void fire(Kit &kit, Target t);
|
||||
};
|
||||
|
||||
struct Page : Composite {
|
||||
Titlebar *titlebar = nullptr;
|
||||
Toolbar *toolbar = nullptr;
|
||||
Sidebar *sidebar = nullptr;
|
||||
Splitter *splitter = nullptr;
|
||||
Widget *content = nullptr;
|
||||
Widget *banner = nullptr;
|
||||
Dialog *dialog = nullptr;
|
||||
Hint *hint = nullptr;
|
||||
ContextMenu *context = nullptr;
|
||||
Menu *app_menu = nullptr;
|
||||
Button *app_menu_button = nullptr;
|
||||
Actor actor;
|
||||
const HostActions *host = nullptr;
|
||||
std::span<const MenuNode> menu_tree = {};
|
||||
std::span<const Action> keys = {};
|
||||
|
||||
enum class Side : uint8_t { None, Left, Right };
|
||||
Side sidebar_side = Side::None;
|
||||
float sidebar_w = 192;
|
||||
bool sidebar_open = true;
|
||||
|
||||
Page(std::unique_ptr<Toolbar> tb, std::unique_ptr<Sidebar> sb, Side side,
|
||||
std::unique_ptr<Widget> body);
|
||||
void set_banner(std::unique_ptr<Widget> w);
|
||||
void open_app_menu(Kit &kit, bool kbd);
|
||||
void sync_app_menu();
|
||||
[[nodiscard]] bool app_menu_open() const
|
||||
{
|
||||
return this->app_menu && this->app_menu->visible;
|
||||
}
|
||||
|
||||
void measure(Kit &kit, int max_w, int max_h) override;
|
||||
void arrange(Kit &kit, Rect alloc) override;
|
||||
bool key(Kit &kit, const Key &ev) override;
|
||||
std::size_t child_count() const override;
|
||||
Widget *child(std::size_t i) const override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Widget> banner_owned_;
|
||||
std::unique_ptr<Menu> app_menu_owned_;
|
||||
std::unique_ptr<Dialog> dialog_owned_;
|
||||
std::unique_ptr<Hint> hint_owned_;
|
||||
std::unique_ptr<ContextMenu> context_owned_;
|
||||
Rect well_{};
|
||||
};
|
||||
|
||||
Actor chain_actor(const HostActions &host, std::function<bool(Action)> apply,
|
||||
std::function<bool(Action)> enabled, std::function<bool(Action)> checked);
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,308 @@
|
||||
//
|
||||
// kit-cie-diagram.cpp: CIE 1931 xy sidebar widget
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include "kit-cie-diagram.hpp"
|
||||
|
||||
#include "cmf-cie1931-2deg-1nm.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPen>
|
||||
#include <QString>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
constexpr float kXMax = 0.8f;
|
||||
constexpr float kYMax = 0.9f;
|
||||
// XXX: I guess we hardcode it and I guess we shouldn't.
|
||||
constexpr float kD65x = 0.3127f;
|
||||
constexpr float kD65y = 0.3290f;
|
||||
constexpr int kRasterW = 256;
|
||||
constexpr int kRasterH = 288;
|
||||
constexpr float kCapGap = 4.f;
|
||||
constexpr Colour kMidGreyCol{188 / 255.f, 188 / 255.f, 188 / 255.f, 1.f};
|
||||
constexpr Colour kBlackCol{0.f, 0.f, 0.f, 1.f};
|
||||
constexpr Colour kWhiteCol{1.f, 1.f, 1.f, 1.f};
|
||||
|
||||
const QString kSourceLab = QStringLiteral("Source");
|
||||
const QString kTargetLab = QStringLiteral("Target");
|
||||
|
||||
static int
|
||||
caption_h(const Kit &kit)
|
||||
{
|
||||
return kit.px(kCapGap) + kit.text_height(kSourceLab, 0, false) +
|
||||
kit.px(4.f);
|
||||
}
|
||||
|
||||
// The raster is a fixed pixel size, and so is the rect: no scale involved.
|
||||
static Rect
|
||||
plot_rect(Rect r)
|
||||
{
|
||||
if (r.w <= 0 || r.h <= 0)
|
||||
return {};
|
||||
const float aspect = kXMax / kYMax;
|
||||
float w = float(min(r.w, int(kRasterW)));
|
||||
float h = w / aspect;
|
||||
if (h > float(r.h) || h > float(kRasterH)) {
|
||||
h = float(min(r.h, int(kRasterH)));
|
||||
w = h * aspect;
|
||||
}
|
||||
const int iw = int(w), ih = int(h);
|
||||
return {r.x + (r.w - iw) / 2, r.y, iw, ih};
|
||||
}
|
||||
|
||||
static QPointF
|
||||
xy_to_px(double x, double y, int w, int h)
|
||||
{
|
||||
return {x / double(kXMax) * w, (1.0 - y / double(kYMax)) * h};
|
||||
}
|
||||
|
||||
static QPainterPath
|
||||
locus_path(int w, int h)
|
||||
{
|
||||
QPainterPath path;
|
||||
if (kCmfN <= 0)
|
||||
return path;
|
||||
const auto xy = [](int i) {
|
||||
const double s = kCmf[i][0] + kCmf[i][1] + kCmf[i][2];
|
||||
if (s <= 0.0)
|
||||
return QPointF{};
|
||||
return QPointF{kCmf[i][0] / s, kCmf[i][1] / s};
|
||||
};
|
||||
QPointF p0 = xy(0);
|
||||
path.moveTo(xy_to_px(p0.x(), p0.y(), w, h));
|
||||
for (int i = 1; i < kCmfN; ++i) {
|
||||
QPointF p = xy(i);
|
||||
path.lineTo(xy_to_px(p.x(), p.y(), w, h));
|
||||
}
|
||||
path.closeSubpath();
|
||||
return path;
|
||||
}
|
||||
|
||||
static int
|
||||
srgb_encode8(double u)
|
||||
{
|
||||
if (u <= 0.0)
|
||||
return 0;
|
||||
if (u >= 1.0)
|
||||
return 255;
|
||||
if (u <= 0.0031308)
|
||||
return int(12.92 * u * 255.0 + 0.5);
|
||||
return int((1.055 * pow(u, 1.0 / 2.4) - 0.055) * 255.0 + 0.5);
|
||||
}
|
||||
|
||||
static QRgb
|
||||
xy_srgb(double x, double y)
|
||||
{
|
||||
if (y < 1e-8)
|
||||
return qRgba(0, 0, 0, 255);
|
||||
double rl =
|
||||
3.2404542 * (x / y) - 1.5371385 - 0.4985314 * ((1.0 - x - y) / y);
|
||||
double gl =
|
||||
-0.9692660 * (x / y) + 1.8760108 + 0.0415560 * ((1.0 - x - y) / y);
|
||||
double bl =
|
||||
0.0556434 * (x / y) - 0.2040259 + 1.0572252 * ((1.0 - x - y) / y);
|
||||
if (rl < 0.0)
|
||||
rl = 0.0;
|
||||
if (gl < 0.0)
|
||||
gl = 0.0;
|
||||
if (bl < 0.0)
|
||||
bl = 0.0;
|
||||
double m = rl;
|
||||
if (gl > m)
|
||||
m = gl;
|
||||
if (bl > m)
|
||||
m = bl;
|
||||
if (m > 0.0) {
|
||||
rl /= m;
|
||||
gl /= m;
|
||||
bl /= m;
|
||||
}
|
||||
return qRgba(srgb_encode8(rl), srgb_encode8(gl), srgb_encode8(bl), 255);
|
||||
}
|
||||
|
||||
static void
|
||||
stroke_poly(
|
||||
QPainter &p, const QPen &pen, const dawn::Chromaticities &c, int w, int h)
|
||||
{
|
||||
if (!c.have_primaries || c.n < 2)
|
||||
return;
|
||||
QPainterPath path;
|
||||
path.moveTo(xy_to_px(c.x[0], c.y[0], w, h));
|
||||
for (int i = 1; i < c.n; ++i)
|
||||
path.lineTo(xy_to_px(c.x[i], c.y[i], w, h));
|
||||
path.closeSubpath();
|
||||
p.strokePath(path, pen);
|
||||
}
|
||||
|
||||
static QImage
|
||||
raster_diagram(int w, int h, const dawn::Chromaticities &image,
|
||||
const dawn::Chromaticities &screen, bool show_screen, bool screen_dashed,
|
||||
bool image_dashed)
|
||||
{
|
||||
QImage img(w, h, QImage::Format_ARGB32_Premultiplied);
|
||||
img.fill(Qt::transparent);
|
||||
if (w <= 0 || h <= 0)
|
||||
return img;
|
||||
|
||||
QImage mask(w, h, QImage::Format_ARGB32_Premultiplied);
|
||||
mask.fill(Qt::transparent);
|
||||
{
|
||||
QPainter mp(&mask);
|
||||
mp.setRenderHint(QPainter::Antialiasing);
|
||||
mp.fillPath(locus_path(w, h), Qt::white);
|
||||
}
|
||||
|
||||
for (int py = 0; py < h; ++py) {
|
||||
auto *dst = (QRgb *) img.scanLine(py);
|
||||
const auto *ms = (const QRgb *) mask.constScanLine(py);
|
||||
const double y = double(kYMax) * (1.0 - (py + 0.5) / h);
|
||||
for (int px = 0; px < w; ++px) {
|
||||
if (!qAlpha(ms[px]))
|
||||
continue;
|
||||
const double x = double(kXMax) * (px + 0.5) / w;
|
||||
dst[px] = xy_srgb(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
QPainter p(&img);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
p.drawImage(0, 0, mask);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
|
||||
p.strokePath(locus_path(w, h),
|
||||
QPen(QColor(0, 0, 0, 180), 1.25, Qt::SolidLine, Qt::RoundCap,
|
||||
Qt::RoundJoin));
|
||||
if (show_screen) {
|
||||
QPen pen(QColor(255, 255, 255), 1.8,
|
||||
screen_dashed ? Qt::CustomDashLine : Qt::SolidLine, Qt::RoundCap,
|
||||
Qt::RoundJoin);
|
||||
if (screen_dashed) {
|
||||
pen.setDashPattern({4, 4});
|
||||
pen.setDashOffset(4);
|
||||
}
|
||||
stroke_poly(p, pen, screen, w, h);
|
||||
}
|
||||
{
|
||||
QPen pen(QColor(0, 0, 0), 1.8,
|
||||
image_dashed ? Qt::CustomDashLine : Qt::SolidLine, Qt::RoundCap,
|
||||
Qt::RoundJoin);
|
||||
if (image_dashed)
|
||||
pen.setDashPattern({4, 4});
|
||||
stroke_poly(p, pen, image, w, h);
|
||||
}
|
||||
|
||||
const QPointF wp = xy_to_px(kD65x, kD65y, w, h);
|
||||
p.setPen(QPen(QColor(0, 0, 0), 1.4, Qt::SolidLine, Qt::RoundCap));
|
||||
p.drawLine(wp + QPointF(-7, 0), wp + QPointF(7, 0));
|
||||
p.drawLine(wp + QPointF(0, -7), wp + QPointF(0, 7));
|
||||
return img;
|
||||
}
|
||||
|
||||
static bool
|
||||
same_chroma(const dawn::Chromaticities &a, const dawn::Chromaticities &b)
|
||||
{
|
||||
if (a.model != b.model || a.have_white != b.have_white ||
|
||||
a.have_primaries != b.have_primaries || a.n != b.n)
|
||||
return false;
|
||||
if (a.have_white && (a.wx != b.wx || a.wy != b.wy))
|
||||
return false;
|
||||
for (int i = 0; i < a.n; ++i) {
|
||||
if (a.x[i] != b.x[i] || a.y[i] != b.y[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
CieDiagram::measure(Kit &kit, int max_w, int max_h)
|
||||