From 0e2b1ce62c570b15c4e0f9c5cfd3bcc4c13179cc Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Thu, 11 Jun 2026 17:03:45 +0200 Subject: [PATCH 1/8] Fix video position marker position when first enabled --- src/audio_marker.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/audio_marker.cpp b/src/audio_marker.cpp index fe584891b8..62ec3600f0 100644 --- a/src/audio_marker.cpp +++ b/src/audio_marker.cpp @@ -87,7 +87,9 @@ class VideoPositionMarker final : public AudioMarker { int position = -1; public: - void SetPosition(int new_pos) { position = new_pos; } + void SetPosition(int new_pos, const VideoController *vc) { + position = vc->TimeAtFrame(new_pos); + } int GetPosition() const override { return position; } FeetStyle GetFeet() const override { return Feet_None; } @@ -106,7 +108,7 @@ VideoPositionMarkerProvider::VideoPositionMarkerProvider(agi::Context *c) VideoPositionMarkerProvider::~VideoPositionMarkerProvider() { } void VideoPositionMarkerProvider::Update(int frame_number) { - marker->SetPosition(vc->TimeAtFrame(frame_number)); + marker->SetPosition(frame_number, vc); AnnounceMarkerMoved(); } @@ -114,7 +116,7 @@ void VideoPositionMarkerProvider::OptChanged(agi::OptionValue const& opt) { if (opt.GetBool()) { video_seek_slot.Unblock(); marker = std::make_unique(); - marker->SetPosition(vc->GetFrameN()); + marker->SetPosition(vc->GetFrameN(), vc); } else { video_seek_slot.Block(); From 65c9a3638a157b84e00a7f5d753b228434560461 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Fri, 19 Jun 2026 14:25:26 +0200 Subject: [PATCH 2/8] Add support for CSS colors with alpha in options --- libaegisub/common/color.cpp | 5 ++++- libaegisub/common/option.cpp | 1 + libaegisub/common/parser.cpp | 1 + tests/tests/color.cpp | 2 ++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libaegisub/common/color.cpp b/libaegisub/common/color.cpp index e46a6cbdf1..d6484db97b 100644 --- a/libaegisub/common/color.cpp +++ b/libaegisub/common/color.cpp @@ -47,7 +47,10 @@ std::string Color::GetHexFormatted(bool rgba) const { } std::string Color::GetRgbFormatted() const { - return agi::format("rgb(%d, %d, %d)", r, g, b); + if (a) + return agi::format("rgba(%d, %d, %d, %d)", r, g, b, a); + else + return agi::format("rgb(%d, %d, %d)", r, g, b); } } diff --git a/libaegisub/common/option.cpp b/libaegisub/common/option.cpp index c75aad91f3..ad2fd897dc 100644 --- a/libaegisub/common/option.cpp +++ b/libaegisub/common/option.cpp @@ -113,6 +113,7 @@ class ConfigVisitor final : public json::ConstVisitor { if ((size == 4 && string[0] == '#') || (size == 7 && string[0] == '#') || (size >= 10 && string.starts_with("rgb(")) || + (size >= 13 && string.starts_with("rgba(")) || ((size == 9 || size == 10) && string.starts_with("&H"))) { values.push_back(std::make_unique(name, agi::Color(string))); diff --git a/libaegisub/common/parser.cpp b/libaegisub/common/parser.cpp index 9301c1510c..ad81d346e5 100644 --- a/libaegisub/common/parser.cpp +++ b/libaegisub/common/parser.cpp @@ -88,6 +88,7 @@ struct color_grammar : qi::grammar { css_color = "rgb(" >> blank >> rgb_component >> comma >> rgb_component >> comma >> rgb_component >> blank >> ')' + | "rgba(" >> blank >> rgb_component >> comma >> rgb_component >> comma >> rgb_component >> comma >> rgb_component >> blank >> ')' | '#' >> hex_byte >> hex_byte >> hex_byte | '#' >> hex_char >> hex_char >> hex_char ; diff --git a/tests/tests/color.cpp b/tests/tests/color.cpp index 57d08a6127..128178c3a3 100644 --- a/tests/tests/color.cpp +++ b/tests/tests/color.cpp @@ -56,11 +56,13 @@ TEST(lagi_color, rgb) { EXPECT_EQ(agi::Color(255, 255, 255), agi::Color("rgb(255,255,255)")); EXPECT_EQ(agi::Color(255, 0, 127), agi::Color("rgb(255, 0, 127)")); EXPECT_EQ(agi::Color(16, 32, 48), agi::Color("rgb( 16 , 32 , 48 )")); + EXPECT_EQ(agi::Color(16, 32, 48, 64), agi::Color("rgba( 16 , 32 , 48 , 64 )")); EXPECT_EQ("rgb(0, 0, 0)", agi::Color(0, 0, 0).GetRgbFormatted()); EXPECT_EQ("rgb(255, 255, 255)", agi::Color(255, 255, 255).GetRgbFormatted()); EXPECT_EQ("rgb(255, 0, 127)", agi::Color(255, 0, 127).GetRgbFormatted()); EXPECT_EQ("rgb(16, 32, 48)", agi::Color(16, 32, 48).GetRgbFormatted()); + EXPECT_EQ("rgba(16, 32, 48, 64)", agi::Color(16, 32, 48, 64).GetRgbFormatted()); } TEST(lagi_color, ass_ovr) { From 489c4f99a97282ffd7e28aed70c0cf9b4802f142 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Fri, 19 Jun 2026 14:56:06 +0200 Subject: [PATCH 3/8] Turn optional OptionAdd arguments into a kwargs struct In preparation for adding Yet Another Argument --- src/preferences.cpp | 34 +++++++++++++++++----------------- src/preferences_base.cpp | 6 +++--- src/preferences_base.h | 9 ++++++++- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/preferences.cpp b/src/preferences.cpp index 9569bb1d67..940260ea63 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -73,10 +73,10 @@ void General(wxTreebook *book, Preferences *parent) { wxString autoload_modes[] = { _("Never"), _("Always"), _("Ask") }; wxArrayString autoload_modes_arr(3, autoload_modes); p->OptionChoice(general, _("Automatically load linked files"), autoload_modes_arr, "App/Auto/Load Linked Files"); - p->OptionAdd(general, _("Undo Levels"), "Limits/Undo Levels", 2, 10000); + p->OptionAdd(general, _("Undo Levels"), "Limits/Undo Levels", {.min = 2, .max = 10000}); auto recent = p->PageSizer(_("Recently Used Lists")); - p->OptionAdd(recent, _("Files"), "Limits/MRU", 0, 16); + p->OptionAdd(recent, _("Files"), "Limits/MRU", {.min = 0, .max = 16}); p->OptionAdd(recent, _("Find/Replace"), "Limits/Find Replace"); p->SetSizerAndFit(p->sizer); @@ -136,13 +136,13 @@ void Audio(wxTreebook *book, Preferences *parent) { p->OptionAdd(general, _("Auto-focus on mouse over"), "Audio/Auto/Focus"); p->OptionAdd(general, _("Play audio when stepping in video"), "Audio/Plays When Stepping Video"); p->OptionAdd(general, _("Left-click-drag moves end marker"), "Audio/Drag Timing"); - p->OptionAdd(general, _("Default timing length (ms)"), "Timing/Default Duration", 0, 36000); - p->OptionAdd(general, _("Default lead-in length (ms)"), "Audio/Lead/IN", 0, 36000); - p->OptionAdd(general, _("Default lead-out length (ms)"), "Audio/Lead/OUT", 0, 36000); + p->OptionAdd(general, _("Default timing length (ms)"), "Timing/Default Duration", {.min = 0, .max = 36000}); + p->OptionAdd(general, _("Default lead-in length (ms)"), "Audio/Lead/IN", {.min = 0, .max = 36000}); + p->OptionAdd(general, _("Default lead-out length (ms)"), "Audio/Lead/OUT", {.min = 0, .max = 36000}); - p->OptionAdd(general, _("Marker drag-start sensitivity (px)"), "Audio/Start Drag Sensitivity", 1, 15); - p->OptionAdd(general, _("Line boundary thickness (px)"), "Audio/Line Boundaries Thickness", 1, 5); - p->OptionAdd(general, _("Maximum snap distance (px)"), "Audio/Snap/Distance", 0, 25); + p->OptionAdd(general, _("Marker drag-start sensitivity (px)"), "Audio/Start Drag Sensitivity", {.min = 1, .max = 15}); + p->OptionAdd(general, _("Line boundary thickness (px)"), "Audio/Line Boundaries Thickness", {.min = 1, .max = 5}); + p->OptionAdd(general, _("Maximum snap distance (px)"), "Audio/Snap/Distance", {.min = 0, .max = 25}); const wxString dtl_arr[] = { _("Don't show"), _("Show previous"), _("Show previous and next"), _("Show all") }; wxArrayString choice_dtl(4, dtl_arr); @@ -223,9 +223,9 @@ void Interface(wxTreebook *book, Preferences *parent) { p->OptionFont(edit_box, "Subtitle/Edit Box/"); auto character_count = p->PageSizer(_("Character Counter")); - p->OptionAdd(character_count, _("Maximum characters per line"), "Subtitle/Character Limit", 0, 1000); - p->OptionAdd(character_count, _("Characters Per Second Warning Threshold"), "Subtitle/Character Counter/CPS Warning Threshold", 0, 1000); - p->OptionAdd(character_count, _("Characters Per Second Error Threshold"), "Subtitle/Character Counter/CPS Error Threshold", 0, 1000); + p->OptionAdd(character_count, _("Maximum characters per line"), "Subtitle/Character Limit", {.min = 0, .max = 1000}); + p->OptionAdd(character_count, _("Characters Per Second Warning Threshold"), "Subtitle/Character Counter/CPS Warning Threshold", {.min = 0, .max = 1000}); + p->OptionAdd(character_count, _("Characters Per Second Error Threshold"), "Subtitle/Character Counter/CPS Error Threshold", {.min = 0, .max = 1000}); p->OptionAdd(character_count, _("Ignore whitespace"), "Subtitle/Character Counter/Ignore Whitespace"); p->OptionAdd(character_count, _("Ignore punctuation"), "Subtitle/Character Counter/Ignore Punctuation"); @@ -310,7 +310,7 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) { // Separate sizer to prevent the colors in the visual tools section from getting resized auto visual_tools_alpha = p->PageSizer(_("Visual Typesetting Tools Alpha")); - p->OptionAdd(visual_tools_alpha, _("Shaded Area"), "Colour/Visual Tools/Shaded Area Alpha", 0, 1, 0.1); + p->OptionAdd(visual_tools_alpha, _("Shaded Area"), "Colour/Visual Tools/Shaded Area Alpha", {.min = 0, .max = 1, .inc = 0.1}); p->sizer = main_sizer; @@ -325,7 +325,7 @@ void Backup(wxTreebook *book, Preferences *parent) { wxControl *cb = p->OptionAdd(save, _("Enable"), "App/Auto/Save"); p->CellSkip(save); p->EnableIfChecked(cb, - p->OptionAdd(save, _("Interval in seconds"), "App/Auto/Save Every Seconds", 1)); + p->OptionAdd(save, _("Interval in seconds"), "App/Auto/Save Every Seconds", {.min = 1})); p->OptionBrowse(save, _("Path"), "Path/Auto/Save", cb, true); p->OptionAdd(save, _("Autosave after every change"), "App/Auto/Save on Every Change"); @@ -403,7 +403,7 @@ void Advanced_Audio(wxTreebook *book, Preferences *parent) { wxArrayString sc_choice(5, sc_arr); p->OptionChoice(spectrum, _("Frequency mapping"), sc_choice, "Audio/Renderer/Spectrum/FreqCurve"); - p->OptionAdd(spectrum, _("Cache memory max (MB)"), "Audio/Renderer/Spectrum/Memory Max", 2, 1024); + p->OptionAdd(spectrum, _("Cache memory max (MB)"), "Audio/Renderer/Spectrum/Memory Max", {.min = 2, .max = 1024}); #ifdef WITH_AVISYNTH auto avisynth = p->PageSizer("Avisynth"); @@ -435,8 +435,8 @@ void Advanced_Audio(wxTreebook *book, Preferences *parent) { #ifdef WITH_DIRECTSOUND auto dsound = p->PageSizer("DirectSound"); - p->OptionAdd(dsound, _("Buffer latency"), "Player/Audio/DirectSound/Buffer Latency", 1, 1000); - p->OptionAdd(dsound, _("Buffer length"), "Player/Audio/DirectSound/Buffer Length", 1, 100); + p->OptionAdd(dsound, _("Buffer latency"), "Player/Audio/DirectSound/Buffer Latency", {.min = 1, .max = 1000}); + p->OptionAdd(dsound, _("Buffer length"), "Player/Audio/DirectSound/Buffer Length", {.min = 1, .max = 100}); #endif p->SetSizerAndFit(p->sizer); @@ -468,7 +468,7 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) { wxArrayString log_levels_choice(8, log_levels); p->OptionChoice(ffms, _("Debug log verbosity"), log_levels_choice, "Provider/FFmpegSource/Log Level", true); - p->OptionAdd(ffms, _("Decoding threads"), "Provider/Video/FFmpegSource/Decoding Threads", -1); + p->OptionAdd(ffms, _("Decoding threads"), "Provider/Video/FFmpegSource/Decoding Threads", {.min = -1}); p->OptionAdd(ffms, _("Enable unsafe seeking"), "Provider/Video/FFmpegSource/Unsafe Seeking"); #endif diff --git a/src/preferences_base.cpp b/src/preferences_base.cpp index c1c0483d09..bf02bd4470 100644 --- a/src/preferences_base.cpp +++ b/src/preferences_base.cpp @@ -126,7 +126,7 @@ void OptionPage::CellSkip(PageSection section) { section.sizer->AddStretchSpacer(); } -wxControl *OptionPage::OptionAdd(PageSection section, const wxString &name, const char *opt_name, double min, double max, double inc) { +wxControl *OptionPage::OptionAdd(PageSection section, const wxString &name, const char *opt_name, OptionAddArgs kwargs) { parent->AddChangeableOption(opt_name); const auto opt = OPT_GET(opt_name); @@ -140,14 +140,14 @@ wxControl *OptionPage::OptionAdd(PageSection section, const wxString &name, cons } case agi::OptionType::Int: { - auto sc = new wxSpinCtrl(section.box, -1, std::to_wstring((int)opt->GetInt()), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, min, max, opt->GetInt()); + auto sc = new wxSpinCtrl(section.box, -1, std::to_wstring((int)opt->GetInt()), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, kwargs.min, kwargs.max, opt->GetInt()); sc->Bind(wxEVT_SPINCTRL, IntUpdater(opt_name, parent)); Add(section, name, sc); return sc; } case agi::OptionType::Double: { - auto scd = new wxSpinCtrlDouble(section.box, -1, std::to_wstring(opt->GetDouble()), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, min, max, opt->GetDouble(), inc); + auto scd = new wxSpinCtrlDouble(section.box, -1, std::to_wstring(opt->GetDouble()), wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, kwargs.min, kwargs.max, opt->GetDouble(), kwargs.inc); scd->Bind(wxEVT_SPINCTRLDOUBLE, DoubleUpdater(opt_name, parent)); Add(section, name, scd); return scd; diff --git a/src/preferences_base.h b/src/preferences_base.h index 6643e642ba..ba870200c1 100644 --- a/src/preferences_base.h +++ b/src/preferences_base.h @@ -33,6 +33,12 @@ struct PageSection { wxWindow *box; }; +struct OptionAddArgs { + double min = 0; + double max = INT_MAX; + double inc = 1; +}; + class OptionPage : public wxScrolled { template void Add(PageSection section, wxString const& label, T *control); @@ -48,7 +54,8 @@ class OptionPage : public wxScrolled { PageSection PageSizer(wxString name); void CellSkip(PageSection section); - wxControl *OptionAdd(PageSection section, const wxString &name, const char *opt_name, double min=0, double max=INT_MAX, double inc=1); + + wxControl *OptionAdd(PageSection section, const wxString &name, const char *opt_name, OptionAddArgs kwargs = {}); void OptionChoice(PageSection section, const wxString &name, const wxArrayString &choices, const char *opt_name, bool translate = false); void OptionBrowse(PageSection section, const wxString &name, const char *opt_name, wxControl *enabler = nullptr, bool do_enable = false); void OptionFont(PageSection section, std::string opt_prefix); From 909f3315a6496e0932d88809cc98a36dda29b77e Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Tue, 9 Jun 2026 22:29:52 +0200 Subject: [PATCH 4/8] Fix snapping audio times to current video position Lots of text incoming, since timestamp wrangling is hell and I want to write down my reasoning before I inevitably forget it again in two weeks. Before commit 5d4973a5f64c54a6a3a84278f385d2d474cfa5ab, snapping an audio line's end time to the "current video position" indicator line would reliably make the line end on the frame before the current video frame (i.e. make the current video frame be the first frame where the selected line is *not* visible any more). Commit 5d4973a5f64c54a6a3a84278f385d2d474cfa5ab broke this, making the behavior of snapping audio times to the "current video position" inconsistent. The commit in question is definitely not fully correct, ultimately just because the entire concept of implicitly converting millisecond timestamps to centisecond timestamps is flawed in and of itself, and bound to always fail in sufficiently crazy edge cases. Fixing the timestamp conversion properly would entail either working with centisecond timestamps from the beginning, or somehow making the process of converting timestamps aware of the context the timestamps are from (e.g. "coming from some frame timestamp"). However, this specific issue was only *exposed* by this commit, and not solely caused by it. Its root cause was just that the "current video position" marker on the audio display would mark the *exact* time of the current video frame, rather than the ideal start/end time for a line to start/end at that video frame. This is why only snapping to the "current video position" broke while snapping to keyframes worked fine. It's quite possible that snapping to the "current video frame" marker was just never thought of as a use case. So, for now, TypesettingTools/Aegisub#421 can just be fixed by making a line snapped to the "current video position" marker snap to the middle of the frame rather than at the frame's exact start, and 5d4973a5f64c54a6a3a84278f385d2d474cfa5ab can be untangled at some later time. We still *draw* the "current video position" marker at the exact start of the frame so that one can read off the audio display which lines are visible on the current frame, only the *snap* position is changed. This does mean that snapping a line to the marker snaps the line to a position that's slightly *away* from the marker, but my impression is that this does not actually cause too much confusion. Fixes TypesettingTools/Aegisub#421. --- src/audio_marker.cpp | 3 +++ src/audio_marker.h | 6 +++++- src/audio_timing_dialogue.cpp | 21 ++++++++++++--------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/audio_marker.cpp b/src/audio_marker.cpp index 62ec3600f0..ff41aa0257 100644 --- a/src/audio_marker.cpp +++ b/src/audio_marker.cpp @@ -85,13 +85,16 @@ void AudioMarkerProviderKeyframes::GetMarkers(TimeRange const& range, AudioMarke class VideoPositionMarker final : public AudioMarker { Pen style{"Colour/Audio Display/Play Cursor"}; int position = -1; + int snap_position = -1; public: void SetPosition(int new_pos, const VideoController *vc) { position = vc->TimeAtFrame(new_pos); + snap_position = vc->TimeAtFrame(new_pos, agi::vfr::START); } int GetPosition() const override { return position; } + int GetSnapPosition() const override { return snap_position; } FeetStyle GetFeet() const override { return Feet_None; } wxPen GetStyle() const override { return style; } operator int() const { return position; } diff --git a/src/audio_marker.h b/src/audio_marker.h index ba725ed4cd..f5899a6628 100644 --- a/src/audio_marker.h +++ b/src/audio_marker.h @@ -51,9 +51,13 @@ class AudioMarker { }; /// @brief Get the marker's position - /// @return The marker's position in milliseconds + /// @return The marker's visual position in milliseconds virtual int GetPosition() const = 0; + /// @brief Get the marker's snap position + /// @return The millisecond to which times should be snapped when snapping to this marker + virtual int GetSnapPosition() const { return GetPosition(); }; + /// @brief Get the marker's drawing style /// @return A pen object describing the marker's drawing style virtual wxPen GetStyle() const = 0; diff --git a/src/audio_timing_dialogue.cpp b/src/audio_timing_dialogue.cpp index 28d7ae53ec..bc3950bdf0 100644 --- a/src/audio_timing_dialogue.cpp +++ b/src/audio_timing_dialogue.cpp @@ -881,11 +881,14 @@ int AudioTimingControllerDialogue::SnapMarkers(int snap_range, std::vector