Initial commit

This commit is contained in:
2026-08-09 06:48:31 +02:00
commit e3836282f3
201 changed files with 42582 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']
+4
View File
@@ -0,0 +1,4 @@
/build/
/build-*/
compile_commands.json
.cache/
+3
View File
@@ -0,0 +1,3 @@
[submodule "submodules/wuffs-mirror-release-c"]
path = submodules/wuffs-mirror-release-c
url = https://github.com/google/wuffs-mirror-release-c
+2
View File
@@ -0,0 +1,2 @@
# This is the list of dawn's significant contributors.
Přemysl Eric Janouch <p@janouch.name>
+218
View File
@@ -0,0 +1,218 @@
cmake_minimum_required(VERSION 3.28)
project(dawn
VERSION 0.1.0
DESCRIPTION "Colour-managed image browser"
LANGUAGES C CXX
)
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(APPLE)
# 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_BUILD_WITH_INSTALL_RPATH OFF)
set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../Frameworks")
else()
# Executables find libdn beside themselves.
# TODO(p): This is only for development purposes, clean it up.
set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
set(CMAKE_INSTALL_RPATH "$ORIGIN")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(-Wall -Wextra -Wpedantic
-Wno-unused-parameter -Wno-missing-field-initializers)
endif()
if(MSVC)
message(FATAL_ERROR "MSVC is not supported; use MinGW-w64")
endif()
# MSYS2 ucrt64 prefix from Win64Depends.sh (cross) so host .pc files cannot leak.
if(WIN32 AND CMAKE_CROSSCOMPILING)
set(DN_MINGW_PREFIX "${CMAKE_BINARY_DIR}/ucrt64")
if(NOT EXISTS "${DN_MINGW_PREFIX}/lib/pkgconfig")
message(FATAL_ERROR
"MSYS2 prefix missing at ${DN_MINGW_PREFIX}\n"
"Run: sh cmake/Win64Depends.sh ${CMAKE_BINARY_DIR}")
endif()
list(APPEND CMAKE_PREFIX_PATH "${DN_MINGW_PREFIX}")
list(APPEND CMAKE_FIND_ROOT_PATH "${DN_MINGW_PREFIX}")
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${CMAKE_BINARY_DIR}")
set(DN_MINGW_PCPATH
"${DN_MINGW_PREFIX}/share/pkgconfig:${DN_MINGW_PREFIX}/lib/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "${DN_MINGW_PCPATH}")
set(ENV{PKG_CONFIG_LIBDIR} "${DN_MINGW_PCPATH}")
if(NOT QT_HOST_PATH)
set(QT_HOST_PATH "/usr" CACHE PATH "Host Qt for moc/rcc when cross-compiling")
endif()
set(QT_NO_QTPATHS_DEPLOYMENT_WARNING ON)
endif()
# Dependency fetching is centralised here in the root CMakeLists.txt.
find_package(PkgConfig REQUIRED)
find_package(Vulkan REQUIRED)
find_package(Qt6 6.11 REQUIRED COMPONENTS Gui OPTIONAL_COMPONENTS WaylandClient)
find_program(AWK NAMES awk REQUIRED)
include(cmake/LxdrGenerate.cmake)
find_program(GLSLANG_VALIDATOR
NAMES glslangValidator glslang
HINTS ${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}
REQUIRED
)
set(DN_WITH_WAYLAND "${Qt6WaylandClient_FOUND}")
if (DN_WITH_WAYLAND)
pkg_check_modules(DN_WAYLAND REQUIRED IMPORTED_TARGET wayland-client)
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(dn_optional_pkg stem doc module)
pkg_check_modules(${stem} IMPORTED_TARGET ${module})
option(DN_WITH_${stem} "${doc}" ${${stem}_FOUND})
if(DN_WITH_${stem} AND NOT ${stem}_FOUND)
message(FATAL_ERROR "DN_WITH_${stem}=ON but ${module} was not found")
endif()
endmacro()
dn_optional_pkg(LIBRSVG "SVG via librsvg" librsvg-2.0)
dn_optional_pkg(XCURSOR "Xcursor support" xcursor)
dn_optional_pkg(LIBHEIF "HEIF/AVIF support" libheif)
dn_optional_pkg(LIBTIFF "TIFF support" libtiff-4)
dn_optional_pkg(GDKPIXBUF "gdk-pixbuf fallback" gdk-pixbuf-2.0)
dn_optional_pkg(LIBRAW "LibRaw support" libraw)
include(FetchContent)
# The GPL fast float plugin improves performance a ton,
# but distributions usually omit it.
option(DN_BUNDLED_LCMS2 "Use a fast bundled lcms2 instead of the system one" ON)
set(DN_WITH_LCMS2_FAST_FLOAT FALSE)
set(DN_WITH_LCMS2_THREADED FALSE)
add_library(dn_lcms2 INTERFACE)
set(DN_LCMS2_TARGET dn_lcms2)
if(NOT DN_BUNDLED_LCMS2)
pkg_check_modules(LCMS2 REQUIRED IMPORTED_TARGET lcms2)
target_link_libraries(dn_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(DN_WITH_LCMS2_FAST_FLOAT TRUE)
target_link_libraries(dn_lcms2 INTERFACE
"${LCMS2_FAST_FLOAT_LIB}")
target_include_directories(dn_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)
if(NOT TARGET lcms2 OR NOT TARGET lcms2_fast_float OR NOT TARGET lcms2_threaded)
message(FATAL_ERROR
"Bundled lcms2 must provide targets 'lcms2', 'lcms2_fast_float' and 'lcms2_threaded'")
endif()
set(DN_WITH_LCMS2_FAST_FLOAT TRUE)
set(DN_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>)
target_link_libraries(dn_lcms2 INTERFACE
lcms2 lcms2_fast_float lcms2_threaded)
endif()
option(DN_WITH_JPEG_QS "Bundled JPEG Quant Smooth" ON)
if(DN_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
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/jpegqs/CMakeLists.txt"
<SOURCE_DIR>/CMakeLists.txt
COMMAND
${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_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()
if(UNIX AND NOT APPLE)
set(DN_WITH_SINGLE_INSTANCE TRUE)
else()
set(DN_WITH_SINGLE_INSTANCE FALSE)
endif()
file(SHA256 "${CMAKE_SOURCE_DIR}/ipc/common.lxdr" DN_IPC_LXDR_H1)
file(SHA256 "${CMAKE_SOURCE_DIR}/ipc/instance.lxdr" DN_IPC_LXDR_H2)
string(SHA256 DN_IPC_LXDR_COMBINED "${DN_IPC_LXDR_H1}${DN_IPC_LXDR_H2}")
string(SUBSTRING "${DN_IPC_LXDR_COMBINED}" 0 12 DN_IPC_LXDR_PREFIX)
set(DN_IPC_BUILD_ID "dawn ${PROJECT_VERSION} ${DN_IPC_LXDR_PREFIX}")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/dn-config.h.in"
"${PROJECT_BINARY_DIR}/dn-config.h"
)
add_subdirectory(libdn)
add_subdirectory(dnthumbd)
add_subdirectory(dn)
if(WIN32)
include(cmake/Win32Install.cmake)
endif()
if(APPLE)
include(cmake/DarwinInstall.cmake)
endif()
include(CTest)
if(BUILD_TESTING)
add_subdirectory(tests)
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.
+114
View File
@@ -0,0 +1,114 @@
Dawn
====
:compact-option:
'Dawn' is a multiplatform colour-managed image browser, with very high standards
for fidelity and responsivity.
This repository deliberately stays a mess until we reach our first big milestone
and clean it up. The project is AI-accelerated, though design-wise it is
an amalgamation of older efforts and desires.
The project is '`daily driven`' and comes with a promise of basic maintenance.
Also see link:doc/plan.adoc[Plan],
and a detailed link:doc/rationale.adoc[Rationale].
[quote,Alan Kay]
____
They're all about this idea, which most programmers have that's wrong, is that
you're going to write the program the right way. Nothing's going to be wrong.
_The whole idea is to make a fucking mess._
____
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 LoHalo is an awesome compromise for upscaling
of 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.;
resolutions up to 65535×65535.
- *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 found to be distracting, frustrating, and even despised.
Packages
--------
Regular releases are sporadic. git master should be stable enough.
On Windows, if your account is '`high integrity`' and you have no true Vulkan
backend installed, manually rename 'vk_swiftshader.dll' to 'vulkan-1.dll'.
You will obtain software rendering.
Building
--------
Build-only dependencies: CMake >= 3.28, a C++20 compiler, pkg-config, awk,
glslang, wayland-protocols + wayland-scanner (Wayland),
rsvg-convert (*nix/Windows), icoutils (Windows) +
Runtime dependencies: Qt6Gui >= 6.11, vulkan, lcms2 (bundled by default),
libjpeg, libwebp, libwebpdemux, resvg, zlib +
*nix: colord, shared-mime-info, Qt6WaylandClient + wayland-client (optional) +
Optional dependencies: librsvg-2.0, xcursor, libheif, libtiff-4, libraw,
gdk-pixbuf-2.0
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DDN_BUNDLED_LCMS2=ON
$ cmake --build build
Configuration
-------------
See link:doc/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 be able to see the finer steps.
The bottom row should look more like '`linear 188`' (a popular bug).
image::doc/p3-srgb-ramps.png[Test image: 10-bit RGB ramps, scaling test]
Note that 'Dawn' currently implements Bayer dithering, so you might not
be able to distinguish an 8-bit swapchain.
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 heavy-weight.
'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/
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 license, the text of
which is included within the package along with the list of authors.
+87
View File
@@ -0,0 +1,87 @@
# 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()
if(NOT _dn_mime_dir)
message(FATAL_ERROR
"share/mime/globs2 or subclasses missing (install shared-mime-info)")
endif()
install(FILES
"${_dn_mime_dir}/globs2"
"${_dn_mime_dir}/subclasses"
DESTINATION "${_dn_bundle}/Contents/Resources/share/mime")
unset(_dn_mime_dir)
unset(_dn_smi_datadir)
unset(_dn_root)
# macdeployqt otherwise copies imageformats/virtualkeyboard/styles and
# pulls QtSvg, QtPdf, QtQuick, QtWidgets, QtNetwork with them.
if(TARGET Qt6::QCocoaIntegrationPlugin)
install(IMPORTED_RUNTIME_ARTIFACTS Qt6::QCocoaIntegrationPlugin
LIBRARY DESTINATION "${_dn_bundle}/Contents/PlugIns/platforms"
RUNTIME DESTINATION "${_dn_bundle}/Contents/PlugIns/platforms")
else()
set(_dn_qcocoa
"${QT6_INSTALL_PREFIX}/${QT6_INSTALL_PLUGINS}/platforms/libqcocoa.dylib")
if(NOT EXISTS "${_dn_qcocoa}")
message(FATAL_ERROR "libqcocoa.dylib not found at ${_dn_qcocoa}")
endif()
install(FILES "${_dn_qcocoa}"
DESTINATION "${_dn_bundle}/Contents/PlugIns/platforms")
unset(_dn_qcocoa)
endif()
# 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}")
unset(_dn_deploy_dn)
set(CPACK_PACKAGE_VENDOR "dawn")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_GENERATOR "TGZ;ZIP")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
include(CPack)
+40
View File
@@ -0,0 +1,40 @@
# Public Domain
function (icon_to_png name svg size output_dir output)
set (_dimensions "${size}x${size}")
set (_png_path "${output_dir}/hicolor/${_dimensions}/apps")
set (_png "${_png_path}/${name}.png")
set (${output} "${_png}" PARENT_SCOPE)
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (rsvg_convert_EXECUTABLE rsvg-convert ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${_png}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_png_path}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png}"
"--width=${size}" "--height=${size}" -- "${svg}"
DEPENDS "${svg}"
COMMENT "Generating ${name} ${_dimensions} application icon" VERBATIM)
endfunction ()
# You should include a 256x256 icon--which takes less space as raw PNG.
function (icon_for_win32 ico pngs pngs_raw)
set (_raws)
foreach (png ${pngs_raw})
list (APPEND _raws "--raw=${png}")
endforeach ()
set (_find_program_REQUIRE)
if (NOT ${CMAKE_VERSION} VERSION_LESS 3.18.0)
set (_find_program_REQUIRE REQUIRED)
endif ()
find_program (icotool_EXECUTABLE icotool ${_find_program_REQUIRE})
add_custom_command (OUTPUT "${ico}"
COMMAND ${icotool_EXECUTABLE} -c -o "${ico}" ${_raws} -- ${pngs}
DEPENDS ${pngs} ${pngs_raw}
COMMENT "Generating Windows program icon" VERBATIM)
endfunction ()
+62
View File
@@ -0,0 +1,62 @@
#
# LxdrGenerate.cmake: build-time LibertyXDR C++ header generation
#
# Copyright The dawn Authors
# SPDX-License-Identifier: MPL-2.0
#
# dn_lxdr_generate(<out_header> <namespace> <prefix_camel> <lxdr>...)
#
# Writes <out_header> (typically
# ${CMAKE_BINARY_DIR}/generated/ipc/<name>.lxdr.hpp) from the listed
# .lxdr files. Does not attach the header to any compile target.
#
find_program(DN_CLANG_FORMAT NAMES clang-format)
function(dn_lxdr_generate out_header namespace prefix_camel)
if(NOT AWK)
message(FATAL_ERROR "dn_lxdr_generate requires AWK")
endif()
if(NOT ARGN)
message(FATAL_ERROR "dn_lxdr_generate: no .lxdr inputs")
endif()
set(lxdrgen "${PROJECT_SOURCE_DIR}/ipc/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)
set(format_cmd)
if(DN_CLANG_FORMAT)
set(format_cmd
COMMAND "${DN_CLANG_FORMAT}"
"--style=file:${PROJECT_SOURCE_DIR}/.clang-format"
-i "${out_header}")
endif()
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}"
${format_cmd}
DEPENDS ${lxdr_abs} "${lxdrgen}" "${lxdrgen_cpp}"
COMMENT "Generating ${out_name}"
VERBATIM
)
add_custom_target(dn_lxdr_${out_ident} DEPENDS "${out_header}")
endfunction()
+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; MinGW/MSVC names on disk are lowercase.
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()
+72
View File
@@ -0,0 +1,72 @@
# Windows ZIP: copy MinGW prefix DLLs, then prune. The C++ runtime is the
# MSYS2 ucrt64 one; do not overlay the host toolchain's copies.
install(TARGETS dn libdn dnthumbd
RUNTIME DESTINATION .)
install(DIRECTORY "${DN_MINGW_PREFIX}/bin/"
DESTINATION .
FILES_MATCHING
PATTERN "*.dll"
PATTERN "vulkan-1.dll" EXCLUDE)
if(NOT EXISTS "${DN_MINGW_PREFIX}/bin/wperl.exe"
OR NOT EXISTS "${DN_MINGW_PREFIX}/bin/exiftool"
OR NOT EXISTS "${DN_MINGW_PREFIX}/lib/perl5")
message(FATAL_ERROR
"Bundled Perl or ExifTool missing at ${DN_MINGW_PREFIX}\n"
"Run: sh cmake/Win64Depends.sh ${CMAKE_BINARY_DIR}")
endif()
install(FILES
"${DN_MINGW_PREFIX}/bin/wperl.exe"
"${DN_MINGW_PREFIX}/bin/exiftool"
DESTINATION .)
install(DIRECTORY "${DN_MINGW_PREFIX}/lib/perl5/"
DESTINATION lib/perl5)
# QPA is dlopend; the cleanup walker will not see it from the exe.
if(TARGET Qt6::QWindowsIntegrationPlugin)
install(IMPORTED_RUNTIME_ARTIFACTS Qt6::QWindowsIntegrationPlugin
RUNTIME DESTINATION platforms
LIBRARY DESTINATION platforms)
else()
set(_dn_qwindows
"${QT6_INSTALL_PREFIX}/${QT6_INSTALL_PLUGINS}/platforms/qwindows.dll")
if(NOT EXISTS "${_dn_qwindows}")
message(FATAL_ERROR "qwindows.dll not found at ${_dn_qwindows}")
endif()
install(FILES "${_dn_qwindows}"
DESTINATION platforms)
unset(_dn_qwindows)
endif()
# Not a DLL; dump above does not copy it. SwiftShader + MSVC CRT come from
# ucrt64/bin via Win64Depends.sh and the directory install.
if(NOT EXISTS "${DN_MINGW_PREFIX}/bin/vk_swiftshader_icd.json")
message(FATAL_ERROR
"vk_swiftshader_icd.json missing at ${DN_MINGW_PREFIX}/bin\n"
"Run: sh cmake/Win64Depends.sh ${CMAKE_BINARY_DIR}")
endif()
install(FILES "${DN_MINGW_PREFIX}/bin/vk_swiftshader_icd.json"
DESTINATION .)
if(NOT EXISTS "${DN_MINGW_PREFIX}/share/mime/globs2"
OR NOT EXISTS "${DN_MINGW_PREFIX}/share/mime/subclasses")
message(FATAL_ERROR
"share/mime/globs2 or subclasses missing at ${DN_MINGW_PREFIX}\n"
"Run: sh cmake/Win64Depends.sh ${CMAKE_BINARY_DIR}")
endif()
install(FILES
"${DN_MINGW_PREFIX}/share/mime/globs2"
"${DN_MINGW_PREFIX}/share/mime/subclasses"
DESTINATION share/mime)
install(SCRIPT "${CMAKE_SOURCE_DIR}/cmake/Win32Cleanup.cmake")
set(CPACK_PACKAGE_VENDOR "dawn")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_GENERATOR ZIP)
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
include(CPack)
+262
View File
@@ -0,0 +1,262 @@
#!/bin/sh -e
# Win64Depends.sh: MSYS2 ucrt64 prefix + resvg, SwiftShader, MSVC CRT,
# shared-mime-info for MinGW-w64 cross builds.
#
# Usage: sh cmake/Win64Depends.sh <build-dir>
# Example: sh dawn/cmake/Win64Depends.sh build-mingw
#
# Host: awk, curl, bsdtar, sha256sum, x86_64-w64-mingw32-gcc, 7z,
# update-mime-database (shared-mime-info).
# resvg uses a rustup toolchain in the build tree (Arch rustc cannot load
# upstream rust-std).
set -e
repository=https://repo.msys2.org/mingw/ucrt64/
pkg=mingw-w64-ucrt-x86_64
resvg_ver=0.48.1
resvg_url="https://github.com/linebender/resvg/archive/refs/tags/v${resvg_ver}.tar.gz"
resvg_sha256=40dafea6b4b9d01e9d28b6d49f1e912daf3e9055676ad9179a5a2db6e7386945
swiftshader_url=https://github.com/jakoch/rasterizers/releases/download/20260731/swiftshader-win64-5.0.0.1.zip
swiftshader_sha256=63e97af8b88c2c8cbc61495fc5ef4fad1ce60a874f2c19207cd072c46944065f
vcredist_url=https://aka.ms/vs/17/release/vc_redist.x64.exe
vcredist_sha256=cc0ff0eb1dc3f5188ae6300faef32bf5beeba4bdd6e8e445a9184072096b713b
exiftool_ver=13.59
exiftool_url="https://sourceforge.net/projects/exiftool/files/Image-ExifTool-${exiftool_ver}.tar.gz/download"
exiftool_sha256=668ea3acececb7235fbd0f4900e72d5f12c9b07e5c778fd36cb1e9b5828fd65a
status() {
echo "$(tput bold)-- $*$(tput sgr0)" >&2
}
if [ -z "$1" ]
then
echo "usage: $0 <build-dir>" >&2
exit 1
fi
builddir=$(realpath "$1")
msys2_root=$builddir/ucrt64
mkdir -p "$msys2_root"
cd "$msys2_root"
dbsync() {
status Fetching repository DB
[ -f db.tsv ] || curl -# "$repository/ucrt64.db" | bsdtar -xOf- | awk '
function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
' > db.tsv
}
fetch() {
status Resolving "$@"
mkdir -p packages
awk -F'\t' 'function get(name, i, a) {
if (visited[name]++ || !(name in filenames)) return
print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
} BEGIN { while ((getline < "db.tsv") > 0) {
filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
while IFS= read -r name
do
status Fetching "$name"
[ -f "packages/$name" ] || curl -#o "packages/$name" "$repository/$name"
done
}
verify() {
status Verifying checksums
sha256sum --ignore-missing --quiet -c db.sums
}
extract() {
status Extracting packages
for subdir in *
do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
done
while IFS= read -r name
do bsdtar -xf "packages/$name" --strip-components 1 \
--exclude '*/share/man' --exclude '*/share/doc'
done < db.want
}
setup_rustup() {
export RUSTUP_HOME=$builddir/rustup
export CARGO_HOME=$builddir/cargo
export PATH="$CARGO_HOME/bin:$PATH"
if [ -x "$CARGO_HOME/bin/rustc" ] && \
"$CARGO_HOME/bin/rustc" --print target-list >/dev/null 2>&1 && \
"$CARGO_HOME/bin/rustc" --print target-libdir \
--target x86_64-pc-windows-gnu >/dev/null 2>&1
then
return
fi
status rustup + x86_64-pc-windows-gnu
export RUSTUP_INIT_SKIP_PATH_CHECK=yes
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable
rustup target add x86_64-pc-windows-gnu
}
install_resvg() {
prefix=$msys2_root
if [ -f "$prefix/bin/resvg.dll" ]
then
status "resvg already in prefix"
return
fi
src=$builddir/resvg-$resvg_ver
tarball=$builddir/resvg-$resvg_ver.tar.gz
[ -f "$tarball" ] || curl -#L -o "$tarball" "$resvg_url"
echo "$resvg_sha256 $tarball" | sha256sum -c
[ -d "$src" ] || bsdtar -xf "$tarball" -C "$builddir"
setup_rustup
status Building resvg $resvg_ver C API
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc
export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc
export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc-ar
unset RUSTC
export CARGO_TARGET_DIR=$src/target
( cd "$src/crates/c-api" && cargo build --release --target x86_64-pc-windows-gnu )
rel=$src/target/x86_64-pc-windows-gnu/release
mkdir -p "$prefix/include/resvg" "$prefix/lib/pkgconfig" "$prefix/bin"
cp "$src/crates/c-api/resvg.h" "$prefix/include/resvg/resvg.h"
cp "$rel/resvg.dll" "$prefix/bin/resvg.dll"
cp "$rel/libresvg.dll.a" "$prefix/lib/libresvg.dll.a"
cat >"$prefix/lib/pkgconfig/resvg.pc" <<-EOF
prefix=/ucrt64
exec_prefix=\${prefix}
libdir=\${prefix}/lib
includedir=\${prefix}/include
Name: resvg
Description: SVG rendering library (C API)
Version: $resvg_ver
Libs: -L\${libdir} -lresvg
Cflags: -I\${includedir}/resvg
EOF
}
install_swiftshader() {
mkdir -p "$msys2_root/bin"
if [ -f "$msys2_root/bin/vk_swiftshader.dll" ]
then
status "SwiftShader already in prefix"
else
zip=$builddir/swiftshader-win64-5.0.0.1.zip
[ -f "$zip" ] || curl -#L -o "$zip" "$swiftshader_url"
echo "$swiftshader_sha256 $zip" | sha256sum -c
status SwiftShader ICD
bsdtar -xf "$zip" -C "$msys2_root/bin" vk_swiftshader.dll
fi
cat >"$msys2_root/bin/vk_swiftshader_icd.json" <<'EOF'
{
"file_format_version": "1.0.0",
"ICD": {
"library_path": ".\\vk_swiftshader.dll",
"api_version": "1.1.0"
}
}
EOF
}
run7z() {
cmd=$1
shift
"$cmd" x "$@" || {
r=$?
[ "$r" -le 1 ] || exit "$r"
}
}
install_msvc_crt() {
if [ -f "$msys2_root/bin/msvcp140.dll" ]
then
status "MSVC CRT already in prefix"
return
fi
z=
for c in 7z 7za 7zz
do
if command -v "$c" >/dev/null
then
z=$c
break
fi
done
if [ -z "$z" ]
then
echo "7z required to peel vc_redist.x64.exe" >&2
exit 1
fi
exe=$builddir/vc_redist.x64.exe
[ -f "$exe" ] || curl -#L -o "$exe" "$vcredist_url"
echo "$vcredist_sha256 $exe" | sha256sum -c
work=$builddir/msvc-crt-work
rm -rf "$work"
mkdir -p "$work/s" "$work/c" "$work/a"
status "Peeling VC++ CRT"
run7z "$z" -t# -y "-o$work/s" "$exe"
run7z "$z" -y "-o$work/c" "$work/s/4.cab"
run7z "$z" -y "-o$work/a" "$work/c/a12"
mkdir -p "$msys2_root/bin"
for n in msvcp140 vcruntime140 vcruntime140_1
do cp "$work/a/${n}.dll_amd64" "$msys2_root/bin/${n}.dll"
done
rm -rf "$work"
}
install_exiftool() {
version_file=$msys2_root/.exiftool-version
installed_ver=
[ ! -f "$version_file" ] || installed_ver=$(cat "$version_file")
if [ -f "$msys2_root/bin/exiftool" ] && \
[ "$installed_ver" = "$exiftool_ver" ]
then
status "ExifTool already in prefix"
return
fi
tarball=$builddir/exiftool-$exiftool_ver.tar.gz
[ -f "$tarball" ] || curl -#L -o "$tarball" "$exiftool_url"
echo "$exiftool_sha256 $tarball" | sha256sum -c
work=$builddir/exiftool-$exiftool_ver
rm -rf "$work"
mkdir -p "$work" "$msys2_root/bin" "$msys2_root/lib/perl5/site_perl"
bsdtar -xf "$tarball" -C "$work" --strip-components 1
cp "$work/exiftool" "$msys2_root/bin/exiftool"
cp -R "$work/lib/." "$msys2_root/lib/perl5/site_perl/"
echo "$exiftool_ver" > "$version_file"
rm -rf "$work"
}
if [ -f "$msys2_root/bin/Qt6Gui.dll" ] && \
[ -f "$msys2_root/bin/wperl.exe" ] && \
[ -d "$msys2_root/lib/perl5" ]
then
status "MSYS2 prefix already extracted"
else
dbsync
fetch $pkg-qt6-base $pkg-vulkan-loader $pkg-vulkan-headers \
$pkg-libjpeg-turbo $pkg-libwebp $pkg-zlib \
$pkg-libheif $pkg-libraw \
$pkg-gcc-libs $pkg-perl $pkg-perl-win32-api
# XML only; do not follow MSYS2 glib/python deps. Host u-m-d compiles it.
mime=$(awk -F'\t' -v n="$pkg-shared-mime-info" \
'$1 == n { print $2; exit }' db.tsv)
echo "$mime" >> db.want
status Fetching "$mime"
[ -f "packages/$mime" ] || curl -#o "packages/$mime" "$repository/$mime"
verify
extract
fi
install_resvg
install_swiftshader
install_msvc_crt
install_exiftool
status Compiling MIME database
update-mime-database "$msys2_root/share/mime"
status Success
+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"
+18
View File
@@ -0,0 +1,18 @@
# Linux-hosted MinGW-w64 toolchain. The MSYS2 prefix lives in the CMake
# build tree (ucrt64/) and is appended to CMAKE_FIND_ROOT_PATH from the
# root CMakeLists.txt.
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_OBJDUMP x86_64-w64-mingw32-objdump)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
+21
View File
@@ -0,0 +1,21 @@
//
// dn-config.h.in: build-time feature macros
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#cmakedefine01 DN_WITH_LCMS2_FAST_FLOAT
#cmakedefine01 DN_WITH_LCMS2_THREADED
#cmakedefine01 DN_WITH_JPEG_QS
#cmakedefine01 DN_WITH_LIBRAW
#cmakedefine01 DN_WITH_LIBRSVG
#cmakedefine01 DN_WITH_XCURSOR
#cmakedefine01 DN_WITH_LIBHEIF
#cmakedefine01 DN_WITH_LIBTIFF
#cmakedefine01 DN_WITH_GDKPIXBUF
#cmakedefine01 DN_WITH_WAYLAND
#cmakedefine01 DN_WITH_SINGLE_INSTANCE
#define DN_IPC_BUILD_ID "@DN_IPC_BUILD_ID@"
+239
View File
@@ -0,0 +1,239 @@
qt_add_executable(dn
main.cpp
app.cpp
window.cpp
renderer.cpp
gpu.cpp
kit.cpp
chrome.cpp
app-menu.cpp
hint.cpp
action.cpp
viewer.cpp
browser.cpp
thumbnailer.cpp
thumbnail-cache.cpp
overlay.cpp
cie-diagram.cpp
sheet.cpp
xdg.cpp
display-profile.cpp
icons.qrc
)
set_target_properties(dn PROPERTIES
AUTOMOC ON
AUTORCC ON
MACOSX_BUNDLE ON
MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.dn)
target_include_directories(dn PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/generated"
"${CMAKE_SOURCE_DIR}/libdn"
)
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
)
set(DN_UI_SHADER_DIR "${CMAKE_CURRENT_SOURCE_DIR}/shaders")
set(DN_UI_SHADER_GEN "${CMAKE_CURRENT_BINARY_DIR}/generated")
file(MAKE_DIRECTORY "${DN_UI_SHADER_GEN}")
function(dn_ui_embed_shader src symbol)
get_filename_component(name "${src}" NAME)
set(spv "${DN_UI_SHADER_GEN}/${name}.spv")
string(REPLACE "." "-" header_stem "${name}")
set(hdr "${DN_UI_SHADER_GEN}/${header_stem}-spv.h")
add_custom_command(
OUTPUT "${spv}" "${hdr}"
COMMAND "${GLSLANG_VALIDATOR}" -V "${src}" -o "${spv}"
COMMAND "${CMAKE_COMMAND}"
"-DINPUT=${spv}" "-DOUTPUT=${hdr}" "-DSYMBOL=${symbol}"
-P "${CMAKE_SOURCE_DIR}/libdn/cmake/embed-spirv.cmake"
DEPENDS "${src}" "${CMAKE_SOURCE_DIR}/libdn/cmake/embed-spirv.cmake"
COMMENT "SPIR-V ${name}"
VERBATIM
)
target_sources(dn PRIVATE "${hdr}")
endfunction()
dn_ui_embed_shader("${DN_UI_SHADER_DIR}/overlay.vert" overlay_vert)
dn_ui_embed_shader("${DN_UI_SHADER_DIR}/overlay.frag" overlay_frag)
dn_ui_embed_shader("${CMAKE_SOURCE_DIR}/libdn/shaders/fullscreen.vert"
fullscreen_vert)
dn_ui_embed_shader("${DN_UI_SHADER_DIR}/dither.frag" dither_frag)
if(WIN32)
target_sources(dn PRIVATE
display-profile-windows.cpp
assoc-windows.cpp
)
target_link_libraries(dn PRIVATE gdi32 shell32 shlwapi ole32 advapi32)
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)
target_sources(dn PRIVATE
display-profile-macos.mm
assoc-macos.mm
app-menu-macos.mm
)
target_link_libraries(dn PRIVATE
"${DN_APPKIT_FRAMEWORK}"
"${DN_COREGRAPHICS_FRAMEWORK}"
"${DN_CORESERVICES_FRAMEWORK}"
)
else()
pkg_check_modules(DN_COLORD REQUIRED IMPORTED_TARGET colord)
target_sources(dn PRIVATE
display-profile-linux.cpp
assoc-unix.cpp
instance.cpp
)
target_link_libraries(dn PRIVATE PkgConfig::DN_COLORD)
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}")
set_target_properties(dn PROPERTIES
MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}")
else()
include("${CMAKE_SOURCE_DIR}/cmake/IconUtils.cmake")
set(DN_ICON_BASE "${CMAKE_CURRENT_BINARY_DIR}/icons")
set(DN_ICON_PNG_LIST)
foreach(DN_ICON_SIZE 16 32 48 256)
icon_to_png(dn "${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")
install(DIRECTORY "${DN_ICON_BASE}"
DESTINATION "${CMAKE_INSTALL_DATADIR}")
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/dn.desktop"
"${CMAKE_CURRENT_SOURCE_DIR}/dn-browse.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
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")
add_custom_command(
OUTPUT "${DN_COLOR_HEADER}" "${DN_COLOR_SOURCE}"
COMMAND "${DN_WAYLAND_SCANNER}" client-header
"${DN_COLOR_XML}" "${DN_COLOR_HEADER}"
COMMAND "${DN_WAYLAND_SCANNER}" private-code
"${DN_COLOR_XML}" "${DN_COLOR_SOURCE}"
DEPENDS "${DN_COLOR_XML}"
COMMENT "wayland-scanner color-management-v1 for dn"
VERBATIM
)
target_sources(dn PRIVATE
wayland-color-bridge.cpp
wayland-window.cpp
"${DN_COLOR_HEADER}"
"${DN_COLOR_SOURCE}"
)
target_include_directories(dn PRIVATE "${DN_PROTOCOL_DIR}")
target_link_libraries(dn PRIVATE PkgConfig::DN_WAYLAND)
endif()
# User guide: build tree at share/doc/dn (../share from bin/), installed
# next to the application on each platform.
set(DN_HELP_DIR "${CMAKE_BINARY_DIR}/share/doc/dn")
file(MAKE_DIRECTORY "${DN_HELP_DIR}")
configure_file(
"${CMAKE_SOURCE_DIR}/doc/dn.html.in"
"${DN_HELP_DIR}/dn.html"
@ONLY)
configure_file(
"${CMAKE_SOURCE_DIR}/doc/stylesheet.css"
"${DN_HELP_DIR}/stylesheet.css"
COPYONLY)
if(APPLE)
set_source_files_properties(
"${DN_HELP_DIR}/dn.html"
"${DN_HELP_DIR}/stylesheet.css"
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/share/doc/dn")
target_sources(dn PRIVATE
"${DN_HELP_DIR}/dn.html"
"${DN_HELP_DIR}/stylesheet.css")
elseif(WIN32)
install(FILES
"${DN_HELP_DIR}/dn.html"
"${DN_HELP_DIR}/stylesheet.css"
DESTINATION share/doc/dn)
else()
include(GNUInstallDirs)
install(FILES
"${DN_HELP_DIR}/dn.html"
"${DN_HELP_DIR}/stylesheet.css"
DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/dn")
endif()
if(BUILD_TESTING)
add_test(NAME dn_help COMMAND dn --help)
set_tests_properties(dn_help PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen"
)
endif()
+525
View File
@@ -0,0 +1,525 @@
//
// 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 <QDataStream>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QIODevice>
#include <QKeySequence>
#include <QList>
#include <QMimeData>
#include <QUrl>
#include <cstdio>
using namespace std;
namespace dn
{
namespace
{
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);
// clang-format off
constexpr ActionDef kDefs[] = {
{},
{kMenu, {"_New Window"}, {}, {{Qt::Key_N, kCtrl}}, {}},
{kMenu, {"_Close Window"}, {}, {{Qt::Key_W, kCtrl}, {Qt::Key_Q}}, {}},
{kMenu, {"_Quit"}, {}, {{Qt::Key_Q, kCtrl}}, {}},
{kToggle, {"_Fullscreen", "E_xit Fullscreen"},
{"view-fullscreen-symbolic", "view-restore-symbolic"},
{{Qt::Key_F11}}, {}},
{kToggle, {"_Dark Mode"}, {"dark-mode-symbolic"}, {{Qt::Key_D}}, {}},
{kMenu, {"_Hint"}, {}, {{Qt::Key_F}}, {}},
{kMenu, {"_Back in History"}, {"curved-arrow-left-symbolic"},
{{Qt::Key_Left, kAlt}, {Qt::Key_Backspace}}, {}},
{kMenu, {"_Forward in History"}, {"curved-arrow-right-symbolic"},
{{Qt::Key_Right, kAlt}}, {}},
{kMenu, {"_Contents"}, {}, {{Qt::Key_F1}}, {}},
{kMenu, {"_About"}, {}, {}, {}},
{kMenu, {"_Keyboard Shortcuts"}, {}, {{Qt::Key_Question, kCtrl}}, {}},
{0, {"_Menu"}, {}, {{Qt::Key_F10}}, {}},
// Mostly 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"},
{{Qt::Key_Up, kAlt}}, {}},
{kMenu, {"_Home"}, {}, {{Qt::Key_Home, kAlt}}, {}},
{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}}, {}},
{0, {"_Open"}, {}, {{Qt::Key_Return}, {Qt::Key_Enter}}, {}},
{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}}, {}},
{kMenu, {"Zoom _Out"}, {"minus-framed-symbolic"}, {{Qt::Key_Minus}}, {}},
{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 _Width"}, {}, {{Qt::Key_W}}, {}},
{0, {"Fit _Height"}, {}, {{Qt::Key_H}}, {}},
{kToggle, {"_Lock View"},
{"padlock-open-symbolic", "padlock-closed-symbolic"}, {}, {}},
{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}}, {}},
{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.
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)));
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::Help,
Action::About,
Action::Shortcuts,
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::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::Quit,
}};
const MenuNode kHelpMenu{"_Help", {
Action::Help,
Action::Shortcuts,
Action::About,
}};
const MenuNode kBrowserMenu[] = {
kFileMenu,
{"_Go", {
Action::Back,
Action::Forward,
{},
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::Hint,
Action::DarkMode,
Action::Fullscreen,
}},
kHelpMenu,
};
const MenuNode kViewerMenu[] = {
kFileMenu,
{"_Go", {
Action::Back,
Action::Forward,
{},
Action::Browse,
Action::PrevFile,
Action::NextFile,
}},
{"_View", {
Action::Information,
{},
Action::ZoomIn,
Action::ZoomOut,
Action::Zoom1,
Action::Fit,
{},
Action::Lock,
Action::Fixate,
{},
Action::ColorManagement,
Action::Smooth,
Action::Checkerboard,
{},
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
} // namespace
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)
{
if (!label)
return {};
QString s = QString::fromUtf8(label);
s.remove(QLatin1Char('_'));
return s;
}
int
mnemonic_index(const char *label)
{
if (!label)
return -1;
const QString s = QString::fromUtf8(label);
int i = 0;
for (int p = 0; p < s.size(); ++p) {
if (s[p] == QLatin1Char('_'))
return p + 1 < s.size() ? i : -1;
++i;
}
return -1;
}
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));
}
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 QString> abs_paths, bool cut)
{
if (!mime)
return;
QList<QUrl> urls;
urls.reserve(int(abs_paths.size()));
for (const QString &path : abs_paths)
urls.append(QUrl::fromLocalFile(path));
mime->setUrls(urls);
#ifdef Q_OS_WIN
QByteArray effect;
QDataStream ds(&effect, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::LittleEndian);
ds << quint32(cut ? 2u : 1u);
mime->setData(
QStringLiteral(
"application/x-qt-windows-mime;value=\"Preferred DropEffect\""),
effect);
#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 ? QByteArrayLiteral("1") : QByteArrayLiteral("0"));
#endif
}
void
copy_files(span<const QString> abs_paths, bool cut)
{
auto *mime = new QMimeData;
copy_files(mime, abs_paths, 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;
fprintf(stderr, "%s: %s\n", qUtf8Printable(abs_path),
qUtf8Printable(file.errorString()));
return false;
}
} // namespace dn
+93
View File
@@ -0,0 +1,93 @@
//
// action.hpp: shared action table (labels, keys, menus)
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <span>
#include <vector>
class QMimeData;
namespace dn
{
enum class Action : uint8_t { None,
// window
NewWindow, CloseWindow, Quit, Fullscreen, DarkMode, Hint, Back, Forward, Help, About, Shortcuts,
Menu, Context, Cancel, NextPane, PrevPane,
// browser
Sidebar, DirPrev, DirNext, DirParent, DirHome,
ThumbMinus, ThumbPlus, ViewTile, ViewGrid, ViewList, Filenames, Filter,
SortDir, SortName, SortTime,
Activate,
// viewer
Browse, PrevFile, NextFile,
ZoomIn, ZoomOut, Zoom1, ZoomLevel, Fit, FitWidth, FitHeight,
Lock, Fixate, ColorManagement, Smooth, Checkerboard,
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 *label);
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 QString> abs_paths,
bool cut = false);
void copy_files(std::span<const QString> abs_paths, bool cut = false);
bool move_to_trash(const QString &abs_path);
} // namespace dn
+23
View File
@@ -0,0 +1,23 @@
//
// app-menu-macos.hpp: native macOS application menu
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
namespace dn
{
class App;
#if defined(__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
+382
View File
@@ -0,0 +1,382 @@
//
// 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 "window.hpp"
#include <QDir>
#include <QKeySequence>
#include <QString>
#import <AppKit/AppKit.h>
#include <span>
#include <vector>
namespace
{
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;
}
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_0 && k <= Qt::Key_9) {
unichar c = unichar('0' + (k - Qt::Key_0));
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_Plus:
c = '+';
break;
case Qt::Key_Minus:
c = '-';
break;
case Qt::Key_Equal:
c = '=';
break;
case Qt::Key_Less:
c = '<';
break;
case Qt::Key_Greater:
c = '>';
break;
case Qt::Key_BracketLeft:
c = '[';
break;
case Qt::Key_BracketRight:
c = ']';
break;
case Qt::Key_BraceLeft:
c = '{';
break;
case Qt::Key_BraceRight:
c = '}';
break;
case Qt::Key_Space:
c = ' ';
break;
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;
default:
return @"";
}
return [NSString stringWithCharacters:&c length:1];
}
const dn::MenuNode *
find_section(std::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) == want)
return &n;
}
return nullptr;
}
bool
skip_action(dn::Action a)
{
return a == dn::Action::Quit || a == dn::Action::About ||
a == dn::Action::Fullscreen;
}
NSMenuItem *
top_item(NSMenu *menu)
{
NSMenu *parent = menu.supermenu;
if (!parent)
return nil;
const NSInteger i = [parent indexOfItemWithSubmenu:menu];
return i >= 0 ? [parent itemAtIndex:i] : nil;
}
void
sync_hidden(NSMenu *main, id delegate, std::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;
}
}
} // namespace
@interface DawnMenuDelegate : NSObject <NSMenuDelegate>
@property(nonatomic, assign) dn::App *app;
@end
@implementation DawnMenuDelegate
- (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(QDir::currentPath());
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if (dn::Action(item.tag) == dn::Action::About)
return [self window] != nullptr;
const dn::Actor *actor = [self actor];
if (!actor || !actor->enabled)
return YES;
return actor->enabled(dn::Action(item.tag));
}
- (void)menuNeedsUpdate:(NSMenu *)menu
{
dn::Window *w = [self window];
const std::span<const dn::MenuNode> tree =
w ? w->active_menu() : std::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) {
if (NSMenuItem *top = top_item(menu))
top.hidden = YES;
return;
}
if (NSMenuItem *top = top_item(menu))
top.hidden = NO;
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));
NSString *key = @"";
NSEventModifierFlags mods = 0;
if (!(def.accel && def.keys[0].key == 0))
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
{
namespace
{
DawnMenuDelegate *g_menu_delegate;
bool
has_menu(NSMenu *main, NSString *title)
{
for (NSMenuItem *it in main.itemArray) {
if ([it.title isEqualToString:title])
return true;
}
return false;
}
void
add_menu(NSMenu *main, NSString *title, id delegate)
{
if (has_menu(main, title))
return;
NSMenuItem *top = [[[NSMenuItem alloc] initWithTitle:title
action:nil
keyEquivalent:@""] autorelease];
NSMenu *sub = [[[NSMenu alloc] initWithTitle:title] autorelease];
sub.delegate = delegate;
top.submenu = sub;
[main addItem:top];
}
void
retarget_about(NSMenu *app_menu, id target)
{
for (NSMenuItem *it in app_menu.itemArray) {
if (![it.title hasPrefix:@"About "])
continue;
if ([it.title isEqualToString:@"About Qt"])
continue;
it.hidden = NO;
it.enabled = YES;
it.tag = NSInteger(Action::About);
it.target = target;
it.action = @selector(invoke:);
return;
}
}
} // namespace
void
sync_macos_app_menu(App *app)
{
NSMenu *main = [NSApp mainMenu];
if (!main || !app || !g_menu_delegate)
return;
g_menu_delegate.app = app;
Window *w = app->key_window();
if (!w)
return;
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 = [[DawnMenuDelegate alloc] init];
DawnMenuDelegate *delegate = g_menu_delegate;
delegate.app = app;
if (NSMenuItem *first = [main itemAtIndex:0]) {
if (NSMenu *app_menu = first.submenu)
retarget_about(app_menu, delegate);
}
std::vector<QString> titles;
auto consider = [&](std::span<const MenuNode> tree) {
for (const MenuNode &n : tree) {
const QString t = menu_label(n.title);
if (t.isEmpty())
continue;
bool seen = false;
for (const QString &e : titles) {
if (e == t) {
seen = true;
break;
}
}
if (!seen)
titles.push_back(t);
}
};
consider(viewer_menu());
consider(browser_menu());
for (const QString &t : titles)
add_menu(main, t.toNSString(), delegate);
}
} // namespace dn
+917
View File
@@ -0,0 +1,917 @@
//
// app-menu.cpp: application menu and information overlay widgets
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "app-menu.hpp"
#include "action.hpp"
#include "assoc.hpp"
#include "chrome.hpp"
#include <QCoreApplication>
#include <QFileInfo>
#include <QKeyEvent>
#include <algorithm>
#include <chrono>
#include <iterator>
#include <memory>
using namespace std;
namespace dn
{
namespace
{
constexpr float kItemGap = 2.0f;
constexpr float kMenuPad = 4.0f;
constexpr float kDialogPad = 16.0f;
constexpr float kMenuHoldMs = 500.0f; // GTK MENU_SHELL_TIMEOUT
unique_ptr<Sep>
hsep()
{
return make_unique<Sep>();
}
Colour
col(const Colour &c, float alpha = 1.0f)
{
return {c.r, c.g, c.b, c.a * alpha};
}
void
emit_icon(
Kit &kit, float x, float y, float size, const char *name, Colour colour)
{
if (!name)
return;
auto it = kit.icons_.find(name);
if (it == kit.icons_.end())
return;
float u0, v0, u1, v1;
kit.atlas_.uv(it->second, &u0, &v0, &u1, &v1);
kit.list_.add_image(x, y, x + size, y + size, u0, v0, u1, v1, colour);
}
unique_ptr<Label>
dialog_label(const QString &text, bool bold = false, bool wrap = false)
{
auto label = make_unique<Label>();
label->text = text;
label->bold = bold;
label->wrap = wrap;
return label;
}
QString
shortcut_accel(const ActionDef &def)
{
if (def.accel)
return QString::fromUtf8(def.accel);
QString s;
for (const Accel &a : def.keys) {
const QString part = accel_label(a);
if (part.isEmpty())
continue;
if (!s.isEmpty())
s += QLatin1String(", ");
s += part;
}
return s;
}
bool
has_shortcut(const ActionDef &def)
{
return !shortcut_accel(def).isEmpty();
}
void
for_leaves(span<const MenuNode> nodes, auto &&fn)
{
for (const MenuNode &n : nodes) {
if (!n.items.empty())
for_leaves(n.items, fn);
else if (n.action != Action::None)
fn(n.action);
}
}
unique_ptr<Row>
shortcut_row(const ActionDef &def, float accel_w)
{
auto row = make_unique<Row>();
row->gap = 8.0f;
auto accel = dialog_label(shortcut_accel(def));
accel->min_w = accel_w;
accel->dim = true;
auto name = dialog_label(menu_label(def.label[0]));
row->add_child(std::move(accel));
row->add_child(std::move(name));
return row;
}
} // namespace
Popup::Popup()
{
this->hittable = true;
this->visible = false;
}
void
Popup::open(Kit &kit, Button *anchor)
{
kit.close_popups();
this->parent_popup = nullptr;
this->opener = anchor;
if (this->opener) {
this->opener->active = true;
this->at = this->opener->r;
}
this->visible = true;
kit.open_popup(this);
place(kit);
}
void
Popup::open_at(Kit &kit, Rect anchor)
{
kit.close_popups();
this->parent_popup = nullptr;
this->opener = nullptr;
this->at = anchor;
this->visible = true;
kit.open_popup(this);
place(kit);
}
void
Popup::open_sub(Kit &kit, Popup &owner, Button &anchor)
{
kit.close_above(&owner);
this->parent_popup = &owner;
this->opener = &anchor;
this->visible = true;
this->opener->active = true;
kit.open_popup(this);
place_sub(kit);
}
void
Popup::paint(Kit &kit) const
{
if (!this->visible)
return;
kit.draw_shadow(this->r);
Panel::paint(kit);
}
void
Popup::close(Kit &kit)
{
if (!this->visible)
return;
kit.close_above(this);
this->visible = false;
this->parent_popup = nullptr;
if (this->opener) {
this->opener->active = false;
this->opener = nullptr;
}
auto &ps = kit.popups_;
ps.erase(remove(ps.begin(), ps.end(), this), ps.end());
if (ps.empty() && kit.scrim_)
kit.scrim_->visible = false;
kit.sync_focus();
}
void
Popup::place(Kit &kit)
{
if (this->opener)
this->at = this->opener->r;
const float cap = kit.host_w_ > 0.0f ? kit.host_w_ : kUnlim;
measure(kit, cap, kUnlim);
float x = kit.snap(this->at.x);
float y = kit.snap(this->at.y + this->at.h);
if (x + this->r.w > kit.host_w_)
x = max(0.0f, kit.host_w_ - this->r.w);
if (x < 0.0f)
x = 0.0f;
if (y + this->r.h > kit.host_h_)
y = max(0.0f, this->at.y - this->r.h);
if (y + this->r.h > kit.host_h_)
y = max(0.0f, kit.host_h_ - this->r.h);
if (y < 0.0f)
y = 0.0f;
arrange(kit, {x, y, this->r.w, this->r.h});
}
void
Popup::place_sub(Kit &kit)
{
const float cap = kit.host_w_ > 0.0f ? kit.host_w_ : kUnlim;
measure(kit, cap, kUnlim);
const Popup *owner = this->parent_popup;
const Widget *anchor = this->opener;
float x = owner ? kit.snap(owner->r.x + owner->r.w) : 0.0f;
if (x + this->r.w > kit.host_w_)
x = owner ? kit.snap(owner->r.x - this->r.w) : 0.0f;
if (x < 0.0f)
x = 0.0f;
float y = anchor ? kit.snap(anchor->r.y) : 0.0f;
if (y + this->r.h > kit.host_h_)
y = max(0.0f, kit.host_h_ - this->r.h);
if (y < 0.0f)
y = 0.0f;
arrange(kit, {x, y, this->r.w, this->r.h});
}
void
Popup::focus_item(Kit &kit, Widget *w, bool kbd) const
{
kit.focus_ = w;
kit.focus_visible_ = kbd;
if (kbd)
kit.hot_ = nullptr;
}
void
Popup::reveal(Kit &kit, Widget *w)
{
auto *item = dynamic_cast<MenuItem *>(w);
if (item && item->sub) {
if (!item->sub->visible)
item->sub->open_sub(kit, *this, *item);
kit.focus_ = item->sub;
kit.focus_visible_ = false;
return;
}
kit.close_above(this);
focus_item(kit, w, false);
}
void
Popup::select_first(Kit &kit)
{
kit.focus_first(this);
}
bool
Popup::motion(Kit &kit, float, float)
{
Widget *w = kit.hot_;
Widget *item = nullptr;
for (; w; w = w->parent_) {
if (w == this)
break;
if (dynamic_cast<const Popup *>(w) && w != this)
return false;
if (!item && w->focusable())
item = w;
}
if (w != this)
return false;
if (item)
reveal(kit, item);
return true;
}
bool
Popup::key(Kit &kit, int key, unsigned mods)
{
if (mods & unsigned(Qt::AltModifier))
return false;
if (key == Qt::Key_Escape) {
Button *op = this->opener;
close(kit);
if (op) {
kit.focus_ = op;
kit.focus_visible_ = true;
}
return true;
}
if (key == Qt::Key_Up || key == Qt::Key_Down) {
kit.cycle_focus(this, key == Qt::Key_Up ? -1 : 1);
focus_item(kit, kit.focus_, true);
return true;
}
if (key == Qt::Key_Right || key == Qt::Key_Return || key == Qt::Key_Enter ||
key == Qt::Key_Space) {
if (auto *item = dynamic_cast<MenuItem *>(kit.focus_);
item && item->sub) {
item->activate(kit);
return true;
}
if (key == Qt::Key_Right)
return true;
return false;
}
if (key == Qt::Key_Left) {
if (this->parent_popup) {
Button *op = this->opener;
close(kit);
if (op) {
kit.focus_ = op;
kit.focus_visible_ = true;
}
}
return true;
}
return false;
}
bool
Popup::release(Kit &kit, float x, float y, Qt::MouseButton button)
{
if (button != Qt::LeftButton && button != Qt::RightButton)
return false;
Widget *hit = hit_at(x, y);
if (auto *b = dynamic_cast<Button *>(hit); b && hit != this) {
b->activate(kit);
if (auto *item = dynamic_cast<MenuItem *>(b);
item && item->sub && item->sub->visible)
return true;
if (this->visible)
close(kit);
return true;
}
const float elapsed = chrono::duration<float, milli>(
chrono::steady_clock::now() - kit.popup_at_)
.count();
if (elapsed >= kMenuHoldMs)
kit.close_popups();
return true;
}
Overflow::Overflow()
{
auto column = make_unique<Column>();
this->col = column.get();
this->col->gap = kItemGap;
this->pad_x = kMenuPad;
this->pad_y = kMenuPad;
this->fill = Fill::Panel;
this->stroke = Stroke::All;
this->hittable = true;
this->visible = false;
add_child(std::move(column));
}
void
Overflow::place(Kit &kit)
{
if (this->fill_items)
this->fill_items();
Popup::place(kit);
}
Menu::Menu()
{
auto c = make_unique<Column>();
this->col = c.get();
this->pad_x = kMenuPad;
this->pad_y = kMenuPad;
this->fill = Fill::Panel;
this->stroke = Stroke::All;
this->hittable = true;
this->visible = false;
add_child(std::move(c));
}
MenuItem &
Menu::add_item(const QString &text)
{
auto item = make_unique<MenuItem>();
item->text = text;
MenuItem &ref = *item;
if (this->col)
this->col->add_child(std::move(item));
return ref;
}
void
Menu::add_sep()
{
if (this->col)
this->col->add_child(hsep());
}
void
Menu::clear()
{
this->subs_.clear();
if (this->col)
this->col->erase_children();
}
void
Menu::build(span<const MenuNode> nodes, const Actor &a)
{
this->actor = a;
clear();
if (!this->col)
return;
this->col->grow = false;
this->min_w = 200.0f;
for (const MenuNode &node : nodes) {
if (!node.items.empty()) {
auto child = make_unique<Menu>();
child->build(node.items, this->actor);
auto &item = add_item(menu_label(node.title));
item.mnemonic = mnemonic_index(node.title);
item.sub = child.get();
this->subs_.push_back(std::move(child));
continue;
}
if (!node.title && node.action == Action::None) {
add_sep();
continue;
}
const Action action = node.action;
const ActionDef &def = action_def(action);
auto &item = add_item(menu_label(action_label(def, false)));
item.action = action;
item.accel = accel_label(def);
item.mnemonic = mnemonic_index(def.label[0]);
item.checkable = (def.flags & ActionToggle) && !def.label[1];
item.on_click = [this, action](Kit &) {
if (this->actor.apply)
this->actor.apply(action);
};
}
}
void
Menu::sync()
{
if (this->col) {
for (auto &k : this->col->kids) {
auto *item = dynamic_cast<MenuItem *>(k.get());
if (!item || item->sub || item->action == Action::None)
continue;
const Action action = item->action;
item->enabled_ =
!this->actor.enabled || this->actor.enabled(action);
item->checked = this->actor.checked && this->actor.checked(action);
const ActionDef &def = action_def(action);
const char *label = action_label(def, item->checked);
item->text = menu_label(label);
item->mnemonic = mnemonic_index(label);
item->accel = accel_label(def);
item->checkable = (def.flags & ActionToggle) && !def.label[1];
}
}
for (auto &sub : this->subs_) {
if (sub)
sub->sync();
}
}
void
Menu::measure(Kit &kit, float max_w, float max_h)
{
if (this->col) {
float lw = 0.0f;
float aw = 0.0f;
for (const auto &k : this->col->kids) {
auto *item = dynamic_cast<MenuItem *>(k.get());
if (!item)
continue;
lw = max(lw, item->label_width(kit));
aw = max(aw, item->accel_width(kit));
}
for (auto &k : this->col->kids) {
if (auto *item = dynamic_cast<MenuItem *>(k.get())) {
item->label_col = lw;
item->accel_col = aw;
}
}
}
Panel::measure(kit, max_w, max_h);
}
bool
Menu::key(Kit &kit, int key, unsigned mods)
{
if (Popup::key(kit, key, mods))
return true;
if (mods)
return false;
if (key < Qt::Key_A || key > Qt::Key_Z || !this->col)
return false;
const QChar letter = QChar(key).toLower();
for (const auto &k : this->col->kids) {
auto *item = dynamic_cast<MenuItem *>(k.get());
if (!item || item->mnemonic < 0 || item->mnemonic >= item->text.size())
continue;
if (item->text[item->mnemonic].toLower() == letter) {
item->activate(kit);
return true;
}
}
return false;
}
void
MenuItem::measure(Kit &kit, float, float)
{
const float lw =
this->label_col > 0.0f ? this->label_col : label_width(kit);
const float aw =
this->accel_col > 0.0f ? this->accel_col : accel_width(kit);
float width = kFramePadX + kIconPx + kFramePadX + lw + kFramePadX + aw;
if (this->sub)
width += kIconPx;
width += kFramePadX;
this->r = {0, 0, width, kButtonH};
}
void
MenuItem::paint(Kit &kit) const
{
if (!this->visible)
return;
const bool pressed = kit.left_down_ && kit.pressed_ == this;
if (this->enabled_ && (pressed || this->active || kit.focus_ == this))
kit.list_.add_rect_filled(this->r.x, this->r.y, this->r.x + this->r.w,
this->r.y + this->r.h, col(kit.press_));
const float pad = kFramePadX;
const float aw =
this->accel_col > 0.0f ? this->accel_col : accel_width(kit);
const float chev = this->sub ? kIconPx : 0.0f;
const float lead_x = this->r.x + pad;
const float label_x = lead_x + kIconPx + pad;
const float accel_x = this->r.x + this->r.w - pad - chev - aw;
const float ty = this->r.y + kFramePadY;
const float iy = this->r.y + max(kFramePadY, (this->r.h - kIconPx) * 0.5f);
const Colour label_c = col(kit.ink_, this->enabled_ ? 1.0f : 0.5f);
if (this->checkable && this->checked)
emit_icon(kit, lead_x, iy, kIconPx, "object-select-symbolic", label_c);
if (!this->text.isEmpty()) {
const float avail = max(1.0f, accel_x - pad - label_x);
const QString shown = kit.elide_lines(this->text, avail, 1, false);
kit.emit_text(label_x, ty, shown, label_c, false);
if (this->mnemonic >= 0 && this->mnemonic < shown.size() &&
this->mnemonic < this->text.size() &&
shown[this->mnemonic] == this->text[this->mnemonic]) {
const QString left = shown.left(this->mnemonic);
const QString ch = shown.mid(this->mnemonic, 1);
const float x0 = label_x + kit.text_width(left, false);
const float x1 = x0 + kit.text_width(ch, false);
const float uy = kit.snap(ty + kit.text_ascent(false) + 1.0f);
kit.list_.add_line(x0, uy, x1, uy, label_c);
}
}
if (!this->accel.isEmpty()) {
const float tw = kit.text_width(this->accel, false);
kit.emit_text(
accel_x + aw - tw, ty, this->accel, col(kit.ink_, 0.5f), false);
}
if (this->sub) {
emit_icon(kit, this->r.x + this->r.w - pad - chev, iy, kIconPx,
"go-next-symbolic",
col(kit.ink_, this->enabled_ ? 1.0f : 0.375f));
}
}
void
MenuItem::prepare(Kit &kit)
{
if (this->sub)
kit.pack_icon("go-next-symbolic", int(kIconPx * kit.dpr_));
if (this->checkable)
kit.pack_icon("object-select-symbolic", int(kIconPx * kit.dpr_));
if (this->text.isEmpty())
return;
const float pad = kFramePadX;
const float aw =
this->accel_col > 0.0f ? this->accel_col : accel_width(kit);
const float chev = this->sub ? kIconPx : 0.0f;
const float label_x = pad + kIconPx + pad;
const float accel_x = this->r.w - pad - chev - aw;
const float avail = max(1.0f, accel_x - pad - label_x);
kit.cache_text(kit.elide_lines(this->text, avail, 1, false), false);
if (!this->accel.isEmpty())
kit.cache_text(this->accel, false);
}
bool
MenuItem::activate(Kit &kit)
{
if (!this->sub)
return Button::activate(kit);
Popup *owner = nullptr;
for (Widget *w = this->parent_; w; w = w->parent_) {
if (auto *p = dynamic_cast<Popup *>(w)) {
owner = p;
break;
}
}
if (!owner)
return false;
this->sub->open_sub(kit, *owner, *this);
kit.focus_first(this->sub);
return true;
}
float
MenuItem::label_width(const Kit &kit) const
{
return kit.text_width(this->text, false);
}
float
MenuItem::accel_width(const Kit &kit) const
{
return kit.text_width(this->accel, false);
}
void
ContextMenu::fill_items(const QString &path)
{
clear();
this->min_w = 200.0f;
const Handler def = default_for(path);
const vector<Handler> rec = recommended_for(path);
const vector<Handler> fall = fallback_for(path);
auto add_app = [&](const Handler &app) {
if (app.id.isEmpty())
return;
auto &item = add_item(app.name.isEmpty() ? app.id : app.name);
item.mnemonic = -1;
item.on_click = [app, path](Kit &) {
launch(app, path);
set_last_used(app, path);
};
};
bool need_sep = false;
auto flush_sep = [&] {
if (!need_sep)
return;
add_sep();
need_sep = false;
};
auto &new_win = add_item(menu_label("Open in New _Window"));
new_win.mnemonic = mnemonic_index("Open in New _Window");
new_win.on_click = [this, path](Kit &) {
if (this->on_new_window)
this->on_new_window(path);
};
need_sep = true;
if (!def.id.isEmpty()) {
flush_sep();
add_app(def);
need_sep = true;
}
if (!rec.empty()) {
flush_sep();
for (const Handler &app : rec)
add_app(app);
need_sep = true;
}
if (!fall.empty()) {
flush_sep();
for (const Handler &app : fall)
add_app(app);
need_sep = true;
}
if (QFileInfo(path).isFile() && this->on_trash) {
flush_sep();
auto &trash = add_item(menu_label("Move to _Trash"));
trash.mnemonic = mnemonic_index("Move to _Trash");
trash.accel = action_accel(action_def(Action::Trash));
trash.on_click = [this, path](Kit &) {
if (this->on_trash)
this->on_trash(path);
};
}
}
void
ContextMenu::show(Kit &kit, const QString &path, Rect anchor, bool kbd)
{
kit.forget_tree(this);
fill_items(path);
bool any = false;
if (this->col) {
for (const auto &k : this->col->kids) {
if (k && !is_sep(k.get())) {
any = true;
break;
}
}
}
if (!any) {
if (this->visible)
close(kit);
return;
}
open_at(kit, anchor);
if (kbd)
select_first(kit);
}
Modal::Modal()
{
this->hittable = true;
this->visible = false;
this->fill = Fill::None;
auto d = make_unique<Panel>();
d->pad_x = kDialogPad;
d->pad_y = kDialogPad;
d->fill = Fill::Popup;
d->stroke = Stroke::All;
d->hittable = false;
d->visible = false;
this->dialog = d.get();
add_child(std::move(d));
}
void
Modal::fill_dialog(Kit &kit)
{
if (!this->dialog)
return;
kit.forget_tree(this->dialog);
this->dialog->erase_children();
if (this->kind == AppOverlay::None)
return;
auto col = make_unique<Column>();
col->gap = 8.0f;
if (this->kind == AppOverlay::About) {
QString name = QCoreApplication::applicationName();
if (name.isEmpty())
name = QStringLiteral("dn");
col->add_child(dialog_label(name, true));
col->add_child(dialog_label(
QStringLiteral("Colour-managed image browser and viewer."), false,
true));
} else {
bool seen[size_t(Action::Count)] = {};
float accel_w = 120.0f;
auto consider = [&](Action action) {
const ActionDef &def = action_def(action);
if (!has_shortcut(def))
return;
accel_w = max(accel_w, kit.text_width(shortcut_accel(def), false));
};
for_leaves(this->tree, consider);
auto consider_other = [&](Action action) {
const ActionDef &def = action_def(action);
if ((def.flags & ActionInMenu) || !has_shortcut(def))
return;
consider(action);
};
bool viewer = false;
for (Action action : this->keys) {
if (action == Action::ZoomIn || action == Action::FitWidth) {
viewer = true;
break;
}
}
for (Action action : window_keys())
consider_other(action);
for (Action action : this->keys)
consider_other(action);
consider_other(Action::Cancel);
if (viewer)
consider_other(Action::ZoomLevel);
col->gap = 2.0f;
col->add_child(
dialog_label(QStringLiteral("Keyboard Shortcuts"), true));
for (const MenuNode &section : this->tree) {
if (section.items.empty())
continue;
bool any = false;
for_leaves(section.items, [&](Action action) {
if (has_shortcut(action_def(action)))
any = true;
});
if (!any)
continue;
col->add_child(dialog_label(menu_label(section.title), true));
for_leaves(section.items, [&](Action action) {
const ActionDef &def = action_def(action);
if (!has_shortcut(def))
return;
seen[size_t(action)] = true;
col->add_child(shortcut_row(def, accel_w));
});
}
bool other = false;
auto emit_other = [&](Action action) {
const size_t i = size_t(action);
if (i >= size(seen) || seen[i])
return;
const ActionDef &def = action_def(action);
if ((def.flags & ActionInMenu) || !has_shortcut(def))
return;
if (!other) {
col->add_child(dialog_label(QStringLiteral("Other"), true));
other = true;
}
seen[i] = true;
col->add_child(shortcut_row(def, accel_w));
};
for (Action action : window_keys())
emit_other(action);
for (Action action : this->keys)
emit_other(action);
emit_other(Action::Cancel);
if (viewer)
emit_other(Action::ZoomLevel);
}
col->add_child(dialog_label(
QStringLiteral("Click or press Escape to dismiss."), false, true));
this->dialog->min_w = this->kind == AppOverlay::About ? 360.0f : 520.0f;
this->dialog->add_child(std::move(col));
}
void
Modal::set_kind(Kit &kit, AppOverlay overlay)
{
if (overlay == AppOverlay::None) {
this->kind = AppOverlay::None;
fill_dialog(kit);
close(kit);
return;
}
if (this->kind == overlay && this->visible)
return;
this->kind = overlay;
fill_dialog(kit);
open(kit);
}
void
Modal::open(Kit &kit)
{
Popup::open(kit);
if (this->dialog)
this->dialog->visible = true;
}
void
Modal::close(Kit &kit)
{
this->kind = AppOverlay::None;
if (this->dialog)
this->dialog->visible = false;
Popup::close(kit);
}
void
Modal::place(Kit &kit)
{
if (this->kind == AppOverlay::None || !this->dialog) {
this->r = {};
return;
}
this->visible = true;
this->dialog->visible = true;
this->r = {0.0f, 0.0f, kit.host_w_, kit.host_h_};
float top = 0.0f;
if (auto *f = dynamic_cast<Page *>(kit.root_))
top = f->toolbar_h();
const float max_w = max(1.0f, min(560.0f, kit.host_w_ - 32.0f));
const float well_h = max(1.0f, kit.host_h_ - top);
this->dialog->measure(kit, max_w, well_h);
const float x = max(0.0f, (kit.host_w_ - this->dialog->r.w) * 0.5f);
const float y = top + max(0.0f, (well_h - this->dialog->r.h) * 0.5f);
this->dialog->arrange(kit, {x, y, this->dialog->r.w, this->dialog->r.h});
}
void
Modal::paint(Kit &kit) const
{
if (!this->visible)
return;
if (this->dialog && this->dialog->visible)
kit.draw_shadow(this->dialog->r);
Panel::paint(kit);
}
bool
Modal::press(Kit &kit, float, float, Qt::MouseButton button)
{
if (button != Qt::LeftButton)
return false;
close(kit);
kit.pressed_ = nullptr;
return true;
}
} // namespace dn
+116
View File
@@ -0,0 +1,116 @@
//
// app-menu.hpp: application menu and information overlay widgets
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "action.hpp"
#include "kit.hpp"
#include "types.hpp"
#include <functional>
#include <memory>
#include <span>
#include <vector>
namespace dn
{
struct MenuItem;
struct Popup : Panel {
Button *opener = nullptr;
Popup *parent_popup = nullptr;
Rect at{};
Popup();
void open(Kit &kit, Button *anchor = nullptr);
void open_at(Kit &kit, Rect anchor);
void open_sub(Kit &kit, Popup &owner, Button &anchor);
virtual void close(Kit &kit);
virtual void place(Kit &kit);
void place_sub(Kit &kit);
void focus_item(Kit &kit, Widget *w, bool kbd) const;
void reveal(Kit &kit, Widget *w);
void select_first(Kit &kit);
bool traps_focus() const override { return true; }
virtual bool captures_keys() const { return false; }
void paint(Kit &kit) const override;
bool motion(Kit &kit, float x, float y) override;
bool key(Kit &kit, int key, unsigned mods) override;
bool release(Kit &kit, float x, float y, Qt::MouseButton button) override;
};
struct Overflow : Popup {
Column *col = nullptr;
std::vector<Widget *> sources;
std::function<void()> fill_items;
Overflow();
void place(Kit &kit) override;
};
struct Menu : Popup {
Column *col = nullptr;
Actor actor;
std::vector<std::unique_ptr<Menu>> subs_;
Menu();
void build(std::span<const MenuNode> nodes, const Actor &actor);
void sync();
MenuItem &add_item(const QString &text);
void add_sep();
void clear();
void measure(Kit &kit, float max_w, float max_h) override;
bool key(Kit &kit, int key, unsigned mods) override;
};
struct MenuItem : Button {
QString accel;
int mnemonic = -1;
Menu *sub = nullptr;
bool checked = false;
bool checkable = false;
float label_col = 0;
float accel_col = 0;
void measure(Kit &kit, float max_w, float max_h) override;
void paint(Kit &kit) const override;
void prepare(Kit &kit) override;
bool activate(Kit &kit) override;
float label_width(const Kit &kit) const;
float accel_width(const Kit &kit) const;
};
struct ContextMenu : Menu {
std::function<void(const QString &path)> on_new_window;
std::function<void(const QString &path)> on_trash;
void show(Kit &kit, const QString &path, Rect anchor, bool kbd);
private:
void fill_items(const QString &path);
};
struct Modal : Popup {
Panel *dialog = nullptr;
AppOverlay kind = AppOverlay::None;
std::span<const MenuNode> tree = {};
std::span<const Action> keys = {};
Modal();
void set_kind(Kit &kit, AppOverlay overlay);
void open(Kit &kit);
void close(Kit &kit) override;
void place(Kit &kit) override;
void paint(Kit &kit) const override;
bool press(Kit &kit, float x, float y, Qt::MouseButton button) override;
private:
void fill_dialog(Kit &kit);
};
} // namespace dn
+194
View File
@@ -0,0 +1,194 @@
//
// app.cpp: process-wide Vulkan, GPU, and top-level windows
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "app.hpp"
#include "dn-config.h"
#include "vk-device.hpp"
#include "window.hpp"
#if DN_WITH_WAYLAND
#include "wayland-window.hpp"
#endif
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QMetaObject>
#include <QVersionNumber>
#include <cstdio>
#if defined(Q_OS_MACOS)
#include "app-menu-macos.hpp"
#include <dlfcn.h>
#endif
using namespace std;
namespace dn
{
bool
App::init()
{
#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", QByteArrayLiteral("0"));
#endif
QGuiApplication::setQuitOnLastWindowClosed(false);
#if defined(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", QByteArrayLiteral("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));
vk_add_bundled_driver_files();
this->vulkan_instance_.setExtensions(
{VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME});
if (!this->vulkan_instance_.create()) {
fprintf(stderr, "Qt Vulkan instance creation failed: VkResult %d\n",
static_cast<int>(this->vulkan_instance_.errorCode()));
return false;
}
#if defined(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());
}
OpenResult
App::open(const QString &path, const QString &activation_token,
BrowseSetup setup)
{
const QString resolved = path.isEmpty() ? QDir::currentPath() : path;
const QFileInfo info(resolved);
if (!info.exists()) {
fprintf(stderr, "%s: not found\n", qUtf8Printable(resolved));
return OpenResult::NotFound;
}
if (!info.isReadable()) {
fprintf(stderr, "%s: permission denied\n", qUtf8Printable(resolved));
return OpenResult::PermissionDenied;
}
#if DN_WITH_WAYLAND
if (QGuiApplication::platformName() == QStringLiteral("wayland")) {
auto window = make_unique<WaylandWindow>(this);
if (!window->initialize(resolved, setup))
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))
return OpenResult::Internal;
apply_activation_token(activation_token);
window->show();
this->windows_.push_back(std::move(window));
#if defined(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(
qGuiApp, [this, top] { close(top); }, Qt::QueuedConnection);
}
void
App::quit()
{
QMetaObject::invokeMethod(
qGuiApp,
[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::quit();
},
Qt::QueuedConnection);
}
GpuContext &
App::gpu()
{
return this->gpu_;
}
QVulkanInstance *
App::vulkan_instance()
{
return &this->vulkan_instance_;
}
Window *
App::key_window() const
{
QWindow *focus = QGuiApplication::focusWindow();
Window *fallback = nullptr;
for (const unique_ptr<QWindow> &w : this->windows_) {
auto *win = dynamic_cast<Window *>(w.get());
if (!win)
continue;
if (!fallback)
fallback = win;
if (w.get() == focus)
return win;
}
return fallback;
}
} // namespace dn
+62
View File
@@ -0,0 +1,62 @@
//
// app.hpp: process-wide Vulkan, GPU, and top-level windows
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "browser.hpp"
#include "display-profile.hpp"
#include "gpu.hpp"
#include "thumbnailer.hpp"
#include <QString>
#include <QVulkanInstance>
#include <QWindow>
#include <cstdint>
#include <memory>
#include <vector>
namespace dn
{
class Window;
enum class OpenResult : uint8_t {
Ok,
NotFound,
PermissionDenied,
InvalidArgument,
Internal
};
class App
{
public:
bool init();
OpenResult open(const QString &path, const QString &activation_token = {},
BrowseSetup setup = {});
void close(const QWindow *top);
void close_later(const QWindow *top);
void quit();
[[nodiscard]] GpuContext &gpu();
[[nodiscard]] Thumbnailer &thumbnailer() { return this->thumbnailer_; }
[[nodiscard]] QVulkanInstance *vulkan_instance();
[[nodiscard]] Window *key_window() const;
[[nodiscard]] DisplayProfileWatch &display_profiles()
{
return this->display_profiles_;
}
private:
QVulkanInstance vulkan_instance_;
GpuContext gpu_;
Thumbnailer thumbnailer_;
DisplayProfileWatch display_profiles_;
std::vector<std::unique_ptr<QWindow>> windows_;
};
} // namespace dn
+198
View File
@@ -0,0 +1,198 @@
//
// 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>
#import <AppKit/AppKit.h>
#import <CoreServices/CoreServices.h>
namespace dn
{
namespace
{
NSURL *
file_url(const QString &path)
{
if (path.isEmpty())
return nil;
return [NSURL fileURLWithPath:path.toNSString()];
}
QString
from_ns(NSString *s)
{
return s ? QString::fromNSString(s) : QString();
}
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;
}
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) {
CFStringRef tagged = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension, (__bridge CFStringRef) ext, nullptr);
if (tagged) {
NSString *copy = [(__bridge NSString *) tagged copy];
CFRelease(tagged);
return copy;
}
}
return nil;
}
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;
}
} // namespace
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;
}
std::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);
std::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;
}
std::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;
NSError *err = nil;
const BOOL ok =
[[NSWorkspace sharedWorkspace] openURLs:@[ file ]
withApplicationAtURL:app_url
options:NSWorkspaceLaunchDefault
configuration:@{}
error:&err];
return ok == YES;
}
void
set_last_used(const Handler &, const QString &)
{
}
} // namespace dn
+862
View File
@@ -0,0 +1,862 @@
//
// assoc-unix.cpp: XDG MIME Applications Open With (filename types, no GIO)
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "assoc.hpp"
#include "xdg.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QLocale>
#include <QMimeDatabase>
#include <QMimeType>
#include <QPair>
#include <QProcess>
#include <QSet>
#include <QUrl>
#include <algorithm>
using namespace std;
namespace dn
{
namespace
{
constexpr auto kSelfDesktop = QLatin1String("dn.desktop");
QString
read_text_file(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
QString text = QString::fromUtf8(file.readAll());
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
text.replace(u'\r', u'\n');
return text;
}
bool
write_text_file(const QString &path, const QString &text)
{
QFileInfo info(path);
if (!QDir().mkpath(info.absolutePath()))
return false;
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
return false;
return file.write(text.toUtf8()) >= 0;
}
QString
unescape_desktop(const QString &value)
{
QString out;
out.reserve(value.size());
for (int i = 0; i < value.size(); ++i) {
if (value[i] == u'\\' && i + 1 < value.size()) {
const QChar n = value[++i];
if (n == u's')
out += u' ';
else if (n == u'n')
out += u'\n';
else if (n == u't')
out += u'\t';
else if (n == u'r')
out += u'\r';
else
out += n;
} else {
out += value[i];
}
}
return out;
}
bool
parse_bool(const QString &value)
{
const QString v = value.trimmed().toLower();
return v == QLatin1String("true") || v == QLatin1String("1");
}
vector<QString>
split_semicolons(const QString &value)
{
vector<QString> out;
for (const QString &part : value.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"));
raw.push_back(QLocale::system().name());
vector<QString> out;
auto add = [&](const QString &s) {
if (!s.isEmpty() && std::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;
}
struct IniGroup {
QString name;
vector<QPair<QString, QString>> keys;
};
struct IniFile {
vector<QString> preamble;
vector<IniGroup> groups;
};
IniFile
parse_ini(const QString &text)
{
IniFile ini;
IniGroup *group = nullptr;
for (const QString &raw : text.split(u'\n')) {
const QString trimmed = raw.trimmed();
if (trimmed.isEmpty() || trimmed.startsWith(u'#')) {
if (!group)
ini.preamble.push_back(raw);
continue;
}
if (trimmed.startsWith(u'[') && trimmed.endsWith(u']') &&
!trimmed.contains(u'=')) {
IniGroup g;
g.name = trimmed.mid(1, trimmed.size() - 2);
ini.groups.push_back(std::move(g));
group = &ini.groups.back();
continue;
}
if (!group)
continue;
const int eq = raw.indexOf(u'=');
if (eq < 0)
continue;
group->keys.push_back({raw.left(eq).trimmed(), raw.mid(eq + 1)});
}
return ini;
}
QString
ini_get(const IniGroup &group, const QString &key)
{
for (const auto &kv : group.keys) {
if (kv.first == key)
return kv.second;
}
return {};
}
void
ini_set(IniGroup &group, const QString &key, const QString &value)
{
for (auto &kv : group.keys) {
if (kv.first == key) {
kv.second = value;
return;
}
}
group.keys.push_back({key, value});
}
QString
serialize_ini(const IniFile &ini)
{
QString out;
for (const QString &line : ini.preamble) {
out += line;
out += u'\n';
}
for (const IniGroup &group : ini.groups) {
out += u'[';
out += group.name;
out += QLatin1String("]\n");
for (const auto &kv : group.keys) {
out += kv.first;
out += u'=';
out += kv.second;
out += u'\n';
}
}
return out;
}
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;
QSet<QString> removed;
};
void
append_unique(vector<QString> &list, const QString &id)
{
if (id.isEmpty())
return;
if (std::find(list.begin(), list.end(), id) != list.end())
return;
list.push_back(id);
}
void
apply_mimeapps(AssocSets &acc, const IniFile &ini, const QString &type)
{
for (const IniGroup &group : ini.groups) {
if (group.name == QLatin1String("Default Applications")) {
for (const QString &id : split_semicolons(ini_get(group, type)))
append_unique(acc.defaults, normalize_desktop_id(id));
} else if (group.name == QLatin1String("Added Associations")) {
for (const QString &id : split_semicolons(ini_get(group, type))) {
const QString nid = normalize_desktop_id(id);
if (!acc.removed.contains(nid))
append_unique(acc.added, nid);
}
} else if (group.name == QLatin1String("Removed Associations")) {
for (const QString &id : split_semicolons(ini_get(group, type))) {
const QString nid = normalize_desktop_id(id);
if (std::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, parse_ini(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 = parse_ini(read_text_file(path));
for (const IniGroup &group : ini.groups) {
if (group.name != QLatin1String("MIME Cache"))
continue;
for (const QString &id : split_semicolons(ini_get(group, type)))
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 no_display = false;
bool application = true;
};
QString
localized_name(const IniGroup &entry)
{
QHash<QString, QString> localized;
QString fallback;
for (const auto &kv : entry.keys) {
if (kv.first == QLatin1String("Name")) {
if (fallback.isEmpty())
fallback = unescape_desktop(kv.second);
continue;
}
if (!kv.first.startsWith(QLatin1String("Name[")) ||
!kv.first.endsWith(u']'))
continue;
const QString loc = kv.first.mid(5, kv.first.size() - 6);
localized.insert(loc, unescape_desktop(kv.second));
}
for (const QString &loc : locale_candidates()) {
const auto it = localized.constFind(loc);
if (it != localized.cend())
return *it;
}
return fallback;
}
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 (std::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 (std::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 = parse_ini(read_text_file(d.path));
const IniGroup *entry = nullptr;
for (const IniGroup &group : ini.groups) {
if (group.name == QLatin1String("Desktop Entry")) {
entry = &group;
break;
}
}
if (!entry)
return d;
const QString type = ini_get(*entry, QStringLiteral("Type")).trimmed();
d.application = type.isEmpty() || type == QLatin1String("Application");
d.name = localized_name(*entry);
d.icon = unescape_desktop(ini_get(*entry, QStringLiteral("Icon")));
d.exec = unescape_desktop(ini_get(*entry, QStringLiteral("Exec")));
d.try_exec = unescape_desktop(ini_get(*entry, QStringLiteral("TryExec")));
d.hidden = parse_bool(ini_get(*entry, QStringLiteral("Hidden")));
d.no_display = parse_bool(ini_get(*entry, QStringLiteral("NoDisplay")));
d.only_show_in =
split_semicolons(ini_get(*entry, QStringLiteral("OnlyShowIn")));
d.not_show_in =
split_semicolons(ini_get(*entry, QStringLiteral("NotShowIn")));
return d;
}
const Desktop *
desktop_by_id(const QString &id)
{
static QHash<QString, Desktop> cache;
if (id.isEmpty())
return nullptr;
auto it = cache.find(id);
if (it == cache.end())
it = cache.insert(id, load_desktop(id));
if (it->path.isEmpty() || !it->application)
return nullptr;
return &*it;
}
bool
listable(const Desktop &d)
{
if (d.id == kSelfDesktop)
return false;
if (d.hidden || d.no_display || d.exec.isEmpty())
return false;
if (!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;
}
vector<QString>
filename_types(const QString &path)
{
// Content sniff (mime/magic / QMimeDatabase::mimeTypeForFile) is a later
// follow-up. Directories are inode/directory from stat (GIO get_content_type).
// Regular files use filename globs only.
if (QFileInfo(path).isDir())
return {QStringLiteral("inode/directory")};
return types_for_filename(path);
}
vector<QString>
ancestor_types(const vector<QString> &types)
{
vector<QString> ancestors;
QMimeDatabase mime;
for (const QString &type : types) {
const QMimeType mt = mime.mimeTypeForName(type);
if (!mt.isValid())
continue;
for (const QString &a : mt.allAncestors()) {
if (std::find(types.begin(), types.end(), a) != types.end())
continue;
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.unite(from.removed);
for (const QString &id : into.added)
into.removed.remove(id);
}
bool
usable_id(const QString &id, const QSet<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))
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 vector<QString> types = filename_types(path);
AssocSets acc;
for (const QString &type : types)
merge_assoc(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 vector<QString> types = filename_types(path);
AssocSets acc;
vector<QString> cache_ids;
for (const QString &type : types) {
merge_assoc(acc, associations_for_type(type));
for (const QString &id : cache_ids_for_type(type))
append_unique(cache_ids, id);
}
const Handler def = default_for(path);
vector<Handler> out;
QSet<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 vector<QString> types = filename_types(path);
const vector<QString> ancestors = ancestor_types(types);
AssocSets acc;
for (const QString &type : types)
merge_assoc(acc, associations_for_type(type));
for (const QString &type : ancestors)
merge_assoc(acc, associations_for_type(type));
QSet<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 vector<QString> types = filename_types(path);
if (types.empty())
return;
const QString dest = user_mimeapps_path();
if (dest.isEmpty())
return;
IniFile ini = parse_ini(read_text_file(dest));
auto find_group = [&](const QLatin1String name) -> IniGroup * {
for (IniGroup &group : ini.groups) {
if (group.name == name)
return &group;
}
return nullptr;
};
if (!find_group(QLatin1String("Added Associations"))) {
IniGroup group;
group.name = QStringLiteral("Added Associations");
ini.groups.push_back(std::move(group));
}
IniGroup *added = find_group(QLatin1String("Added Associations"));
IniGroup *removed = find_group(QLatin1String("Removed Associations"));
auto drop_id = [&](IniGroup &group, const QString &type) {
vector<QString> kept;
for (const QString &existing : split_semicolons(ini_get(group, type))) {
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(std::remove_if(group.keys.begin(), group.keys.end(),
[&](const QPair<QString, QString> &kv) {
return kv.first == type;
}),
group.keys.end());
} else {
ini_set(group, type, value);
}
};
for (const QString &type : types) {
ini_set(*added, type, prepend_id(ini_get(*added, type), id));
if (removed)
drop_id(*removed, type);
}
write_text_file(dest, serialize_ini(ini));
}
} // namespace dn
+239
View File
@@ -0,0 +1,239 @@
//
// 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 <QSet>
#include <windows.h>
#include <initguid.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shlwapi.h>
#include <shobjidl.h>
#include <string>
#include <vector>
namespace dn
{
namespace
{
void
ensure_com()
{
static const HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
(void) hr;
}
QString
extension_of(const QString &path)
{
if (QFileInfo(path).isDir())
return QStringLiteral("Directory");
const QString suffix = QFileInfo(path).suffix();
if (suffix.isEmpty())
return {};
return QStringLiteral(".") + suffix.toLower();
}
QString
from_wide(const wchar_t *s)
{
return s ? QString::fromWCharArray(s) : QString();
}
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;
}
std::vector<Handler>
enum_handlers(const QString &ext, ASSOC_FILTER filter)
{
std::vector<Handler> out;
if (ext.isEmpty())
return out;
const std::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;
}
IAssocHandler *
find_handler(const QString &ext, const QString &id)
{
if (ext.isEmpty() || id.isEmpty())
return nullptr;
const std::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;
}
} // namespace
Handler
default_for(const QString &path)
{
ensure_com();
const QString ext = extension_of(path);
if (ext.isEmpty())
return {};
const std::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 std::vector<Handler> rec = enum_handlers(ext, ASSOC_FILTER_RECOMMENDED);
if (!rec.empty())
return rec.front();
return {};
}
std::vector<Handler>
recommended_for(const QString &path)
{
ensure_com();
const Handler def = default_for(path);
std::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;
}
std::vector<Handler>
fallback_for(const QString &path)
{
ensure_com();
const QString ext = extension_of(path);
const std::vector<Handler> rec = enum_handlers(ext, ASSOC_FILTER_RECOMMENDED);
QSet<QString> seen;
for (const Handler &a : rec)
seen.insert(a.id);
std::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;
const QString ext = extension_of(path);
IAssocHandler *handler = find_handler(ext, app.id);
if (!handler)
return false;
const QString abs = QFileInfo(path).absoluteFilePath();
const std::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
+2749
View File
File diff suppressed because it is too large Load Diff
+170
View File
@@ -0,0 +1,170 @@
//
// browser.hpp: directory browser (fiv-style masonry thumbs)
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "chrome.hpp"
#include "kit.hpp"
#include "sheet.hpp"
#include "thumbnailer.hpp"
#include "types.hpp"
#include <libdn.h>
#include <QString>
#include <cstdint>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace dn
{
constexpr float kBrowseSidebarPts = 192.0f;
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;
std::vector<uint16_t> ram;
bool ram_interim = false;
bool ram_pending = false;
bool cache_bypass = false;
bool regen_failed = false;
dn::Transfer transfer = dn::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;
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;
};
Kit &kit_;
Thumbnailer &thumbnailer_;
uint64_t thumbnail_client_ = 0;
Page *page_ = nullptr;
ScrollColumn *places_ = nullptr;
std::vector<PlaceItem> place_items_;
QString dir_path_;
std::shared_ptr<Cmm> cmm_;
std::shared_ptr<Profile> screen_profile_;
bool show_names_ = false;
BrowseSetup setup_;
BrowserView view_ = BrowserView::Tile;
int thumb_size_ = 256;
bool places_dirty_ = true;
Scroll scroll_;
struct GridRow {
int first = 0;
int count = 0;
float y = 0;
float 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;
float layout_w_ = 0;
int mid_file_ = -1;
std::vector<File> files_;
std::vector<DirRow> side_dirs_;
std::unordered_map<std::string, CachedSize> size_cache_;
std::unordered_map<std::string, Thumbnailer::Priority> thumb_inflight_;
struct HistEntry {
QString path;
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, float max_w, float 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]] 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 QString &path, bool record = true);
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 std::string &path);
void file_gone(const std::string &path);
[[nodiscard]] BrowseSetup browse_setup() const { return this->setup_; }
void set_screen_profile(
std::shared_ptr<Cmm> cmm, std::shared_ptr<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;
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, int key, unsigned mods) 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
+676
View File
@@ -0,0 +1,676 @@
//
// chrome.cpp: toolbar, sidebar, and page chrome widgets
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "chrome.hpp"
#include <QKeyEvent>
#include <QtGlobal>
#include <algorithm>
#include <functional>
#include <memory>
#include <vector>
using namespace std;
namespace dn
{
namespace
{
constexpr float kWinPadX = 4.0f;
constexpr float kWinPadY = 2.0f;
void
sync_overflow_proxy(Widget &source, Widget &proxy)
{
Widget *parent = proxy.parent_;
const bool visible = proxy.visible;
const bool layout_visible = proxy.layout_visible;
if (auto *button = dynamic_cast<Button *>(&source))
static_cast<Button &>(proxy) = *button;
else if (auto *label = dynamic_cast<Label *>(&source))
static_cast<Label &>(proxy) = *label;
proxy.parent_ = parent;
proxy.visible = visible;
proxy.layout_visible = layout_visible;
}
unique_ptr<Widget>
overflow_proxy(Widget &source)
{
if (dynamic_cast<Button *>(&source))
return make_unique<Button>();
if (dynamic_cast<Label *>(&source))
return make_unique<Label>();
if (dynamic_cast<Sep *>(&source))
return make_unique<Sep>();
return nullptr;
}
void
add_overflow_proxies(Overflow &overflow, ToolbarSlot *row)
{
if (!row)
return;
const size_t end = row->item_count();
for (size_t i = 0; i < end; ++i) {
if (!row->kids[i])
continue;
auto proxy = overflow_proxy(*row->kids[i]);
if (!proxy)
continue;
sync_overflow_proxy(*row->kids[i], *proxy);
proxy->visible = false;
if (overflow.col) {
overflow.sources.push_back(row->kids[i].get());
overflow.col->add_child(std::move(proxy));
}
}
}
void
fill_overflow_items(ToolbarSlot &row, Overflow &overflow)
{
if (!overflow.col)
return;
for (auto &item : overflow.col->kids)
item->visible = false;
const size_t end = row.item_count();
size_t a = min(row.split_, end);
size_t b = end;
while (a < b && is_sep(row.kids[a].get()))
++a;
while (b > a && is_sep(row.kids[b - 1].get()))
--b;
for (size_t i = a; i < b; ++i) {
Widget *source = row.kids[i].get();
for (size_t j = 0; j < overflow.sources.size(); ++j) {
if (overflow.sources[j] != source)
continue;
Widget &proxy = *overflow.col->kids[j];
sync_overflow_proxy(*source, proxy);
proxy.visible = source->visible;
break;
}
}
}
} // namespace
// --- ToolbarSlot ------------------------------------------------------------
ToolbarSlot::ToolbarSlot()
{
auto button = make_unique<Button>();
button->visible = false;
button->icon = "disclose-arrow-down-symbolic";
button->tip_text = "More";
this->more = button.get();
Composite::add_child(std::move(button));
}
Widget *
ToolbarSlot::add_item(unique_ptr<Widget> item, size_t at)
{
return Composite::add_child(std::move(item), min(at, item_count()));
}
size_t
ToolbarSlot::item_count() const
{
return this->more && !this->kids.empty() ? this->kids.size() - 1 : 0;
}
void
ToolbarSlot::measure(Kit &kit, float max_w, float max_h)
{
for (size_t i = 0; i < item_count(); ++i) {
if (this->kids[i])
this->kids[i]->layout_visible = true;
}
this->more->visible = false;
this->split_ = item_count();
Row::measure(kit, max_w, max_h);
}
void
ToolbarSlot::arrange(Kit &kit, Rect alloc)
{
if (!this->visible) {
this->r = {};
this->split_ = 0;
return;
}
const Rect in = kit.snap_rect(alloc).inset(this->pad_x, this->pad_y);
const size_t end = item_count();
measure(kit, kUnlim, alloc.h);
const float total = max(0.0f, this->r.w - this->pad_x * 2.0f);
this->split_ = end;
this->more->visible = false;
if (total > in.w) {
this->more->visible = true;
this->more->measure(kit, kUnlim, in.h);
const float budget = max(0.0f, in.w - this->more->r.w - this->gap);
float used = 0.0f;
int kept = 0;
bool full = false;
this->split_ = 0;
for (size_t i = 0; i < end; ++i) {
Widget *item = this->kids[i].get();
if (!item || !item->shown()) {
if (!full)
this->split_ = i + 1;
continue;
}
const float need = item->r.w + (kept ? this->gap : 0.0f);
if (full || used + need > budget) {
full = true;
continue;
}
used += need;
++kept;
this->split_ = i + 1;
}
while (this->split_ > 0 &&
(!this->kids[this->split_ - 1] ||
is_sep(this->kids[this->split_ - 1].get())))
--this->split_;
this->more->visible = this->split_ < end;
}
for (size_t i = this->split_; i < end; ++i) {
if (this->kids[i])
this->kids[i]->layout_visible = false;
}
Row::arrange(kit, alloc);
}
// --- Toolbar ---------------------------------------------------------------
Toolbar::Toolbar(unique_ptr<ToolbarSlot> left_row,
unique_ptr<ToolbarSlot> mid_row, unique_ptr<ToolbarSlot> right_row)
{
this->pad_x = kWinPadX;
this->pad_y = kWinPadY;
this->fill = Fill::Gradient;
this->stroke = Stroke::Bottom;
this->hittable = true;
this->left = left_row.get();
if (left_row)
add_child(std::move(left_row));
this->mid = mid_row.get();
if (mid_row)
add_child(std::move(mid_row));
this->right = right_row.get();
if (right_row)
add_child(std::move(right_row));
#if !defined(Q_OS_MACOS)
if (this->left) {
auto app = make_unique<Button>();
app->icon = "open-menu-symbolic";
app->tip_text = "Menu";
this->app_menu_button = app.get();
this->left->add_item(make_unique<Sep>(), 0);
this->left->add_item(std::move(app), 0);
}
#endif
this->overflow_owned_ = make_unique<Overflow>();
this->overflow_owned_->pad_y = kWinPadY;
this->overflow = this->overflow_owned_.get();
add_overflow_proxies(*this->overflow, this->left);
add_overflow_proxies(*this->overflow, this->mid);
add_overflow_proxies(*this->overflow, this->right);
this->app_menu_owned_ = make_unique<Menu>();
this->app_menu = this->app_menu_owned_.get();
this->overflow->fill_items = [this] {
if (Button *m = this->overflow->opener) {
if (ToolbarSlot *slot = slot_for_more(m))
fill_overflow_items(*slot, *this->overflow);
}
};
if (this->app_menu_button) {
this->app_menu_button->activate_on_press = true;
this->app_menu_button->on_click = [this](Kit &kit) {
open_app_menu(kit, false);
};
}
auto bind_more = [this](Button *m) {
if (!m)
return;
m->activate_on_press = true;
m->on_click = [this, m](Kit &kit) {
if (this->overflow->visible && this->overflow->opener == m)
this->overflow->close(kit);
else
this->overflow->open(kit, m);
};
};
bind_more(this->left ? this->left->more : nullptr);
bind_more(this->mid ? this->mid->more : nullptr);
bind_more(this->right ? this->right->more : nullptr);
}
unique_ptr<Overflow>
Toolbar::take_overflow()
{
return std::move(this->overflow_owned_);
}
unique_ptr<Menu>
Toolbar::take_app_menu()
{
return std::move(this->app_menu_owned_);
}
void
Toolbar::open_app_menu(Kit &kit, bool kbd)
{
if (!this->app_menu)
return;
if (this->app_menu->visible) {
this->app_menu->close(kit);
return;
}
if (!this->app_menu_button)
return;
Button *anchor = this->app_menu_button;
if (!anchor->shown() && this->left && this->left->more->shown())
anchor = this->left->more;
this->app_menu->open(kit, anchor);
if (kbd)
this->app_menu->select_first(kit);
}
void
Toolbar::sync_buttons()
{
auto apply = [this](Widget *w) {
auto *btn = dynamic_cast<Button *>(w);
if (!btn)
return;
if (btn->action == Action::None)
return;
const ActionDef &d = action_def(btn->action);
const bool on = this->actor.checked && this->actor.checked(btn->action);
btn->enabled_ =
!this->actor.enabled || this->actor.enabled(btn->action);
btn->active = on && btn->action != Action::SortDir;
btn->icon = action_icon(d, on);
btn->tip_text = action_tip(d, on);
btn->tip_accel = action_accel(d);
};
auto walk = [&apply](const ToolbarSlot *row) {
if (!row)
return;
for (auto &k : row->kids)
apply(k.get());
};
walk(this->left);
walk(this->mid);
walk(this->right);
for (ToolbarSlot *slot : {this->left, this->mid, this->right}) {
if (slot && slot->more)
slot->more->active = this->overflow && this->overflow->visible &&
this->overflow->opener == slot->more;
}
if (this->app_menu_button)
this->app_menu_button->active = app_menu_open();
if (this->app_menu)
this->app_menu->sync();
}
void
Toolbar::measure(Kit &kit, float avail_w, float avail_h)
{
const float ih = max(0.0f, avail_h - this->pad_y * 2.0f);
float h = 0.0f;
auto slot = [&](Widget *w) {
if (!w)
return;
w->measure(kit, kUnlim, ih);
h = max(h, w->r.h);
};
slot(this->left);
slot(this->mid);
slot(this->right);
this->r.w = avail_w;
this->r.h = this->pad_y * 2.0f + h;
if (avail_h > 0.0f)
this->r.h = min(this->r.h, avail_h);
}
void
Toolbar::arrange(Kit &kit, Rect alloc)
{
if (!this->visible) {
this->r = {};
return;
}
this->r = kit.snap_rect(alloc);
place_slots(kit);
}
void
Toolbar::place_slots(Kit &kit)
{
if (this->r.w <= 0.0f)
return;
const Rect bar = this->r.inset(this->pad_x, this->pad_y);
if (bar.w <= 0.0f)
return;
const float avail = bar.w;
const float h = bar.h;
const float x0 = bar.x;
const float y0 = bar.y;
auto nat = [&](Widget *w) {
if (!w)
return 0.0f;
w->measure(kit, kUnlim, h);
return w->r.w;
};
const float lw = nat(this->left);
const float mw = nat(this->mid);
const float rw = nat(this->right);
float mmin = 0.0f;
if (mw > 0.0f && this->mid && this->mid->more) {
this->mid->more->measure(kit, kUnlim, h);
mmin = min(mw, this->mid->more->r.w);
}
float left_w = lw;
float right_w = rw;
float mid_x;
if (lw + mw + rw <= avail) {
mid_x = x0 + clamp((avail - mw) * 0.5f, lw, avail - rw - mw);
} else {
const float keep = min(mmin, avail);
const float rest = max(0.0f, avail - keep);
left_w = min(lw, rest * 0.5f);
right_w = min(rw, rest - left_w);
left_w = min(lw, rest - right_w);
mid_x = x0 + left_w;
}
const float mid_w = x0 + avail - right_w - mid_x;
if (this->left)
this->left->arrange(kit, {x0, y0, left_w, h});
if (this->mid) {
this->mid->align = Align::Start;
this->mid->arrange(kit, {mid_x, y0, mid_w, h});
}
if (this->right)
this->right->arrange(kit, {x0 + avail - right_w, y0, right_w, h});
}
ToolbarSlot *
Toolbar::slot_for_more(const Button *more) const
{
if (!more)
return nullptr;
for (ToolbarSlot *slot : {this->left, this->mid, this->right}) {
if (slot && more == slot->more)
return slot;
}
return nullptr;
}
Sidebar::Sidebar(unique_ptr<Widget> child)
{
this->content = child.get();
this->fill = Fill::Solid;
this->hittable = true;
this->clip = true;
if (child)
add_child(std::move(child));
}
bool
Sidebar::key(Kit &kit, int key, unsigned mods)
{
if (mods)
return false;
if (key != Qt::Key_Up && key != Qt::Key_Down)
return false;
const int dir = key == Qt::Key_Up ? -1 : 1;
return kit.cycle_focus(this, dir, false);
}
// --- Page -------------------------------------------------------------------
namespace
{
constexpr float kMinWell = 80.0f;
constexpr float kMinSide = 120.0f;
constexpr float kSplitW = 8.0f;
} // namespace
Page::Page(unique_ptr<Toolbar> tb, unique_ptr<Sidebar> sb, Side s,
unique_ptr<Widget> body)
: side(s)
{
this->toolbar = tb.get();
add_child(std::move(tb));
this->sidebar = sb.get();
add_child(std::move(sb));
if (this->sidebar) {
auto split = make_unique<Splitter>();
split->min_w = kSplitW;
split->hittable = true;
this->splitter = split.get();
this->splitter->on_drag = [this](float mx) {
if (!this->sidebar_open || this->side == Side::None)
return;
const float max_side = max(kMinSide, this->r.w - kMinWell);
if (this->side == Side::Right)
this->sidebar_w =
clamp(this->r.x + this->r.w - mx, kMinSide, max_side);
else
this->sidebar_w = clamp(mx - this->r.x, kMinSide, max_side);
};
add_child(std::move(split));
}
this->content = body.get();
add_child(std::move(body));
if (this->toolbar) {
this->overflow_owned_ = this->toolbar->take_overflow();
this->app_menu_owned_ = this->toolbar->take_app_menu();
}
this->modal_owned_ = make_unique<Modal>();
this->modal = this->modal_owned_.get();
this->hint_owned_ = make_unique<Hint>();
this->hint = this->hint_owned_.get();
this->hint->page = this;
this->context_owned_ = make_unique<ContextMenu>();
this->context = this->context_owned_.get();
if (this->sidebar) {
if (this->sidebar->min_w > 0.0f)
this->sidebar_w = this->sidebar->min_w;
this->sidebar_open = this->sidebar->visible;
} else {
this->sidebar_open = false;
this->side = Side::None;
}
}
void
Page::set_banner(unique_ptr<Widget> w)
{
if (this->banner)
this->banner->parent_ = nullptr;
this->banner = w.get();
if (this->banner)
this->banner->parent_ = this;
this->banner_owned_ = std::move(w);
}
void
Page::measure(Kit &, float max_w, float max_h)
{
this->r = {0.0f, 0.0f, max_w, max_h};
}
void
Page::arrange(Kit &kit, Rect alloc)
{
if (!this->visible) {
this->r = {};
this->well_ = {};
return;
}
this->r = kit.snap_rect(alloc);
float y = this->r.y;
if (this->toolbar && this->toolbar->visible) {
this->toolbar->measure(kit, this->r.w, this->r.h);
this->toolbar->arrange(
kit, {this->r.x, y, this->r.w, this->toolbar->r.h});
y += this->toolbar->r.h;
}
if (this->banner && this->banner->visible) {
const float rest = max(0.0f, this->r.y + this->r.h - y);
this->banner->measure(kit, this->r.w, rest);
this->banner->arrange(
kit, {this->r.x, y, this->r.w, this->banner->r.h});
y += this->banner->r.h;
}
const float body_y = y;
const float body_h = max(0.0f, this->r.y + this->r.h - body_y);
float side_w = 0.0f;
if (this->sidebar) {
this->sidebar->visible =
this->sidebar_open && this->side != Side::None && body_h > 0.0f;
if (this->sidebar->visible) {
side_w = max(0.0f, this->sidebar_w);
this->sidebar->min_w = side_w;
if (this->side == Side::Left)
this->sidebar->arrange(
kit, {this->r.x, body_y, side_w, body_h});
else
this->sidebar->arrange(kit,
{this->r.x + this->r.w - side_w, body_y, side_w, body_h});
} else {
this->sidebar->r = {};
}
}
this->well_ = {this->r.x, body_y, this->r.w, body_h};
if (this->sidebar && this->sidebar->visible) {
if (this->side == Side::Left)
this->well_.x += side_w;
this->well_.w = max(0.0f, this->well_.w - side_w);
}
if (this->content && this->content->visible)
this->content->arrange(kit, this->well_);
kit.default_focus_ = this->content;
if (this->splitter) {
this->splitter->visible = this->sidebar && this->sidebar->visible;
if (this->splitter->visible) {
const float sw =
this->splitter->min_w > 0.0f ? this->splitter->min_w : kSplitW;
float sx = this->side == Side::Right
? this->well_.x + this->well_.w - sw * 0.5f
: this->well_.x - sw * 0.5f;
this->splitter->arrange(kit, {sx, body_y, sw, body_h});
} else {
this->splitter->r = {};
}
}
}
bool
Page::key(Kit &kit, int key, unsigned mods)
{
constexpr Action pane[] = {Action::NextPane, Action::PrevPane};
const Action a = match_key(pane, key, mods);
if (a == Action::None) {
const Action mode = match_key(this->keys, key, mods);
if (mode == Action::None)
return false;
if (this->actor.apply)
this->actor.apply(mode);
return true;
}
Widget *here = nullptr;
for (Widget *w = kit.focus_; w; w = w->parent_) {
if (w == this->toolbar || w == this->sidebar || w == this->content) {
here = w;
break;
}
}
Widget *panes[3];
int n = 0, i = 0;
for (size_t c = 0; c < child_count(); ++c) {
Widget *k = child(c);
if (!k || !k->visible ||
(k != this->toolbar && k != this->sidebar && k != this->content))
continue;
if (k == here)
i = n;
panes[n++] = k;
}
if (!n)
return false;
const int dir = a == Action::PrevPane ? -1 : 1;
kit.focus_first(panes[(i + dir + n) % n]);
return true;
}
size_t
Page::child_count() const
{
return 5;
}
Widget *
Page::child(size_t i) const
{
const bool right = this->side == Side::Right;
switch (i) {
case 0:
return this->toolbar;
case 1:
return this->banner;
case 2:
return right ? this->content : this->sidebar;
case 3:
return right ? this->sidebar : this->content;
case 4:
return this->splitter;
default:
return nullptr;
}
}
Actor
chain_actor(const HostActions &host, function<bool(Action)> apply,
function<bool(Action)> enabled, function<bool(Action)> checked)
{
auto can = [&host, enabled](Action action) {
if (action == Action::Back || action == Action::Forward)
return host.enabled && host.enabled(action);
return !enabled || enabled(action);
};
Actor actor;
actor.apply = [&host, apply, can](Action action) {
if (!can(action))
return;
if (apply && apply(action))
return;
if (host.apply)
host.apply(action);
};
actor.enabled = std::move(can);
actor.checked = std::move(checked);
return actor;
}
} // namespace dn
+132
View File
@@ -0,0 +1,132 @@
//
// chrome.hpp: toolbar, sidebar, and page chrome widgets
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "action.hpp"
#include "app-menu.hpp"
#include "hint.hpp"
#include "kit.hpp"
#include "types.hpp"
#include <functional>
#include <memory>
#include <span>
#include <string>
namespace dn
{
struct HostActions {
std::function<void(Action)> apply;
std::function<bool(Action)> enabled;
std::function<void(std::string path)> activate;
std::function<void(std::string path)> new_window;
std::function<void(std::string path)> trash;
std::function<void(QString path)> launch_exiftool;
};
struct ToolbarSlot : Row {
Button *more = nullptr;
std::size_t split_ = 0;
ToolbarSlot();
Widget *add_item(
std::unique_ptr<Widget> item, std::size_t at = std::size_t(-1));
[[nodiscard]] std::size_t item_count() const;
void measure(Kit &kit, float max_w, float max_h) override;
void arrange(Kit &kit, Rect alloc) override;
private:
using Composite::add_child;
using Composite::erase_children;
};
struct Toolbar : Panel {
Actor actor;
Button *app_menu_button = nullptr;
ToolbarSlot *left = nullptr;
ToolbarSlot *mid = nullptr;
ToolbarSlot *right = nullptr;
Overflow *overflow = nullptr;
Menu *app_menu = nullptr;
Toolbar(std::unique_ptr<ToolbarSlot> left_row,
std::unique_ptr<ToolbarSlot> mid_row,
std::unique_ptr<ToolbarSlot> right_row);
[[nodiscard]] std::unique_ptr<Overflow> take_overflow();
[[nodiscard]] std::unique_ptr<Menu> take_app_menu();
void open_app_menu(Kit &kit, bool kbd);
[[nodiscard]] bool app_menu_open() const
{
return this->app_menu && this->app_menu->visible;
}
void sync_buttons();
void measure(Kit &kit, float max_w, float max_h) override;
void arrange(Kit &kit, Rect alloc) override;
private:
std::unique_ptr<Overflow> overflow_owned_;
std::unique_ptr<Menu> app_menu_owned_;
void place_slots(Kit &kit);
ToolbarSlot *slot_for_more(const Button *more) const;
};
struct Sidebar : Panel {
Widget *content = nullptr;
explicit Sidebar(std::unique_ptr<Widget> child);
bool key(Kit &kit, int key, unsigned mods) override;
};
struct Page : Composite {
Toolbar *toolbar = nullptr;
Sidebar *sidebar = nullptr;
Splitter *splitter = nullptr;
Widget *content = nullptr;
Widget *banner = nullptr;
Modal *modal = nullptr;
Hint *hint = nullptr;
ContextMenu *context = 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 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 measure(Kit &kit, float max_w, float max_h) override;
void arrange(Kit &kit, Rect alloc) override;
bool key(Kit &kit, int key, unsigned mods) override;
std::size_t child_count() const override;
Widget *child(std::size_t i) const override;
[[nodiscard]] float toolbar_h() const { return this->well_.y - this->r.y; }
[[nodiscard]] Rect well() const { return this->well_; }
private:
std::unique_ptr<Widget> banner_owned_;
std::unique_ptr<Overflow> overflow_owned_;
std::unique_ptr<Menu> app_menu_owned_;
std::unique_ptr<Modal> modal_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
+301
View File
@@ -0,0 +1,301 @@
//
// cie-diagram.cpp: CIE 1931 xy sidebar widget
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "cie-diagram.hpp"
#include <QColor>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QString>
#include <algorithm>
#include <cmath>
namespace dn
{
namespace
{
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.0f;
constexpr Colour kMidGreyCol{188 / 255.0f, 188 / 255.0f, 188 / 255.0f, 1.0f};
constexpr Colour kBlackCol{0.0f, 0.0f, 0.0f, 1.0f};
constexpr Colour kWhiteCol{1.0f, 1.0f, 1.0f, 1.0f};
const QString kSourceLab = QStringLiteral("Source");
const QString kTargetLab = QStringLiteral("Target");
float
caption_h(const Kit &kit)
{
return kCapGap + kit.text_height(kSourceLab, 0.0f, false) + 4.;
}
Rect
plot_rect(Rect r, float dpr)
{
if (r.w <= 0.0f || r.h <= 0.0f || dpr <= 0.0f)
return {};
const float aspect = kXMax / kYMax;
const float nw = float(kRasterW) / dpr;
const float nh = float(kRasterH) / dpr;
float w = std::min(r.w, nw);
float h = w / aspect;
if (h > r.h || h > nh) {
h = std::min(r.h, nh);
w = h * aspect;
}
return {r.x + (r.w - w) * 0.5f, r.y, w, h};
}
QPointF
xy_to_px(double x, double y, int w, int h)
{
return {x / double(kXMax) * w, (1.0 - y / double(kYMax)) * h};
}
QPainterPath
locus_path(int w, int h)
{
QPainterPath path;
const auto locus = cie1931_locus();
if (locus.empty())
return path;
path.moveTo(xy_to_px(locus.front().x, locus.front().y, w, h));
for (size_t i = 1; i < locus.size(); ++i)
path.lineTo(xy_to_px(locus[i].x, locus[i].y, w, h));
path.closeSubpath();
return path;
}
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 * std::pow(u, 1.0 / 2.4) - 0.055) * 255.0 + 0.5);
}
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);
}
void
stroke_poly(QPainter &p, const QPen &pen, const 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);
}
QImage
raster_diagram(int w, int h, const Chromaticities &image,
const 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;
}
bool
same_chroma(const Chromaticities &a, const 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;
}
} // namespace
void
CieDiagram::measure(Kit &kit, float max_w, float max_h)
{
const float cap = caption_h(kit);
const float plot_h = std::max(0.0f, max_h - cap);
const Rect fit = plot_rect({0, 0, max_w, plot_h}, kit.dpr_);
const float labs = kit.text_width(kSourceLab, false) + 8. +
kit.text_width(kTargetLab, false);
this->r.w = std::max(fit.w, labs);
this->r.h = fit.h + cap;
}
void
CieDiagram::arrange(Kit &kit, Rect alloc)
{
this->r = kit.snap_rect(alloc);
}
void
CieDiagram::prepare(Kit &kit)
{
kit.cache_text(kSourceLab, false);
kit.cache_text(kTargetLab, false);
const float cap = caption_h(kit);
const Rect plot = plot_rect(
{this->r.x, this->r.y, this->r.w, std::max(0.0f, this->r.h - cap)},
kit.dpr_);
if (plot.w < 8.0f || plot.h < 8.0f)
return;
const bool epoch_ok =
this->epoch_ == kit.atlas_epoch_ && !this->slot_.empty();
const bool chroma_ok = this->packed_show_screen_ == this->show_screen &&
this->packed_screen_dashed_ == this->screen_dashed &&
this->packed_image_dashed_ == this->image_dashed &&
same_chroma(this->packed_image_, this->image) &&
same_chroma(this->packed_screen_, this->screen);
if (epoch_ok && chroma_ok)
return;
if (epoch_ok)
kit.atlas_.release(this->slot_);
this->slot_ = kit.pack_bitmap(
raster_diagram(kRasterW, kRasterH, this->image, this->screen,
this->show_screen, this->screen_dashed, this->image_dashed));
if (this->slot_.empty())
return;
this->epoch_ = kit.atlas_epoch_;
this->packed_image_ = this->image;
this->packed_screen_ = this->screen;
this->packed_show_screen_ = this->show_screen;
this->packed_screen_dashed_ = this->screen_dashed;
this->packed_image_dashed_ = this->image_dashed;
}
void
CieDiagram::paint(Kit &kit) const
{
const float cap = caption_h(kit);
const Rect plot = plot_rect(
{this->r.x, this->r.y, this->r.w, std::max(0.0f, this->r.h - cap)},
kit.dpr_);
const float x0 = plot.x > 0.0f ? plot.x : this->r.x;
const float cap_y0 = plot.h >= 8.0f ? plot.y + plot.h : this->r.y;
const float y = cap_y0 + kCapGap;
const float th = kit.text_height(kSourceLab, 0.0f, true);
const float cap_w = plot.w >= 8.0f ? plot.w : this->r.w;
kit.list_.add_rect_filled(x0, y, x0 + cap_w, y + th, kMidGreyCol);
if (plot.w >= 8.0f && plot.h >= 8.0f && !this->slot_.empty()) {
float u0 = 0, v0 = 0, u1 = 0, v1 = 0;
kit.atlas_.uv(this->slot_, &u0, &v0, &u1, &v1);
kit.list_.add_image(plot.x, plot.y, plot.x + plot.w, plot.y + plot.h,
u0, v0, u1, v1, {1, 1, 1, 1});
}
const float cx = x0 + cap_w / 2;
const float widthS = kit.text_width(kSourceLab, true);
kit.emit_text(cx - 4. - widthS, y, kSourceLab, kBlackCol, true);
kit.emit_text(cx + 4., y, kTargetLab, kWhiteCol, true);
}
} // namespace dn
+39
View File
@@ -0,0 +1,39 @@
//
// cie-diagram.hpp: CIE 1931 xy sidebar widget
//
// Copyright The dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "kit.hpp"
#include <libdn.h>
namespace dn
{
struct CieDiagram : Widget {
Chromaticities image{};
Chromaticities screen{};
bool show_screen = false;
bool screen_dashed = false;
bool image_dashed = false;
void measure(Kit &kit, float max_w, float max_h) override;
void arrange(Kit &kit, Rect alloc) override;
void prepare(Kit &kit) override;
void paint(Kit &kit) const override;
private:
Kit::Packed slot_{};
uint32_t epoch_ = 0;
Chromaticities packed_image_{};
Chromaticities packed_screen_{};
bool packed_show_screen_ = false;
bool packed_screen_dashed_ = false;
bool packed_image_dashed_ = false;
};
} // namespace dn
+322
View File
@@ -0,0 +1,322 @@
//
// 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 <cstdio>
#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) {
fprintf(stderr, "display profile: Qt has not assigned a screen yet\n");
return result;
}
if (!client || !cd_client_get_connected(client))
return result;
const string connector = screen->name().toStdString();
if (connector.empty()) {
fprintf(
stderr, "display profile: Qt screen has no connector name yet\n");
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) {
fprintf(stderr, "colord: get display devices: %s\n",
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) {
fprintf(
stderr, "colord: no device for connector %s\n", connector.c_str());
return result;
}
CdProfile *profile = cd_device_get_default_profile(matched);
if (!profile || !cd_profile_connect_sync(profile, nullptr, &error)) {
fprintf(stderr, "colord: display profile unavailable for %s\n",
connector.c_str());
return result;
}
CdIcc *icc =
cd_profile_load_icc(profile, CD_ICC_LOAD_FLAGS_ALL, nullptr, &error);
if (!icc) {
fprintf(stderr, "colord: load ICC: %s\n",
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");
printf("ICC source: colord (connector=%s via %s, profile=%s)\n",
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)) {
fprintf(stderr, "colord: connect: %s\n",
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;
}
fprintf(stderr, "colord: connect: %s\n",
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
+102
View File
@@ -0,0 +1,102 @@
//
// 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>
#import <AppKit/AppKit.h>
#import <CoreGraphics/CoreGraphics.h>
#include <cstdio>
#include <functional>
#include <utility>
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();
printf("ICC source: ColorSync (%s)\n", result.label.c_str());
return result;
}
struct CocoaSource final : DisplayProfileSource {
std::function<void()> on_change;
id observer = nil;
~CocoaSource() override;
void start(std::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(std::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
std::unique_ptr<DisplayProfileSource>
make_display_profile_source()
{
return std::make_unique<CocoaSource>();
}
} // namespace dn
+191
View File
@@ -0,0 +1,191 @@
//
// 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 <windows.h>
#include <cstdio>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
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);
std::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)) {
fprintf(stderr, "Windows ICM: cannot read %s\n",
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();
printf("ICC source: Windows ICM (%s)\n", result.label.c_str());
return result;
}
struct Watch {
HKEY key = nullptr;
HANDLE event = nullptr;
std::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 {
std::function<void()> on_change;
Watch system;
Watch user;
void start(std::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 = std::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(std::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))
fprintf(stderr,
"Windows ICM: cannot watch system profile associations\n");
if (!this->bind(this->user, HKEY_CURRENT_USER, kUserLeaf) &&
!this->bind(this->user, HKEY_CURRENT_USER, kUserParent))
fprintf(stderr,
"Windows ICM: cannot watch user profile associations\n");
}
DisplayProfile
WcsSource::load(QScreen *screen)
{
return load_display_profile(screen);
}
} // namespace
std::unique_ptr<DisplayProfileSource>
make_display_profile_source()
{
return std::make_unique<WcsSource>();
}
} // namespace dn
+68
View File
@@ -0,0 +1,68 @@
//
// 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>
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, std::function<void()> fn)
{
unlisten(key);
this->listeners_.emplace_back(key, std::move(fn));
}
void
DisplayProfileWatch::unlisten(const void *key)
{
std::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
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Type=Application
Name=dn
GenericName=Image Browser
X-GNOME-FullName=dn Colour-Managed Image Browser
Icon=dn
Exec=dn -- %u
NoDisplay=true
Terminal=false
StartupNotify=true
Categories=Utility;FileTools;
MimeType=inode/directory;
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=dn
GenericName=Image Browser
X-GNOME-FullName=dn Colour-Managed Image Browser
Icon=dn
Exec=dn -- %U
Terminal=false
StartupNotify=true
Categories=Graphics;2DGraphics;Viewer;
MimeType=image/bmp;image/gif;image/png;image/x-tga;image/jpeg;image/webp;image/svg+xml;
+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="#ffee00" offset="0" />
<stop stop-color="#ff7a00" 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

+90
View File
@@ -0,0 +1,90 @@
// gen-icon.swift: generate a program icon for dn in the Apple icon format
//
// Copyright The dawn Au