Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
BasedOnStyle: LLVM
|
||||
ColumnLimit: 80
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: ForContinuationAndIndentation
|
||||
AccessModifierOffset: -4
|
||||
# Member definitions keep the return type on its own line in .cpp files.
|
||||
# In-class getters stay on one line (AllDefinitions forces them to wrap).
|
||||
AlwaysBreakAfterReturnType: TopLevelDefinitions
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
BreakBeforeBraces: Linux
|
||||
SpaceAfterCStyleCast: true
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignOperands: DontAlign
|
||||
SpacesBeforeTrailingComments: 2
|
||||
WhitespaceSensitiveMacros: ['G_DEFINE_QUARK']
|
||||
@@ -0,0 +1,4 @@
|
||||
/build/
|
||||
/build-*/
|
||||
compile_commands.json
|
||||
.cache/
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# This is the list of dawn's significant contributors.
|
||||
Přemysl Eric Janouch <p@janouch.name>
|
||||
+218
@@ -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()
|
||||
@@ -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
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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 ()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Executable
+262
@@ -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
|
||||
@@ -0,0 +1,68 @@
|
||||
# Overlay for jpeg-quantsmooth (copied into the FetchContent source tree).
|
||||
# Port of fiv/subprojects/packagefiles/libjpegqs/meson.build.
|
||||
#
|
||||
# One static archive (not several link_with deps): the selector and SIMD
|
||||
# variants reference each other, and separate .a files lose do_quantsmooth
|
||||
# when linked into libdn.so (GNU ld single-pass archives).
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(jpegqs LANGUAGES C)
|
||||
|
||||
if(NOT TARGET PkgConfig::JPEG)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(JPEG REQUIRED IMPORTED_TARGET libjpeg)
|
||||
endif()
|
||||
|
||||
find_library(JPEGQS_LIBM m)
|
||||
|
||||
set(_jpegqs_defs WITH_LOG)
|
||||
set(_jpegqs_libs PkgConfig::JPEG)
|
||||
if(JPEGQS_LIBM)
|
||||
list(APPEND _jpegqs_libs "${JPEGQS_LIBM}")
|
||||
endif()
|
||||
|
||||
# Do not put the quantsmooth source dir on the include path: it ships a
|
||||
# jconfig.h that shadows the system jpeg-turbo one under multiarch layouts.
|
||||
|
||||
function(dn_jpegqs_object name)
|
||||
cmake_parse_arguments(A "" "" "DEFS;FLAGS" ${ARGN})
|
||||
add_library(jpegqs-${name} OBJECT libjpegqs.c)
|
||||
target_compile_definitions(jpegqs-${name} PRIVATE ${_jpegqs_defs} ${A_DEFS})
|
||||
if(A_FLAGS)
|
||||
target_compile_options(jpegqs-${name} PRIVATE ${A_FLAGS})
|
||||
endif()
|
||||
target_link_libraries(jpegqs-${name} PUBLIC ${_jpegqs_libs})
|
||||
set_target_properties(jpegqs-${name} PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
endfunction()
|
||||
|
||||
set(_jpegqs_objects "")
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")
|
||||
dn_jpegqs_object(avx512
|
||||
DEFS SIMD_SELECT SIMD_NAME=avx512 SIMD_AVX512
|
||||
FLAGS -mavx512f -mfma)
|
||||
dn_jpegqs_object(avx2
|
||||
DEFS SIMD_SELECT SIMD_NAME=avx2 SIMD_AVX2
|
||||
FLAGS -mavx2 -mfma)
|
||||
dn_jpegqs_object(sse2
|
||||
DEFS SIMD_SELECT SIMD_NAME=sse2 SIMD_SSE2
|
||||
FLAGS -msse2)
|
||||
dn_jpegqs_object(base
|
||||
DEFS SIMD_SELECT SIMD_NAME=base SIMD_BASE)
|
||||
dn_jpegqs_object(select DEFS SIMD_SELECT)
|
||||
set(_jpegqs_objects
|
||||
$<TARGET_OBJECTS:jpegqs-base>
|
||||
$<TARGET_OBJECTS:jpegqs-sse2>
|
||||
$<TARGET_OBJECTS:jpegqs-avx2>
|
||||
$<TARGET_OBJECTS:jpegqs-avx512>
|
||||
$<TARGET_OBJECTS:jpegqs-select>)
|
||||
else()
|
||||
dn_jpegqs_object(nosimd DEFS NO_SIMD)
|
||||
set(_jpegqs_objects $<TARGET_OBJECTS:jpegqs-nosimd>)
|
||||
endif()
|
||||
|
||||
add_library(jpegqs STATIC ${_jpegqs_objects})
|
||||
set_target_properties(jpegqs PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
target_link_libraries(jpegqs PUBLIC ${_jpegqs_libs})
|
||||
target_include_directories(jpegqs PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
@@ -0,0 +1,4 @@
|
||||
// This separate directory is necessary for Debian's multiarch with jpeg-turbo,
|
||||
// because its jpeglib.h cannot perform local inclusion of jconfig.h,
|
||||
// resulting in it being found within jpeg-quantsmooth and breaking the build.
|
||||
#include "../libjpegqs.h"
|
||||
@@ -0,0 +1,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)
|
||||
@@ -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@"
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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 §ion : 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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+170
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// display-profile.hpp: display ICC profile lookup
|
||||
//
|
||||
// Copyright The dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class QScreen;
|
||||
|
||||
namespace dn
|
||||
{
|
||||
|
||||
struct DisplayProfile {
|
||||
std::vector<unsigned char> icc;
|
||||
std::string source;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
/// Platform lookup and change notification for display profiles.
|
||||
class DisplayProfileSource
|
||||
{
|
||||
public:
|
||||
virtual ~DisplayProfileSource() = default;
|
||||
virtual void start(std::function<void()> on_change) = 0;
|
||||
virtual DisplayProfile load(QScreen *screen) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<DisplayProfileSource> make_display_profile_source();
|
||||
|
||||
/// Process-wide display ICC lookup and change notification.
|
||||
class DisplayProfileWatch
|
||||
{
|
||||
public:
|
||||
DisplayProfileWatch();
|
||||
~DisplayProfileWatch();
|
||||
DisplayProfileWatch(const DisplayProfileWatch &) = delete;
|
||||
DisplayProfileWatch &operator=(const DisplayProfileWatch &) = delete;
|
||||
|
||||
void start();
|
||||
DisplayProfile load(QScreen *screen);
|
||||
void listen(void *key, std::function<void()> fn);
|
||||
void unlisten(const void *key);
|
||||
|
||||
private:
|
||||
void notify() const;
|
||||
|
||||
std::vector<std::pair<void *, std::function<void()>>> listeners_;
|
||||
std::unique_ptr<DisplayProfileSource> source_;
|
||||
};
|
||||
|
||||
} // namespace dn
|
||||
@@ -0,0 +1,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;
|
||||
@@ -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;
|
||||
@@ -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 |
@@ -0,0 +1,90 @@
|
||||
// gen-icon.swift: generate a program icon for dn in the Apple icon format
|
||||
//
|
||||
// Copyright The dawn Au | ||||