Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,7 @@ endif
deps += dependency('icu-uc', version: '>=4.8.1.1')
deps += dependency('icu-i18n', version: '>=4.8.1.1')

dep_avail = []
foreach dep: [
check_deps = [
# audio, in order of precedence
['libpulse', [], 'PulseAudio', [], []],
['alsa', [], 'ALSA', [], []],
Expand All @@ -232,6 +231,13 @@ foreach dep: [
['hunspell', [], 'Hunspell', ['hunspell', 'hunspell_dep'], []],
['uchardet', [], 'uchardet', ['uchardet', 'uchardet_dep'], []],
]

if host_machine.system() == 'linux'
check_deps += [['libportal-gtk3', [], 'libportal', [], []]]
endif

dep_avail = []
foreach dep: check_deps
optname = dep[0].split('-')[0]
if not get_option(optname).disabled()
# [provide] section is ignored if required is false;
Expand Down
1 change: 1 addition & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ option('avisynth', type: 'feature', description: 'AviSynth video source')
option('fftw3', type: 'feature', description: 'FFTW3 support')
option('hunspell', type: 'feature', description: 'Hunspell spell checker')
option('uchardet', type: 'feature', description: 'uchardet character encoding detection')
option('libportal', type: 'feature', description: 'XDG Desktop Portal support through libportal')
option('csri', type: 'feature', description: 'CSRI support')

option('system_luajit', type: 'boolean', value: false, description: 'Force using system luajit')
Expand Down
111 changes: 95 additions & 16 deletions src/dialog_colorpicker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@
#include "persist_location.h"
#include "utils.h"
#include "value_event.h"
#include "xdg_desktop_portal_utils.h"

#include <libaegisub/log.h>
#include <libaegisub/scoped_ptr.h>

#include <algorithm>
#include <cmath>
#include <memory>
#include <vector>

#include <wx/bitmap.h>
#include <wx/bmpbuttn.h>
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/dcbuffer.h>
Expand Down Expand Up @@ -75,6 +79,15 @@ enum class PickerDirection {

static const int spectrum_horz_vert_arrow_size = 4;

#ifdef WITH_LIBPORTAL
// this could be made configurable
static constexpr bool enable_os_eyedropper = true;
static constexpr bool enable_custom_eyedropper = false;
#else
static constexpr bool enable_os_eyedropper = false;
static constexpr bool enable_custom_eyedropper = true;
#endif

wxDEFINE_EVENT(EVT_SPECTRUM_CHANGE, wxCommandEvent);

class ColorPickerSpectrum final : public wxControl {
Expand Down Expand Up @@ -415,6 +428,8 @@ void ColorPickerScreenDropper::DropFromScreenXY(int x, int y) {
Refresh(false);
}

wxDEFINE_EVENT(EVT_OS_SELECT, ValueEvent<agi::Color>);

class DialogColorPicker final : public wxDialog {
std::unique_ptr<PersistLocation> persist;

Expand Down Expand Up @@ -456,9 +471,10 @@ class DialogColorPicker final : public wxDialog {
wxStaticBitmap *preview_box; ///< A box which simply shows the current color
ColorPickerRecent *recent_box; ///< A grid of recently used colors

ColorPickerScreenDropper *screen_dropper;
ColorPickerScreenDropper *screen_dropper = nullptr;
wxStaticBitmap *screen_dropper_icon = nullptr;

wxStaticBitmap *screen_dropper_icon;
wxBitmapButton *os_screen_dropper_button = nullptr;

/// Update all other controls as a result of modifying an RGB control
void UpdateFromRGB(bool dirty = true);
Expand Down Expand Up @@ -497,6 +513,7 @@ class DialogColorPicker final : public wxDialog {
void OnDropperMouse(wxMouseEvent &evt);
void OnMouse(wxMouseEvent &evt);
void OnCaptureLost(wxMouseCaptureLostEvent&);
void OnOsDropperClick(wxCommandEvent&);

std::function<void (agi::Color)> callback;

Expand Down Expand Up @@ -584,8 +601,13 @@ DialogColorPicker::DialogColorPicker(wxWindow *parent, agi::Color initial_color,
recent_box = new ColorPickerRecent(this, 8, 4, 16);

eyedropper_bitmap = GETBUNDLE(eyedropper_tool, 24);
screen_dropper_icon = new wxStaticBitmap(this, -1, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxRAISED_BORDER);
screen_dropper = new ColorPickerScreenDropper(this, 7, 7, 8);
if (enable_os_eyedropper) {
os_screen_dropper_button = new wxBitmapButton(this, wxID_ANY, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxBORDER_DEFAULT);
}
if (enable_custom_eyedropper) {
screen_dropper_icon = new wxStaticBitmap(this, -1, eyedropper_bitmap, wxDefaultPosition, wxDefaultSize, wxRAISED_BORDER);
screen_dropper = new ColorPickerScreenDropper(this, 7, 7, 8);
}

// Arrange the controls in a nice way
wxSizer *spectop_sizer = new wxBoxSizer(wxHORIZONTAL);
Expand Down Expand Up @@ -629,9 +651,15 @@ DialogColorPicker::DialogColorPicker(wxWindow *parent, agi::Color initial_color,

wxSizer *picker_sizer = new wxBoxSizer(wxHORIZONTAL);
picker_sizer->AddStretchSpacer();
picker_sizer->Add(screen_dropper_icon, 0, wxALIGN_CENTER|wxRIGHT, 5);
picker_sizer->Add(screen_dropper, 0, wxALIGN_CENTER);
picker_sizer->AddStretchSpacer();
if (os_screen_dropper_button) {
picker_sizer->Add(os_screen_dropper_button, 0, wxALIGN_CENTER);
picker_sizer->AddStretchSpacer();
}
if (screen_dropper) {
picker_sizer->Add(screen_dropper_icon, 0, wxALIGN_CENTER|wxRIGHT, 5);
picker_sizer->Add(screen_dropper, 0, wxALIGN_CENTER);
picker_sizer->AddStretchSpacer();
}
picker_sizer->Add(recent_box, 0, wxALIGN_CENTER);
picker_sizer->AddStretchSpacer();

Expand Down Expand Up @@ -675,19 +703,26 @@ DialogColorPicker::DialogColorPicker(wxWindow *parent, agi::Color initial_color,
alpha_input->Bind(wxEVT_SPINCTRL, bind(&DialogColorPicker::UpdateFromAlpha, this));
alpha_input->Bind(wxEVT_TEXT, bind(&DialogColorPicker::UpdateFromAlpha, this));

screen_dropper_icon->Bind(wxEVT_MOTION, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_MOUSE_CAPTURE_LOST, &DialogColorPicker::OnCaptureLost, this);
Bind(wxEVT_MOTION, &DialogColorPicker::OnMouse, this);
Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnMouse, this);
Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnMouse, this);
if (screen_dropper) {
screen_dropper_icon->Bind(wxEVT_MOTION, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnDropperMouse, this);
screen_dropper_icon->Bind(wxEVT_MOUSE_CAPTURE_LOST, &DialogColorPicker::OnCaptureLost, this);
Bind(wxEVT_MOTION, &DialogColorPicker::OnMouse, this);
Bind(wxEVT_LEFT_DOWN, &DialogColorPicker::OnMouse, this);
Bind(wxEVT_LEFT_UP, &DialogColorPicker::OnMouse, this);
}

if (os_screen_dropper_button)
os_screen_dropper_button->Bind(wxEVT_BUTTON, &DialogColorPicker::OnOsDropperClick, this);

spectrum->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSpectrumChange, this);
slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnSliderChange, this);
alpha_slider->Bind(EVT_SPECTRUM_CHANGE, &DialogColorPicker::OnAlphaSliderChange, this);
recent_box->Bind(EVT_RECENT_SELECT, &DialogColorPicker::OnRecentSelect, this);
screen_dropper->Bind(EVT_DROPPER_SELECT, &DialogColorPicker::OnRecentSelect, this);
if (screen_dropper)
screen_dropper->Bind(EVT_DROPPER_SELECT, &DialogColorPicker::OnRecentSelect, this);
Bind(EVT_OS_SELECT, &DialogColorPicker::OnRecentSelect, this);

colorspace_choice->Bind(wxEVT_CHOICE, &DialogColorPicker::OnChangeMode, this);

Expand All @@ -706,7 +741,7 @@ wxSizer *DialogColorPicker::MakeColorInputSizer(wxWindow *parent, wxString (&lab
}

DialogColorPicker::~DialogColorPicker() {
if (screen_dropper_icon->HasCapture()) screen_dropper_icon->ReleaseMouse();
if (screen_dropper && screen_dropper_icon->HasCapture()) screen_dropper_icon->ReleaseMouse();
}

static void change_value(wxSpinCtrl *ctrl, int value) {
Expand Down Expand Up @@ -1083,6 +1118,7 @@ void DialogColorPicker::OnDropperMouse(wxMouseEvent &evt) {

/// @brief Hack to redirect events to the screen dropper icon
void DialogColorPicker::OnMouse(wxMouseEvent &evt) {
// this handler is only registered if screen_dropper is enabled
if (!screen_dropper_icon->HasCapture()) {
evt.Skip();
return;
Expand All @@ -1100,6 +1136,49 @@ void DialogColorPicker::OnCaptureLost(wxMouseCaptureLostEvent&) {
screen_dropper_icon->SetBitmap(eyedropper_bitmap);
}

#ifdef WITH_LIBPORTAL

static unsigned char float_to_byte(double x) {
if (std::isnan(x) || x <= 0.0) return 0;
if (x >= 1.0) return 255;
return static_cast<unsigned char>(x * 255 + 0.5);
// NOTE: I have encountered cases where the returned value was off by one,
// probably due to incorrect rounding. However, I am unable to reproduce it.
}

static void PortalPickColorCallback(GObject *source, GAsyncResult *res, gpointer data) {
// this will be called from GLib main loop, which likely runs on wxWidgets main thread, but don't rely on it
wxEvtHandler *event_target = static_cast<wxEvtHandler *>(data);
GError *error = nullptr;
GVariant *color_result = xdp_portal_pick_color_finish(XDP_PORTAL(source), res, &error);
if (error) {
LOG_W("dialog_colorpicker") << "XDG Desktop Portal PickColor failed: " << error->message;
// TODO inform the user about the error, except for user cancellation
g_error_free(error);
return;
}
gdouble r, g, b;
g_variant_get(color_result, "(ddd)", &r, &g, &b);
g_variant_unref(color_result);
agi::Color color(float_to_byte(r), float_to_byte(g), float_to_byte(b), 0);
// FIXME what if event_target has been destroyed in the meantime? (unlikely, but cannot be completely ruled out)
wxQueueEvent(event_target, new ValueEvent<agi::Color>(EVT_OS_SELECT, 0, color));
}

void DialogColorPicker::OnOsDropperClick(wxCommandEvent&) {
XdpParent *parent = agi::xdp_utils::xdp_parent_new_wx(this);
xdp_portal_pick_color(agi::xdp_utils::portal, parent, nullptr, PortalPickColorCallback, static_cast<wxEvtHandler *>(this));
xdp_parent_free(parent);
}

#else // WITH_LIBPORTAL

void DialogColorPicker::OnOsDropperClick(wxCommandEvent&) {
throw agi::InternalError("unimplemented");
}

#endif // WITH_LIBPORTAL

}

bool GetColorFromUser(wxWindow* parent, agi::Color original, bool alpha, std::function<void (agi::Color)> callback) {
Expand Down
5 changes: 5 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include "utils.h"
#include "value_event.h"
#include "version.h"
#include "xdg_desktop_portal_utils.h"

#include <libaegisub/dispatch.h>
#include <libaegisub/format_path.h>
Expand Down Expand Up @@ -257,6 +258,8 @@ bool AegisubApp::OnInit() {

exception_message = _("Oops, Aegisub has crashed!\n\nAn attempt has been made to save a copy of your file to:\n\n%s\n\nAegisub will now close.");

agi::xdp_utils::Initialize();

// Load plugins
Automation4::ScriptFactory::Register(std::make_unique<Automation4::LuaScriptFactory>());
libass::CacheFonts();
Expand Down Expand Up @@ -344,6 +347,8 @@ int AegisubApp::OnExit() {

AssExportFilterChain::Clear();

agi::xdp_utils::Cleanup();

// Keep this last!
delete agi::log::log;
crash_writer::Cleanup();
Expand Down
2 changes: 2 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ opt_src = [
'ffmpegsource_common.cpp']],

['Hunspell', 'spellchecker_hunspell.cpp'],

['libportal', 'xdg_desktop_portal_utils.cpp'],
]

foreach opt: opt_src
Expand Down
66 changes: 66 additions & 0 deletions src/xdg_desktop_portal_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2026, Aegisub contributors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project https://aegisub.org/

/// @file xdg_desktop_portal_utils.cpp
/// @brief Utilities related to XDG Desktop Portals
/// @ingroup utility linux
///


#ifdef WITH_LIBPORTAL
#include "xdg_desktop_portal_utils.h"

#ifdef __WXGTK3__
#include <libportal-gtk3/portal-gtk3.h>
#endif

namespace agi::xdp_utils {
XdpPortal *portal = nullptr;

XdpParent *xdp_parent_new_wx(wxWindow *window) {
#ifdef __WXGTK3__
GtkWidget *gtk_widget = GTK_WIDGET(window->GetHandle());
GtkWindow *gtk_window = GTK_WINDOW(gtk_widget_get_toplevel(gtk_widget));
return xdp_parent_new_gtk(gtk_window);
#else
return nullptr;
#endif
}

void Initialize() {
portal = xdp_portal_new();
}

void Cleanup() {
g_object_unref(portal);
portal = nullptr;
}
}

#endif // WITH_LIBPORTAL
57 changes: 57 additions & 0 deletions src/xdg_desktop_portal_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2026, Aegisub contributors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project https://aegisub.org/

/// @file xdg_desktop_portal_utils.h
/// @see xdg_desktop_portal_utils.cpp
/// @ingroup utility linux
///

#pragma once

#ifdef WITH_LIBPORTAL

#include <libportal/portal.h>

#include <wx/window.h>

namespace agi::xdp_utils {
extern XdpPortal *portal;
XdpParent *xdp_parent_new_wx(wxWindow *window);
void Initialize();
void Cleanup();
}

#else // WITH_LIBPORTAL

namespace agi::xdp_utils {
inline void Initialize() {}
inline void Cleanup() {}
}

#endif // WITH_LIBPORTAL
Loading