Prepare for future modes
This commit is contained in:
+7
-1
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#cmakedefine01 DAWN_WIP
|
||||
|
||||
#define DAWN_NAME "@PROJECT_NAME@"
|
||||
#define DAWN_NAMESPACE "@DAWN_NAMESPACE@"
|
||||
#define DAWN_VERSION "@PROJECT_VERSION@"
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user