Prepare for future modes
Alpine 3.24 Success
Alpine 3.24 Flatpak Success
Arch Linux AUR Success

This commit is contained in:
p
2026-09-09 22:42:54 +02:00
parent b4838da0ce
commit 3521c25c37
41 changed files with 1014 additions and 556 deletions
+7 -1
View File
@@ -229,6 +229,8 @@ if (DAWN_WITH_JPEG_QS)
endif()
endif()
option(DAWN_WIP "Expose unfinished subapplications" OFF)
configure_file(
"${PROJECT_SOURCE_DIR}/dawn-config.h.in"
"${PROJECT_BINARY_DIR}/dawn-config.h")
@@ -328,6 +330,10 @@ include(CPack)
# We've already figured out msitools.
if (WIN32 AND CMAKE_CROSSCOMPILING)
set(DAWN_ICON_ICO "${PROJECT_BINARY_DIR}/dn/dn.ico")
set(DAWN_MSI_WIP OFF)
if (DAWN_WIP)
set(DAWN_MSI_WIP ON)
endif()
set(dawn_wxs "${PROJECT_BINARY_DIR}/dawn.wxs")
configure_file(dawn.wxs.in "${dawn_wxs}" @ONLY)
@@ -336,7 +342,7 @@ if (WIN32 AND CMAKE_CROSSCOMPILING)
add_custom_target(msi
COMMAND "${PROJECT_SOURCE_DIR}/msys2-package-msi.sh"
"${CMAKE_SYSTEM_PROCESSOR}" "${dawn_msi}" "${dawn_wxs}"
"${PROJECT_DESCRIPTION}"
"${PROJECT_DESCRIPTION}" "${DAWN_MSI_WIP}"
WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
VERBATIM)
endif()
+63
View File
@@ -0,0 +1,63 @@
# AppKit-only identities beside Dawn.app. Compile with swiftc directly so this
# also works with Makefile generators, without enabling CMake's Swift language.
find_program(DN_SWIFTC_EXECUTABLE swiftc REQUIRED)
find_program(DN_LIPO_EXECUTABLE lipo REQUIRED)
set(dn_launcher_arches ${CMAKE_OSX_ARCHITECTURES})
if (NOT dn_launcher_arches)
set(dn_launcher_arches "${CMAKE_SYSTEM_PROCESSOR}")
endif()
set(dn_launcher_sdk "${CMAKE_OSX_SYSROOT}")
if (NOT IS_DIRECTORY "${dn_launcher_sdk}")
execute_process(COMMAND xcrun --sdk macosx --show-sdk-path
OUTPUT_VARIABLE dn_launcher_sdk OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY)
endif()
set(dn_launcher_slices)
foreach (arch IN LISTS dn_launcher_arches)
set(slice "${CMAKE_CURRENT_BINARY_DIR}/dn-launcher-${arch}")
add_custom_command(OUTPUT "${slice}"
COMMAND "${DN_SWIFTC_EXECUTABLE}" -swift-version 5 -O
-target "${arch}-apple-macosx${CMAKE_OSX_DEPLOYMENT_TARGET}"
-sdk "${dn_launcher_sdk}" -framework AppKit
"${CMAKE_CURRENT_SOURCE_DIR}/mode-launcher.swift" -o "${slice}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/mode-launcher.swift"
VERBATIM)
list(APPEND dn_launcher_slices "${slice}")
endforeach()
set(dn_launcher "${CMAKE_CURRENT_BINARY_DIR}/dn-launcher")
add_custom_command(OUTPUT "${dn_launcher}"
COMMAND "${DN_LIPO_EXECUTABLE}" -create ${dn_launcher_slices} -output "${dn_launcher}"
DEPENDS ${dn_launcher_slices} VERBATIM)
foreach (DN_LAUNCH_ID IN ITEMS CropJpeg Commander)
if (DN_LAUNCH_ID STREQUAL CropJpeg)
set(DN_LAUNCH_NAME "Dawn JPEG Cropper")
set(DN_LAUNCH_MODE cropjpeg)
set(DN_LAUNCH_TYPE_NAME JPEG)
set(DN_LAUNCH_UTI public.jpeg)
set(DN_LAUNCH_ROLE Editor)
else()
set(DN_LAUNCH_NAME "Dawn Commander")
set(DN_LAUNCH_MODE commander)
set(DN_LAUNCH_TYPE_NAME Folder)
set(DN_LAUNCH_UTI public.folder)
set(DN_LAUNCH_ROLE Viewer)
endif()
set(bundle "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${DN_LAUNCH_NAME}.app")
file(MAKE_DIRECTORY "${bundle}/Contents/MacOS" "${bundle}/Contents/Resources")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Launcher-Info.plist.in"
"${bundle}/Contents/Info.plist" @ONLY)
add_custom_command(OUTPUT "${bundle}/Contents/MacOS/dn-launcher"
"${bundle}/Contents/Resources/dn.icns"
COMMAND "${CMAKE_COMMAND}" -E copy "${dn_launcher}" "${bundle}/Contents/MacOS/dn-launcher"
COMMAND "${CMAKE_COMMAND}" -E copy "${DN_ICON_ICNS}" "${bundle}/Contents/Resources/dn.icns"
DEPENDS "${dn_launcher}" "${DN_ICON_ICNS}" VERBATIM)
add_custom_target(dn-launcher-${DN_LAUNCH_ID} ALL
DEPENDS "${bundle}/Contents/MacOS/dn-launcher" "${bundle}/Contents/Resources/dn.icns")
add_dependencies(dn-launcher-${DN_LAUNCH_ID} dn)
install(DIRECTORY "${bundle}" DESTINATION . USE_SOURCE_PERMISSIONS)
endforeach()
+2
View File
@@ -7,6 +7,8 @@
#pragma once
#cmakedefine01 DAWN_WIP
#define DAWN_NAME "@PROJECT_NAME@"
#define DAWN_NAMESPACE "@DAWN_NAMESPACE@"
#define DAWN_VERSION "@PROJECT_VERSION@"
+8
View File
@@ -54,6 +54,14 @@
WorkingDirectory='INSTALLDIR'
Arguments='"%USERPROFILE%"'
Icon='dn.ico' />
<?if @DAWN_MSI_WIP@ = ON ?>
<Shortcut Id='CropJpegShortcut' Name='Dawn JPEG Cropper'
Target='[INSTALLDIR]\dn.exe' WorkingDirectory='INSTALLDIR'
Arguments='--mode=cropjpeg' Icon='dn.ico' />
<Shortcut Id='CommanderShortcut' Name='Dawn Commander'
Target='[INSTALLDIR]\dn.exe' WorkingDirectory='INSTALLDIR'
Arguments='--mode=commander "%USERPROFILE%"' Icon='dn.ico' />
<?endif?>
<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
<RegistryValue Root='HKCU'
Key='Software\[Manufacturer]\[ProductName]'
+12
View File
@@ -4,6 +4,8 @@ qt_add_executable(dn
display-profile.cpp
gpu.cpp
kit-browser.cpp
kit-crop-jpeg.cpp
kit-commander.cpp
kit-chrome.cpp
kit-cie-diagram.cpp
kit.cpp
@@ -250,6 +252,12 @@ else()
add_custom_target(desktop-media-types ALL DEPENDS "${generated}")
set(desktop "${generated}")
endif()
if (DAWN_WIP)
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.CropJpeg.desktop"
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.Commander.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
endif()
install(FILES "${desktop}"
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.Browse.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
@@ -360,3 +368,7 @@ endif()
if (BUILD_TESTING)
add_test(NAME dn-help COMMAND $<TARGET_FILE:dn> --help)
endif()
if (APPLE AND DAWN_WIP)
include("${PROJECT_SOURCE_DIR}/cmake/macos-launchers.cmake")
endif()
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>dn-launcher</string>
<key>CFBundleIdentifier</key>
<string>@DAWN_NAMESPACE@.@DN_LAUNCH_ID@</string>
<key>CFBundleName</key>
<string>@DN_LAUNCH_NAME@</string>
<key>CFBundleDisplayName</key>
<string>@DN_LAUNCH_NAME@</string>
<key>CFBundleIconFile</key>
<string>dn.icns</string>
<key>CFBundleShortVersionString</key>
<string>@PROJECT_VERSION@</string>
<key>CFBundleVersion</key>
<string>@PROJECT_VERSION@</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>@CMAKE_OSX_DEPLOYMENT_TARGET@</string>
<key>LSUIElement</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>DawnMode</key>
<string>@DN_LAUNCH_MODE@</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>@DN_LAUNCH_TYPE_NAME@</string>
<key>CFBundleTypeRole</key>
<string>@DN_LAUNCH_ROLE@</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>@DN_LAUNCH_UTI@</string>
</array>
</dict>
</array>
</dict>
</plist>
+26 -18
View File
@@ -523,16 +523,36 @@ action_accel(const ActionDef &def)
return accel_label(def);
}
span<const MenuNode>
browser_menu()
static const MenuNode kCropJpegMenu[] = {kFileMenu, kHelpMenu};
static const MenuNode kCommanderMenu[] = {kFileMenu, kHelpMenu};
static constexpr ModeDef kModes[] = {
{"view", "Dawn", kViewerMenu, kViewerKeys},
{"browse", "Dawn", kBrowserMenu, kBrowserKeys},
{"cropjpeg", "Dawn JPEG Cropper", kCropJpegMenu, {}},
{"commander", "Dawn Commander", kCommanderMenu, {}},
};
static_assert(size(kModes) == size_t(Mode::Count));
span<const ModeDef>
modes()
{
return kBrowserMenu;
return kModes;
}
span<const MenuNode>
viewer_menu()
const ModeDef &
mode_def(Mode mode)
{
return kViewerMenu;
return kModes[size_t(mode)];
}
optional<Mode>
parse_mode(string_view name)
{
for (size_t i = 0; i < size(kModes); i++)
if (name == kModes[i].name)
return Mode(i);
return {};
}
span<const Action>
@@ -541,18 +561,6 @@ window_keys()
return kWindowKeys;
}
span<const Action>
browser_keys()
{
return kBrowserKeys;
}
span<const Action>
viewer_keys()
{
return kViewerKeys;
}
void
set_file_mime_data(QMimeData *mime, span<const QUrl> urls, bool cut)
{
+30 -4
View File
@@ -13,7 +13,9 @@
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <optional>
#include <span>
#include <string_view>
#include <vector>
class QMimeData;
@@ -21,6 +23,22 @@ class QMimeData;
namespace dn
{
enum class Mode : uint8_t { View, Browse, CropJpeg, Commander, Count };
constexpr bool
viewer_mode(Mode mode)
{
return mode == Mode::View || mode == Mode::Browse;
}
constexpr Mode
application_mode(Mode mode)
{
return viewer_mode(mode) ? Mode::View : mode;
}
std::optional<Mode> parse_mode(std::string_view name);
enum class Action : uint8_t {
None,
@@ -126,6 +144,18 @@ struct MenuNode {
const char *title, std::initializer_list<MenuNode> items);
};
struct ModeDef {
// Stable spelling of the mode, on the command line and on the wire.
const char *name;
// The application half of the window title.
const char *title;
std::span<const MenuNode> menu;
std::span<const Action> keys;
};
std::span<const ModeDef> modes();
const ModeDef &mode_def(Mode mode);
struct Actor {
std::function<void(Action)> apply;
std::function<bool(Action)> enabled;
@@ -142,11 +172,7 @@ 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 set_file_mime_data(QMimeData *mime, std::span<const QUrl> urls, bool cut);
void copy_files(std::span<const QUrl> urls, bool cut);
+4 -3
View File
@@ -193,7 +193,8 @@ sync_hidden(NSMenu *main, id delegate, span<const dn::MenuNode> tree)
if (w->host().apply)
w->host().apply(a);
} else if (a == dn::Action::NewWindow && _app)
_app->open(dn::path_to_url(QDir::currentPath()), {}, {}, false);
_app->open(
dn::path_to_url(QDir::currentPath()), {}, {}, dn::Mode::View);
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
@@ -374,8 +375,8 @@ install_macos_app_menu(App *app)
titles.push_back(t);
}
};
consider(viewer_menu());
consider(browser_menu());
for (const auto &mode : modes())
consider(mode.menu);
for (const QString &t : titles) {
// Both trees end with Help, and macOS wants Window just before it.
if (t == QStringLiteral("Help"))
+56 -24
View File
@@ -349,12 +349,25 @@ App::event(QEvent *event)
return true;
}
if (event->type() == QEvent::FileOpen) {
open(url_normalized(((QFileOpenEvent *) event)->url()), {}, {}, false);
const QUrl url = url_normalized(((QFileOpenEvent *) event)->url());
if (this->accepting_files)
open(url, {}, {}, this->startup_mode);
else
this->pending_files.push_back(url);
return true;
}
return QGuiApplication::event(event);
}
void
App::accept_files()
{
this->accepting_files = true;
auto pending = std::move(this->pending_files);
for (const QUrl &url : pending)
open(url, {}, {}, this->startup_mode);
}
bool
App::init()
{
@@ -428,32 +441,51 @@ raise_window(Window *window)
OpenResult
App::open(const QUrl &url, const QString &activation_token, BrowseSetup setup,
bool browse)
Mode mode)
{
// Until there is a VFS, this is where anything but file:// stops.
const QString path = url_to_path(url);
if (path.isEmpty()) {
qWarning("%s: unsupported location",
qUtf8Printable(url.toString(QUrl::PrettyDecoded)));
if (size_t(mode) >= size_t(Mode::Count))
return OpenResult::InvalidArgument;
}
const QFileInfo info(path);
if (!info.exists()) {
qWarning("%s: not found", qUtf8Printable(path));
return OpenResult::NotFound;
}
if (!info.isReadable()) {
qWarning("%s: permission denied", qUtf8Printable(path));
return OpenResult::PermissionDenied;
}
QUrl resolved = url;
if (resolved.isEmpty() && mode != Mode::CropJpeg)
resolved = path_to_url(QDir::currentPath());
// Windows compare and store this, so hand them a canonical form.
const QUrl resolved = QUrl::fromLocalFile(info.absoluteFilePath());
if (!resolved.isEmpty()) {
const QString path = url_to_path(resolved);
if (path.isEmpty()) {
qWarning("%s: unsupported location",
qUtf8Printable(resolved.toString()));
return OpenResult::InvalidArgument;
}
QFileInfo info(path);
if (mode == Mode::CropJpeg) {
if (info.isDir())
return OpenResult::InvalidArgument;
} else {
if (mode == Mode::Commander && !info.isDir())
info = QFileInfo(info.absolutePath());
if (!info.exists()) {
qWarning("%s: not found", qUtf8Printable(info.filePath()));
return OpenResult::NotFound;
}
if (!info.isReadable()) {
qWarning(
"%s: permission denied", qUtf8Printable(info.filePath()));
return OpenResult::PermissionDenied;
}
if (mode == Mode::Commander && !info.isDir())
return OpenResult::InvalidArgument;
}
resolved = path_to_url(info.absoluteFilePath());
}
if (Window *target = this->default_window) {
this->default_window = nullptr;
if (target->current_url() == path_to_url(QDir::currentPath())) {
target->open_any(resolved, browse);
this->default_window.clear();
if (target->application() == application_mode(mode)) {
if (mode == Mode::Browse)
target->reveal_file(resolved);
else
target->open_any(resolved);
apply_activation_token(activation_token);
raise_window(target);
return OpenResult::Ok;
@@ -463,7 +495,7 @@ App::open(const QUrl &url, const QString &activation_token, BrowseSetup setup,
#if DN_WITH_WAYLAND
if (QGuiApplication::platformName() == QStringLiteral("wayland")) {
auto window = make_unique<WaylandWindow>(this);
if (!window->initialize(resolved, setup, browse))
if (!window->initialize(resolved, setup, mode))
return OpenResult::Internal;
apply_activation_token(activation_token);
window->show();
@@ -473,7 +505,7 @@ App::open(const QUrl &url, const QString &activation_token, BrowseSetup setup,
#endif
auto window = make_unique<Window>(this, nullptr);
if (!window->initialize(resolved, setup, browse))
if (!window->initialize(resolved, setup, mode))
return OpenResult::Internal;
apply_activation_token(activation_token);
window->show();
+9 -3
View File
@@ -84,8 +84,9 @@ protected:
bool event(QEvent *event) override;
public:
App(int &argc, char **argv)
: QGuiApplication(argc, argv), thumbnailer(nullptr, 0)
App(int &argc, char **argv, Mode mode)
: QGuiApplication(argc, argv), thumbnailer(nullptr, 0),
startup_mode(mode)
{
}
@@ -98,9 +99,14 @@ public:
QPointer<Window> default_window;
bool needs_csd = false;
const Mode startup_mode;
bool accepting_files = false;
std::vector<QUrl> pending_files;
void accept_files();
bool init();
OpenResult open(const QUrl &url, const QString &activation_token,
BrowseSetup setup, bool browse);
BrowseSetup setup, Mode mode);
void close(const QWindow *top);
void close_later(const QWindow *top);
// Not quit(): that name is taken by a static QCoreApplication slot,
+9 -4
View File
@@ -36,7 +36,12 @@ using namespace std;
namespace dn
{
constexpr auto kSelfDesktop = QLatin1String(DAWN_NAMESPACE ".desktop");
static bool
self_desktop(const QString &id)
{
return id == QLatin1String(DAWN_NAMESPACE ".desktop") ||
id == QLatin1String(DAWN_NAMESPACE ".Browse.desktop");
}
static string
read_text_file(const QString &path)
@@ -469,7 +474,7 @@ desktop_by_id(const QString &id)
static bool
listable(const Desktop &d)
{
if (d.id == kSelfDesktop)
if (self_desktop(d.id))
return false;
if (d.hidden || d.exec.isEmpty() || !shown_on_desktop(d))
return false;
@@ -522,7 +527,7 @@ merge_assoc(AssocSets &into, const AssocSets &from)
static bool
usable_id(const QString &id, const unordered_set<QString> &removed)
{
if (id.isEmpty() || id == kSelfDesktop || removed.contains(id))
if (id.isEmpty() || self_desktop(id) || removed.contains(id))
return false;
const Desktop *d = desktop_by_id(id);
return d && listable(*d);
@@ -747,7 +752,7 @@ set_last_used(const Handler &app, const QString &path)
return;
const QString id = normalize_desktop_id(app.id);
if (id.isEmpty() || id == kSelfDesktop)
if (id.isEmpty() || self_desktop(id))
return;
const QString type = db().mimeTypeForFile(path).name();
+7 -2
View File
@@ -173,11 +173,16 @@ InstanceHost::Impl::on_request(
this->app_.default_window.clear();
const QString token = QString::fromUtf8(open_body->open.activation_token);
const bool browse = open_body->open.browse;
const auto mode = parse_mode(open_body->open.mode);
if (!mode || open_body->open.urls.empty()) {
call.fail(dawn::ipc::ErrorCode::InvalidArgument,
"unknown mode or empty open request");
return;
}
for (const string_view url : open_body->open.urls) {
const OpenResult r = this->app_.open(
QUrl::fromEncoded(QByteArray(url.data(), qsizetype(url.size()))),
token, {}, browse);
token, {}, *mode);
if (r != OpenResult::Ok) {
call.fail(map_open_error(r), {});
return;
+67 -182
View File
@@ -115,7 +115,6 @@ namespace dn
{
constexpr float kWinPadX = 4.f;
constexpr float kItemGap = 2.f;
constexpr float kGridPad = 8.f;
constexpr float kGlowAlpha = 0.375f;
constexpr float kBorder = 2.f;
@@ -124,7 +123,6 @@ constexpr int kCapLines = 2;
constexpr float kCapPad = 4.f;
constexpr int kCheck = 40;
constexpr float kPrefetchRows = 2.f;
constexpr const char *kMoreIcon = "disclose-arrow-down-symbolic";
constexpr const char *kPendingIcon = "dots-horizontal-symbolic";
constexpr const char *kMissingIcon = "image-missing-symbolic";
@@ -133,51 +131,37 @@ constexpr int kThumbSizeN = size(kThumbSizes);
constexpr int kThumbWide = 2;
constexpr size_t kThumbRamBudget = 2ull << 30;
enum class Slot : uint8_t { Left, Middle, Right };
enum class Kind : uint8_t { Icon, Text, Sep, Search };
namespace
{
struct Spec {
Kind kind;
Slot slot;
Action action;
};
} // namespace
constexpr Spec kItems[] = {
{Kind::Icon, Slot::Left, Action::Sidebar},
{Kind::Icon, Slot::Left, Action::Back},
{Kind::Icon, Slot::Left, Action::Forward},
{Kind::Icon, Slot::Left, Action::Reload},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Icon, Slot::Left, Action::DirPrev},
{Kind::Icon, Slot::Left, Action::DirParent},
{Kind::Icon, Slot::Left, Action::DirNext},
{Kind::Sep, Slot::Left, Action::None},
constexpr ToolbarSpec kItems[] = {
{Slot::Left, Action::Sidebar},
{Slot::Left, Action::Back},
{Slot::Left, Action::Forward},
{Slot::Left, Action::Reload},
{Slot::Left, Action::None},
{Slot::Left, Action::DirPrev},
{Slot::Left, Action::DirParent},
{Slot::Left, Action::DirNext},
{Slot::Left, Action::None},
// I don't know if these should be Slot::Middle.
{Kind::Icon, Slot::Left, Action::ThumbMinus},
{Kind::Icon, Slot::Left, Action::ThumbPlus},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Icon, Slot::Left, Action::ViewTile},
{Kind::Icon, Slot::Left, Action::ViewGrid},
// TODO: {Kind::Icon, Slot::Left, Action::ViewList},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Icon, Slot::Left, Action::Filenames},
{Kind::Icon, Slot::Left, Action::Filter},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Icon, Slot::Left, Action::SortDir},
{Kind::Text, Slot::Left, Action::SortName},
{Kind::Text, Slot::Left, Action::SortTime},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Search, Slot::Left, Action::Search},
{Slot::Left, Action::ThumbMinus},
{Slot::Left, Action::ThumbPlus},
{Slot::Left, Action::None},
{Slot::Left, Action::ViewTile},
{Slot::Left, Action::ViewGrid},
// TODO: {Slot::Left, Action::ViewList},
{Slot::Left, Action::None},
{Slot::Left, Action::Filenames},
{Slot::Left, Action::Filter},
{Slot::Left, Action::None},
{Slot::Left, Action::SortDir},
{Slot::Left, Action::SortName},
{Slot::Left, Action::SortTime},
{Slot::Left, Action::None},
{Slot::Left, Action::Search},
{Kind::Sep, Slot::Right, Action::None},
{Kind::Icon, Slot::Right, Action::DarkMode},
{Kind::Icon, Slot::Right, Action::Fullscreen},
{Slot::Right, Action::None},
{Slot::Right, Action::DarkMode},
{Slot::Right, Action::Fullscreen},
};
namespace
@@ -312,16 +296,6 @@ is_image_filename(const QString &name)
return false;
}
static shared_ptr<dawn::Profile>
profile_from_icc(dawn::Cmm &cmm, const shared_ptr<const vector<uint8_t>> &icc)
{
if (icc && !icc->empty()) {
if (auto profile = cmm.get_profile(*icc))
return profile;
}
return cmm.get_profile_sRGB();
}
static int
thumb_atlas_max(const Browser &b)
{
@@ -2189,47 +2163,17 @@ pack_standin_icons(Browser &b)
b.kit_.pack_icon(kMissingIcon, px);
}
static void
pack_toolbar_icons(Browser &b)
void
Browser::rescale(Kit &)
{
const int px = b.kit_.icon_px();
for (const Spec &spec : kItems) {
const ActionDef &d = action_def(spec.action);
b.kit_.pack_icon(action_icon(d, false), px);
if (d.icon[1])
b.kit_.pack_icon(d.icon[1], px);
}
b.kit_.pack_icon(kMoreIcon, px);
b.kit_.pack_icon("go-up-symbolic", px);
b.kit_.pack_icon("go-down-symbolic", px);
b.kit_.pack_icon("dot-large-symbolic", px);
b.kit_.pack_icon("computer-symbolic", px);
b.kit_.pack_icon("drive-optical-symbolic", px);
b.kit_.pack_icon("drive-removable-media-symbolic", px);
b.kit_.pack_icon("network-server-symbolic", px);
b.kit_.pack_icon("drive-ssd-symbolic", px);
b.kit_.pack_icon("drive-harddisk-symbolic", px);
b.kit_.pack_icon("go-home-symbolic", px);
b.kit_.pack_icon("image-symbolic", px);
b.kit_.pack_icon("folder-symbolic", px);
b.kit_.pack_icon("open-menu-symbolic", px);
}
static bool
set_dpr(Browser &b, float dpr)
{
if (!b.kit_.set_dpr(dpr))
return false;
pack_toolbar_icons(b);
if (!b.files_.empty()) {
invalidate_thumbs(b);
enqueue_thumbs(b);
for (Browser::File &f : b.files_) {
if (!this->files_.empty()) {
invalidate_thumbs(*this);
enqueue_thumbs(*this);
for (File &f : this->files_) {
f.cap = {};
f.cap_text.clear();
}
}
return true;
}
static bool
@@ -2291,11 +2235,9 @@ spec_active(const Browser &b, Action action)
}
static unique_ptr<Widget>
make_item(Browser &b, const Spec &spec)
make_item(Browser &b, const ToolbarSpec &spec)
{
if (spec.kind == Kind::Sep)
return make_unique<Sep>();
if (spec.kind == Kind::Search) {
if (spec.action == Action::Search) {
auto e = make_unique<Entry>();
b.search_ = e.get();
e->flat = true;
@@ -2316,42 +2258,18 @@ make_item(Browser &b, const Spec &spec)
};
return e;
}
if (spec.action != Action::SortTime && spec.action != Action::SortName)
return {};
auto n = make_unique<Button>();
n->flat = true;
n->focus_on_press = false;
const Action action = spec.action;
n->action = action;
if (spec.kind == Kind::Text) {
n->pad_x = 2.f;
n->text = action == Action::SortTime ? QStringLiteral("Time")
: QStringLiteral("Name");
}
n->action = spec.action;
n->pad_x = 2.f;
n->text = spec.action == Action::SortTime ? QStringLiteral("Time")
: QStringLiteral("Name");
return n;
}
static unique_ptr<ToolbarSlot>
make_slot_row(Browser &b, Slot slot)
{
auto row = make_unique<ToolbarSlot>();
row->gap = kItemGap;
for (const Spec &spec : kItems) {
if (spec.slot == slot)
row->add_item(make_item(b, spec), size_t(-1));
}
return row;
}
static unique_ptr<Toolbar>
make_toolbar(Browser &b)
{
auto left = make_slot_row(b, Slot::Left);
auto mid = make_slot_row(b, Slot::Middle);
auto right = make_slot_row(b, Slot::Right);
right->align = Align::End;
return make_unique<Toolbar>(
std::move(left), std::move(mid), std::move(right));
}
static unique_ptr<Sidebar>
make_sidebar(Browser &b)
{
@@ -2567,15 +2485,6 @@ apply_action(Browser &b, Action action)
}
}
static Actor
make_actor(Browser &b, const HostActions &host)
{
return chain_actor(
host, [&b](Action a) { return apply_action(b, a); },
[&b](Action a) { return spec_enabled(b, a); },
[&b](Action a) { return spec_active(b, a); });
}
// --- Browser -----------------------------------------------------------------
Browser::Browser(Kit &kit, Thumbnailer &thumbnailer)
@@ -2592,7 +2501,6 @@ Browser::~Browser()
void
Browser::init()
{
pack_toolbar_icons(*this);
this->thumbnail_client_ = this->thumbnailer_.add_client(
this->thumb_gen_, [this] { request_render(*this); });
}
@@ -2771,26 +2679,23 @@ unique_ptr<Page>
make_browser_page(
Kit &kit, const HostActions &host, Thumbnailer &thumbnailer, Browser **out)
{
auto browser = make_unique<Browser>(kit, thumbnailer);
Browser *b = browser.get();
auto content = make_unique<Browser>(kit, thumbnailer);
Browser *b = content.get();
b->init();
auto toolbar = make_toolbar(*b);
auto sidebar = make_sidebar(*b);
auto page = make_unique<Page>(std::move(toolbar), std::move(sidebar),
Page::Side::Left, std::move(browser));
page->host = &host;
if (page->context) {
page->context->on_new_window = host.new_window;
page->context->on_trash = host.trash;
page->context->on_bookmarked = host.bookmarked;
page->context->on_toggle_bookmark = host.toggle_bookmark;
}
page->menu_tree = browser_menu();
page->keys = browser_keys();
page->actor = make_actor(*b, host);
b->places_dirty_ = true;
b->page_ = page.get();
page->bind_actions(kit);
PageSetup setup;
setup.mode = Mode::Browse;
setup.toolbar = make_toolbar(
kItems, [b](const ToolbarSpec &spec) { return make_item(*b, spec); });
setup.sidebar = make_sidebar(*b);
setup.side = Page::Side::Left;
setup.actor = chain_actor(
host, [b](Action a) { return apply_action(*b, a); },
[b](Action a) { return spec_enabled(*b, a); },
[b](Action a) { return spec_active(*b, a); });
setup.content = std::move(content);
auto page = make_page(kit, host, std::move(setup));
if (out)
*out = b;
return page;
@@ -2812,17 +2717,6 @@ Browser::destroy()
this->page_ = nullptr;
}
void
Browser::set_host(float width_pts, float height_pts, float dpr)
{
// The platform speaks logical points; everything past here is pixels,
// so the scale has to be current before the conversion.
if (!set_dpr(*this, dpr))
pack_toolbar_icons(*this);
this->kit_.host_w_ = this->kit_.px(width_pts);
this->kit_.host_h_ = this->kit_.px(height_pts);
}
void
Browser::open_dir(const QUrl &url, bool record)
{
@@ -2887,32 +2781,23 @@ Browser::hist_can_forward() const
}
void
Browser::set_screen_profile(shared_ptr<dawn::Cmm> cmm,
shared_ptr<dawn::Profile> profile, bool force_reload)
Browser::screen_changed(
const ScreenState &state, bool changed, bool force_reload)
{
auto screen_icc = profile
? make_shared<const vector<uint8_t>>(profile->to_bytes())
: nullptr;
const bool reload_thumbs = force_reload ||
bool(this->screen_icc_) != bool(screen_icc) ||
(this->screen_icc_ && *this->screen_icc_ != *screen_icc);
this->cmm_ = std::move(cmm);
this->screen_icc_ = std::move(screen_icc);
this->screen_profile_ = std::move(profile);
this->kit_.bake_colours(this->cmm_.get(), this->screen_profile_.get());
if (this->kit_.renderer_)
this->kit_.renderer_->set_transfer(
profile_transfer(this->screen_profile_.get()));
if (reload_thumbs) {
const bool reload = force_reload || changed;
this->cmm_ = state.cmm;
this->screen_profile_ = state.profile;
this->screen_icc_ = state.icc;
if (reload) {
invalidate_thumbs(*this);
enqueue_thumbs(*this);
}
if (this->kit_.request_render)
this->kit_.request_render();
request_render(*this);
}
void
Browser::present(Page &ui)
Browser::present(Kit &, Page &ui)
{
invalidate_arrange();
if (!this->kit_.inited_)
+5 -5
View File
@@ -118,7 +118,6 @@ struct Browser : Widget {
Kit &kit_;
Thumbnailer &thumbnailer_;
uint64_t thumbnail_client_ = 0;
Page *page_ = nullptr;
ScrollColumn *places_ = nullptr;
std::vector<PlaceItem> place_items_;
@@ -186,7 +185,6 @@ struct Browser : Widget {
void init();
void destroy();
void set_host(float width_pts, float height_pts, float dpr);
void open_dir(const QUrl &url, bool record);
void rescan();
bool hist_back();
@@ -198,9 +196,11 @@ struct Browser : Widget {
void file_gone(const QUrl &url);
[[nodiscard]] QUrl file_url(int index) const;
[[nodiscard]] BrowseSetup browse_setup() const { return this->setup_; }
void set_screen_profile(std::shared_ptr<dawn::Cmm> cmm,
std::shared_ptr<dawn::Profile> profile, bool force_reload);
void present(Page &ui);
void screen_changed(
const ScreenState &state, bool changed, bool force_reload) override;
void rescale(Kit &kit) override;
void present(Kit &kit, Page &ui) override;
bool busy() const override { return thumbs_busy(); }
bool press(Kit &kit, float x, float y, Qt::MouseButton button) override;
bool release(Kit &kit, float x, float y, Qt::MouseButton button) override;
Scroll *scrollbar() override { return &this->scroll_; }
+60 -1
View File
@@ -990,7 +990,8 @@ Page::Page(unique_ptr<Toolbar> tb, unique_ptr<Sidebar> sb, Side s,
app->activate_on_press = true;
app->on_click = [this](Kit &kit) { open_app_menu(kit, false); };
this->app_menu_button = app.get();
this->toolbar->left->add_item(make_unique<Sep>(), 0);
if (!this->toolbar->left->items_.empty())
this->toolbar->left->add_item(make_unique<Sep>(), 0);
this->toolbar->left->add_item(std::move(app), 0);
}
#endif
@@ -1228,6 +1229,64 @@ Page::child(size_t i) const
}
}
shared_ptr<dawn::Profile>
profile_from_icc(dawn::Cmm &cmm, const shared_ptr<const vector<uint8_t>> &icc)
{
if (icc && !icc->empty())
if (auto profile = cmm.get_profile(*icc))
return profile;
return cmm.get_profile_sRGB();
}
unique_ptr<Toolbar>
make_toolbar(span<const ToolbarSpec> items,
const function<unique_ptr<Widget>(const ToolbarSpec &)> &custom)
{
auto row = [&](Slot slot) {
auto result = make_unique<ToolbarSlot>();
result->gap = 2.f;
for (const auto &spec : items) {
if (spec.slot != slot)
continue;
unique_ptr<Widget> item = custom ? custom(spec) : nullptr;
if (!item && spec.action == Action::None)
item = make_unique<Sep>();
if (!item) {
auto button = make_unique<Button>();
button->flat = true;
button->focus_on_press = false;
button->action = spec.action;
item = std::move(button);
}
result->add_item(std::move(item), size_t(-1));
}
return result;
};
auto left = row(Slot::Left), middle = row(Slot::Middle),
right = row(Slot::Right);
right->align = Align::End;
return make_unique<Toolbar>(
std::move(left), std::move(middle), std::move(right));
}
unique_ptr<Page>
make_page(Kit &kit, const HostActions &host, PageSetup setup)
{
auto page = make_unique<Page>(std::move(setup.toolbar),
std::move(setup.sidebar), setup.side, std::move(setup.content));
page->host = &host;
page->context->on_new_window = host.new_window;
page->context->on_trash = host.trash;
page->context->on_bookmarked = host.bookmarked;
page->context->on_toggle_bookmark = host.toggle_bookmark;
page->menu_tree = mode_def(setup.mode).menu;
page->keys = mode_def(setup.mode).keys;
page->actor = std::move(setup.actor);
page->content->page_ = page.get();
page->bind_actions(kit);
return page;
}
Actor
chain_actor(const HostActions &host, function<bool(Action)> apply,
function<bool(Action)> enabled, function<bool(Action)> checked)
+20
View File
@@ -176,6 +176,26 @@ private:
Rect well_{};
};
enum class Slot : uint8_t { Left, Middle, Right };
struct ToolbarSpec {
Slot slot;
Action action;
};
std::unique_ptr<Toolbar> make_toolbar(std::span<const ToolbarSpec> items,
const std::function<std::unique_ptr<Widget>(const ToolbarSpec &)> &custom);
struct PageSetup {
Mode mode = Mode::View;
std::unique_ptr<Widget> content;
std::unique_ptr<Toolbar> toolbar;
std::unique_ptr<Sidebar> sidebar;
Page::Side side = Page::Side::None;
Actor actor;
};
std::unique_ptr<Page> make_page(
Kit &kit, const HostActions &host, PageSetup setup);
std::shared_ptr<dawn::Profile> profile_from_icc(
dawn::Cmm &cmm, const std::shared_ptr<const std::vector<uint8_t>> &icc);
Actor chain_actor(const HostActions &host, std::function<bool(Action)> apply,
std::function<bool(Action)> enabled, std::function<bool(Action)> checked);
+36
View File
@@ -0,0 +1,36 @@
//
// kit-commander.cpp: orthodox file manager subapplication
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "kit-commander.hpp"
using namespace std;
namespace dn
{
unique_ptr<Page>
make_commander_page(Kit &kit, const HostActions &host, Commander **out)
{
auto content = make_unique<Commander>();
if (out)
*out = content.get();
PageSetup setup;
setup.mode = Mode::Commander;
setup.content = std::move(content);
setup.toolbar = make_toolbar({}, {});
setup.actor = chain_actor(
host, {}, [](Action a) { return a != Action::Reload; },
[&kit](Action a) {
return a == Action::DarkMode
? kit.dark_
: a == Action::Fullscreen && kit.fullscreen_;
});
return make_page(kit, host, std::move(setup));
}
} // namespace dn
+28
View File
@@ -0,0 +1,28 @@
//
// kit-commander.hpp: orthodox file manager subapplication
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "kit-chrome.hpp"
namespace dn
{
struct Commander : Widget {
QUrl dir_url_;
Commander() { hittable = true; }
Size measure_content(Kit &, int max_w, int max_h) override
{
return {max_w, max_h};
}
void arrange_content(Kit &, Rect alloc) override { r = alloc; }
};
std::unique_ptr<Page> make_commander_page(
Kit &kit, const HostActions &host, Commander **out);
} // namespace dn
+36
View File
@@ -0,0 +1,36 @@
//
// kit-crop-jpeg.cpp: JPEG Cropper subapplication
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "kit-crop-jpeg.hpp"
using namespace std;
namespace dn
{
unique_ptr<Page>
make_crop_jpeg_page(Kit &kit, const HostActions &host, Cropper **out)
{
auto content = make_unique<Cropper>();
if (out)
*out = content.get();
PageSetup setup;
setup.mode = Mode::CropJpeg;
setup.content = std::move(content);
setup.toolbar = make_toolbar({}, {});
setup.actor = chain_actor(
host, {}, [](Action a) { return a != Action::Reload; },
[&kit](Action a) {
return a == Action::DarkMode
? kit.dark_
: a == Action::Fullscreen && kit.fullscreen_;
});
return make_page(kit, host, std::move(setup));
}
} // namespace dn
+28
View File
@@ -0,0 +1,28 @@
//
// kit-crop-jpeg.hpp: JPEG Cropper subapplication
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include "kit-chrome.hpp"
namespace dn
{
struct Cropper : Widget {
QUrl jpeg_url_;
Cropper() { hittable = true; }
Size measure_content(Kit &, int max_w, int max_h) override
{
return {max_w, max_h};
}
void arrange_content(Kit &, Rect alloc) override { r = alloc; }
};
std::unique_ptr<Page> make_crop_jpeg_page(
Kit &kit, const HostActions &host, Cropper **out);
} // namespace dn
+64 -179
View File
@@ -60,7 +60,6 @@ constexpr float kRotateMinR = 8.f;
constexpr float kScaleMin = 0.0125f;
constexpr float kScaleMax = 64.f;
constexpr float kAngleFast = 1e-5f;
constexpr const char *kMoreIcon = "disclose-arrow-down-symbolic";
// The loader below is a local-filesystem reader, and keys its jobs by path.
static string
@@ -69,62 +68,48 @@ viewer_local_path(const Viewer &v)
return url_to_path(v.url_).toStdString();
}
enum class Slot : uint8_t { Left, Middle, Right };
enum class Kind : uint8_t { Icon, Scale, Sep };
constexpr ToolbarSpec kItems[] = {
{Slot::Left, Action::Browse},
{Slot::Left, Action::PrevFile},
{Slot::Left, Action::NextFile},
{Slot::Left, Action::Reload},
{Slot::Left, Action::None},
namespace
{
struct Spec {
Kind kind;
Slot slot;
Action action;
};
} // namespace
constexpr Spec kItems[] = {
{Kind::Icon, Slot::Left, Action::Browse},
{Kind::Icon, Slot::Left, Action::PrevFile},
{Kind::Icon, Slot::Left, Action::NextFile},
{Kind::Icon, Slot::Left, Action::Reload},
{Kind::Sep, Slot::Left, Action::None},
{Kind::Icon, Slot::Middle, Action::PageFirst},
{Kind::Icon, Slot::Middle, Action::PagePrevious},
{Kind::Icon, Slot::Middle, Action::PageNext},
{Kind::Icon, Slot::Middle, Action::PageLast},
{Kind::Sep, Slot::Middle, Action::None},
{Kind::Icon, Slot::Middle, Action::FrameFirst},
{Kind::Icon, Slot::Middle, Action::FramePrevious},
{Kind::Icon, Slot::Middle, Action::PlayPause},
{Kind::Icon, Slot::Middle, Action::FrameNext},
{Kind::Sep, Slot::Middle, Action::None},
{Kind::Icon, Slot::Middle, Action::Lock},
{Kind::Icon, Slot::Middle, Action::Fixate},
{Kind::Icon, Slot::Middle, Action::ZoomOut},
{Kind::Scale, Slot::Middle, Action::ZoomLevel},
{Kind::Icon, Slot::Middle, Action::ZoomIn},
{Kind::Icon, Slot::Middle, Action::Zoom1},
{Kind::Icon, Slot::Middle, Action::Fit},
{Kind::Sep, Slot::Middle, Action::None},
{Kind::Icon, Slot::Middle, Action::ColorManagement},
{Kind::Icon, Slot::Middle, Action::Smooth},
{Kind::Icon, Slot::Middle, Action::Checkerboard},
{Kind::Sep, Slot::Middle, Action::None},
{Slot::Middle, Action::PageFirst},
{Slot::Middle, Action::PagePrevious},
{Slot::Middle, Action::PageNext},
{Slot::Middle, Action::PageLast},
{Slot::Middle, Action::None},
{Slot::Middle, Action::FrameFirst},
{Slot::Middle, Action::FramePrevious},
{Slot::Middle, Action::PlayPause},
{Slot::Middle, Action::FrameNext},
{Slot::Middle, Action::None},
{Slot::Middle, Action::Lock},
{Slot::Middle, Action::Fixate},
{Slot::Middle, Action::ZoomOut},
{Slot::Middle, Action::ZoomLevel},
{Slot::Middle, Action::ZoomIn},
{Slot::Middle, Action::Zoom1},
{Slot::Middle, Action::Fit},
{Slot::Middle, Action::None},
{Slot::Middle, Action::ColorManagement},
{Slot::Middle, Action::Smooth},
{Slot::Middle, Action::Checkerboard},
{Slot::Middle, Action::None},
#if 0
{Kind::Icon, Slot::Middle, Action::None},
{Kind::Icon, Slot::Middle, Action::None},
{Slot::Middle, Action::None},
{Slot::Middle, Action::None},
#endif
{Kind::Icon, Slot::Middle, Action::Information},
{Kind::Sep, Slot::Middle, Action::None},
{Kind::Icon, Slot::Middle, Action::RotateLeft},
{Kind::Icon, Slot::Middle, Action::Mirror},
{Kind::Icon, Slot::Middle, Action::RotateRight},
{Slot::Middle, Action::Information},
{Slot::Middle, Action::None},
{Slot::Middle, Action::RotateLeft},
{Slot::Middle, Action::Mirror},
{Slot::Middle, Action::RotateRight},
{Kind::Sep, Slot::Right, Action::None},
{Kind::Icon, Slot::Right, Action::DarkMode},
{Kind::Icon, Slot::Right, Action::Fullscreen},
{Slot::Right, Action::None},
{Slot::Right, Action::DarkMode},
{Slot::Right, Action::Fullscreen},
};
static bool
@@ -211,11 +196,9 @@ sync_scale_label(Viewer &v)
}
static unique_ptr<Widget>
make_item(Viewer &v, const Spec &spec)
make_item(Viewer &v, const ToolbarSpec &spec)
{
if (spec.kind == Kind::Sep)
return make_unique<Sep>();
if (spec.kind == Kind::Scale) {
if (spec.action == Action::ZoomLevel) {
auto n = make_unique<Label>();
n->hittable = true;
n->align = Align::Center;
@@ -226,24 +209,7 @@ make_item(Viewer &v, const Spec &spec)
sync_scale_label(v);
return n;
}
auto n = make_unique<Button>();
n->flat = true;
n->focus_on_press = false;
const Action action = spec.action;
n->action = action;
return n;
}
static unique_ptr<ToolbarSlot>
make_slot_row(Viewer &v, Slot slot)
{
auto row = make_unique<ToolbarSlot>();
row->gap = kItemGap;
for (const Spec &spec : kItems) {
if (spec.slot == slot)
row->add_item(make_item(v, spec), size_t(-1));
}
return row;
return {};
}
static unique_ptr<Row>
@@ -363,16 +329,6 @@ join_load_text(const vector<string> &warnings, const dawn::Error &error,
return out;
}
static shared_ptr<dawn::Profile>
profile_from_icc(dawn::Cmm &cmm, const shared_ptr<const vector<uint8_t>> &icc)
{
if (icc && !icc->empty()) {
if (auto profile = cmm.get_profile(*icc))
return profile;
}
return cmm.get_profile_sRGB();
}
static bool apply_action(Viewer &v, Action action);
static void reload_open(Viewer &v);
@@ -400,30 +356,6 @@ struct Viewer::Worker {
array<thread, 2> preloads;
};
static void
pack_toolbar_icons(Viewer &v)
{
const int px = v.kit_.icon_px();
for (const Spec &spec : kItems) {
const ActionDef &d = action_def(spec.action);
v.kit_.pack_icon(action_icon(d, false), px);
if (d.icon[1])
v.kit_.pack_icon(d.icon[1], px);
}
v.kit_.pack_icon(kMoreIcon, px);
v.kit_.pack_icon("open-menu-symbolic", px);
v.kit_.pack_icon("x-symbolic", px);
}
static bool
set_dpr(Viewer &v, float dpr)
{
if (!v.kit_.set_dpr(dpr))
return false;
pack_toolbar_icons(v);
return true;
}
static void
set_message(Viewer &v, string text)
{
@@ -636,17 +568,6 @@ stop_worker(Viewer &v)
v.worker_.reset();
}
static unique_ptr<Toolbar>
make_toolbar(Viewer &v)
{
auto left = make_slot_row(v, Slot::Left);
auto mid = make_slot_row(v, Slot::Middle);
auto right = make_slot_row(v, Slot::Right);
right->align = Align::End;
return make_unique<Toolbar>(
std::move(left), std::move(mid), std::move(right));
}
static unique_ptr<Panel>
make_error(Viewer &v)
{
@@ -1883,21 +1804,9 @@ apply_view(const Viewer &v)
renderer.set_filter(v.filter_);
renderer.set_checkerboard(v.checkerboard_);
renderer.set_blend_linear_light(v.blend_linear_light_);
renderer.set_transfer(v.enable_cms_
? profile_transfer(v.screen_profile_.get())
: dawn::Transfer::Srgb);
renderer.set_view(gpu_scale, pan_x, pan_y, v.orientation_, v.angle_);
}
static Actor
make_actor(Viewer &v, const HostActions &host)
{
return chain_actor(
host, [&v](Action a) { return apply_action(v, a); },
[&v](Action a) { return spec_enabled(v, a); },
[&v](Action a) { return spec_active(v, a); });
}
Viewer::Viewer(Kit &kit) : kit_(kit)
{
this->hittable = true;
@@ -1912,7 +1821,6 @@ void
Viewer::init()
{
this->scale_text_.clear();
pack_toolbar_icons(*this);
start_worker(*this);
}
@@ -1952,41 +1860,28 @@ Viewer::paint(Kit &) const
unique_ptr<Page>
make_viewer_page(Kit &kit, const HostActions &host, Viewer **out)
{
auto viewer = make_unique<Viewer>(kit);
Viewer *v = viewer.get();
auto content = make_unique<Viewer>(kit);
Viewer *v = content.get();
v->init();
auto toolbar = make_toolbar(*v);
auto err = make_error(*v);
auto side = make_sidebar(*v);
auto page = make_unique<Page>(std::move(toolbar), std::move(side),
Page::Side::Right, std::move(viewer));
page->host = &host;
if (page->context) {
page->context->on_new_window = host.new_window;
page->context->on_trash = host.trash;
}
page->set_banner(std::move(err));
page->menu_tree = viewer_menu();
page->keys = viewer_keys();
page->actor = make_actor(*v, host);
v->page_ = page.get();
page->bind_actions(kit);
auto error = make_error(*v);
PageSetup setup;
setup.mode = Mode::View;
setup.toolbar = make_toolbar(
kItems, [v](const ToolbarSpec &spec) { return make_item(*v, spec); });
setup.sidebar = make_sidebar(*v);
setup.side = Page::Side::Right;
setup.actor = chain_actor(
host, [v](Action a) { return apply_action(*v, a); },
[v](Action a) { return spec_enabled(*v, a); },
[v](Action a) { return spec_active(*v, a); });
setup.content = std::move(content);
auto page = make_page(kit, host, std::move(setup));
page->set_banner(std::move(error));
if (out)
*out = v;
return page;
}
void
Viewer::set_host(float width_pts, float height_pts, float dpr)
{
// The platform speaks logical points; everything past here is pixels,
// so the scale has to be current before the conversion.
if (!set_dpr(*this, dpr))
pack_toolbar_icons(*this);
this->kit_.host_w_ = this->kit_.px(width_pts);
this->kit_.host_h_ = this->kit_.px(height_pts);
}
void
Viewer::destroy()
{
@@ -2097,25 +1992,15 @@ Viewer::consume_open_done()
}
void
Viewer::set_screen_profile(shared_ptr<dawn::Cmm> cmm,
shared_ptr<dawn::Profile> profile, bool fallback, bool force_reload)
Viewer::screen_changed(
const ScreenState &state, bool changed, bool force_reload)
{
auto screen_icc = profile
? make_shared<const vector<uint8_t>>(profile->to_bytes())
: nullptr;
const bool changed = bool(this->screen_icc_) != bool(screen_icc) ||
(this->screen_icc_ && *this->screen_icc_ != *screen_icc);
const bool reload = !this->url_.isEmpty() &&
(force_reload || (this->enable_cms_ && changed));
this->cmm_ = std::move(cmm);
this->screen_icc_ = std::move(screen_icc);
this->screen_profile_ = std::move(profile);
this->screen_profile_fallback_ = fallback;
this->kit_.bake_colours(this->cmm_.get(), this->screen_profile_.get());
if (this->kit_.renderer_)
this->kit_.renderer_->set_transfer(this->enable_cms_
? profile_transfer(this->screen_profile_.get())
: dawn::Transfer::Srgb);
this->cmm_ = state.cmm;
this->screen_profile_ = state.profile;
this->screen_icc_ = state.icc;
this->screen_profile_fallback_ = state.fallback;
if (reload) {
this->restore_view_ = {true, this->scale_, this->pan_x_, this->pan_y_,
this->orientation_, this->angle_, this->view_locked_};
@@ -2124,7 +2009,7 @@ Viewer::set_screen_profile(shared_ptr<dawn::Cmm> cmm,
}
void
Viewer::present(Page &ui)
Viewer::present(Kit &, Page &ui)
{
invalidate_arrange();
animate(*this);
+4 -6
View File
@@ -55,7 +55,6 @@ struct Viewer : Widget {
};
Kit &kit_;
Page *page_ = nullptr;
Panel *error_ = nullptr;
Label *error_label_ = nullptr;
Label *scale_label_ = nullptr;
@@ -139,16 +138,15 @@ struct Viewer : Widget {
void init();
void destroy();
void set_host(float width_pts, float height_pts, float dpr);
void open(const QUrl &url);
void set_preload_urls(const QUrl &previous, const QUrl &next);
void cancel_loads();
[[nodiscard]] bool has_view() const;
[[nodiscard]] bool consume_open_done();
void set_screen_profile(std::shared_ptr<dawn::Cmm> cmm,
std::shared_ptr<dawn::Profile> profile, bool fallback,
bool force_reload);
void present(Page &ui);
void screen_changed(
const ScreenState &state, bool changed, bool force_reload) override;
void present(Kit &kit, Page &ui) override;
bool busy() const override { return opening_; }
[[nodiscard]] int wake_ms() const override;
bool press(Kit &kit, float x, float y, Qt::MouseButton button) override;
bool release(Kit &kit, float x, float y, Qt::MouseButton button) override;
+21
View File
@@ -6,6 +6,7 @@
//
#include "kit.hpp"
#include "kit-chrome.hpp"
#include "renderer.hpp"
#include <QFile>
@@ -982,6 +983,8 @@ Button::paint(Kit &kit) const
void
Button::prepare(Kit &kit)
{
if (shown())
kit.pack_icon(this->icon, kit.icon_px());
if (shown() && !this->text.isEmpty())
cache_text(kit, this->text_cache_, button_shown(kit, *this), false, 0);
}
@@ -5384,6 +5387,24 @@ Kit::prepare_popups()
}
}
void
Widget::present(Kit &kit, Page &page)
{
if (page.toolbar)
page.toolbar->sync_buttons();
page.sync_app_menu();
kit.frame_ui(page, {});
}
bool
Kit::set_host(float width_pts, float height_pts, float dpr)
{
const bool changed = set_dpr(dpr);
this->host_w_ = px(width_pts);
this->host_h_ = px(height_pts);
return changed;
}
void
Kit::frame_ui(Widget &ui, const function<void()> &placed)
{
+14
View File
@@ -36,6 +36,14 @@ namespace dn
{
class Renderer;
struct Page;
struct ScreenState {
std::shared_ptr<dawn::Cmm> cmm;
std::shared_ptr<dawn::Profile> profile;
std::shared_ptr<const std::vector<uint8_t>> icc;
bool fallback = true;
};
struct [[nodiscard]] Size {
int w = 0;
@@ -161,6 +169,7 @@ struct TextCache {
};
struct Widget {
Page *page_ = nullptr;
Rect r;
bool visible = true;
bool layout_visible = true;
@@ -191,6 +200,10 @@ struct Widget {
Size measure(Kit &kit, int max_w, int max_h);
virtual ~Widget() = default;
virtual void present(Kit &kit, Page &page);
virtual bool busy() const { return false; }
virtual void screen_changed(const ScreenState &, bool, bool) {}
virtual void rescale(Kit &) {}
// At least as of now, we don't seem to need baseline measurements.
// Returns the requested size without changing arranged geometry.
virtual Size measure_content(Kit &kit, int max_w, int max_h) = 0;
@@ -922,6 +935,7 @@ struct Kit {
bool mouse_double_click(
float x, float y, Qt::MouseButton button, unsigned mods);
bool set_dpr(float dpr);
bool set_host(float width_pts, float height_pts, float dpr);
void bake_colours(dawn::Cmm *cmm, dawn::Profile *target);
void pack_icon(const char *name, int px);
void draw_icon(int x, int y, int size, const char *name, Colour colour);
+38 -24
View File
@@ -81,8 +81,8 @@ error_fallback(dawn::ipc::ErrorCode code)
}
static bool
handoff_open(
dawn::ipc::instance::Client &client, const vector<QUrl> &urls, bool browse)
handoff_open(dawn::ipc::instance::Client &client, const vector<QUrl> &urls,
dn::Mode mode)
{
const string token =
qEnvironmentVariable("XDG_ACTIVATION_TOKEN").toUtf8().toStdString();
@@ -97,7 +97,8 @@ handoff_open(
AllowSetForegroundWindow(DWORD(pid));
#endif
dawn::ipc::Error error;
if (client.open(encoded, token, browse, &error, dawn::ipc::kRequestTimeout))
if (client.open(encoded, token, dn::mode_def(mode).name, &error,
dawn::ipc::kRequestTimeout))
return true;
if (!error.message.empty())
qWarning("%s", error.message.c_str());
@@ -108,7 +109,7 @@ handoff_open(
// Returns an exit code once a running instance has taken the URLs over.
static optional<int>
try_remote_open(const QString &session, const vector<QUrl> &urls, bool browse,
try_remote_open(const QString &session, const vector<QUrl> &urls, dn::Mode mode,
bool &reported_mismatch)
{
using HelloStatus = dawn::ipc::HelloStatus;
@@ -116,8 +117,7 @@ try_remote_open(const QString &session, const vector<QUrl> &urls, bool browse,
auto client = dawn::ipc::instance::Client::connect(
session.toUtf8().toStdString(), &status, dawn::ipc::kHelloTimeout);
if (client) {
return handoff_open(*client, urls, browse) ? EXIT_SUCCESS
: EXIT_FAILURE;
return handoff_open(*client, urls, mode) ? EXIT_SUCCESS : EXIT_FAILURE;
}
const char *mismatch = nullptr;
@@ -160,9 +160,10 @@ main(int argc, char **argv)
QStringLiteral("Remove invalid wide thumbnails and exit."));
parser.addOption(invalidate_opt);
const QCommandLineOption browse_opt(QStringLiteral("browse"),
QStringLiteral("Start in filesystem browsing mode."));
parser.addOption(browse_opt);
const QCommandLineOption mode_opt(QStringLiteral("mode"),
QStringLiteral("Application: view, browse, cropjpeg, commander."),
QStringLiteral("mode"));
parser.addOption(mode_opt);
const QCommandLineOption list_supported_opt(
QStringLiteral("list-supported-media-types"),
@@ -177,7 +178,7 @@ main(int argc, char **argv)
parser.addPositionalArgument(QStringLiteral("path | URL"),
QStringLiteral(
"Image file or directory. Repeat to open multiple windows. "
"Defaults to the current directory."),
"Defaults to an empty cropper or the current directory."),
QStringLiteral("[path | URL]..."));
{
@@ -202,21 +203,33 @@ main(int argc, char **argv)
}
}
dn::App app(argc, argv);
dn::Mode mode = dn::Mode::View;
if (parser.isSet(mode_opt)) {
auto parsed = dn::parse_mode(parser.value(mode_opt).toStdString());
if (!parsed) {
qWarning(
"unknown mode: %s", qUtf8Printable(parser.value(mode_opt)));
return EXIT_FAILURE;
}
mode = *parsed;
}
#if !DAWN_WIP
if (!dn::viewer_mode(mode)) {
qWarning("unsupported mode: %s", dn::mode_def(mode).name);
return EXIT_FAILURE;
}
#endif
dn::App app(argc, argv, mode);
QStringList raw = parser.positionalArguments();
const bool bare = raw.isEmpty();
if (bare)
raw.push_back(QStringLiteral("."));
// Without the working directory, relative arguments do not resolve to
// a local file, and every one of them is rejected as a foreign scheme.
const QString cwd = QDir::currentPath();
vector<QUrl> to_open;
for (const QString &arg : raw)
to_open.push_back(dn::url_from_user_input(arg, cwd));
const bool browse = parser.isSet(browse_opt);
if (bare)
to_open.push_back(
mode == dn::Mode::CropJpeg ? QUrl{} : dn::path_to_url(cwd));
#ifndef Q_OS_MACOS
unique_ptr<dn::InstanceHost> host;
@@ -224,15 +237,15 @@ main(int argc, char **argv)
const QString session = instance_session();
bool reported_mismatch = false;
if (auto code =
try_remote_open(session, to_open, browse, reported_mismatch))
try_remote_open(session, to_open, mode, reported_mismatch))
return *code;
auto listen =
dawn::ipc::Endpoint::listen(dawn::ipc::instance::kService);
if (listen.status == dawn::ipc::Endpoint::ListenStatus::InUse) {
// Someone else bound it in the meantime.
if (auto code = try_remote_open(
session, to_open, browse, reported_mismatch))
if (auto code =
try_remote_open(session, to_open, mode, reported_mismatch))
return *code;
} else if (listen.status == dawn::ipc::Endpoint::ListenStatus::Ok) {
// Notifiers armed; Qt delivers them only in exec().
@@ -246,13 +259,14 @@ main(int argc, char **argv)
return EXIT_FAILURE;
for (const QUrl &url : to_open) {
if (app.open(url, {}, {}, browse) != dn::OpenResult::Ok)
if (app.open(url, {}, {}, mode) != dn::OpenResult::Ok)
return EXIT_FAILURE;
}
// A bare launch opened the CWD above on a guess; see default_window().
// Finder may replace an untouched window from a documentless launch.
if (bare)
app.default_window = app.key_window();
app.accept_files();
return app.exec();
}
+92
View File
@@ -0,0 +1,92 @@
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
import AppKit
// The two bundles share this executable; their plists supply the launch mode.
final class Launcher: NSObject, NSApplicationDelegate {
private var pending = 0
private var receivedDocuments = false
private var submittedEmptyLaunch = false
private let mainApp = Bundle.main.bundleURL.deletingLastPathComponent()
.appendingPathComponent("Dawn.app", isDirectory: true)
func application(_ application: NSApplication, open urls: [URL]) {
receivedDocuments = true
launch(urls)
}
func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
return !receivedDocuments && !submittedEmptyLaunch
}
func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool {
if !receivedDocuments && !submittedEmptyLaunch {
submittedEmptyLaunch = true
launch([])
}
return true
}
func applicationShouldHandleReopen(_ sender: NSApplication,
hasVisibleWindows flag: Bool) -> Bool {
launch([])
// We handled it; do not also request an untitled document.
return false
}
private func launch(_ urls: [URL]) {
pending += 1
guard let mode = Bundle.main.object(
forInfoDictionaryKey: "DawnMode") as? String,
mode == "cropjpeg" || mode == "commander" else {
finish("The launcher has no valid application mode.", urls)
return
}
let config = NSWorkspace.OpenConfiguration()
config.activates = true
config.createsNewApplicationInstance = true
config.allowsRunningApplicationSubstitution = false
config.arguments = ["--mode=\(mode)"]
let completion: (NSRunningApplication?, Error?) -> Void = { app, error in
let failure = error?.localizedDescription ??
(app == nil ? "No application was launched." : nil)
// NSWorkspace completion handlers may run on a concurrent queue.
DispatchQueue.main.async { self.finish(failure, urls) }
}
if urls.isEmpty {
NSWorkspace.shared.openApplication(at: mainApp,
configuration: config,
completionHandler: completion)
} else {
NSWorkspace.shared.open(urls, withApplicationAt: mainApp,
configuration: config,
completionHandler: completion)
}
}
private func finish(_ failure: String?, _ urls: [URL]) {
if let failure = failure {
let alert = NSAlert()
alert.messageText = "Could not open Dawn"
let documents = urls.map { $0.path }.joined(separator: "\n")
alert.informativeText = "\(mainApp.path)\n\(documents)\n\n\(failure)"
NSApp.activate(ignoringOtherApps: true)
alert.runModal()
}
pending -= 1
// Give any already queued open events a chance to join this request.
DispatchQueue.main.async {
if self.pending == 0 { NSApp.terminate(nil) }
}
}
}
let application = NSApplication.shared
let delegate = Launcher()
application.setActivationPolicy(.accessory)
application.delegate = delegate
application.run()
+1 -1
View File
@@ -4,7 +4,7 @@ Name=dn
GenericName=Image Browser
X-GNOME-FullName=Dawn Colour-Managed Image Browser
Icon=name.janouch.Dawn
Exec=dn --browse -- %u
Exec=dn --mode=browse -- %u
NoDisplay=true
Terminal=false
StartupNotify=true
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=Dawn Commander
GenericName=File Manager
X-GNOME-FullName=Dawn Commander
Icon=name.janouch.Dawn
Exec=dn --mode=commander -- %U
Terminal=false
StartupNotify=true
Categories=System;FileTools;FileManager;
MimeType=inode/directory;
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=Dawn JPEG Cropper
GenericName=Image Cropper
X-GNOME-FullName=Dawn JPEG Cropper
Icon=name.janouch.Dawn
Exec=dn --mode=cropjpeg -- %U
Terminal=false
StartupNotify=true
Categories=Graphics;2DGraphics;RasterGraphics;
MimeType=image/jpeg;
+2 -2
View File
@@ -87,11 +87,11 @@ WaylandWindow::~WaylandWindow()
}
bool
WaylandWindow::initialize(const QUrl &url, BrowseSetup setup, bool browse)
WaylandWindow::initialize(const QUrl &url, BrowseSetup setup, Mode mode)
{
create();
place_content();
if (!this->content_.initialize(url, setup, browse))
if (!this->content_.initialize(url, setup, mode))
return false;
attach_color_management(true);
return true;
+1 -1
View File
@@ -54,7 +54,7 @@ public:
explicit WaylandWindow(App *app);
~WaylandWindow() override;
bool initialize(const QUrl &url, BrowseSetup setup, bool browse);
bool initialize(const QUrl &url, BrowseSetup setup, Mode mode);
};
// What Qt's own CSD does on titlebar right-click, in shell-local coordinates.
+137 -71
View File
@@ -207,8 +207,9 @@ Window::pixel_size() const
}
bool
Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
Window::initialize(const QUrl &url, BrowseSetup setup, Mode mode)
{
this->mode_ = mode;
QVulkanInstance *const instance = &this->app_->vulkan_instance;
create();
#ifdef Q_OS_MACOS
@@ -229,7 +230,7 @@ Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
qWarning("chosen GPU cannot present to this window surface");
return false;
}
if (!this->app_->thumbnailer.init(this->app_->gpu))
if (viewer_mode(mode) && !this->app_->thumbnailer.init(this->app_->gpu))
return false;
// A parent currently identifies the Vulkan subsurface owned by
// WaylandWindow. Prefer MAILBOX there because Mesa's legacy Wayland FIFO
@@ -262,10 +263,20 @@ Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
});
this->kit_.init(host_dpr(*this));
this->kit_.renderer_ = &this->renderer_;
this->browser_ui_ = make_browser_page(
this->kit_, this->host_, this->app_->thumbnailer, &this->browser_);
this->viewer_ui_ =
make_viewer_page(this->kit_, this->host_, &this->viewer_);
if (viewer_mode(mode)) {
this->pages_[size_t(Mode::Browse)] = make_browser_page(
this->kit_, this->host_, this->app_->thumbnailer, &this->browser_);
this->pages_[size_t(Mode::View)] =
make_viewer_page(this->kit_, this->host_, &this->viewer_);
} else if (mode == Mode::CropJpeg) {
this->pages_[size_t(mode)] =
make_crop_jpeg_page(this->kit_, this->host_, &this->cropper_);
} else if (mode == Mode::Commander) {
this->pages_[size_t(mode)] =
make_commander_page(this->kit_, this->host_, &this->commander_);
} else {
return false;
}
if (this->viewer_)
this->viewer_->loaders_ = this->app_->settings.enabled_loaders;
if (this->browser_) {
@@ -277,7 +288,10 @@ Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
}
apply_screen_profile(screen(), false);
open_any(url.isEmpty() ? path_to_url(QDir::currentPath()) : url, browse);
if (mode == Mode::Browse)
reveal_file(url);
else
open_any(url);
return true;
}
@@ -293,14 +307,18 @@ Window::drop_frames()
this->alt_armed_ = false;
this->browser_ = nullptr;
this->viewer_ = nullptr;
this->browser_ui_.reset();
this->viewer_ui_.reset();
this->cropper_ = nullptr;
this->commander_ = nullptr;
this->awaiting_view_ = false;
for (auto &page : this->pages_)
page.reset();
}
void
Window::bind_host()
{
this->host_.apply = [this](Action a) {
this->app_->default_window.clear();
switch (a) {
case Action::CloseWindow:
begin_close();
@@ -344,8 +362,8 @@ Window::bind_host()
dialog_location(
this->kit_, *ui->dialog, [this](const QString &location) {
open_any(url_from_user_input(location, QDir::currentPath()),
false);
open_any(
url_from_user_input(location, QDir::currentPath()));
});
request_render();
break;
@@ -403,12 +421,12 @@ Window::bind_host()
};
this->host_.enabled = [this](Action a) {
if (a == Action::Back) {
if (this->mode_ == Mode::Browser && this->browser_)
if (this->mode_ == Mode::Browse && this->browser_)
return this->browser_->hist_can_back();
return this->browser_ && !this->browser_->dir_url_.isEmpty();
}
if (a == Action::Forward) {
if (this->mode_ == Mode::Browser && this->browser_)
if (this->mode_ == Mode::Browse && this->browser_)
return this->browser_->hist_can_forward() ||
(this->viewer_ && this->viewer_->has_view());
return this->browser_ && this->browser_->hist_can_forward();
@@ -436,7 +454,7 @@ Window::bind_host()
BrowseSetup setup;
if (this->browser_)
setup = this->browser_->browse_setup();
this->app_->open(url, {}, setup, false);
this->app_->open(url, {}, setup, application());
};
this->host_.launch_exiftool = [this](QUrl url) { launch_exiftool(url); };
this->host_.trash = [this](QUrl url) { trash_url(url); };
@@ -587,6 +605,8 @@ Window::launch_exiftool(const QUrl &url)
void
Window::set_mode(Mode m)
{
if (application_mode(m) != application() || !this->pages_[size_t(m)])
return;
if (this->mode_ == m)
return;
this->mode_ = m;
@@ -597,13 +617,8 @@ Window::set_mode(Mode m)
void
Window::sync_title()
{
QUrl url;
if (this->mode_ == Mode::View && this->viewer_ &&
!this->viewer_->url_.isEmpty())
url = this->viewer_->url_;
else if (this->browser_ && !this->browser_->dir_url_.isEmpty())
url = this->browser_->dir_url_;
const QString app = QStringLiteral(DAWN_NAME);
const QUrl url = current_url();
const QString app = QString::fromUtf8(mode_def(this->mode_).title);
const QString title = url.isEmpty()
? app
: url_parse_name(url) + QStringLiteral(" \u2014 ") + app;
@@ -618,24 +633,20 @@ Window::sync_title()
ui->titlebar->invalidate_arrange();
}
};
set_bar(this->browser_ui_.get());
set_bar(this->viewer_ui_.get());
for (auto &page : this->pages_)
set_bar(page.get());
}
Page *
Window::active_ui()
{
if (this->mode_ == Mode::Browser)
return this->browser_ui_.get();
return this->viewer_ui_.get();
return this->pages_[size_t(this->mode_)].get();
}
const Page *
Window::active_ui() const
{
if (this->mode_ == Mode::Browser)
return this->browser_ui_.get();
return this->viewer_ui_.get();
return this->pages_[size_t(this->mode_)].get();
}
const Actor *
@@ -713,15 +724,22 @@ Window::refresh_screen_profile(QScreen *target_screen)
void
Window::apply_screen_profile(QScreen *target_screen, bool force_reload)
{
if (!refresh_screen_profile(target_screen) && !force_reload)
return;
// This compares the profiles themselves, modulo their creation
// timestamps, so re-reading the same one does not count as a change.
const bool changed = refresh_screen_profile(target_screen);
if (this->viewer_)
this->viewer_->set_screen_profile(this->cmm_, this->screen_profile_,
this->screen_profile_fallback_, force_reload);
if (this->browser_)
this->browser_->set_screen_profile(
this->cmm_, this->screen_profile_, force_reload);
auto icc = this->screen_profile_
? make_shared<const vector<uint8_t>>(this->screen_profile_->to_bytes())
: nullptr;
this->screen_state_ = {this->cmm_, this->screen_profile_, std::move(icc),
this->screen_profile_fallback_};
this->kit_.bake_colours(this->cmm_.get(), this->screen_profile_.get());
this->renderer_.set_transfer(profile_transfer(this->screen_profile_.get()));
for (auto &page : this->pages_)
if (page && page->content)
page->content->screen_changed(
this->screen_state_, changed, force_reload);
}
QWindow *
@@ -848,15 +866,18 @@ Window::open_sibling(int delta)
if (!this->viewer_ || !this->browser_ || this->browser_->files_.empty() ||
delta == 0)
return;
const int i = viewer_file_index(this->viewer_->url_);
if (i < 0)
return;
const int n = int(this->browser_->files_.size());
int j = (i + delta) % n;
if (j < 0)
j += n;
if (j == i)
return;
open_viewer(this->browser_->file_url(j));
set_mode(Mode::View);
request_render();
@@ -867,6 +888,7 @@ Window::request_render()
{
if (!this->renderer_ready_ || this->update_pending_)
return;
this->update_pending_ = true;
// This window presents FIFO itself. QWindow::requestUpdate asks the
// platform frame clock for a slot we do not drive (wl_surface.frame,
@@ -904,10 +926,10 @@ Window::render()
const float dpr = host_dpr(*this);
this->kit_.dpi_ = host_dpi(*this);
const bool fullscreen = bool(shell()->windowState() & Qt::WindowFullScreen);
if (this->mode_ == Mode::Browser && this->browser_)
this->browser_->set_host(w, h, dpr);
else if (this->viewer_)
this->viewer_->set_host(w, h, dpr);
if (this->kit_.set_host(w, h, dpr))
for (auto &page : this->pages_)
if (page && page->content)
page->content->rescale(this->kit_);
// Nothing to do for a resize: relayout_popups() re-places every popup
// and drops the ones whose opener stopped being shown.
@@ -924,17 +946,9 @@ Window::render()
sync_title();
if (ui->titlebar)
ui->titlebar->sync(this->kit_);
if (ui->toolbar) {
if (this->mode_ == Mode::Browser)
ui->toolbar->busy = this->awaiting_view_ ||
(this->browser_ && this->browser_->thumbs_busy());
else
ui->toolbar->busy = this->viewer_ && this->viewer_->opening_;
}
if (this->mode_ == Mode::Browser && this->browser_)
this->browser_->present(*ui);
else if (this->viewer_)
this->viewer_->present(*ui);
if (ui->toolbar)
ui->toolbar->busy = this->awaiting_view_ || ui->content->busy();
ui->content->present(this->kit_, *ui);
sync_csd();
if (this->viewer_ && this->viewer_->consume_open_done() &&
this->awaiting_view_) {
@@ -967,8 +981,8 @@ void
Window::arm_ui_wake()
{
int ms = this->kit_.wake_ms();
if (this->mode_ == Mode::View && this->viewer_)
ms = sooner(ms, this->viewer_->wake_ms());
if (Page *page = active_ui(); page && page->content)
ms = sooner(ms, page->content->wake_ms());
if (ms >= 0)
this->ui_wake_.start(ms);
else
@@ -1010,12 +1024,12 @@ Window::show_browser(bool select)
{
if (!this->browser_ || this->browser_->dir_url_.isEmpty())
return;
if (this->viewer_ui_)
if (this->pages_[size_t(Mode::View)])
this->kit_.close_popups();
if (select && this->viewer_)
this->browser_->select_file(this->viewer_->url_);
cancel_viewer_loads();
set_mode(Mode::Browser);
set_mode(Mode::Browse);
request_render();
}
@@ -1036,7 +1050,7 @@ Window::go_forward()
if (this->browser_ && this->browser_->hist_forward()) {
this->kit_.close_popups();
cancel_viewer_loads();
set_mode(Mode::Browser);
set_mode(Mode::Browse);
request_render();
return;
}
@@ -1051,6 +1065,18 @@ Window::go_forward()
bool
Window::event(QEvent *event)
{
switch (event->type()) {
case QEvent::MouseButtonPress:
case QEvent::KeyPress:
case QEvent::Wheel:
case QEvent::TouchBegin:
case QEvent::Drop:
// Whatever dn guessed at startup has now been used; see new_window.
this->app_->default_window.clear();
break;
default:
break;
}
switch (event->type()) {
case QEvent::NativeGesture:
return handle_native_gesture((QNativeGestureEvent *) event);
@@ -1123,7 +1149,7 @@ Window::event(QEvent *event)
event->ignore();
return true;
}
open_any(url, false);
open_any(url);
drop->acceptProposedAction();
return true;
}
@@ -1243,6 +1269,10 @@ Window::apply_window(Action a)
QUrl
Window::current_url() const
{
if (this->cropper_)
return this->cropper_->jpeg_url_;
if (this->commander_)
return this->commander_->dir_url_;
if (this->mode_ == Mode::View && this->viewer_ &&
!this->viewer_->url_.isEmpty())
return this->viewer_->url_;
@@ -1252,8 +1282,34 @@ Window::current_url() const
}
void
Window::open_any(const QUrl &url, bool browse)
Window::open_any(const QUrl &input)
{
QUrl url = input;
if (url.isEmpty() && !this->cropper_)
url = path_to_url(QDir::currentPath());
if (this->cropper_) {
if (!url.isEmpty() &&
(url_to_path(url).isEmpty() || QFileInfo(url_to_path(url)).isDir()))
return;
this->cropper_->jpeg_url_ =
url.isEmpty() ? QUrl{} : url_normalized(url);
sync_title();
request_render();
return;
}
if (this->commander_) {
const QString path = url_to_path(url);
if (path.isEmpty())
return;
const QFileInfo target(path);
const QFileInfo dir(target.isDir() ? path : target.absolutePath());
if (!dir.isDir() || !dir.isReadable())
return;
this->commander_->dir_url_ = path_to_url(dir.absoluteFilePath());
sync_title();
request_render();
return;
}
// The browser always wants a directory: a file opens the one holding it.
const QString path = url_to_path(url);
const QFileInfo info(path);
@@ -1261,16 +1317,7 @@ Window::open_any(const QUrl &url, bool browse)
if (this->browser_)
this->browser_->open_dir(url, true);
cancel_viewer_loads();
set_mode(Mode::Browser);
} else if (browse) {
// A file with --browse is a request to point at it, not to view it:
// browse the parent and put the cursor on the file.
if (this->browser_) {
this->browser_->open_dir(path_to_url(info.absolutePath()), true);
this->browser_->select_file(url);
}
cancel_viewer_loads();
set_mode(Mode::Browser);
set_mode(Mode::Browse);
} else {
if (this->viewer_)
this->viewer_->open(url);
@@ -1287,6 +1334,24 @@ Window::open_any(const QUrl &url, bool browse)
sync_title();
}
void
Window::reveal_file(const QUrl &input)
{
if (!this->browser_)
return;
const QUrl url = input.isEmpty() ? path_to_url(QDir::currentPath()) : input;
const QFileInfo info(url_to_path(url));
this->browser_->open_dir(
info.isDir() ? url : path_to_url(info.absolutePath()), true);
if (!info.isDir())
this->browser_->select_file(url);
cancel_viewer_loads();
set_mode(Mode::Browse);
request_render();
sync_title();
}
void
Window::keyPressEvent(QKeyEvent *event)
{
@@ -1332,12 +1397,12 @@ Window::keyPressEvent(QKeyEvent *event)
show_browser(true);
return;
}
if (this->mode_ == Mode::Browser && this->awaiting_view_) {
if (this->mode_ == Mode::Browse && this->awaiting_view_) {
cancel_viewer_loads();
request_render();
return;
}
if (this->mode_ == Mode::Browser)
if (this->mode_ == Mode::Browse)
return;
begin_close();
}
@@ -1534,6 +1599,7 @@ Window::wheelEvent(QWheelEvent *event)
const QPoint ang = event->angleDelta();
const QPoint pix = event->pixelDelta();
const bool alt = event->modifiers() & Qt::AltModifier;
// Qt Wayland (and Windows) transpose Alt+wheel onto the X axis.
// Sway still sends a vertical wl_pointer.axis; Qt swaps it before
// QWheelEvent. Read Y, then X.
+10 -5
View File
@@ -9,6 +9,8 @@
#include "kit-browser.hpp"
#include "kit-chrome.hpp"
#include "kit-commander.hpp"
#include "kit-crop-jpeg.hpp"
#include "kit-viewer.hpp"
#include "kit.hpp"
#include "renderer.hpp"
@@ -45,7 +47,6 @@ class App;
class Window final : public QWindow
{
enum class Mode : uint8_t { View, Browser };
bool refresh_screen_profile(QScreen *target_screen);
void apply_screen_profile(QScreen *target_screen, bool force_reload);
@@ -93,11 +94,13 @@ class Window final : public QWindow
Renderer renderer_;
Kit kit_;
HostActions host_;
std::unique_ptr<Page> browser_ui_;
std::unique_ptr<Page> viewer_ui_;
std::unique_ptr<Page> pages_[size_t(Mode::Count)];
Cropper *cropper_ = nullptr;
Commander *commander_ = nullptr;
Browser *browser_ = nullptr;
Viewer *viewer_ = nullptr;
Mode mode_ = Mode::View;
ScreenState screen_state_;
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
std::shared_ptr<dawn::Cmm> cmm_;
std::shared_ptr<dawn::Profile> screen_profile_;
@@ -145,9 +148,11 @@ public:
explicit Window(App *app, QWindow *parent);
~Window() override;
bool initialize(const QUrl &url, BrowseSetup setup, bool browse);
bool initialize(const QUrl &url, BrowseSetup setup, Mode mode);
void shutdown();
void open_any(const QUrl &url, bool browse);
void open_any(const QUrl &url);
void reveal_file(const QUrl &url);
Mode application() const { return application_mode(mode_); }
[[nodiscard]] QUrl current_url() const;
[[nodiscard]] HostActions &host() { return this->host_; }
[[nodiscard]] Page *active_page() { return active_ui(); }
+5 -5
View File
@@ -25,11 +25,11 @@ the _User Guide_.
Options
-------
*--browse*::
When an image is passed, start in browsing mode, and preselect that
image in its containing directory. This is used by *dn*'s inode/directory
handler to implement the "Open Containing Folder" feature of certain
applications.
*--mode*=_view|browse|cropjpeg|commander_::
Select the application to open. View and Browse are the image browser's
two faces; Browse preselects files in their directory, used to implement
the "Open Containing Folder" feature of certain applications.
JPEG Cropper and Commander are unimplemented.
*--invalidate-cache*::
Invalidate the wide thumbnail cache, removing thumbnails for files that can
+3 -4
View File
@@ -5,7 +5,7 @@
// SPDX-License-Identifier: MPL-2.0
//
const INSTANCE_PROTOCOL_VERSION = 2;
const INSTANCE_PROTOCOL_VERSION = 3;
struct Request {
u64 id;
@@ -16,9 +16,8 @@ struct Request {
struct OpenRequest {
string urls<>;
string activation_token;
// Route a file argument to its parent directory,
// with the file selected, rather than to the viewer.
bool browse;
// Stable application/mode name, independent of enum ordering.
string mode;
} open;
} body;
};
+3 -3
View File
@@ -440,15 +440,15 @@ Client::call(const Request &req, Received<ResponseView> &out, Error *error,
bool
Client::open(const vector<string> &urls, string_view activation_token,
bool browse, Error *error, chrono::milliseconds timeout)
string_view mode, Error *error, chrono::milliseconds timeout)
{
OpenRequest open_req;
open_req.urls = urls;
open_req.activation_token = string(activation_token);
open_req.browse = browse;
open_req.mode = string(mode);
Request req;
req.id = this->last_id_ += 1;
req.id = ++this->last_id_;
req.body.value = RequestBodyOpen{std::move(open_req)};
Received<ResponseView> response;
+3 -3
View File
@@ -156,15 +156,15 @@ public:
[[nodiscard]] uint32_t server_pid() const;
// Ask the running dn to open these URLs, and wait for it to say
// whether it did. browse routes a file argument to its parent
// directory, with the file selected, rather than to the viewer.
// whether it did. mode is the stable application/mode name. An empty
// URL represents an application window without a document.
//
// On a refusal *error is filled in when non-null; on a timeout,
// protocol, or I/O failure it gets Internal and an empty message.
// Do not retry: once the request is out, the server may already have
// opened windows.
bool open(const std::vector<std::string> &urls,
std::string_view activation_token, bool browse, Error *error,
std::string_view activation_token, std::string_view mode, Error *error,
std::chrono::milliseconds timeout);
};
+23 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh -e
# msys2-package-msi.sh: build an MSI from a MinGW/MSYS2 cross-build
export LC_ALL=C
arch=$1 msi=$2 wxs=$3 description=$4
arch=$1 msi=$2 wxs=$3 description=$4 wip=${5:-OFF}
destdir=$PWD/package/${msi%.*}
# CMAKE_SYSTEM_PROCESSOR is x86_64 for a 64-bit MinGW cross-build.
@@ -63,6 +63,28 @@ do cat <<END
</RegistryKey>
END
done)
$(if [ "$wip" = ON ]; then
cat <<'END'
<RegistryKey Root='HKCR' Key='dawn.cropjpeg'>
<RegistryValue Type='string' Value='Dawn JPEG Cropper' />
<RegistryValue Type='string' Key='DefaultIcon' Value='[INSTALLDIR]dn.ico' />
<RegistryValue Type='string' Key='shell\open\command'
Value='"[INSTALLDIR]dn.exe" --mode=cropjpeg "%1"' />
</RegistryKey>
<RegistryKey Root='HKCR' Key='Directory\shell\Dawn Commander'>
<RegistryValue Type='string' Value='Open in Dawn Commander' />
<RegistryValue Type='string' Key='command'
Value='"[INSTALLDIR]dn.exe" --mode=commander "%1"' />
</RegistryKey>
END
for ext in .jpg .jpe .jpeg; do
cat <<END
<RegistryKey Root='HKCR' Key='$ext\OpenWithProgids'>
<RegistryValue Type='string' Name='dawn.cropjpeg' Value='' />
</RegistryKey>
END
done
fi)
</Component>
</DirectoryRef>
</Fragment>
+4 -1
View File
@@ -147,7 +147,10 @@ find_program(DAWN_DESKTOP_FILE_VALIDATE desktop-file-validate)
if(DAWN_DESKTOP_FILE_VALIDATE)
foreach(desktop
name.janouch.Dawn.desktop
name.janouch.Dawn.Browse.desktop)
name.janouch.Dawn.Browse.desktop
name.janouch.Dawn.Commander.desktop
name.janouch.Dawn.CropJpeg.desktop
)
add_test(NAME ${desktop} COMMAND "${DAWN_DESKTOP_FILE_VALIDATE}"
"${PROJECT_SOURCE_DIR}/dn/${desktop}")
endforeach()
+3 -3
View File
@@ -150,7 +150,7 @@ test_round_trip_frames()
inst::OpenRequest open;
open.urls = {"/one", "/two"};
open.activation_token = "tok";
open.browse = true;
open.mode = "browse";
const vector<byte> buf = wire(open);
dawn::ipc::Decoder dec(buf);
inst::OpenRequestView view{};
@@ -160,7 +160,7 @@ test_round_trip_frames()
CHECK(view.urls[0] == "/one");
CHECK(view.urls[1] == "/two");
CHECK(view.activation_token == "tok");
CHECK(view.browse);
CHECK(view.mode == "browse");
}
{
@@ -217,7 +217,7 @@ test_round_trip_frames()
CHECK(o.urls.size() == 2);
CHECK(o.urls[0] == "/p");
CHECK(o.urls[1] == "/q");
CHECK(!o.browse);
CHECK(o.mode.empty());
}
{
inst::Response resp;