Initial commit
Alpine 3.24 Success
Alpine 3.24 Flatpak Success
Arch Linux AUR Success

This commit is contained in:
p
2026-09-07 08:17:40 +02:00
commit 0f2fa009aa
236 changed files with 54951 additions and 0 deletions
+16
View File
@@ -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']
+6
View File
@@ -0,0 +1,6 @@
/.qtcreator
/.flatpak/
/build/
/build-*/
compile_commands.json
.cache/
+6
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# This is the list of dawn's significant contributors.
Přemysl Eric Janouch <p@janouch.name>
+335
View File
@@ -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()
+373
View File
@@ -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
View File
@@ -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.
+50
View File
@@ -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()
+42
View File
@@ -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}")
+68
View File
@@ -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>)
+4
View File
@@ -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"
+50
View File
@@ -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()
+60
View File
@@ -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}")
+46
View File
@@ -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()
+30
View File
@@ -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
View File
@@ -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>
+361
View File
@@ -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()
+96
View File
@@ -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
View File
@@ -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
View File
@@ -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
+29
View File
@@ -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
+393
View File
@@ -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
View File
@@ -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
View File
@@ -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
+188
View File
@@ -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
+791
View File
@@ -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
+255
View File
@@ -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
+30
View File
@@ -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
+481
View File
@@ -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
1 # the 1931 color matching functions at 1nm interval
2 #
3 # the data is from
4 # Wyszecki & Stiles
5 # Color Science, 2nd edition.
6 # Table I(3.3.1) pp. 723-735.
7 #
8 # It was downloaded from http://www.cvrl.org/
9 Wavelength,x,y,z
10 360,0.000129900000,0.000003917000,0.000606100000
11 361,0.000145847000,0.000004393581,0.000680879200
12 362,0.000163802100,0.000004929604,0.000765145600
13 363,0.000184003700,0.000005532136,0.000860012400
14 364,0.000206690200,0.000006208245,0.000966592800
15 365,0.000232100000,0.000006965000,0.001086000000
16 366,0.000260728000,0.000007813219,0.001220586000
17 367,0.000293075000,0.000008767336,0.001372729000
18 368,0.000329388000,0.000009839844,0.001543579000
19 369,0.000369914000,0.000011043230,0.001734286000
20 370,0.000414900000,0.000012390000,0.001946000000
21 371,0.000464158700,0.000013886410,0.002177777000
22 372,0.000518986000,0.000015557280,0.002435809000
23 373,0.000581854000,0.000017442960,0.002731953000
24 374,0.000655234700,0.000019583750,0.003078064000
25 375,0.000741600000,0.000022020000,0.003486000000
26 376,0.000845029600,0.000024839650,0.003975227000
27 377,0.000964526800,0.000028041260,0.004540880000
28 378,0.001094949000,0.000031531040,0.005158320000
29 379,0.001231154000,0.000035215210,0.005802907000
30 380,0.001368000000,0.000039000000,0.006450001000
31 381,0.001502050000,0.000042826400,0.007083216000
32 382,0.001642328000,0.000046914600,0.007745488000
33 383,0.001802382000,0.000051589600,0.008501152000
34 384,0.001995757000,0.000057176400,0.009414544000
35 385,0.002236000000,0.000064000000,0.010549990000
36 386,0.002535385000,0.000072344210,0.011965800000
37 387,0.002892603000,0.000082212240,0.013655870000
38 388,0.003300829000,0.000093508160,0.015588050000
39 389,0.003753236000,0.000106136100,0.017730150000
40 390,0.004243000000,0.000120000000,0.020050010000
41 391,0.004762389000,0.000134984000,0.022511360000
42 392,0.005330048000,0.000151492000,0.025202880000
43 393,0.005978712000,0.000170208000,0.028279720000
44 394,0.006741117000,0.000191816000,0.031897040000
45 395,0.007650000000,0.000217000000,0.036210000000
46 396,0.008751373000,0.000246906700,0.041437710000
47 397,0.010028880000,0.000281240000,0.047503720000
48 398,0.011421700000,0.000318520000,0.054119880000
49 399,0.012869010000,0.000357266700,0.060998030000
50 400,0.014310000000,0.000396000000,0.067850010000
51 401,0.015704430000,0.000433714700,0.074486320000
52 402,0.017147440000,0.000473024000,0.081361560000
53 403,0.018781220000,0.000517876000,0.089153640000
54 404,0.020748010000,0.000572218700,0.098540480000
55 405,0.023190000000,0.000640000000,0.110200000000
56 406,0.026207360000,0.000724560000,0.124613300000
57 407,0.029782480000,0.000825500000,0.141701700000
58 408,0.033880920000,0.000941160000,0.161303500000
59 409,0.038468240000,0.001069880000,0.183256800000
60 410,0.043510000000,0.001210000000,0.207400000000
61 411,0.048995600000,0.001362091000,0.233692100000
62 412,0.055022600000,0.001530752000,0.262611400000
63 413,0.061718800000,0.001720368000,0.294774600000
64 414,0.069212000000,0.001935323000,0.330798500000
65 415,0.077630000000,0.002180000000,0.371300000000
66 416,0.086958110000,0.002454800000,0.416209100000
67 417,0.097176720000,0.002764000000,0.465464200000
68 418,0.108406300000,0.003117800000,0.519694800000
69 419,0.120767200000,0.003526400000,0.579530300000
70 420,0.134380000000,0.004000000000,0.645600000000
71 421,0.149358200000,0.004546240000,0.718483800000
72 422,0.165395700000,0.005159320000,0.796713300000
73 423,0.181983100000,0.005829280000,0.877845900000
74 424,0.198611000000,0.006546160000,0.959439000000
75 425,0.214770000000,0.007300000000,1.039050100000
76 426,0.230186800000,0.008086507000,1.115367300000
77 427,0.244879700000,0.008908720000,1.188497100000
78 428,0.258777300000,0.009767680000,1.258123300000
79 429,0.271807900000,0.010664430000,1.323929600000
80 430,0.283900000000,0.011600000000,1.385600000000
81 431,0.294943800000,0.012573170000,1.442635200000
82 432,0.304896500000,0.013582720000,1.494803500000
83 433,0.313787300000,0.014629680000,1.542190300000
84 434,0.321645400000,0.015715090000,1.584880700000
85 435,0.328500000000,0.016840000000,1.622960000000
86 436,0.334351300000,0.018007360000,1.656404800000
87 437,0.339210100000,0.019214480000,1.685295900000
88 438,0.343121300000,0.020453920000,1.709874500000
89 439,0.346129600000,0.021718240000,1.730382100000
90 440,0.348280000000,0.023000000000,1.747060000000
91 441,0.349599900000,0.024294610000,1.760044600000
92 442,0.350147400000,0.025610240000,1.769623300000
93 443,0.350013000000,0.026958570000,1.776263700000
94 444,0.349287000000,0.028351250000,1.780433400000
95 445,0.348060000000,0.029800000000,1.782600000000
96 446,0.346373300000,0.031310830000,1.782968200000
97 447,0.344262400000,0.032883680000,1.781699800000
98 448,0.341808800000,0.034521120000,1.779198200000
99 449,0.339094100000,0.036225710000,1.775867100000
100 450,0.336200000000,0.038000000000,1.772110000000
101 451,0.333197700000,0.039846670000,1.768258900000
102 452,0.330041100000,0.041768000000,1.764039000000
103 453,0.326635700000,0.043766000000,1.758943800000
104 454,0.322886800000,0.045842670000,1.752466300000
105 455,0.318700000000,0.048000000000,1.744100000000
106 456,0.314025100000,0.050243680000,1.733559500000
107 457,0.308884000000,0.052573040000,1.720858100000
108 458,0.303290400000,0.054980560000,1.705936900000
109 459,0.297257900000,0.057458720000,1.688737200000
110 460,0.290800000000,0.060000000000,1.669200000000
111 461,0.283970100000,0.062601970000,1.647528700000
112 462,0.276721400000,0.065277520000,1.623412700000
113 463,0.268917800000,0.068042080000,1.596022300000
114 464,0.260422700000,0.070911090000,1.564528000000
115 465,0.251100000000,0.073900000000,1.528100000000
116 466,0.240847500000,0.077016000000,1.486111400000
117 467,0.229851200000,0.080266400000,1.439521500000
118 468,0.218407200000,0.083666800000,1.389879900000
119 469,0.206811500000,0.087232800000,1.338736200000
120 470,0.195360000000,0.090980000000,1.287640000000
121 471,0.184213600000,0.094917550000,1.237422300000
122 472,0.173327300000,0.099045840000,1.187824300000
123 473,0.162688100000,0.103367400000,1.138761100000
124 474,0.152283300000,0.107884600000,1.090148000000
125 475,0.142100000000,0.112600000000,1.041900000000
126 476,0.132178600000,0.117532000000,0.994197600000
127 477,0.122569600000,0.122674400000,0.947347300000
128 478,0.113275200000,0.127992800000,0.901453100000
129 479,0.104297900000,0.133452800000,0.856619300000
130 480,0.095640000000,0.139020000000,0.812950100000
131 481,0.087299550000,0.144676400000,0.770517300000
132 482,0.079308040000,0.150469300000,0.729444800000
133 483,0.071717760000,0.156461900000,0.689913600000
134 484,0.064580990000,0.162717700000,0.652104900000
135 485,0.057950010000,0.169300000000,0.616200000000
136 486,0.051862110000,0.176243100000,0.582328600000
137 487,0.046281520000,0.183558100000,0.550416200000
138 488,0.041150880000,0.191273500000,0.520337600000
139 489,0.036412830000,0.199418000000,0.491967300000
140 490,0.032010000000,0.208020000000,0.465180000000
141 491,0.027917200000,0.217119900000,0.439924600000
142 492,0.024144400000,0.226734500000,0.416183600000
143 493,0.020687000000,0.236857100000,0.393882200000
144 494,0.017540400000,0.247481200000,0.372945900000
145 495,0.014700000000,0.258600000000,0.353300000000
146 496,0.012161790000,0.270184900000,0.334857800000
147 497,0.009919960000,0.282293900000,0.317552100000
148 498,0.007967240000,0.295050500000,0.301337500000
149 499,0.006296346000,0.308578000000,0.286168600000
150 500,0.004900000000,0.323000000000,0.272000000000
151 501,0.003777173000,0.338402100000,0.258817100000
152 502,0.002945320000,0.354685800000,0.246483800000
153 503,0.002424880000,0.371698600000,0.234771800000
154 504,0.002236293000,0.389287500000,0.223453300000
155 505,0.002400000000,0.407300000000,0.212300000000
156 506,0.002925520000,0.425629900000,0.201169200000
157 507,0.003836560000,0.444309600000,0.190119600000
158 508,0.005174840000,0.463394400000,0.179225400000
159 509,0.006982080000,0.482939500000,0.168560800000
160 510,0.009300000000,0.503000000000,0.158200000000
161 511,0.012149490000,0.523569300000,0.148138300000
162 512,0.015535880000,0.544512000000,0.138375800000
163 513,0.019477520000,0.565690000000,0.128994200000
164 514,0.023992770000,0.586965300000,0.120075100000
165 515,0.029100000000,0.608200000000,0.111700000000
166 516,0.034814850000,0.629345600000,0.103904800000
167 517,0.041120160000,0.650306800000,0.096667480000
168 518,0.047985040000,0.670875200000,0.089982720000
169 519,0.055378610000,0.690842400000,0.083845310000
170 520,0.063270000000,0.710000000000,0.078249990000
171 521,0.071635010000,0.728185200000,0.073208990000
172 522,0.080462240000,0.745463600000,0.068678160000
173 523,0.089739960000,0.761969400000,0.064567840000
174 524,0.099456450000,0.777836800000,0.060788350000
175 525,0.109600000000,0.793200000000,0.057250010000
176 526,0.120167400000,0.808110400000,0.053904350000
177 527,0.131114500000,0.822496200000,0.050746640000
178 528,0.142367900000,0.836306800000,0.047752760000
179 529,0.153854200000,0.849491600000,0.044898590000
180 530,0.165500000000,0.862000000000,0.042160000000
181 531,0.177257100000,0.873810800000,0.039507280000
182 532,0.189140000000,0.884962400000,0.036935640000
183 533,0.201169400000,0.895493600000,0.034458360000
184 534,0.213365800000,0.905443200000,0.032088720000
185 535,0.225749900000,0.914850100000,0.029840000000
186 536,0.238320900000,0.923734800000,0.027711810000
187 537,0.251066800000,0.932092400000,0.025694440000
188 538,0.263992200000,0.939922600000,0.023787160000
189 539,0.277101700000,0.947225200000,0.021989250000
190 540,0.290400000000,0.954000000000,0.020300000000
191 541,0.303891200000,0.960256100000,0.018718050000
192 542,0.317572600000,0.966007400000,0.017240360000
193 543,0.331438400000,0.971260600000,0.015863640000
194 544,0.345482800000,0.976022500000,0.014584610000
195 545,0.359700000000,0.980300000000,0.013400000000
196 546,0.374083900000,0.984092400000,0.012307230000
197 547,0.388639600000,0.987418200000,0.011301880000
198 548,0.403378400000,0.990312800000,0.010377920000
199 549,0.418311500000,0.992811600000,0.009529306000
200 550,0.433449900000,0.994950100000,0.008749999000
201 551,0.448795300000,0.996710800000,0.008035200000
202 552,0.464336000000,0.998098300000,0.007381600000
203 553,0.480064000000,0.999112000000,0.006785400000
204 554,0.495971300000,0.999748200000,0.006242800000
205 555,0.512050100000,1.000000000000,0.005749999000
206 556,0.528295900000,0.999856700000,0.005303600000
207 557,0.544691600000,0.999304600000,0.004899800000
208 558,0.561209400000,0.998325500000,0.004534200000
209 559,0.577821500000,0.996898700000,0.004202400000
210 560,0.594500000000,0.995000000000,0.003900000000
211 561,0.611220900000,0.992600500000,0.003623200000
212 562,0.627975800000,0.989742600000,0.003370600000
213 563,0.644760200000,0.986444400000,0.003141400000
214 564,0.661569700000,0.982724100000,0.002934800000
215 565,0.678400000000,0.978600000000,0.002749999000
216 566,0.695239200000,0.974083700000,0.002585200000
217 567,0.712058600000,0.969171200000,0.002438600000
218 568,0.728828400000,0.963856800000,0.002309400000
219 569,0.745518800000,0.958134900000,0.002196800000
220 570,0.762100000000,0.952000000000,0.002100000000
221 571,0.778543200000,0.945450400000,0.002017733000
222 572,0.794825600000,0.938499200000,0.001948200000
223 573,0.810926400000,0.931162800000,0.001889800000
224 574,0.826824800000,0.923457600000,0.001840933000
225 575,0.842500000000,0.915400000000,0.001800000000
226 576,0.857932500000,0.907006400000,0.001766267000
227 577,0.873081600000,0.898277200000,0.001737800000
228 578,0.887894400000,0.889204800000,0.001711200000
229 579,0.902318100000,0.879781600000,0.001683067000
230 580,0.916300000000,0.870000000000,0.001650001000
231 581,0.929799500000,0.859861300000,0.001610133000
232 582,0.942798400000,0.849392000000,0.001564400000
233 583,0.955277600000,0.838622000000,0.001513600000
234 584,0.967217900000,0.827581300000,0.001458533000
235 585,0.978600000000,0.816300000000,0.001400000000
236 586,0.989385600000,0.804794700000,0.001336667000
237 587,0.999548800000,0.793082000000,0.001270000000
238 588,1.009089200000,0.781192000000,0.001205000000
239 589,1.018006400000,0.769154700000,0.001146667000
240 590,1.026300000000,0.757000000000,0.001100000000
241 591,1.033982700000,0.744754100000,0.001068800000
242 592,1.040986000000,0.732422400000,0.001049400000
243 593,1.047188000000,0.720003600000,0.001035600000
244 594,1.052466700000,0.707496500000,0.001021200000
245 595,1.056700000000,0.694900000000,0.001000000000
246 596,1.059794400000,0.682219200000,0.000968640000
247 597,1.061799200000,0.669471600000,0.000929920000
248 598,1.062806800000,0.656674400000,0.000886880000
249 599,1.062909600000,0.643844800000,0.000842560000
250 600,1.062200000000,0.631000000000,0.000800000000
251 601,1.060735200000,0.618155500000,0.000760960000
252 602,1.058443600000,0.605314400000,0.000723680000
253 603,1.055224400000,0.592475600000,0.000685920000
254 604,1.050976800000,0.579637900000,0.000645440000
255 605,1.045600000000,0.566800000000,0.000600000000
256 606,1.039036900000,0.553961100000,0.000547866700
257 607,1.031360800000,0.541137200000,0.000491600000
258 608,1.022666200000,0.528352800000,0.000435400000
259 609,1.013047700000,0.515632300000,0.000383466700
260 610,1.002600000000,0.503000000000,0.000340000000
261 611,0.991367500000,0.490468800000,0.000307253300
262 612,0.979331400000,0.478030400000,0.000283160000
263 613,0.966491600000,0.465677600000,0.000265440000
264 614,0.952847900000,0.453403200000,0.000251813300
265 615,0.938400000000,0.441200000000,0.000240000000
266 616,0.923194000000,0.429080000000,0.000229546700
267 617,0.907244000000,0.417036000000,0.000220640000
268 618,0.890502000000,0.405032000000,0.000211960000
269 619,0.872920000000,0.393032000000,0.000202186700
270 620,0.854449900000,0.381000000000,0.000190000000
271 621,0.835084000000,0.368918400000,0.000174213300
272 622,0.814946000000,0.356827200000,0.000155640000
273 623,0.794186000000,0.344776800000,0.000135960000
274 624,0.772954000000,0.332817600000,0.000116853300
275 625,0.751400000000,0.321000000000,0.000100000000
276 626,0.729583600000,0.309338100000,0.000086133330
277 627,0.707588800000,0.297850400000,0.000074600000
278 628,0.685602200000,0.286593600000,0.000065000000
279 629,0.663810400000,0.275624500000,0.000056933330
280 630,0.642400000000,0.265000000000,0.000049999990
281 631,0.621514900000,0.254763200000,0.000044160000
282 632,0.601113800000,0.244889600000,0.000039480000
283 633,0.581105200000,0.235334400000,0.000035720000
284 634,0.561397700000,0.226052800000,0.000032640000
285 635,0.541900000000,0.217000000000,0.000030000000
286 636,0.522599500000,0.208161600000,0.000027653330
287 637,0.503546400000,0.199548800000,0.000025560000
288 638,0.484743600000,0.191155200000,0.000023640000
289 639,0.466193900000,0.182974400000,0.000021813330
290 640,0.447900000000,0.175000000000,0.000020000000
291 641,0.429861300000,0.167223500000,0.000018133330
292 642,0.412098000000,0.159646400000,0.000016200000
293 643,0.394644000000,0.152277600000,0.000014200000
294 644,0.377533300000,0.145125900000,0.000012133330
295 645,0.360800000000,0.138200000000,0.000010000000
296 646,0.344456300000,0.131500300000,0.000007733333
297 647,0.328516800000,0.125024800000,0.000005400000
298 648,0.313019200000,0.118779200000,0.000003200000
299 649,0.298001100000,0.112769100000,0.000001333333
300 650,0.283500000000,0.107000000000,0.000000000000
301 651,0.269544800000,0.101476200000,0.000000000000
302 652,0.256118400000,0.096188640000,0.000000000000
303 653,0.243189600000,0.091122960000,0.000000000000
304 654,0.230727200000,0.086264850000,0.000000000000
305 655,0.218700000000,0.081600000000,0.000000000000
306 656,0.207097100000,0.077120640000,0.000000000000
307 657,0.195923200000,0.072825520000,0.000000000000
308 658,0.185170800000,0.068710080000,0.000000000000
309 659,0.174832300000,0.064769760000,0.000000000000
310 660,0.164900000000,0.061000000000,0.000000000000
311 661,0.155366700000,0.057396210000,0.000000000000
312 662,0.146230000000,0.053955040000,0.000000000000
313 663,0.137490000000,0.050673760000,0.000000000000
314 664,0.129146700000,0.047549650000,0.000000000000
315 665,0.121200000000,0.044580000000,0.000000000000
316 666,0.113639700000,0.041758720000,0.000000000000
317 667,0.106465000000,0.039084960000,0.000000000000
318 668,0.099690440000,0.036563840000,0.000000000000
319 669,0.093330610000,0.034200480000,0.000000000000
320 670,0.087400000000,0.032000000000,0.000000000000
321 671,0.081900960000,0.029962610000,0.000000000000
322 672,0.076804280000,0.028076640000,0.000000000000
323 673,0.072077120000,0.026329360000,0.000000000000
324 674,0.067686640000,0.024708050000,0.000000000000
325 675,0.063600000000,0.023200000000,0.000000000000
326 676,0.059806850000,0.021800770000,0.000000000000
327 677,0.056282160000,0.020501120000,0.000000000000
328 678,0.052971040000,0.019281080000,0.000000000000
329 679,0.049818610000,0.018120690000,0.000000000000
330 680,0.046770000000,0.017000000000,0.000000000000
331 681,0.043784050000,0.015903790000,0.000000000000
332 682,0.040875360000,0.014837180000,0.000000000000
333 683,0.038072640000,0.013810680000,0.000000000000
334 684,0.035404610000,0.012834780000,0.000000000000
335 685,0.032900000000,0.011920000000,0.000000000000
336 686,0.030564190000,0.011068310000,0.000000000000
337 687,0.028380560000,0.010273390000,0.000000000000
338 688,0.026344840000,0.009533311000,0.000000000000
339 689,0.024452750000,0.008846157000,0.000000000000
340 690,0.022700000000,0.008210000000,0.000000000000
341 691,0.021084290000,0.007623781000,0.000000000000
342 692,0.019599880000,0.007085424000,0.000000000000
343 693,0.018237320000,0.006591476000,0.000000000000
344 694,0.016987170000,0.006138485000,0.000000000000
345 695,0.015840000000,0.005723000000,0.000000000000
346 696,0.014790640000,0.005343059000,0.000000000000
347 697,0.013831320000,0.004995796000,0.000000000000
348 698,0.012948680000,0.004676404000,0.000000000000
349 699,0.012129200000,0.004380075000,0.000000000000
350 700,0.011359160000,0.004102000000,0.000000000000
351 701,0.010629350000,0.003838453000,0.000000000000
352 702,0.009938846000,0.003589099000,0.000000000000
353 703,0.009288422000,0.003354219000,0.000000000000
354 704,0.008678854000,0.003134093000,0.000000000000
355 705,0.008110916000,0.002929000000,0.000000000000
356 706,0.007582388000,0.002738139000,0.000000000000
357 707,0.007088746000,0.002559876000,0.000000000000
358 708,0.006627313000,0.002393244000,0.000000000000
359 709,0.006195408000,0.002237275000,0.000000000000
360 710,0.005790346000,0.002091000000,0.000000000000
361 711,0.005409826000,0.001953587000,0.000000000000
362 712,0.005052583000,0.001824580000,0.000000000000
363 713,0.004717512000,0.001703580000,0.000000000000
364 714,0.004403507000,0.001590187000,0.000000000000
365 715,0.004109457000,0.001484000000,0.000000000000
366 716,0.003833913000,0.001384496000,0.000000000000
367 717,0.003575748000,0.001291268000,0.000000000000
368 718,0.003334342000,0.001204092000,0.000000000000
369 719,0.003109075000,0.001122744000,0.000000000000
370 720,0.002899327000,0.001047000000,0.000000000000
371 721,0.002704348000,0.000976589600,0.000000000000
372 722,0.002523020000,0.000911108800,0.000000000000
373 723,0.002354168000,0.000850133200,0.000000000000
374 724,0.002196616000,0.000793238400,0.000000000000
375 725,0.002049190000,0.000740000000,0.000000000000
376 726,0.001910960000,0.000690082700,0.000000000000
377 727,0.001781438000,0.000643310000,0.000000000000
378 728,0.001660110000,0.000599496000,0.000000000000
379 729,0.001546459000,0.000558454700,0.000000000000
380 730,0.001439971000,0.000520000000,0.000000000000
381 731,0.001340042000,0.000483913600,0.000000000000
382 732,0.001246275000,0.000450052800,0.000000000000
383 733,0.001158471000,0.000418345200,0.000000000000
384 734,0.001076430000,0.000388718400,0.000000000000
385 735,0.000999949300,0.000361100000,0.000000000000
386 736,0.000928735800,0.000335383500,0.000000000000
387 737,0.000862433200,0.000311440400,0.000000000000
388 738,0.000800750300,0.000289165600,0.000000000000
389 739,0.000743396000,0.000268453900,0.000000000000
390 740,0.000690078600,0.000249200000,0.000000000000
391 741,0.000640515600,0.000231301900,0.000000000000
392 742,0.000594502100,0.000214685600,0.000000000000
393 743,0.000551864600,0.000199288400,0.000000000000
394 744,0.000512429000,0.000185047500,0.000000000000
395 745,0.000476021300,0.000171900000,0.000000000000
396 746,0.000442453600,0.000159778100,0.000000000000
397 747,0.000411511700,0.000148604400,0.000000000000
398 748,0.000382981400,0.000138301600,0.000000000000
399 749,0.000356649100,0.000128792500,0.000000000000
400 750,0.000332301100,0.000120000000,0.000000000000
401 751,0.000309758600,0.000111859500,0.000000000000
402 752,0.000288887100,0.000104322400,0.000000000000
403 753,0.000269539400,0.000097335600,0.000000000000
404 754,0.000251568200,0.000090845870,0.000000000000
405 755,0.000234826100,0.000084800000,0.000000000000
406 756,0.000219171000,0.000079146670,0.000000000000
407 757,0.000204525800,0.000073858000,0.000000000000
408 758,0.000190840500,0.000068916000,0.000000000000
409 759,0.000178065400,0.000064302670,0.000000000000
410 760,0.000166150500,0.000060000000,0.000000000000
411 761,0.000155023600,0.000055981870,0.000000000000
412 762,0.000144621900,0.000052225600,0.000000000000
413 763,0.000134909800,0.000048718400,0.000000000000
414 764,0.000125852000,0.000045447470,0.000000000000
415 765,0.000117413000,0.000042400000,0.000000000000
416 766,0.000109551500,0.000039561040,0.000000000000
417 767,0.000102224500,0.000036915120,0.000000000000
418 768,0.000095394450,0.000034448680,0.000000000000
419 769,0.000089023900,0.000032148160,0.000000000000
420 770,0.000083075270,0.000030000000,0.000000000000
421 771,0.000077512690,0.000027991250,0.000000000000
422 772,0.000072313040,0.000026113560,0.000000000000
423 773,0.000067457780,0.000024360240,0.000000000000
424 774,0.000062928440,0.000022724610,0.000000000000
425 775,0.000058706520,0.000021200000,0.000000000000
426 776,0.000054770280,0.000019778550,0.000000000000
427 777,0.000051099180,0.000018452850,0.000000000000
428 778,0.000047676540,0.000017216870,0.000000000000
429 779,0.000044485670,0.000016064590,0.000000000000
430 780,0.000041509940,0.000014990000,0.000000000000
431 781,0.000038733240,0.000013987280,0.000000000000
432 782,0.000036142030,0.000013051550,0.000000000000
433 783,0.000033723520,0.000012178180,0.000000000000
434 784,0.000031464870,0.000011362540,0.000000000000
435 785,0.000029353260,0.000010600000,0.000000000000
436 786,0.000027375730,0.000009885877,0.000000000000
437 787,0.000025524330,0.000009217304,0.000000000000
438 788,0.000023793760,0.000008592362,0.000000000000
439 789,0.000022178700,0.000008009133,0.000000000000
440 790,0.000020673830,0.000007465700,0.000000000000
441 791,0.000019272260,0.000006959567,0.000000000000
442 792,0.000017966400,0.000006487995,0.000000000000
443 793,0.000016749910,0.000006048699,0.000000000000
444 794,0.000015616480,0.000005639396,0.000000000000
445 795,0.000014559770,0.000005257800,0.000000000000
446 796,0.000013573870,0.000004901771,0.000000000000
447 797,0.000012654360,0.000004569720,0.000000000000
448 798,0.000011797230,0.000004260194,0.000000000000
449 799,0.000010998440,0.000003971739,0.000000000000
450 800,0.000010253980,0.000003702900,0.000000000000
451 801,0.000009559646,0.000003452163,0.000000000000
452 802,0.000008912044,0.000003218302,0.000000000000
453 803,0.000008308358,0.000003000300,0.000000000000
454 804,0.000007745769,0.000002797139,0.000000000000
455 805,0.000007221456,0.000002607800,0.000000000000
456 806,0.000006732475,0.000002431220,0.000000000000
457 807,0.000006276423,0.000002266531,0.000000000000
458 808,0.000005851304,0.000002113013,0.000000000000
459 809,0.000005455118,0.000001969943,0.000000000000
460 810,0.000005085868,0.000001836600,0.000000000000
461 811,0.000004741466,0.000001712230,0.000000000000
462 812,0.000004420236,0.000001596228,0.000000000000
463 813,0.000004120783,0.000001488090,0.000000000000
464 814,0.000003841716,0.000001387314,0.000000000000
465 815,0.000003581652,0.000001293400,0.000000000000
466 816,0.000003339127,0.000001205820,0.000000000000
467 817,0.000003112949,0.000001124143,0.000000000000
468 818,0.000002902121,0.000001048009,0.000000000000
469 819,0.000002705645,0.000000977058,0.000000000000
470 820,0.000002522525,0.000000910930,0.000000000000
471 821,0.000002351726,0.000000849251,0.000000000000
472 822,0.000002192415,0.000000791721,0.000000000000
473 823,0.000002043902,0.000000738090,0.000000000000
474 824,0.000001905497,0.000000688110,0.000000000000
475 825,0.000001776509,0.000000641530,0.000000000000
476 826,0.000001656215,0.000000598090,0.000000000000
477 827,0.000001544022,0.000000557575,0.000000000000
478 828,0.000001439440,0.000000519808,0.000000000000
479 829,0.000001341977,0.000000484612,0.000000000000
480 830,0.000001251141,0.000000451810,0.000000000000
+49
View File
@@ -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
}
+317
View File
@@ -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
+103
View File
@@ -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
+186
View File
@@ -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
+70
View File
@@ -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
+59
View File
@@ -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
+20
View File
@@ -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"
+19
View File
@@ -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

+94
View File
@@ -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
View File
@@ -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
View File
@@ -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
+62
View File
@@ -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
View File
@@ -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
+35
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+195
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+181
View File
@@ -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
+308
View File
@@ -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)