diff --git a/README.md b/README.md index 6bd3d65f2..da928b020 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## arch1t3cht's Aegisub "fork" -Go [here](#branchfeature-list) for the new features. +Download release builds [here](https://github.com/arch1t3cht/Aegisub/releases), or the latest CI builds [here](https://github.com/arch1t3cht/Aegisub/actions). + +The release page also has detailed list of all changes and new features. If you're interested in the technical details or want to compile yourself, read on. ### Don't we have enough Aegisub forks already?? We absolutely do, and I'm aware that adding another one [doesn't sound like](https://xkcd.com/927/) a [good idea on paper](https://cdn.discordapp.com/attachments/425357202963038208/1007103606421459004/unknown.png). However, @@ -50,9 +52,6 @@ This is probably because you're building with wxgtk2. Building with wxgtk3 fixes The exact way of switching depends on your Linux distribution, but essentially you need to ensure that `wx-config` or the next best variant of it points to wxgtk3. If it points to wxgtk2 by default and deinstalling wxgtk2 isn't an option, you can also temporarily move it out of the path or use a `native-file` in your meson project. Then, fully reconfigure meson using `meson configure --clearcache` and `meson setup --reconfigure`. -#### I get errors like "Option not found" after merging one of these branches -The changes to `default_config.json` or similar files weren't detected by meson due to missing regen dependencies. You can either merge the `bugfixes` branch or rebuild from scratch. - #### The video is desynced / Frames don't appear at the right time This is probably due to the ffms2 seeking bug ([#394](https://github.com/FFMS/ffms2/issues/394)). On Windows, this specific regression shouldn't happen anymore. On Linux, you need to install the latest git version of ffms2 - for example the [`ffms2-git`](https://aur.archlinux.org/packages/ffms2-git) AUR package on Arch linux, or just compile it yourself. @@ -63,11 +62,13 @@ If you're compiling yourself, try adding `--force-fallback-for=zlib` to the meso ### Compilation +If you're just looking to install Aegisub, you might want to check out the [releases page](https://github.com/arch1t3cht/Aegisub/releases) or the [CI builds](https://github.com/arch1t3cht/Aegisub/actions) first. + For compilation on Windows, see the TSTools documentation below. Also check the [GitHub workflow](https://github.com/arch1t3cht/Aegisub/blob/cibuilds/.github/workflows/ci.yml) for the project arguments. On Arch Linux, there is an AUR package called [aegisub-arch1t3cht-git](https://aur.archlinux.org/packages/aegisub-arch1t3cht-git). It's not maintained by me but seems to work. -On other distributions or for manual compilation you can use this package or the [TSTools PKGBUILD](https://aur.archlinux.org/packages/aegisub-ttools-meson-git) as a reference, in particular for installing the necessary dependencies if you don't want to compile them yourself. +On other Linux distributions or for manual compilation you can use this package or the [TSTools PKGBUILD](https://aur.archlinux.org/packages/aegisub-ttools-meson-git) as a reference, in particular for installing the necessary dependencies if you don't want to compile them yourself. If all dependencies are installed: - Install Meson - Clone the repository @@ -132,7 +133,7 @@ All other dependencies are either stored in the repository or are included as su Building: -1. Clone Aegisub's repository: `git clone https://github.com/TypesettingTools/Aegisub.git` +1. Clone Aegisub's repository: `git clone https://github.com/arch1t3cht/Aegisub.git` 2. From the Visual Studio "x64 Native Tools Command Prompt", generate the build directory: `meson build -Ddefault_library=static` (if building for release, add `--buildtype=release`) 3. Build with `cd build` and `ninja` diff --git a/automation/vapoursynth/aegisub_vs.py b/automation/vapoursynth/aegisub_vs.py index 1eca5f5d3..6ccd7b9b9 100644 --- a/automation/vapoursynth/aegisub_vs.py +++ b/automation/vapoursynth/aegisub_vs.py @@ -28,6 +28,8 @@ other data. import os import os.path import re +from enum import Enum +from tkinter.messagebox import askyesno from collections import deque from typing import Any, Dict, List, Tuple @@ -216,25 +218,36 @@ def save_keyframes(filename: str, keyframes: List[int]): f.write("".join(f"{n}\n" for n in keyframes)) -def try_get_keyframes(filename: str, default: str | List[int]) -> str | List[int]: - """ - Checks if a keyframes file for the given filename is present and, if so, - returns it. Otherwise, returns the given list of keyframes. - """ - kffilename = make_keyframes_filename(filename) - - return kffilename if os.path.exists(kffilename) else default +class GenKeyframesMode(Enum): + NEVER = 0 + ALWAYS = 1 + ASK = 2 -def get_keyframes(filename: str, clip: vs.VideoNode, **kwargs: Any) -> str: +def get_keyframes(filename: str, clip: vs.VideoNode, fallback: str | List[int], + generate: GenKeyframesMode = GenKeyframesMode.ASK, **kwargs: Any) -> str | List[int]: """ - When not already present, creates a keyframe file for the given clip next + Looks for a keyframes file for the given filename. + If no file was found, this function can generate a keyframe file for the given clip next to the given filename using WWXD or Scxvid (see the make_keyframes docstring). + Whether or not keyframes are generated depends on the `generate` argument. + Depending on the `generate` argument, the function will + - always generate keyframes when no file was found + - never generate keyframes when no file was found + (and return the fallback keyframes instead) + - show a dialog to ask the user whether keyframes should be + generated or not Additional keyword arguments are passed on to make_keyframes. """ kffilename = make_keyframes_filename(filename) if not os.path.exists(kffilename): + if generate == GenKeyframesMode.NEVER: + return fallback + if generate == GenKeyframesMode.ASK and not askyesno("Generate Keyframes", \ + "No keyframes file was found for this video file.\nShould Aegisub detect keyframes from the video?\nThis will take a while.", default="no"): + return fallback + vs.core.log_message(vs.MESSAGE_TYPE_INFORMATION, "No keyframes file found, detecting keyframes...\n") keyframes = make_keyframes(clip, **kwargs) save_keyframes(kffilename, keyframes) diff --git a/libaegisub/include/libaegisub/option_value.h b/libaegisub/include/libaegisub/option_value.h index 63870419c..04947086f 100644 --- a/libaegisub/include/libaegisub/option_value.h +++ b/libaegisub/include/libaegisub/option_value.h @@ -84,6 +84,12 @@ public: Color const& GetColor() const; bool const& GetBool() const; + std::string const& GetDefaultString() const; + int64_t const& GetDefaultInt() const; + double const& GetDefaultDouble() const; + Color const& GetDefaultColor() const; + bool const& GetDefaultBool() const; + void SetString(const std::string); void SetInt(const int64_t); void SetDouble(const double); @@ -96,6 +102,12 @@ public: std::vector const& GetListColor() const; std::vector const& GetListBool() const; + std::vector const& GetDefaultListString() const; + std::vector const& GetDefaultListInt() const; + std::vector const& GetDefaultListDouble() const; + std::vector const& GetDefaultListColor() const; + std::vector const& GetDefaultListBool() const; + void SetListString(std::vector); void SetListInt(std::vector); void SetListDouble(std::vector); @@ -117,6 +129,7 @@ public: : OptionValue(std::move(member_name)) \ , value(member_value), value_default(member_value) { } \ type const& GetValue() const { return value; } \ + type const& GetDefault() const { return value_default; } \ void SetValue(type new_val) { value = std::move(new_val); NotifyChanged(); } \ OptionType GetType() const { return OptionType::type_name; } \ void Reset() { value = value_default; NotifyChanged(); } \ @@ -141,6 +154,7 @@ CONFIG_OPTIONVALUE(Bool, bool) : OptionValue(std::move(name)) \ , array(value), array_default(value) { } \ std::vector const& GetValue() const { return array; } \ + std::vector const& GetDefault() const { return array_default; } \ void SetValue(std::vector val) { array = std::move(val); NotifyChanged(); } \ OptionType GetType() const { return OptionType::List##type_name; } \ void Reset() { array = array_default; NotifyChanged(); } \ @@ -156,6 +170,7 @@ CONFIG_OPTIONVALUE_LIST(Bool, bool) #define CONFIG_OPTIONVALUE_ACCESSORS(ReturnType, Type) \ inline ReturnType const& OptionValue::Get##Type() const { return As(OptionType::Type)->GetValue(); } \ + inline ReturnType const& OptionValue::GetDefault##Type() const { return As(OptionType::Type)->GetDefault(); } \ inline void OptionValue::Set##Type(ReturnType v) { As(OptionType::Type)->SetValue(std::move(v)); } CONFIG_OPTIONVALUE_ACCESSORS(std::string, String) diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 0431e63d7..e58126c97 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -392,7 +392,7 @@ }, "VapourSynth" : { "Log Level": "Information", - "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment the first following line to read keyframes from a file when present.\n#__aegi_keyframes = a.try_get_keyframes(filename, __aegi_keyframes)\n\n# Uncomment the following line to automatically generate keyframes at scene changes. This will take some time when first loading a video.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" + "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment this line to make Aegisub look for a keyframes file for the video, or ask to detect keyframes on scene changes if no file was found.\n# You can also change the GenKeyframesMode. Valid values are NEVER, ALWAYS, and ASK.\n#__aegi_keyframes = a.get_keyframes(filename, clip, __aegi_keyframes, generate=a.GenKeyframesMode.ASK)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } }, diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 35de4944b..d3b0f281a 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -392,7 +392,7 @@ }, "VapourSynth" : { "Log Level": "Information", - "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment the first following line to read keyframes from a file when present.\n#__aegi_keyframes = a.try_get_keyframes(filename, __aegi_keyframes)\n\n# Uncomment the following line to automatically generate keyframes at scene changes. This will take some time when first loading a video.\n#__aegi_keyframes = a.get_keyframes(filename, clip)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" + "Default Script" : "# This default script will load a video file using LWLibavSource.\n# It requires the `lsmas` plugin.\n# See ?data/automation/vapoursynth/aegisub_vs.py for more information.\n\nimport vapoursynth as vs\nimport time\nimport aegisub_vs as a\na.set_paths(locals())\n\nclip, videoinfo = a.wrap_lwlibavsource(filename)\nclip.set_output()\n__aegi_timecodes = videoinfo[\"timecodes\"]\n__aegi_keyframes = videoinfo[\"keyframes\"]\n\n# Uncomment this line to make Aegisub look for a keyframes file for the video, or ask to detect keyframes on scene changes if no file was found.\n# You can also change the GenKeyframesMode. Valid values are NEVER, ALWAYS, and ASK.\n#__aegi_keyframes = a.get_keyframes(filename, clip, __aegi_keyframes, generate=a.GenKeyframesMode.ASK)\n\n# Check if the file has an audio track. This requires the `bas` plugin.\n__aegi_hasaudio = 1 if a.check_audio(filename) else 0" } } }, diff --git a/src/preferences.cpp b/src/preferences.cpp index b6bcd15db..74fbc721c 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -501,22 +501,36 @@ void VapourSynth(wxTreebook *book, Preferences *parent) { auto video = p->PageSizer(_("Default Video Script")); + auto make_default_button = [=](std::string optname, wxTextCtrl *ctrl) { + auto showdefault = new wxButton(p, -1, _("Set to Default")); + showdefault->Bind(wxEVT_BUTTON, [=](auto e) { + ctrl->SetValue(OPT_GET(optname)->GetDefaultString()); + }); + return showdefault; + }; + auto vhint = new wxStaticText(p, wxID_ANY, _("This script will be executed to load video files that aren't\nVapourSynth scripts (i.e. end in .py or .vpy).\nThe filename variable stores the path to the file.")); p->sizer->Fit(p); vhint->Wrap(400); video->Add(vhint, 0, wxALL, 5); - video->AddSpacer(16); + p->CellSkip(video); - p->OptionAddMultiline(video, "Provider/Video/VapourSynth/Default Script"); + auto vdef = p->OptionAddMultiline(video, "Provider/Video/VapourSynth/Default Script"); + p->CellSkip(video); + + video->Add(make_default_button("Provider/Video/VapourSynth/Default Script", vdef), wxSizerFlags().Right()); auto audio = p->PageSizer(_("Default Audio Script")); auto ahint = new wxStaticText(p, wxID_ANY, _("This script will be executed to load audio files that aren't\nVapourSynth scripts (i.e. end in .py or .vpy).\nThe filename variable stores the path to the file.")); p->sizer->Fit(p); ahint->Wrap(400); audio->Add(ahint, 0, wxALL, 5); - audio->AddSpacer(16); + p->CellSkip(audio); - p->OptionAddMultiline(audio, "Provider/Audio/VapourSynth/Default Script"); + auto adef = p->OptionAddMultiline(audio, "Provider/Audio/VapourSynth/Default Script"); + p->CellSkip(audio); + + audio->Add(make_default_button("Provider/Audio/VapourSynth/Default Script", adef), wxSizerFlags().Right()); p->SetSizerAndFit(p->sizer); #endif diff --git a/src/preferences_base.cpp b/src/preferences_base.cpp index b0c345c2f..5b052ebb6 100644 --- a/src/preferences_base.cpp +++ b/src/preferences_base.cpp @@ -156,7 +156,7 @@ wxControl *OptionPage::OptionAdd(wxFlexGridSizer *flex, const wxString &name, co } } -wxControl *OptionPage::OptionAddMultiline(wxSizer *sizer, const char *opt_name) { +wxTextCtrl *OptionPage::OptionAddMultiline(wxSizer *sizer, const char *opt_name) { parent->AddChangeableOption(opt_name); const auto opt = OPT_GET(opt_name); diff --git a/src/preferences_base.h b/src/preferences_base.h index 6581f2d0d..3bf098f64 100644 --- a/src/preferences_base.h +++ b/src/preferences_base.h @@ -43,7 +43,7 @@ public: void CellSkip(wxFlexGridSizer *flex); wxControl *OptionAdd(wxFlexGridSizer *flex, const wxString &name, const char *opt_name, double min=0, double max=INT_MAX, double inc=1); - wxControl *OptionAddMultiline(wxSizer *flex, const char *opt_name); + wxTextCtrl *OptionAddMultiline(wxSizer *flex, const char *opt_name); void OptionChoice(wxFlexGridSizer *flex, const wxString &name, const wxArrayString &choices, const char *opt_name); void OptionBrowse(wxFlexGridSizer *flex, const wxString &name, const char *opt_name, wxControl *enabler = nullptr, bool do_enable = false); void OptionFont(wxSizer *sizer, std::string opt_prefix);