From b39451823ee9fb58cb4b49afaab9fad1691cb264 Mon Sep 17 00:00:00 2001 From: Niels Martin Hansen Date: Thu, 28 Dec 2006 21:18:35 +0000 Subject: [PATCH] Continuing auto3 removal/auto4 merge Originally committed to SVN as r644. --- automation/{ => auto3}/automation-lua.txt | 0 automation/{ => auto3}/demos/1-minimal.lua | 0 automation/{ => auto3}/demos/10-furigana.ass | 0 automation/{ => auto3}/demos/10-furigana.lua | 0 automation/{ => auto3}/demos/2-dump.lua | 0 automation/{ => auto3}/demos/3-include.lua | 0 .../{ => auto3}/demos/4-text_extents.lua | 0 .../{ => auto3}/demos/5-configuration.lua | 0 .../{ => auto3}/demos/6-simple-effect.lua | 0 .../{ => auto3}/demos/7-advanced-effect.lua | 0 automation/{ => auto3}/demos/8-skeleton.lua | 0 .../{ => auto3}/demos/9-advanced-skeleton.lua | 0 automation/{ => auto3}/demos/readme.txt | 0 .../factorybrew/line-per-syllable.lua | 0 .../factorybrew/multi-template.lua | 0 automation/{ => auto3}/factorybrew/readme.txt | 0 .../factorybrew/simple-k-replacer.lua | 0 .../{ => auto3}/include/karaskel-adv.lua | 0 .../{ => auto3}/include/karaskel-base.lua | 0 automation/{ => auto3}/include/karaskel.lua | 0 automation/{ => auto3}/include/readme.txt | 0 automation/{ => auto3}/include/utils.lua | 0 automation/autoload/test1.lua | 15 + automation/autoload/test2.lua | 50 + automation/autoload/test3.lua | 33 + automation/autoload/test4.lua | 40 + automation/autoload/test5.lua | 40 + automation/autoload/test6.lua | 82 ++ automation/autoload/test7.lua | 92 ++ .../v4-docs/basic-function-interface.txt | 351 ------- automation/v4-docs/configuration-dialogs.txt | 180 ---- automation/v4-docs/file-streams.txt | 216 ---- automation/v4-docs/misc.txt | 32 - automation/v4-docs/overview.txt | 176 ---- automation/v4-docs/progress-reporting.txt | 102 -- automation/v4-docs/subtitle-data.txt | 398 -------- core/auto4-status.txt | 77 ++ core/auto4_base.cpp | 704 +++++++++++++ core/auto4_base.h | 363 +++++++ core/auto4_lua.cpp | 778 ++++++++++++++ core/auto4_lua.h | 244 +++++ core/auto4_lua_assfile.cpp | 954 ++++++++++++++++++ core/auto4_lua_dialog.cpp | 592 +++++++++++ core/changelog.txt | 4 + 44 files changed, 4068 insertions(+), 1455 deletions(-) rename automation/{ => auto3}/automation-lua.txt (100%) rename automation/{ => auto3}/demos/1-minimal.lua (100%) rename automation/{ => auto3}/demos/10-furigana.ass (100%) rename automation/{ => auto3}/demos/10-furigana.lua (100%) rename automation/{ => auto3}/demos/2-dump.lua (100%) rename automation/{ => auto3}/demos/3-include.lua (100%) rename automation/{ => auto3}/demos/4-text_extents.lua (100%) rename automation/{ => auto3}/demos/5-configuration.lua (100%) rename automation/{ => auto3}/demos/6-simple-effect.lua (100%) rename automation/{ => auto3}/demos/7-advanced-effect.lua (100%) rename automation/{ => auto3}/demos/8-skeleton.lua (100%) rename automation/{ => auto3}/demos/9-advanced-skeleton.lua (100%) rename automation/{ => auto3}/demos/readme.txt (100%) rename automation/{ => auto3}/factorybrew/line-per-syllable.lua (100%) rename automation/{ => auto3}/factorybrew/multi-template.lua (100%) rename automation/{ => auto3}/factorybrew/readme.txt (100%) rename automation/{ => auto3}/factorybrew/simple-k-replacer.lua (100%) rename automation/{ => auto3}/include/karaskel-adv.lua (100%) rename automation/{ => auto3}/include/karaskel-base.lua (100%) rename automation/{ => auto3}/include/karaskel.lua (100%) rename automation/{ => auto3}/include/readme.txt (100%) rename automation/{ => auto3}/include/utils.lua (100%) create mode 100644 automation/autoload/test1.lua create mode 100644 automation/autoload/test2.lua create mode 100644 automation/autoload/test3.lua create mode 100644 automation/autoload/test4.lua create mode 100644 automation/autoload/test5.lua create mode 100644 automation/autoload/test6.lua create mode 100644 automation/autoload/test7.lua delete mode 100644 automation/v4-docs/basic-function-interface.txt delete mode 100644 automation/v4-docs/configuration-dialogs.txt delete mode 100644 automation/v4-docs/file-streams.txt delete mode 100644 automation/v4-docs/misc.txt delete mode 100644 automation/v4-docs/overview.txt delete mode 100644 automation/v4-docs/progress-reporting.txt delete mode 100644 automation/v4-docs/subtitle-data.txt create mode 100644 core/auto4-status.txt create mode 100644 core/auto4_base.cpp create mode 100644 core/auto4_base.h create mode 100644 core/auto4_lua.cpp create mode 100644 core/auto4_lua.h create mode 100644 core/auto4_lua_assfile.cpp create mode 100644 core/auto4_lua_dialog.cpp diff --git a/automation/automation-lua.txt b/automation/auto3/automation-lua.txt similarity index 100% rename from automation/automation-lua.txt rename to automation/auto3/automation-lua.txt diff --git a/automation/demos/1-minimal.lua b/automation/auto3/demos/1-minimal.lua similarity index 100% rename from automation/demos/1-minimal.lua rename to automation/auto3/demos/1-minimal.lua diff --git a/automation/demos/10-furigana.ass b/automation/auto3/demos/10-furigana.ass similarity index 100% rename from automation/demos/10-furigana.ass rename to automation/auto3/demos/10-furigana.ass diff --git a/automation/demos/10-furigana.lua b/automation/auto3/demos/10-furigana.lua similarity index 100% rename from automation/demos/10-furigana.lua rename to automation/auto3/demos/10-furigana.lua diff --git a/automation/demos/2-dump.lua b/automation/auto3/demos/2-dump.lua similarity index 100% rename from automation/demos/2-dump.lua rename to automation/auto3/demos/2-dump.lua diff --git a/automation/demos/3-include.lua b/automation/auto3/demos/3-include.lua similarity index 100% rename from automation/demos/3-include.lua rename to automation/auto3/demos/3-include.lua diff --git a/automation/demos/4-text_extents.lua b/automation/auto3/demos/4-text_extents.lua similarity index 100% rename from automation/demos/4-text_extents.lua rename to automation/auto3/demos/4-text_extents.lua diff --git a/automation/demos/5-configuration.lua b/automation/auto3/demos/5-configuration.lua similarity index 100% rename from automation/demos/5-configuration.lua rename to automation/auto3/demos/5-configuration.lua diff --git a/automation/demos/6-simple-effect.lua b/automation/auto3/demos/6-simple-effect.lua similarity index 100% rename from automation/demos/6-simple-effect.lua rename to automation/auto3/demos/6-simple-effect.lua diff --git a/automation/demos/7-advanced-effect.lua b/automation/auto3/demos/7-advanced-effect.lua similarity index 100% rename from automation/demos/7-advanced-effect.lua rename to automation/auto3/demos/7-advanced-effect.lua diff --git a/automation/demos/8-skeleton.lua b/automation/auto3/demos/8-skeleton.lua similarity index 100% rename from automation/demos/8-skeleton.lua rename to automation/auto3/demos/8-skeleton.lua diff --git a/automation/demos/9-advanced-skeleton.lua b/automation/auto3/demos/9-advanced-skeleton.lua similarity index 100% rename from automation/demos/9-advanced-skeleton.lua rename to automation/auto3/demos/9-advanced-skeleton.lua diff --git a/automation/demos/readme.txt b/automation/auto3/demos/readme.txt similarity index 100% rename from automation/demos/readme.txt rename to automation/auto3/demos/readme.txt diff --git a/automation/factorybrew/line-per-syllable.lua b/automation/auto3/factorybrew/line-per-syllable.lua similarity index 100% rename from automation/factorybrew/line-per-syllable.lua rename to automation/auto3/factorybrew/line-per-syllable.lua diff --git a/automation/factorybrew/multi-template.lua b/automation/auto3/factorybrew/multi-template.lua similarity index 100% rename from automation/factorybrew/multi-template.lua rename to automation/auto3/factorybrew/multi-template.lua diff --git a/automation/factorybrew/readme.txt b/automation/auto3/factorybrew/readme.txt similarity index 100% rename from automation/factorybrew/readme.txt rename to automation/auto3/factorybrew/readme.txt diff --git a/automation/factorybrew/simple-k-replacer.lua b/automation/auto3/factorybrew/simple-k-replacer.lua similarity index 100% rename from automation/factorybrew/simple-k-replacer.lua rename to automation/auto3/factorybrew/simple-k-replacer.lua diff --git a/automation/include/karaskel-adv.lua b/automation/auto3/include/karaskel-adv.lua similarity index 100% rename from automation/include/karaskel-adv.lua rename to automation/auto3/include/karaskel-adv.lua diff --git a/automation/include/karaskel-base.lua b/automation/auto3/include/karaskel-base.lua similarity index 100% rename from automation/include/karaskel-base.lua rename to automation/auto3/include/karaskel-base.lua diff --git a/automation/include/karaskel.lua b/automation/auto3/include/karaskel.lua similarity index 100% rename from automation/include/karaskel.lua rename to automation/auto3/include/karaskel.lua diff --git a/automation/include/readme.txt b/automation/auto3/include/readme.txt similarity index 100% rename from automation/include/readme.txt rename to automation/auto3/include/readme.txt diff --git a/automation/include/utils.lua b/automation/auto3/include/utils.lua similarity index 100% rename from automation/include/utils.lua rename to automation/auto3/include/utils.lua diff --git a/automation/autoload/test1.lua b/automation/autoload/test1.lua new file mode 100644 index 000000000..ba50becaf --- /dev/null +++ b/automation/autoload/test1.lua @@ -0,0 +1,15 @@ +-- Automation 4 test file +-- Create a Macro feature, that displays some text + +script_name = "Automation 4 test 1" +script_description = "Hello World in Automation 4/Lua" +script_author = "Niels Martin Hansen" +script_version = "1" + + +function macro_test1(subtitles, selected_lines, active_line) + --aegisub.debug.out(3, "Hello World from %s", "Automation 4/Lua") + aegisub.debug.out("Hello Automation 4 World!") +end + +aegisub.register_macro("Hello", "Shows a message", "tools", macro_test1) diff --git a/automation/autoload/test2.lua b/automation/autoload/test2.lua new file mode 100644 index 000000000..87ae52026 --- /dev/null +++ b/automation/autoload/test2.lua @@ -0,0 +1,50 @@ +-- Automation 4 test file +-- Create a Macro feature, that displays some text + +script_name = "Automation 4 test 2" +script_description = "Some additional non-hello-world tests" +script_author = "Niels Martin Hansen" +script_version = "2" + + +function macro_test2(subtitles, selected_lines, active_line) + aegisub.debug.out(subtitles.n .. " and " .. #subtitles .. " should be the same value") + aegisub.debug.out(subtitles[selected_lines[1]].raw) +end + +function dumper(subtitles, selected_lines, active_line) + for i = 1, #subtitles do + local l = subtitles[i] + local s = l.raw .. "\n" + s = s .. l.class .. "\n" + if l.class == "comment" then + s = s .. "text: " .. l.text .. "\n" + elseif l.class == "info" then + s = s .. string.format("key: %s\nvalue:%s\n", l.key, l.value) + elseif l.class == "format" then + -- skip + elseif l.class == "style" then + s = s .. string.format("name: %s\nfont: %s %d\ncolors: %s %s %s %s\n", l.name, l.fontname, l.fontsize, l.color1, l.color2, l.color3, l.color4) + elseif l.class == "dialogue" then + s = s .. string.format("layer: %d\nstyle: %s\ntext: %s\n", l.layer, l.style, l.text) + end + aegisub.debug.out(s) + end +end + +function inserttest(subtitles, selected_lines, active_line) + local lid = selected_lines[1] + subtitles[-lid] = subtitles[lid] + subtitles[0] = subtitles[lid] + local l = subtitles[lid] + l.text = "A4 was here!" + subtitles[lid] = l + aegisub.set_undo_point("Insert Stuff") +end + + +aegisub.register_macro("File line count", "Count the number of lines in the ASS file", "tools", macro_test2, nil) + +aegisub.register_macro("Dump", "Dumps info on every line in the file", "tools", dumper, nil) + +aegisub.register_macro("Insert stuff", "Inserts some lines near the active line", "edit", inserttest, nil) diff --git a/automation/autoload/test3.lua b/automation/autoload/test3.lua new file mode 100644 index 000000000..af3bcca34 --- /dev/null +++ b/automation/autoload/test3.lua @@ -0,0 +1,33 @@ +-- Automation 4 test file +-- Create a Macro feature, that displays some text + +script_name = "Automation 4 test 3" +script_description = "Test parse_karaoke_data" +script_author = "Niels Martin Hansen" +script_version = "1" + + +function copy_table(tab) + local res = {} + for key, val in pairs(tab) do + res[key] = val + end + return res +end + +function karatest(subtitles, selected_lines, active_line) + for i = #selected_lines, 1, -1 do + local ri = selected_lines[i] + local l = subtitles[ri] + local k = aegisub.parse_karaoke_data(l) + for j = 1, #k do + local nl = copy_table(l) + l.text = string.format("{\\t(%d,%d,\\fscx50)}%s", k[j].start_time, k[j].end_time, k[j].text_stripped) + subtitles.insert(ri+j, l) + end + end + aegisub.set_undo_point("karaoke-like stuff") +end + + +aegisub.register_macro("Karaoke fun", "Makes karaoke-like stuff", "edit", karatest, nil) diff --git a/automation/autoload/test4.lua b/automation/autoload/test4.lua new file mode 100644 index 000000000..e55f678d9 --- /dev/null +++ b/automation/autoload/test4.lua @@ -0,0 +1,40 @@ +-- Automation 4 test file +-- Create a Macro feature, that displays some text + +script_name = "Automation 4 test 4" +script_description = "Test progress reporting" +script_author = "Niels Martin Hansen" +script_version = "1" + + +function wait() + local s = "" + for i = 0, 500 do + s = s .. i + end + return s +end + +function progression(subtitles, selected_lines, active_line) + while not aegisub.progress.is_cancelled() do + aegisub.progress.task("Counting up...") + for i = 0, 100, 0.2 do + aegisub.progress.set(i) + if aegisub.progress.is_cancelled() then + break + end + wait() + end + if aegisub.progress.is_cancelled() then + break + end + aegisub.progress.task("Counting down...") + for i = 0, 100 do + aegisub.progress.set(100-i) + wait() + end + end +end + + +aegisub.register_macro("Progress fun", "Does absolutely nothing", "video", progression, nil) diff --git a/automation/autoload/test5.lua b/automation/autoload/test5.lua new file mode 100644 index 000000000..fb29b53e8 --- /dev/null +++ b/automation/autoload/test5.lua @@ -0,0 +1,40 @@ +-- Automation 4 test file +-- Create a Macro feature, that displays some text + +script_name = "Automation 4 test 5" +script_description = "Test include and text_extents" +script_author = "Niels Martin Hansen" +script_version = "1" + +include("utils.lua") + + +function test5(subtitles, selected_lines, active_line) + local styles = {} + for i = 1, #subtitles do + local l = subtitles[i] + if l.class == "dialogue" then + break + end + if l.class == "style" then + styles[l.name] = l + end + end + + for i = #selected_lines, 1, -1 do + local ri = selected_lines[i] + local l = subtitles[ri] + local k = aegisub.parse_karaoke_data(l) + local left = 0 + for j = 1, #k do + local nl = table.copy(l) + l.text = string.format("{\\t(%d,%d,\\fscx50)\\pos(%d,20)}%s", k[j].start_time, k[j].end_time, left, k[j].text_stripped) + left = left + (aegisub.text_extents(styles[l.style], k[j].text_stripped)) + subtitles.insert(ri+j, l) + end + end + aegisub.set_undo_point("karaoke-like stuff") +end + + +aegisub.register_macro("More karaoke fun", "Makes some more karaoke-like stuff", "tools", test5, nil) diff --git a/automation/autoload/test6.lua b/automation/autoload/test6.lua new file mode 100644 index 000000000..997d68a9a --- /dev/null +++ b/automation/autoload/test6.lua @@ -0,0 +1,82 @@ +-- Automation 4 test file +-- Create a Filter feature that does some kara stuff + +script_name = "Automation 4 test 6" +script_description = "Test basic export filters" +script_author = "Niels Martin Hansen" +script_version = "1" + +include("utils.lua") + + +function test6_2(subtitles, config) + --[[for i = 1, #subtitles do + local l = subtitles[i] + if l.class == "dialogue" then + local nl = table.copy(l) + nl.text = "Copied!" + subtitles.insert(i, nl) + break + end + end]] +end + + +function test6(subtitles, config) + aegisub.progress.task("Collecting style data") + local styles = {} + for i = 1, #subtitles do + aegisub.debug.out("finding styles, line " .. i) + local l = subtitles[i] + if l.class == "dialogue" then + break + end + if l.class == "style" then + aegisub.debug.out(" found style: " .. l.name) + styles[l.name] = l + end + aegisub.progress.set(100 * i / #subtitles) + end + + local res = {} + + local i = 1 + while i <= #subtitles do + aegisub.debug.out("producing effect, line " .. i) + local l = subtitles[i] + if l.class == "dialogue" then + aegisub.debug.out(" found dialogue: " .. l.text) + local res = {} + do_line(styles, l, config, res) + aegisub.debug.out(" lines returned by do_line: " .. #res) + for j,nl in ipairs(res) do + subtitles.insert(i+j, nl) + end + aegisub.debug.out(" done inserting generated lines") + subtitles.delete(i) + i = i + #res + else + aegisub.debug.out(" not dialogue") + i = i + 1 + end + aegisub.progress.task(string.format("Producing effect (%d/%d)", i, #subtitles)) + aegisub.progress.set(100 * i / #subtitles) + end +end + +function do_line(styles, line, config, res) + local k = aegisub.parse_karaoke_data(line) + aegisub.debug.out(" syllables generated from line: " .. #k) + local left = 0 + for j = 1, #k do + aegisub.debug.out(" syllable " .. j .. " is: " .. k[j].text) + local nl = table.copy(line) + nl.text = string.format("{\\t(%d,%d,\\fscx50)\\pos(%d,20)}%s", k[j].start_time, k[j].end_time, left, k[j].text_stripped) + left = left + (aegisub.text_extents(styles[nl.style], k[j].text_stripped)) + table.insert(res, nl) + end +end + + +aegisub.register_filter("Stupid karaoke", "Makes some more karaoke-like stuff", 2000, test6, nil) +aegisub.register_filter("Lame test", "Simple test of filters, just inserting a new line", 2000, test6_2) diff --git a/automation/autoload/test7.lua b/automation/autoload/test7.lua new file mode 100644 index 000000000..5fa4e2448 --- /dev/null +++ b/automation/autoload/test7.lua @@ -0,0 +1,92 @@ +-- Automation 4 test file +-- Create a Filter feature that does some kara stuff + +script_name = "Automation 4 test 7" +script_description = "Test config dialogs" +script_author = "Niels Martin Hansen" +script_version = "1" + +include("utils.lua") + +function test7(subtitles, selected_lines, active_line) + local a, b = aegisub.dialog.display({{class="label", label="Test..."}}, {}) + report_dialog_result(a, b) + aegisub.progress.set(50) + a, b = aegisub.dialog.display({{class="edit", name="foo", text=""}}, {"foo", "bar"}) + report_dialog_result(a, b) +end + +function report_dialog_result(button, controls) + aegisub.debug.out("Dialog closed: ") + if button == false then + aegisub.debug.out("cancelled\n") + elseif button == true then + aegisub.debug.out("clicked Ok\n") + else + aegisub.debug.out("clicked '" .. button .. "'\n") + end + for key, val in pairs(controls) do + aegisub.debug.out(key .. ': ' .. val .. '\n') + end + aegisub.debug.out(" - - - - -\n") +end + + +function exporter(subs, config) + for i = 1, #subs do + local l = subs[i] + if l.class == "dialogue" and not l.comment then + if config.style == "" or l.style == config.style then + l.text = config.text .. l.text + subs[i] = l + end + end + end +end + +function export_config_dialog(subs, store) + local styles = {""} + for i = 1, #subs do + local l = subs[i] + if l.class == "style" then + table.insert(styles, l.name) + end + end + + return { + { + class = "label", + label = "This will insert a given text in\n" .. + "front of all dialogue lines of\n" .. + "the given style, or every line\n" .. + "if no specific style is selected.", + x = 0, y = 0, width = 2, height = 1 + }, + { + class = "label", + label = "Text to insert:", + x = 0, y = 1, width = 1, height = 1 + }, + { + class = "edit", + name = "text", + x = 1, y = 1, width = 1, height = 1 + }, + { + class = "label", + label = "Style to apply on:", + x = 0, y = 2, width = 1, height = 1 + }, + { + class = "dropdown", + name = "style", + value = "", + items = styles, + x = 1, y = 2, width = 1, height = 1 + } + } +end + + +aegisub.register_macro("Config Dialog 1", "Show a stupid config dialog", "video", test7, nil) +aegisub.register_filter("Export Config", "Test export filter config dialog stuff", 500, exporter, export_config_dialog) diff --git a/automation/v4-docs/basic-function-interface.txt b/automation/v4-docs/basic-function-interface.txt deleted file mode 100644 index 0d11b8acd..000000000 --- a/automation/v4-docs/basic-function-interface.txt +++ /dev/null @@ -1,351 +0,0 @@ -Automation 4 Basic Interface - -This document described the basic functions needed to define an Automation 4 -script. This covers Feature registration and the prototypes of functions used -to implement the various features. - ---- - -Macro Registation Function - -This is a function called from top-level of an Automation script to register -a new Macro Feature. - -function aegisub.register_macro( - name, - description, - menu, - processing_function, - validation_function) - -@name (string) - The displayed name of the menu item this macro will generate. - -@description (string) - A longer description of the function of this macro. This will appear - on the status bar when hovering over the menu item. - -@menu (string) - The menu this macro will appear in. Must be one of: - o "edit" - o "video" - o "audio" - o "tools" - o "right" (the subtitles grid right-click menu) - The menu chosen should be relevant to the function of the macro. - -@processing_function (function) - The actual function called for the macro execution. - This function must be an instance of the Macro Processing Function - described below. - -@validation_function (function) - Optional. A function called when it is to be determined whether the - macro can act on the current subtitles. - This function, if provided, must execute very quickly to avoid lag - in the GUI. - This function must be an instance of the Macro Validation Function - described below. - -Returns: nothing. - ---- - -Filter Registration Function - -This is a function called from top level of an Automation script to register -a new Export Filter Feature. - -function aegisub.register_filter( - name, - description, - priority, - processing_function, - options_window_provider) - -@name (string) - The name of the filter, as presented to the user. - -@description (string) - A longer description of the filter presented to the user. - -@priority (number) - A number determining the default order the enabled filters will be - processed. The order can be overridden by the user. - Priorities of some built-in filters: - o Clean Script Info = 0 - o Fix Styles = -5000 - o Transform Framerate = 1000 - Filters with higher priority will be executed earlier by default. - -@processing_function (function) - The function called to do the actual filter processing. - This function must be an instance of the Filter Processing Function - described below. - -@options_window_provider (function) - Optional. A function providing a dialog template for setting options - prior to filter processing. - This function must be an instance of the Filter Options Window Provider - function described below. - -Returns: nothing. - ---- - -Format Reader Registration - -This is a function called from top level in an Automation script to register -a new File Format Reader Feature. - -function aegisub.register_reader( - name, - extension, - is_text_format, - processing_function) - -@name (string) - The name of the file format. - -@extension (string) - The file extension usually given to this file format. This must not - include any wildcards. (Ie. extension could be "srt", "sub", "ssa" and - so on.) - -@is_text_format (boolean) - Determines whether the user can select a default encoding for reading text - from the file. This only affects whether a default encoding is chosen or not, - even with this argument set to false, you can still read text. You should set - this to true, if the format allows storing in different encodings, and doesn't - explicitly store the used encoding anywhere. - -@processing_function (function) - The function called to do the actual file import. - This function must be an instance of the Format Reader Function described - below. - -Returns: nothing. - ---- - -Format Writer Registration - -This is a function called from top level in an Automation script to register -a new File Format Writer Feature. - -function aegisub.register_writer( - name, - extension, - is_text_format, - processing_function) - -@name (string) - Name of the file format, as presented to the user. - -@extension (string) - The usual file extension given to this file format. This is automatically - be attached to the file name on export, unless the user chooses to - override it. - -@is_text_format (boolean) - Determines whether the user can select a target encoding or not. This - option should be true if the format written allows choosing between - different encodings, and the user should be able to select which one to - use. This option doesn't affect whether you can write text to the file or - not, but only whether the user can select an encoding or not. - -@processing_function (function) - The function doing the actual file export. - This function must be an instance of the Format Writer Function described - below. - -Returns: nothing. - ---- - -Macro Processing Function - -This function is called by Aegisub to execute a macro. - -function process_macro( - subtitles, - selected_lines, - active_line) - -The name of the function is script-defined. (It doesn't have to be -process_macro.) - -@subtitles (user data) - A Subtitles Object, that can be used to retrieve information about the - subtitle file the macro is being applied on. - -@selected_lines (table) - An Array Table of numbers, each entry being an index into the file - represented by @subtitles. Each of the entries in this table describe that - a line is marked as selected by the user. - -@active_line (number) - Index of the currently active line in the subtitle file. - -Returns: nothing. - ---- - -Macro Validation Function - -This function is called by Aegisub to determine whether a macro can be applied -to the current state of the subtitles and selection. - -This function needs to execute very fast, since it may be called for several -macros whenever a menu is opened. It is suggested not to use @subtitles at all -in this function. - -This function does not have to be defined. If it's undefined, it's taken as if -it always returned true. - -function validate_macro( - subtitles, - selected_lines, - active_line) - -The name of the function is script-defined. (It doesn't have to be -validate_macro.) - -@subtitles (user data) - A Subtitles Object, that can be used to retrieve information about the - subtitle file the macro is to be be applied on. - -@selected_lines (table) - An Array Table of numbers, each entry being an index into the file - represented by @subtitles. Each of the entries in this table describe that - a line is marked as selected by the user. - -@active_line (number) - Index of the currently active line in the subtitle file. - -Returns: Boolean. - true is the macro can be applied to the current state of the subtitles, - false if not. - ---- - -Filter Processing Function - -This function is called by Aegisub to filter the subtitles during an export -operation. - -function process_filter( - subtitles, - config) - -The name of the function is script-defined. (It doesn't have to be -process_filter.) - -@subtitles (user data) - A Subtitles Object, that can be used to retrieve information about the - subtitle file the filter is being applied on. - -@config (table) - A Dialog Result table representing the options the user selected for the - filter before starting the export operation. The fields present in this - table are defined by the dialog provided by the Filter Options Window - Provider function. - -Returns: nothing. - ---- - -Filter Options Window Provider function - -This function is called by Aegisub to get a Dialog Window definition to prompt -the user for input before an export operation. -The data input into the dialog returned by this function are automatically -stored into the original subtitle file when an export operation is started. - -function filter_options_dialog( - subtitles, - stored_options) - -The name of the function is script-defined. (It doesn't have to be -filter_options_dialog.) - -@subtitles (user data) - A Subtitles Object, that can be used to retrieve information about the - subtitle file the filter is to be applied on. - -@stored_options (table) - The currently stored options for this export filter. The keys in this table - are the option names, and the values are the values stored for those options. - -Returns: A Dialog Window table. - ---- - -Format Reader Function - -This function is called by Aegisub to import a file from a foreign file -format. - -function read_format( - input_file, - output_subs) - -The name of the function is script-defined. (It doesn't have to be -read_format.) - -@input_file (user data) - An Input File Stream, representing the file selected for import. - -@output_subs (user data) - An empty Subtitles Object the imported data should be added to. - -Returns: Boolean. - True if the import succeeded, false if it failed. - ---- - -Format Writer Function - -This function is called by Aegisub to export a file to a foreign file format. - -function write_format( - input_subs, - output_file) - -The name of the function is script-defined. (It doesn't have to be -write_format.) - -@input_subs (user data) - A Subtitles Object representing the subtitles to be exported. - -@output_file (user data) - An Ouput File Stream, representing the file the exported data should be - written to. - -Returns: Boolean. - True if the export succeeded, false if it failed. - If this function returns false, the output file is deleted from disk. - ---- - -Script information globals - -These are a series of global variables, the script author can set. They are -purely informational, and won't have any actual influence on how the script -is treated. - -script_name (string) - A short, descriptive name for the script, used for presenting it to the - user in the UI. - -script_description (string) - A longer description of the purpose of the script, presented to the user - in the UI. - -script_author (string) - Name(s) of the author(s) of the script. - -script_version (string) - Version number of the script. - ---- diff --git a/automation/v4-docs/configuration-dialogs.txt b/automation/v4-docs/configuration-dialogs.txt deleted file mode 100644 index 4cd7e9b10..000000000 --- a/automation/v4-docs/configuration-dialogs.txt +++ /dev/null @@ -1,180 +0,0 @@ -Automation 4 Configuration Dialog interface - -This file describes the functions and data structures used for the -Configuration Dialog functionality in Automation 4. - ---- - -Dialog Control table format - -A Dialog Control table describes a single control in a configuration dialog, -which can display information to the user and allow them to change it. - -There are a number of different classes of controls, and the keys a Dialog -Control table must contain depends on the control class. - - -Common keys for all control classes: - -class (string) - Defines which class this control has. Must be one of: - "label", - "edit", "intedit", "floatedit", "textbox", - "dropdown", - "checkbox", - "color", "coloralpha", "alpha" - -name (string) - A name that uniquely identifies the control. This is recommended to be a - string easily used as an identifier in Lua, since it will be used to access - the value input into the control. - -x (number) -y (number) -width (number) -height (number) - Determines the position and size of the control in the dialog. These values - are used to create a grid containing the controls. They should all be - integer. The top left corner is x,y=0,0. - If any of width and height are set to zero or less, it will be set to one - instead. - - -Key defined for all classes except "label": - -hint (string) - A string displayed to the - - -Keys defined only for "label" and "checkbox" classes: - -label (string) - The text displayed to the user on the control. - - -Key defined only for the "edit" and "textbox" classes: - -text (string) - The contents of the control when the dialog is first displayed. - This can contain newlines if the control is of the "textbox" class. - - -Keys defined only for the "intedit" and "floatedit" classes: - -value (number) - The value in the control when the dialog is first displayed. For the - "intedit" class, if this is a non-integer point number it is truncated - before being used. - -min (number or nil) -max (number or nil) -step (number or nil) - If one of these are nil, the other must also be nil. (Ie. undefined.) - If all are present, the control gets a spin button, the user can click to - update the value of the control. The value is changed by "step" amount - every time, up to "max" or down to "min". The user won't be able to close - the dialog if the value is outside the range between "min" and "max" either. - - -Keys defined only for the "dropdown" class: - -items (table) - This is an Array Table containing only strings. They are used for the - options displayed to the user in the dropdown box. - All strings in the array table should be unique. (There is not way to - distinguish non-unique strings from each other.) - -value (string) - Determines which item is selected when the dialog id first displayed. If - this is not one of the items specified, no item is selected. This is case- - sensitive. - - -Key defined only for the "checkbox" class: - -value (boolean) - Determines whether the checkbox is checked or not when the dialog is first - displayed. - - -Keys defined only for the "color", "coloralpha" and "alpha" classes: - -value (string) - A color value in VB or HTML hexadecimal format. - For the "color" class, this should be a 3 byte value, ie. "#RRGGBB". - For the "coloralpha" class, this should be a 4 byte value, ie. "#RRGGBBAA". - For the "alpha" class, this should be a one-byte value, ie. "#AA". - ---- - -Dialog Definition table format - -The Dialog Definition table is simply an Array Table of Dialog Control tables. -Note, however, that while the visual ordering of the controls are decided -entirely by the "x", "y", "width" and "height" of the controls, the -"tab order" of the controls are decided by their ordering in the Dialog -Definition table. - ---- - -Dialog Result table format - -A Dialog Result table contains the user input from a configuration dialog. - -The control "name" properties are used as keys in this table. - -The type of the value for each entry in the table depends on the class of the -control. The control classes map to types in the following manner: - -"label" - None. Since the user cannot change a label, they do not produce any value. - -"edit", "textbox" - String. The text input in the box. This can contain newlines in the case of - a "textbox" class control. - -"intedit", "floatedit" - Number. The number input into the control, guaranteed to be within the - constraints set by the class (integer or float) and the min/max properties. - -"dropdown" - String. The case-exact text of the selected item. - -"checkbox", - Boolean. The checked-state of the checkbox. - -"color", "coloralpha", "alpha" - String. A VB colorstring following the same scheme as for setting the - "value" property. - ---- - -Display Configuration Dialog function - -This function displays a configuration dialog to the user and waits for it to -close. It then returns whether the user accepted or cancelled the dialog, and -what values were input. - -function aegisub.dialog.display(dialog, buttons) - -@dialog (table) - A Dialog Definition table containing the controls to be in the dialog. - -@buttons (table) - Optional. This is an Array Table of strings defining the buttons that appear - in the dialog. If this is left out or is otherwise not a table, the standard - Ok and Cancel buttons appear. - The strings in this Array Table are used as labels on the buttons, and for - identifying them in the return values of the function. - -Returns: Two values. - 1. Boolean or string. - If no custom buttons were specified, this is a boolean telling whether Ok - (true) or Cancel (false) were clicked in the dialog. - If custom buttons were specified, this is the text on the button clicked - by the user. - 2. Table. - The Dialog Result table corresponding to the values the user input in the - dialog. - ---- diff --git a/automation/v4-docs/file-streams.txt b/automation/v4-docs/file-streams.txt deleted file mode 100644 index 1fe259061..000000000 --- a/automation/v4-docs/file-streams.txt +++ /dev/null @@ -1,216 +0,0 @@ -Automation 4 File Stream interface - -This file describes the interface used for reading and writing files in -Automation 4. This includes text encoding conversion routines. - ---- - -About encodings - -All file streams always have a text encoding. By default, this is 'utf-8', -unless the file format reader/writer was registered as a text format. In -that case, the default encoding will be the one set by the user before the -reader or writer is invoked. - -All string operations on a stream follow the current encoding. You can -change the encoding during reading/writing, and the change will only take -effect from that point on. - -You can perform binary IO by setting the encoding to 'binary' and using -strings consisting only of codepoints 0 to 255. - ---- - -Output File Stream user data object - -This object is passed to functions operating on an Output File Stream. - ---- - -Input File Stream user data object - -This object is passed to functions operating on an Input File Stream. - ---- - -Getting text encoding - -This function returns a string describing the current text encoding used for -a file stream. - -function aegisub.fstream.get_encoding(stream) - -@stream (user data) - The Input File Stream or Output File Stream to get the encoding for. - -Returns: String describing the encoding. This string can be used for setting - the encoding later. - ---- - -Setting text encoding - -This function changes the current text encoding used for a file stream. - -function aegisub.fstream.set_encoding(stream, encoding) - -@stream (user data) - The Input File Stream or Output File Stream to change the encoding for. - -@encoding (string) - The new encoding to use. - -Returns: String describing the old encoding. - ---- - -File Pointer operations - -function aegisub.fstream.tell(stream) - -@stream (user data) - The Input File Stream or Output File Stream get position of. - -Returns: Number, the number of bytes since the beginning of the file. - - -function aegisub.fstream.seek(stream, length) - -@stream (user data) - The Input File Stream or Output File Stream to seek in. - -@length (number) - Number of bytes to skip. This can be negative. - You can only seek backwards in an Output File Stream, and doing so truncates - the file. - -Returns: nothing. - - -function aegisub.fstream.reset(stream) - -Resets the file pointer to the start of the file. Truncates an Output File -Stream to zero bytes. - -@stream (user data) - The Input File Stream or Output File Stream to seek in. - -Returns: nothing. - ---- - -Reading text - -All these functions assume the file is in the current encoding specified. - - -function aegisub.fstream.skip_utf_bom(stream, change_encoding) - -This function has undefined behaviour unless called as the first -read-operation on the stream. - -It detects whether the file stream starts with an UTF Byte Order Mark, skips -the number of bytes used by that BOM, and optionally changes the current file -encoding to match the detected BOM. - -@stream (user data) - The Input File Stream to read from. - -@change_encoding (boolean) - If true, change encoding to match the detected BOM. - -Returns: Boolean, whether a BOM was detected or not. - - -function aegisub.fstream.read(stream, length) - -Read a number of characters from a file. - -@stream (user data) - The Input File Stream to read from. - -@length (number) - Number of characters to read. If this is zero, no data are read. If this - is larger than the number of characters available, data are read until the - end of file. - -Returns: String, the string read from the file. - - -function aegisub.fstream.read_bytes(stream, length) - -Read a number of bytes from a file and convert to a string. - -@stream (user data) - The Input File Stream to read from. - -@length (number) - The number of bytes to read. - -Returns: String, best-effort converted from the bytes read. - - -function aegisub.fstream.read_line(stream, include_newline) - -Read until next newline in the file. A newline is defined asone of these -sequences of Unicode codepoints in the decoded text: - 0x0A ("\n") - 0x0D ("\r") - 0x0D 0x0A ("\r\n") -The sequence "\n\r" is interpreted as two newlines, ie. a newline, a blank -line and yet another newline. - -@stream (user data) - The Input File Stream to read from. - -@include_newline (boolean) - If true, include the newline character(s) in the returned string. - -Returns: String. - ---- - -Writing text - -All these functions assume the file is in the current encoding specified. - - -function aegisub.fstream.write_utf_bom(stream) - -This function will corrupt your file if used anywhere else than on position 0. - -Write the correct UTF BOM character to the file, or nothing if not currently -in an UTF encoding. - -@stream (user data) - The Output File Stream to write the BOM to. - -Returns: nothing. - - -function aegisub.fstream.write(stream, text) - -Write a string to a file. - -@stream (user data) - The Output File Stream to write to. - -@text (string) - The text to write. - -Returns: nothing. - - -function aegisub.fstream.write_line(stream, text) - -Write a string to a file, followed by an "\r\n" newline. - -@stream (user data) - The Output File Stream to write to. - -@text (string) - The text to write. - -Returns: nothing. - ---- diff --git a/automation/v4-docs/misc.txt b/automation/v4-docs/misc.txt deleted file mode 100644 index 5f9050f4d..000000000 --- a/automation/v4-docs/misc.txt +++ /dev/null @@ -1,32 +0,0 @@ -Miscellaneous functions in Automation 4 - -This document describes various functions that couldn't be placed in any of -the other Automation 4 documents. - ---- - -Getting the rendered size of a string - -This function might later on be part of a full rendering-interface for -creating actual bitmaps of text. - -This function does NOT attempt to handle line breaks, automatic line breaking, -fomatting override tags, vector drawings or anything else to that effect. -If you need such functionality, you need to implement it yourself. (For now, -at least.) - -function aegisub.text_extents(style, text) - -@style (table) - A "style" class Subtitle Line table. This - -@text (string) - The text to calculate the rendered size of. - -Returns: 4 values, all numbers. - 1. Width of text in pixels. - 2. Height of text in pixels. - 3. Descent of text in pixels. - 4. External leading of text in pixels. - ---- diff --git a/automation/v4-docs/overview.txt b/automation/v4-docs/overview.txt deleted file mode 100644 index 5479af0e9..000000000 --- a/automation/v4-docs/overview.txt +++ /dev/null @@ -1,176 +0,0 @@ -Aegisub Automation documentation -Version 4 -Copyright 2005-2006 Niels Martin Hansen - -THIS IS OUT OF DATE COMPARED TO THE REST OF THE DOCS! - ---- - -This document describes version 4 of the automation system used in Aegisub. -The automation system uses the Lua language for scripting engine. -See for more information. - ---- - -Overview - -Aegisub Automation is a scripting environment that allows you to automate -almost any task working with subtitles in Aegisub, ie. a macro environment. - -Automation allows you to: - - Create macros (adding extra menu items to the main menu) - o Those macros can optionally also display dialog boxes to the user - o Allows adding new features to Aegisub without recompiling the entire - program! - - Write export filters - o This is what Automation 3 did, but with more options - o Useful for adding complicated special effects to a script - - Write file-format importers and exporters - o Load every strange subtitle format you come by - o Save in those formats as well - o Exporters write directly to a file stream, allowing you to generate - those huge karaoke effects much faster! - -Automation runs in a true Unicode environment, meaning strings are internally -represented as UTF-32, so you, as programmer, don't have to worry about text -encodings, prefix encodings etc. to write scripts that can handle text in -mostly any language of the world. - ---- - -Scripts, files functions - -An automation script is a Lua script following certain conventions described -in this document. A script consists of one or more files, with one of them -being the master script, and the others being include files. - -Every script runs in a separate Lua interpreter, so separate scripts cannot -communicate directly with each other. Scripts can share code by having common -include files. Scripts can share data by storing data in the subtitle files, -either in the dialogue lines or in the Script Info headers. - -Files containing Automation scripts must in UTF-8 encoding, with or without -BOM (Byte Order Mark). Compiled Lua scripts should also work, as long as all -strings are UTF-8 encoded, but this is untested and unsupported. - -Automation scripts implement one or more of four possible features. A feature -is implemented by filling a specially named global table with certain values. -See below for a discussion about the various features and how they differ. - ---- - -Scriptable features - -The following four features can be implemented by an Automation script: - - - Macro - A macro is presented as a new menu item in the Automation menu on the menu - bar in Aegisub. When the user select the menu item, a function in the - Automation script is called to do processing. Features are present to allow - direct interaction with the subtitle data. - - The macro can create and display dialog windows to the user. - - A macro can provide a function, that determines whether the macro cen be - run, based on the current selection in the program, and the contents of - the subtitles. - - - Export filter - An export filter is presented as a filter in the Export dialog accessed - from the File menu. The export filter is called when the user uses the - Export feature. The export filter is given access every line (including - Styles and Script Info lines) in the subtitle file, and can add/modify/ - remove lines in those. - - The export filter can provide a function, that returns a configuration - dialog, which is presented to the user before the export is run. This - function can access the subtitle data in order to customise the - configuration dialog, before it's presented to the user. - - - File format reader - It is not yet decided how the file format reader is accessed. - - Current ideas: - o It provides two functions, one to test whether it can handle a given - file and one to actually convert that file to ASS. Which import filter - to use is decided by Aegisub, based on the result of the first function. - o The user selects an import filter and a file. The import filter is - applied to the selected file. - - The file format reader can present dialog windows to the user. - - The file format reader is given access to the raw file stream. - - - File format writer - The file format writer is selected in the Export dialog access from the - File menu. The file format writer is handed all the lines of the subtitles - file and a file stream to write to. - - The file format writer can report itself as writing a binary format or a - text format. In the case of a text format, all output is passed through the - character set conversion routines in Aegisub. - - The file format writer can present dialog windows to the user. - -Every feature is given access to the following in addition to what's described -above: - - - Displaying/hiding/updating a progress bar. - - Outputting messages to the user. - - Accessing framerate data - - (Not fully decided yet) Raw video frame data (RGB and/or YUV) - - (Not fully decided yet) Raw and FFT transformed wave data - - (Not fully decided yet) Utilising FexTracker functions - - Calculating the rendered size of a text string, given a style definition - ---- - -Script registration - -Scripts can be loaded in two ways, through autoload or by assigning them to -a subtitle file. - -Autoloading of scripts happens by placing the master script file into the -"automation/autoload" directory under the Aegisub installation directory. - -Assining scripts to a subtitle file is done through the Automation Manager -GUI. Scripts assigned to a subtitle file are stored in the ASS Script Info -line "Automation Scripts", using a pipe character as separator between the -master script filenames. - -The automatic loading/storing of configuration options from Automation 3 has -been removed, but can still be implemented in an Export Filter feature using -the initialisation function. - ---- - -Actual documentation for functions, data structures and other interfaces is -yet to be written. - ---- - - -Versions of the scripting interface - -Here's a quick history of the scripting interface: - -Version 1 - Using Lua as engine. - The scripts used in the Karaoke Effector application, avaible at: - (currently down) - -Version 2 - Using Python as engine. - The first draft for an Aegisub automation engine. - Never implemented. - -Version 3 - Using Lua as engine. - Aegisub 1.09 was the last release-version to use Automation 3. - (Tentative release date only!) - -Version 4 - Using Lua as engine - Present in Aegisub 1.10 and later (tentative!) - Heavily expanded feature set, allowing a much wider range of modifications, - and more direct integration into the Aegisub user interface. diff --git a/automation/v4-docs/progress-reporting.txt b/automation/v4-docs/progress-reporting.txt deleted file mode 100644 index 3fd1e6093..000000000 --- a/automation/v4-docs/progress-reporting.txt +++ /dev/null @@ -1,102 +0,0 @@ -Automation 4 Progress Reporting and Debugging interface - -This document describes the functions used for reporting progress and -outputting debug information during the running of a script. - ---- - -Showing/hiding the progress dialog - -This function is used to show or hide the progress dialog. - -function aegisub.progress.show(do_show, can_cancel) - -@do_show (boolean) - True if the dialog should be shown, false if it should be hidden. - -@can_cancel (boolean) - Determines whether the Cancel button is shown. If you set this to true, - you should remember to periodically test whether the script has been - cancelled. - -Returns: nothing. - ---- - -Setting the progress bar position - -function aegisub.progress.set(precent) - -@percent (number) - The percentage completed. - -Returns: nothing. - ---- - -Showing the current task - -Used to set a message describing the current task being done. - -function aegisub.progress.task(msg, ...) - -@msg (string) - A format string used for the message. - -@... - Parameters to the format string. - -Returns: nothing. - ---- - -Setting the progress dialog title - -function aegisub.progress.title(title, ...) - -@title (string) - A format string used for the title. - -@... - Parameters to the format string. - -Returns: nothing. - ---- - -Getting the "cancelled" status - -Call this function to determine whether the Cancel button in the progress -dialog has been clicked. - -function aegisub.progress.is_cancelled() - -Returns: Boolean. True is the user has clicked the Cancel button, false if it - has not been clicked, nil if there is no Cancel button. - ---- - -Outputting text to the debug log - -function aegisub.debug.out(level, msg, ...) - -@level (number) - Integer describing the verbosity of this message. Here are some suggested - values you can use: - 0: Fatal, this is really an error that can't be ignored. - 1: Error, this kind of error can be recovered from, but might result in a - fatal error later on. - 2: Warning, something might be going wrong. - 3: Hint, something isn't entirely sane, but nothing wrong. - 4: Debug, some additional data only needed for development. - 5: Trace, extremely verbose data showing every tiny step during execution. - -@msg (string) - A format string used for the message. - -@... - Parameters for the format string. - -Returns: nothing. - ---- diff --git a/automation/v4-docs/subtitle-data.txt b/automation/v4-docs/subtitle-data.txt deleted file mode 100644 index 5b192d977..000000000 --- a/automation/v4-docs/subtitle-data.txt +++ /dev/null @@ -1,398 +0,0 @@ -Automation 4 Subtitle Data format and functions API - -This file describes the API for retrieving and manipulating subtitle data in -Automation 4, as well as the data structures generated and accepted by these -APIs. - ---- - -Subtitle Line table - -A Subtitle Line table contains various information about a line in the -subtitle file being processed. There are several classes of subtitle lines, -which all have a few fields in common, but otherwise have different fields. - - -Common keys for all Subtitle Line classes: - -class (string) - The class of the Subtitle Line. Must be one of: - "clear", (empty line) - "comment", (semicolon-style comment line) - "head", (section heading) - "info", (key/value pair in Script Info section) - "format", (line format definition) - "style", (style definition line) - "stylex", (style extension line, tentative AS5 feature) - "dialogue", (dialogue line or dialogue-style comment line) - "unknown" (unknown kind of line) - Lines of the "unknown" class should never appear in well-formed subtitle - scripts. They will usually be a result of a line being outside the section - it belongs in. - -raw (string) - The raw text of the line. - You should not change this field directly, since the data in it will never - be used for generating lines in internal representation. It will, however, - be updated from the remaining data in the line whenever it is required for - one thing or another by an internal function. - -section (string) - The section this line is placed in. If it is placed before the first section - heading, this field is nil. - - -Key defined only for the "comment" class: - -text (string) - The text of the comment line, ie. everything after the initial semicolon, - including any spaces. - - -Key defined only for the "head" class: - -No special keys are defined for this class, but the "section" field will be -the name of the new section started. - - -Keys defined only for the "info" class: - -key (string) - The "key" part of the line, ie. everything before the first colon. - -value (string) - The "value" part of the line, ie. everything after the first colon, - discarding any leading whitespace. - - -Keys defined only for the "format" class: - -fields (table) - An Array Table of strings, each being the name of a field on the line. - - -Keys defined only for the "style" class: - -name (string) - Name of the style. - -fontname (string) - Name of the font used. - -fontsize (string) - Size of the font used, in pixels. - -color1 (string) -color2 (string) -color3 (string) -color4 (string) - The four colors for the style. (Fill, pre-karaoke fill, border, shadow) - In VB hexadecimal, ie. "&HAABBGGRR&" - -bold (boolean/number) - The boldness/weight of the font. This will usually be a boolean, but it - can be a number, in which case it must be one of 0, 100, 200, ..., 900. - -italic (boolean) -underline (boolean) -strikeout (boolean) - Other properties of the font. - -scale_x (number) -scale_y (number) - Scaling of the text, in percent. - -spacing (number) - Additional spacing between letters. Always integer. - -angle (number) - Rotation of the text on the Z axis in degrees. - -borderstyle (number) - 1 = outline and drop shadow; 3 = opaque box. - -outline (number) - Width of the outline. - -shadow (number) - Distance between shadow and text. - -align (number) - Numpad alignment of the text. - -margin_l (number) -margin_r (number) -margin_t (number) -margin_b (number) - Left/right/top/bottom margins of the text in pixels. - If using a format without support for separate top and bottom margins, the - margin_t value will be used for vertical margin when converting back to - textual representation. - -encoding (number) - Font encoding used for text. This follows the MS Windows font encoding - constants. - -relative_to (number) - From STS.h: "0: window, 1: video, 2: undefined (~window)" - -vertical (boolean) - Whether vertical text semantics is used or not. - - -Keys defined only for the "stylex" class: - -Remember that this class is only for the tentative AS5 format. - -name (string) - Name of the new style defined. - -basename (string) - Name of the style the new style is based on. - -overrides (string) - String of override tags defining the new style. - - -Keys only defined for the "dialogue" class: - -comment (boolean) - True if the line is a comment line, otherwise false. - -layer (number) - The layer the line is rendered in. - -start_time (number) -end_time (number) - Start/end time of the line in milliseconds. - -style (string) - Name of the style assigned to this line. - -actor (string) - Name of the actor performing this line. - -margin_l (number) -margin_r (number) -margin_t (number) -margin_b (number) - Left/right/top/bottom margins of the text in pixels. - If any of these are zero, it's considered "not overriding". - Same comment as for style lines applies. - -effect (string) - Effect to apply to the line. - -userdata (string) - Authoring-application defined data. (Tentative AS5 field.) - -text (string) - The text for this line. - ---- - -Subtitle File user data object - -The Subtitle File object is a user data object with some of the metatable -methods overridden to provide table-like access to the subtitle lines, as -well as some functions to modify the subtitles. - -The following operations are supported. - -n = #subs -n = subs.n - Retrieve the number of lines in total. - The first syntax is preferred. - -line = subs[i] - Retrieve line i, assuming 1 <= i <= n. - -subs[i] = line - Replace line i with new data. - -subs[i] = nil -subs.delete(i[, i2, ...]) - Delete line i, pushing all following lines up an index. (Ie. by repeatedly - deleting line 1 this way, the file will eventually end up empty.) - The function syntax for this function can also take multiple line indexes, - in which case it deletes each of those lines. All indexes are relative to - the line numbering before the function is called. - -subs.deleterange(a, b) - Deletes all lines from index a to index b, both inclusive. If b < a, - nothing is done. - -subs[0] = line -subs.append(line[, line2, ...]) - Append one or more lines to a file. - -subs[-i] = line -subs.insert(i, line[, line2, ...]) - Insert one or more lines before index i. - -Note that the array-style accessors are most likely faster for any case only -involving one line at a time, while the function syntax versions are probably -faster if operating on multiple lines at a time. - ---- - -Parsing tag data - -This function uses the Aegisub SSA parser to split a string into override -blocks and text, and give separate access to each tag in override blocks. - -function aegisub.parse_tag_data(text) - -@text (string) - The SSA format string to parse. - -Returns: A Parsed Tag Data table. - ---- - -Recreating a line from tag data - -This function takes a Parsed Tag Data table and creates an SSA string from it. - -function aegisub.unparse_tag_data(tagdata) - -@tagdata (table) - The Parsed Tag Data table to "unparse". - -Returns: A string, being the "unparsed" data. - ---- - -Parsing karaoke data - -Tihs function uses the Aegisub SSA parser to split a string into karaoke -syllables with additional calculated information added. - -function aegisub.parse_karaoke_data(text) - -@text (string) - The SSA format string to parse. - -Returns: A Parsed Karaoke Data table. - ---- - -Parsed Tag Data table - -The Parsed Tag Data table is an Array Table containing a number of Parsed Line -Block tables. - - -Parsed Line Block table - -A Parsed Line Block describes part of a line. (See ass_dialogue.cpp:70 for a -more complete description of this. -There are several classes of Parsed Line Block tables, which have slightly -varying fields. - - -Base Parsed Line Block table class - -class (string) - One of: - "plain", - "override", - "drawing" - - -"plain" and "drawing" Parsed Line Block table classes - -text (string) - The text contained in this block. - - -"override" Parsed Line Block table class - -This class doesn't have any new, specifically named fields. It does, however, -have multiple integer indexed fields, ie. acts as an Array Table. -Each of these indexes refer to a table of type Parsed Override Tag. - - -Parsed Override Tag table - -This table describes a single override-tag in an SSA line. - -valid (boolean) - Whether this tag was parsed as a valid tag, or is just used for representing - junk in the middle of the line. - -name (string) - Name of the tag, including leading backslash character. (In the case of - invalid tags, the leading backslash might not be present.) - -paran (boolean) - Whether this tag has parantheses in its textual representation. - -params (table) - This is an Array Table containing the parameters for this tag. It will - always have the maximum number of entries that can be supported by the tag, - but in case of omitted parameters, the parameters omitted will have 'false' - for value in this table. - ---- - -Parsed Karaoke Data table - -The Parsed Karaoke Data table is simply an Array Table of Karaoke Syllable -Data tables. However, the Parsed Karaoke Data table will always have one more -item than its count shows, which is numbered 0 (zero). This item contains -everything before the first syllable. - - -Karaoke Syllable Data table - -This table contains information about a single karaoke syllable. - -duration (number) - Duration of the syllable in milliseconds. - (In case of syllable zero, this is always zero.) - -start_time (number) -end_time (number) - Start and end times of the syllable, relative to the start of the line, - given in milliseconds. - (While these can technically easily be calculated from the duration data, - they are too convenient to leave out from the standard interface.) - -tag (string) - The tag used for marking up this syllable. Usually one of: - "k", "K", "kf", "ko" - (In case of syllable zero, this is always the empty string.) - -text (string) - The text of the syllable, including all additional override tags. - -text_stripped (string) - The text of the syllable, stripped of any override tags. - ---- - -Setting undo points - -This function can only be used in macro features, it will have no effect when -used in any other feature. -It sets an undo point. - -You should always call this function after every operation that must be -undoable as a separate step. It is considered very bad practice to modify -the subtitles in a macro without setting at least one undo-point, which must -be at the end. - -Furthermore, this function also marks the subtitles as "modified". No other -function does this. - -function aegisub.set_undo_point(description) - -@description (string) - A short description of the operation that will be undone, if this undo-point - is used. Note that this is currently unused in Aegisub, and this parameter - is simply ignored. - -Returns: nothing. - ---- diff --git a/core/auto4-status.txt b/core/auto4-status.txt new file mode 100644 index 000000000..98139c85d --- /dev/null +++ b/core/auto4-status.txt @@ -0,0 +1,77 @@ +Aegisub Automation 4 branch build +Alpha-quality + +Please install in a folder separate from your main Aegisub installation. + +What works: +- UI-wise: + o Scanning the automation/autoload/ dir for scripts and loading them + o Rescanning said dir + o Macros not in right-click menu + o Reloading scripts on command + o Adding arbitrary other scripts to the local-scripts list (ie. bound to + subtitles rather than the application) + o Storing locally loaded scripts into subtiles on save + o Restoring locally loaded scripts from subtitle file on load + o Graceful recovery from (most) errors + o Scripts with errors are not retained, but marked as "has errors" + o Nice presentation of script debug-output +- Scripting-wise + o Global script info variables are read + o Macros: + # Registration function + # Processing function + # Validation function + # Config dialogs + o Export filters: + # Registration function + # Processing function + o Basic subtitle manipulation: + # Getting subtitle data from a line + # Storing subtitle data back into a line + # Appending lines to script + # Inserting lines at random positions in the script + # Deleting lines and (UNTESTED) ranges of lines + # All of the above needs more testing + o Setting undo-points + o Table of selected lines is correctly filled + o Parsing karaoke tags + o Modifying subtitles/setting undo points *should* be impossible in + Validation functions + o text_extents (currently identical to Auto3 version, meaning it also has + the same strange inaccuracies) + o include() function + o Making debug-output + +What's broken: +- UI-wise: + o No support for right-click menu macros + o Macro appearance in menus doesn't look too nice (needs a separator before + the macros) + o All functions in the program (including stuff unrelated to Automation) + should mark what kind of operation is put into the undo/redo buffer, but + this seems to be lost somewhere and isn't properly displayed. +- Scripting-wise + o active_line parameter to macro validation and processing functions seems + to not be filled correctly + o Lua needs a string library for processing UTF-8 strings natively, as well + as a regex library for working with Unicode strings. Perhaps even + introduce a new userdata type for storing widestrings? + o Stored options for export script config dialogs aren't supported yet. + +What's missing: +- UI wise: + o Apart from things relying on other Features being implemented in + scripting, nothing I can think of. +- Scripting-wise: + o Override (un)parsing of tags + o Subtitle Format reader/writer features + o File streams (required for subtitle format features) + +Things to try: +- Test, test, TEST! + Write some scripts that do more or less useful things. + Try to break stuff. + Report bugs. +- Config dialogs definately need more testing, whether all controls + behave sensibly etc. diff --git a/core/auto4_base.cpp b/core/auto4_base.cpp new file mode 100644 index 000000000..53dcc7367 --- /dev/null +++ b/core/auto4_base.cpp @@ -0,0 +1,704 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#include "auto4_base.h" +#include "ass_style.h" +#include "options.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#else +#include +#include FT_FREETYPE_H +#endif + +namespace Automation4 { + + bool CalculateTextExtents(AssStyle *style, wxString &text, int &width, int &height, int &descent, int &extlead) + { + width = height = descent = extlead = 0; + + double fontsize = style->fontsize; + +#ifdef WIN32 + // This is almost copypasta from TextSub + HDC thedc = CreateCompatibleDC(0); + if (!thedc) return false; + SetMapMode(thedc, MM_TEXT); + + HDC dczero = GetDC(0); + fontsize = -MulDiv((int)(fontsize+0.5), GetDeviceCaps(dczero, LOGPIXELSY), 72); + ReleaseDC(0, dczero); + + LOGFONT lf; + ZeroMemory(&lf, sizeof(lf)); + lf.lfHeight = fontsize; + lf.lfWeight = style->bold ? FW_BOLD : FW_NORMAL; + lf.lfItalic = style->italic; + lf.lfUnderline = style->underline; + lf.lfStrikeOut = style->strikeout; + lf.lfCharSet = style->encoding; + lf.lfOutPrecision = OUT_TT_PRECIS; + lf.lfClipPrecision = CLIP_DEFAULT_PRECIS; + lf.lfQuality = ANTIALIASED_QUALITY; + lf.lfPitchAndFamily = DEFAULT_PITCH|FF_DONTCARE; + wcsncpy(lf.lfFaceName, style->font.wc_str(), 32); + + HFONT thefont = CreateFontIndirect(&lf); + if (!thefont) return false; + SelectObject(thedc, thefont); + + SIZE sz; + size_t thetextlen = text.length(); + const wchar_t *thetext = text.wc_str(); + if (style->spacing) { + width = 0; + for (unsigned int i = 0; i < thetextlen; i++) { + GetTextExtentPoint32(thedc, &thetext[i], 1, &sz); + width += sz.cx + (int)style->spacing; + height = sz.cy; + } + } else { + GetTextExtentPoint32(thedc, thetext, (int)thetextlen, &sz); + width = sz.cx; + height = sz.cy; + } + + // HACKISH FIX! This seems to work, but why? It shouldn't be needed?!? + fontsize = style->fontsize; + width = (int)(width * fontsize/height + 0.5); + height = (int)(fontsize + 0.5); + + TEXTMETRIC tm; + GetTextMetrics(thedc, &tm); + descent = tm.tmDescent; + extlead= tm.tmExternalLeading; + + DeleteObject(thedc); + DeleteObject(thefont); + +#else // not WIN32 + wxMemoryDC thedc; + + // fix fontsize to be 72 DPI + fontsize = -FT_MulDiv((int)(fontsize+0.5), 72, thedc.GetPPI().y); + + // now try to get a font! + // use the font list to get some caching... (chance is the script will need the same font very often) + // USING wxTheFontList SEEMS TO CAUSE BAD LEAKS! + //wxFont *thefont = wxTheFontList->FindOrCreateFont( + wxFont thefont( + fontsize, + wxFONTFAMILY_DEFAULT, + style->italic ? wxFONTSTYLE_ITALIC : wxFONTSTYLE_NORMAL, + style->bold ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL, + style->underline, + style->font, + wxFONTENCODING_SYSTEM); // FIXME! make sure to get the right encoding here, make some translation table between windows and wx encodings + thedc.SetFont(thefont); + + if (style->spacing) { + // If there's inter-character spacing, kerning info must not be used, so calculate width per character + // NOTE: Is kerning actually done either way?! + for (unsigned int i = 0; i < intext.length(); i++) { + int a, b, c, d; + thedc.GetTextExtent(intext[i], &a, &b, &c, &d); + width += a + spacing; + height = b > height ? b : height; + descent = c > descent ? c : descent; + extlead= d > extlead ? d : extlead; + } + } else { + // If the inter-character spacing should be zero, kerning info can (and must) be used, so calculate everything in one go + thedc.GetTextExtent(intext, &width, &height, &descent, &extlead); + } +#endif + + // Compensate for scaling + width = (int)(style->scalex / 100 * width + 0.5); + height = (int)(style->scaley / 100 * height + 0.5); + descent = (int)(style->scaley / 100 * descent + 0.5); + extlead = (int)(style->scaley / 100 * extlead + 0.5); + + return true; + } + + + // Feature + + Feature::Feature(ScriptFeatureClass _featureclass, const wxString &_name) + : featureclass(_featureclass) + , name(_name) + { + // nothing to do + } + + ScriptFeatureClass Feature::GetClass() const + { + return featureclass; + } + + FeatureMacro* Feature::AsMacro() + { + if (featureclass == SCRIPTFEATURE_MACRO) + // For VS, remember to enable building with RTTI, otherwise dynamic_cast<> won't work + return dynamic_cast(this); + return 0; + } + + FeatureFilter* Feature::AsFilter() + { + if (featureclass == SCRIPTFEATURE_FILTER) + return dynamic_cast(this); + return 0; + } + + FeatureSubtitleFormat* Feature::AsSubFormat() + { + if (featureclass == SCRIPTFEATURE_SUBFORMAT) + return dynamic_cast(this); + return 0; + } + + const wxString& Feature::GetName() const + { + return name; + } + + + // FeatureMacro + + FeatureMacro::FeatureMacro(const wxString &_name, const wxString &_description, MacroMenu _menu) + : Feature(SCRIPTFEATURE_MACRO, _name) + , description(_description) + , menu(_menu) + { + // nothing to do + } + + const wxString& FeatureMacro::GetDescription() const + { + return description; + } + + MacroMenu FeatureMacro::GetMenu() const + { + return menu; + } + + + // FeatureFilter + + FeatureFilter::FeatureFilter(const wxString &_name, const wxString &_description, int _priority) + : Feature(SCRIPTFEATURE_FILTER, _name) + , AssExportFilter() + , config_dialog(0) + { + description = _description; // from AssExportFilter + Register(_name, _priority); + } + + FeatureFilter::~FeatureFilter() + { + Unregister(); + } + + wxWindow* FeatureFilter::GetConfigDialogWindow(wxWindow *parent) { + if (config_dialog) { + delete config_dialog; + config_dialog = 0; + } + if (config_dialog = GenerateConfigDialog(parent)) { + return config_dialog->GetWindow(parent); + } else { + return 0; + } + } + + void FeatureFilter::LoadSettings(bool IsDefault) { + if (config_dialog) { + config_dialog->ReadBack(); + } + } + + + // FeatureSubtitleFormat + + FeatureSubtitleFormat::FeatureSubtitleFormat(const wxString &_name, const wxString &_extension) + : Feature(SCRIPTFEATURE_SUBFORMAT, _name) + , extension(_extension) + { + // nothing to do + } + + const wxString& FeatureSubtitleFormat::GetExtension() const + { + return extension; + } + + bool FeatureSubtitleFormat::CanWriteFile(wxString filename) + { + return !filename.Right(extension.Length()).CmpNoCase(extension); + } + + bool FeatureSubtitleFormat::CanReadFile(wxString filename) + { + return !filename.Right(extension.Length()).CmpNoCase(extension); + } + + + // ShowConfigDialogEvent + + const wxEventType EVT_SHOW_CONFIG_DIALOG_t = wxNewEventType(); + + + // ScriptConfigDialog + + wxWindow* ScriptConfigDialog::GetWindow(wxWindow *parent) + { + if (win) return win; + return win = CreateWindow(parent); + } + + void ScriptConfigDialog::DeleteWindow() + { + if (win) delete win; + win = 0; + } + + + // ProgressSink + + ProgressSink::ProgressSink(wxWindow *parent) + : wxDialog(parent, -1, _T("Automation"), wxDefaultPosition, wxDefaultSize, wxDOUBLE_BORDER) + , cancelled(false) + , has_inited(false) + , script_finished(false) + , debug_visible(false) + { + // make the controls + progress_display = new wxGauge(this, -1, 1000, wxDefaultPosition, wxSize(300, 20)); + title_display = new wxStaticText(this, -1, _T(""), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE|wxST_NO_AUTORESIZE); + task_display = new wxStaticText(this, -1, _T(""), wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE|wxST_NO_AUTORESIZE); + cancel_button = new wxButton(this, wxID_CANCEL); + debug_output = new wxTextCtrl(this, -1, _T(""), wxDefaultPosition, wxSize(300, 120), wxTE_MULTILINE|wxTE_READONLY); + + // put it in a sizer + sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(title_display, 0, wxEXPAND | wxALL, 5); + sizer->Add(progress_display, 0, wxALL&~wxTOP, 5); + sizer->Add(task_display, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 5); + sizer->Add(cancel_button, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT | wxBOTTOM, 5); + sizer->Add(debug_output, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 5); + sizer->Show(debug_output, false); + + // make the title a slightly larger font + wxFont title_font = title_display->GetFont(); + int fontsize = title_font.GetPointSize(); + title_font.SetPointSize(fontsize + fontsize/4 + fontsize/8); + title_font.SetWeight(wxFONTWEIGHT_BOLD); + title_display->SetFont(title_font); + + sizer->SetSizeHints(this); + SetSizer(sizer); + Center(); + } + + ProgressSink::~ProgressSink() + { + } + + void ProgressSink::OnIdle(wxIdleEvent &evt) + { + // The big glossy "update display" event + DoUpdateDisplay(); + + if (script_finished) { + if (!debug_visible) { + EndModal(0); + } else { + cancel_button->Enable(true); + cancel_button->SetLabel(_("Close")); + SetProgress(100.0); + SetTask(_("Script completed")); + } + } + } + + void ProgressSink::DoUpdateDisplay() + { + // If debug output isn't handled before the test for script_finished later, + // there might actually be some debug output but the debug_visible flag won't + // be set before the dialog closes itself. + wxMutexLocker lock(data_mutex); + if (!pending_debug_output.IsEmpty()) { + if (!debug_visible) { + sizer->Show(debug_output, true); + Layout(); + sizer->Fit(this); + + debug_visible = true; + } + + *debug_output << pending_debug_output; + debug_output->SetInsertionPointEnd(); + + pending_debug_output = _T(""); + } + + progress_display->SetValue((int)(progress*10)); + task_display->SetLabel(task); + title_display->SetLabel(title); + } + + void ProgressSink::SetProgress(float _progress) + { + wxMutexLocker lock(data_mutex); + progress = _progress; + } + + void ProgressSink::SetTask(const wxString &_task) + { + wxMutexLocker lock(data_mutex); + task = _task; + } + + void ProgressSink::SetTitle(const wxString &_title) + { + wxMutexLocker lock(data_mutex); + title = _title; + } + + void ProgressSink::AddDebugOutput(const wxString &msg) + { + wxMutexLocker lock(data_mutex); + pending_debug_output << msg; + } + + BEGIN_EVENT_TABLE(ProgressSink, wxWindow) + EVT_INIT_DIALOG(ProgressSink::OnInit) + EVT_BUTTON(wxID_CANCEL, ProgressSink::OnCancel) + EVT_IDLE(ProgressSink::OnIdle) + EVT_SHOW_CONFIG_DIALOG(ProgressSink::OnConfigDialog) + END_EVENT_TABLE() + + void ProgressSink::OnInit(wxInitDialogEvent &evt) + { + has_inited = true; + } + + void ProgressSink::OnCancel(wxCommandEvent &evt) + { + if (!script_finished) { + cancelled = true; + cancel_button->Enable(false); + } else { + EndModal(0); + } + } + + void ProgressSink::OnConfigDialog(ShowConfigDialogEvent &evt) + { + // assume we're in the GUI thread here + + DoUpdateDisplay(); + + if (evt.config_dialog) { + wxDialog *w = new wxDialog(this, -1, title); // container dialog box + wxBoxSizer *s = new wxBoxSizer(wxHORIZONTAL); // sizer for putting contents in + wxWindow *ww = evt.config_dialog->GetWindow(w); // get/generate actual dialog contents + s->Add(ww, 0, wxALL, 5); // add contents to dialog + w->SetSizerAndFit(s); + w->CenterOnParent(); + w->ShowModal(); + evt.config_dialog->ReadBack(); + evt.config_dialog->DeleteWindow(); + delete w; + } else { + wxMessageBox(_T("Uh... no config dialog?")); + } + + // See note in auto4_base.h + if (evt.sync_sema) { + evt.sync_sema->Post(); + } + } + + + // Script + + Script::Script(const wxString &_filename) + : filename(_filename) + , name(_T("")) + , description(_T("")) + , author(_T("")) + , version(_T("")) + , loaded(false) + { + // copied from auto3 + include_path.clear(); + include_path.EnsureFileAccessible(filename); + wxStringTokenizer toker(Options.AsText(_T("Automation Include Path")), _T("|"), false); + while (toker.HasMoreTokens()) { + // todo? make some error reporting here + wxFileName path(toker.GetNextToken()); + if (!path.IsOk()) continue; + if (path.IsRelative()) continue; + if (!path.DirExists()) continue; + if (include_path.Member(path.GetLongPath())) continue; + include_path.Add(path.GetLongPath()); + } + } + + Script::~Script() + { + for (std::vector::iterator f = features.begin(); f != features.end(); ++f) { + delete *f; + } + } + + const wxString& Script::GetFilename() const + { + return filename; + } + + const wxString& Script::GetName() const + { + return name; + } + + const wxString& Script::GetDescription() const + { + return description; + } + + const wxString& Script::GetAuthor() const + { + return author; + } + + const wxString& Script::GetVersion() const + { + return version; + } + + bool Script::GetLoadedState() const + { + return loaded; + } + + std::vector& Script::GetFeatures() + { + return features; + } + + + // ScriptManager + + ScriptManager::ScriptManager() + { + // do nothing...? + } + + ScriptManager::~ScriptManager() + { + RemoveAll(); + } + + void ScriptManager::Add(Script *script) + { + for (std::vector::iterator i = scripts.begin(); i != scripts.end(); ++i) { + if (script == *i) return; + } + scripts.push_back(script); + } + + void ScriptManager::Remove(Script *script) + { + for (std::vector::iterator i = scripts.begin(); i != scripts.end(); ++i) { + if (script == *i) { + delete *i; + scripts.erase(i); + return; + } + } + } + + void ScriptManager::RemoveAll() + { + for (std::vector::iterator i = scripts.begin(); i != scripts.end(); ++i) { + delete *i; + } + scripts.clear(); + } + + const std::vector& ScriptManager::GetScripts() const + { + return scripts; + } + + const std::vector& ScriptManager::GetMacros(MacroMenu menu) + { + macros[menu].clear(); + for (std::vector::iterator i = scripts.begin(); i != scripts.end(); ++i) { + std::vector &sfs = (*i)->GetFeatures(); + for (std::vector::iterator j = sfs.begin(); j != sfs.end(); ++j) { + FeatureMacro *m = dynamic_cast(*j); + if (!m) continue; + if (menu == MACROMENU_ALL || m->GetMenu() == menu) + macros[menu].push_back(m); + } + } + return macros[menu]; + } + + + // AutoloadScriptManager + + AutoloadScriptManager::AutoloadScriptManager(const wxString &_path) + : path(_path) + { + Reload(); + } + + void AutoloadScriptManager::Reload() + { + wxDir dir(path); + if (!dir.IsOpened()) { + // crap + return; + } + + RemoveAll(); + + int error_count = 0; + + wxString fn; + wxFileName script_path(path, _T("")); + bool more = dir.GetFirst(&fn, wxEmptyString, wxDIR_FILES); + while (more) { + script_path.SetName(fn); + try { + Add(ScriptFactory::CreateFromFile(script_path.GetFullPath())); + } + catch (const wchar_t *e) { + error_count++; + wxLogError(_T("Error loading Automation script: %s\n%s"), fn.c_str(), e); + } + catch (...) { + error_count++; + wxLogError(_T("Error loading Automation script: %s\nUnknown error."), fn.c_str()); + } + more = dir.GetNext(&fn); + } + if (error_count) { + wxLogWarning(_T("One or more scripts placed in the Automation autoload directory failed to load\nPlease review the errors above, correct them and use the Reload Autoload dir button in Automation Manager to attempt loading the scripts again.")); + } + } + + + + // ScriptFactory + + std::vector ScriptFactory::factories; + + const wxString& ScriptFactory::GetEngineName() const + { + return engine_name; + } + + const wxString& ScriptFactory::GetFilenamePattern() const + { + return filename_pattern; + } + + void ScriptFactory::Register(ScriptFactory *factory) + { + for (std::vector::iterator i = factories.begin(); i != factories.end(); ++i) { + if (*i == factory) { + throw _T("Automation 4: Attempt to register the same script factory multiple times."); + } + } + factories.push_back(factory); + } + + void ScriptFactory::Unregister(ScriptFactory *factory) + { + for (std::vector::iterator i = factories.begin(); i != factories.end(); ++i) { + if (*i == factory) { + factories.erase(i); + return; + } + } + } + + Script* ScriptFactory::CreateFromFile(const wxString &filename) + { + for (std::vector::iterator i = factories.begin(); i != factories.end(); ++i) { + Script *s = (*i)->Produce(filename); + if (s) return s; + } + return new UnknownScript(filename); + } + + const std::vector& ScriptFactory::GetFactories() + { + return factories; + } + + + // UnknownScript + + UnknownScript::UnknownScript(const wxString &filename) + : Script(filename) + { + wxFileName fn(filename); + name = fn.GetName(); + description = _("File was not recognized as a script"); + loaded = false; + } + +}; diff --git a/core/auto4_base.h b/core/auto4_base.h new file mode 100644 index 000000000..a2fb67a1d --- /dev/null +++ b/core/auto4_base.h @@ -0,0 +1,363 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#pragma once + +#ifndef _AUTO4_BASE_H +#define _AUTO4_BASE_H + +#include +#include + +#include "ass_export_filter.h" +#include "subtitle_format.h" + +class AssFile; +class AssStyle; +class wxWindow; +class wxDialog; +class wxStopWatch; +class wxPathList; + + +DECLARE_EVENT_TYPE(wxEVT_AUTOMATION_SCRIPT_COMPLETED, -1) + + +namespace Automation4 { + + // Calculate the extents of a text string given a style + bool CalculateTextExtents(AssStyle *style, wxString &text, int &width, int &height, int &descent, int &extlead); + + + // The top-level menus a macro can appear in + enum MacroMenu { + MACROMENU_NONE = 0, // pseudo-index, dunno if this has any real meaning + MACROMENU_ALL = 0, // pseudo-index, used to retrieve information about all macros loaded + MACROMENU_EDIT, + MACROMENU_VIDEO, + MACROMENU_AUDIO, + MACROMENU_TOOLS, + MACROMENU_RIGHT, // right-click menu + + MACROMENU_MAX // must be last, one higher than the last, used for array sizing + }; + // The class of a Feature... + enum ScriptFeatureClass { + SCRIPTFEATURE_MACRO = 0, + SCRIPTFEATURE_FILTER, + SCRIPTFEATURE_SUBFORMAT, + + SCRIPTFEATURE_MAX // must be last + }; + + + // A Feature describes a function provided by a Script. + // There are several distinct classes of features. + class FeatureMacro; + class FeatureFilter; + class FeatureSubtitleFormat; + class Feature { + private: + ScriptFeatureClass featureclass; + wxString name; + + protected: + Feature(ScriptFeatureClass _featureclass, const wxString &_name); + + public: + virtual ~Feature() { } + + ScriptFeatureClass GetClass() const; + FeatureMacro* AsMacro(); + FeatureFilter* AsFilter(); + FeatureSubtitleFormat* AsSubFormat(); + + virtual const wxString& GetName() const; + }; + + + // The Macro feature; adds a menu item that runs script code + class FeatureMacro : public virtual Feature { + private: + wxString description; + MacroMenu menu; + + protected: + FeatureMacro(const wxString &_name, const wxString &_description, MacroMenu _menu); + + public: + virtual ~FeatureMacro() { } + + const wxString& GetDescription() const; + MacroMenu GetMenu() const; + + virtual bool Validate(AssFile *subs, std::vector &selected, int active) = 0; + virtual void Process(AssFile *subs, std::vector &selected, int active, wxWindow *progress_parent) = 0; + }; + + + class ScriptConfigDialog; + // The Export Filter feature; adds a new export filter + class FeatureFilter : public virtual Feature, public AssExportFilter { + private: + ScriptConfigDialog *config_dialog; + + protected: + FeatureFilter(const wxString &_name, const wxString &_description, int _priority); + + // Subclasses should probably implement AssExportFilter::Init + + virtual ScriptConfigDialog* GenerateConfigDialog(wxWindow *parent) = 0; // subclasses should implement this, producing a new ScriptConfigDialog + + public: + virtual ~FeatureFilter(); + + wxWindow* GetConfigDialogWindow(wxWindow *parent); + void LoadSettings(bool IsDefault); + + // Subclasses must implement ProcessSubs from AssExportFilter + }; + + + // The Subtitle Format feature; adds new subtitle format readers/writers + class FeatureSubtitleFormat : public virtual Feature, public SubtitleFormat { + private: + wxString extension; + + protected: + FeatureSubtitleFormat(const wxString &_name, const wxString &_extension); + + public: + virtual ~FeatureSubtitleFormat() { } + + const wxString& GetExtension() const; + + // Default implementations of these are provided, that just checks extension, + // but subclasses can provide more elaborate implementations, or go back to + // the "return false" implementation, in case of reader-only or writer-only. + virtual bool CanWriteFile(wxString filename); + virtual bool CanReadFile(wxString filename); + + // Subclasses should implement ReadFile and/or WriteFile here + }; + + + // Base class for script-provided config dialogs + class ScriptConfigDialog { + private: + wxWindow *win; + + protected: + virtual wxWindow* CreateWindow(wxWindow *parent) = 0; + + public: + ScriptConfigDialog() : win(0) { } + wxWindow* GetWindow(wxWindow *parent); + void DeleteWindow(); + virtual void ReadBack() = 0; + }; + + + // Config dialog event class and related stuff (wx features; + + Script(const wxString &_filename); + + public: + virtual ~Script(); + + virtual void Reload() = 0; + + const wxString& GetFilename() const; + const wxString& GetName() const; + const wxString& GetDescription() const; + const wxString& GetAuthor() const; + const wxString& GetVersion() const; + bool GetLoadedState() const; + + std::vector& GetFeatures(); + }; + + + // Manages loaded scripts; for whatever reason, multiple managers might be instantiated. In truth, this is more + // like a macro manager at the moment, since Export Filter and Subtitle Format are already managed by other + // classes. + class ScriptManager { + private: + std::vector scripts; + + std::vector macros[MACROMENU_MAX]; // array of vectors... + + public: + ScriptManager(); + virtual ~ScriptManager(); // Deletes all scripts managed + void Add(Script *script); // Add a script to the manager. The ScriptManager takes owvership of the script and will automatically delete it. + void Remove(Script *script); // Remove a script from the manager, and delete the Script object. + void RemoveAll(); // Deletes all scripts managed + + const std::vector& GetScripts() const; + + const std::vector& GetMacros(MacroMenu menu); + // No need to have getters for the other kinds of features, I think. + // They automatically register themselves in the relevant places. + }; + + + // Scans a directory for scripts and attempts to load all of them + class AutoloadScriptManager : public ScriptManager { + private: + wxString path; + public: + AutoloadScriptManager(const wxString &_path); + void Reload(); + }; + + + // Script factory; each scripting engine should create exactly one instance of this object and register it. + // This is used to create Script objects from a file. + class ScriptFactory { + private: + static std::vector factories; + protected: + ScriptFactory() { } + wxString engine_name; + wxString filename_pattern; + public: + virtual Script* Produce(const wxString &filename) const = 0; + const wxString& GetEngineName() const; + const wxString& GetFilenamePattern() const; + + static void Register(ScriptFactory *factory); + static void Unregister(ScriptFactory *factory); + static Script* CreateFromFile(const wxString &filename); + static const std::vector& GetFactories(); + }; + + // Dummy class for scripts that could not be loaded by the ScriptFactory + class UnknownScript : public Script { + public: + UnknownScript(const wxString &filename); + void Reload() { }; + }; + +}; + +#endif diff --git a/core/auto4_lua.cpp b/core/auto4_lua.cpp new file mode 100644 index 000000000..c0b074b74 --- /dev/null +++ b/core/auto4_lua.cpp @@ -0,0 +1,778 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#include "auto4_lua.h" +#include "ass_dialogue.h" +#include "ass_style.h" +#include "ass_file.h" +#include "ass_override.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Automation4 { + + // LuaStackcheck + +#ifdef _DEBUG + struct LuaStackcheck { + lua_State *L; + int startstack; + void check(int additional) + { + int top = lua_gettop(L); + if (top - additional != startstack) { + wxLogDebug(_T("Lua stack size mismatch.")); + dump(); + assert(top - additional == startstack); + } + } + void dump() + { + int top = lua_gettop(L); + wxLogDebug(_T("Dumping Lua stack...")); + for (int i = top; i > 0; i--) { + lua_pushvalue(L, i); + wxString type(lua_typename(L, lua_type(L, -1)), wxConvUTF8); + if (lua_isstring(L, i)) { + wxLogDebug(type + _T(": ") + wxString(lua_tostring(L, -1), wxConvUTF8)); + } else { + wxLogDebug(type); + } + lua_pop(L, 1); + } + wxLogDebug(_T("--- end dump")); + } + LuaStackcheck(lua_State *_L) : L(_L) { startstack = lua_gettop(L); } + ~LuaStackcheck() { check(0); } + }; +#else + struct LuaStackcheck { + void check(int additional) { } + void dump() { } + LuaStackcheck(lua_State *L) { } + ~LuaStackcheck() { } + }; +#endif + + + // LuaScript + + LuaScript::LuaScript(const wxString &filename) + : Script(filename) + , L(0) + { + Create(); + } + + LuaScript::~LuaScript() + { + if (L) Destroy(); + } + + void LuaScript::Create() + { + Destroy(); + + loaded = true; + + try { + // create lua environment + L = lua_open(); + LuaStackcheck _stackcheck(L); + + // register standard libs + lua_pushcfunction(L, luaopen_base); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_package); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_string); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_table); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_math); lua_call(L, 0, 0); + _stackcheck.check(0); + // dofile and loadfile are replaced with include + lua_pushnil(L); + lua_setglobal(L, "dofile"); + lua_pushnil(L); + lua_setglobal(L, "loadfile"); + lua_pushcfunction(L, LuaInclude); + lua_setglobal(L, "include"); + + // prepare stuff in the registry + // reference to the script object + lua_pushlightuserdata(L, this); + lua_setfield(L, LUA_REGISTRYINDEX, "aegisub"); + // the "feature" table + // integer indexed, using same indexes as "features" vector in the base Script class + lua_newtable(L); + lua_setfield(L, LUA_REGISTRYINDEX, "features"); + _stackcheck.check(0); + + // make "aegisub" table + lua_pushstring(L, "aegisub"); + lua_newtable(L); + // aegisub.register_macro + lua_pushcfunction(L, LuaFeatureMacro::LuaRegister); + lua_setfield(L, -2, "register_macro"); + // aegisub.register_filter + lua_pushcfunction(L, LuaFeatureFilter::LuaRegister); + lua_setfield(L, -2, "register_filter"); + // aegisub.text_extents + lua_pushcfunction(L, LuaTextExtents); + lua_setfield(L, -2, "text_extents"); + // store aegisub table to globals + lua_settable(L, LUA_GLOBALSINDEX); + _stackcheck.check(0); + + // load user script + if (luaL_loadfile(L, GetFilename().mb_str(wxConvUTF8))) { + wxString *err = new wxString(lua_tostring(L, -1), wxConvUTF8); + err->Prepend(_T("An error occurred loading the Lua script file \"") + GetFilename() + _T("\":\n\n")); + throw err->c_str(); + } + _stackcheck.check(1); + // and execute it + // this is where features are registered + // this should run really fast so a progress window isn't needed + // (if it infinite-loops, scripter is an idiot and already got his punishment) + { + LuaThreadedCall call(L, 0, 0); + if (call.Wait()) { + // error occurred, assumed to be on top of Lua stack + wxString *err = new wxString(lua_tostring(L, -1), wxConvUTF8); + err->Prepend(_T("An error occurred initialising the Lua script file \"") + GetFilename() + _T("\":\n\n")); + throw err->c_str(); + } + } + _stackcheck.check(0); + lua_getglobal(L, "script_name"); + if (lua_isstring(L, -1)) { + name = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + name = GetFilename(); + } + lua_getglobal(L, "script_description"); + if (lua_isstring(L, -1)) { + description = wxString(lua_tostring(L, -1), wxConvUTF8); + } + lua_getglobal(L, "script_author"); + if (lua_isstring(L, -1)) { + author = wxString(lua_tostring(L, -1), wxConvUTF8); + } + lua_getglobal(L, "script_version"); + if (lua_isstring(L, -1)) { + version = wxString(lua_tostring(L, -1), wxConvUTF8); + } + lua_pop(L, 4); + // if we got this far, the script should be ready + _stackcheck.check(0); + + } + catch (...) { + Destroy(); + loaded = false; + throw; + } + } + + void LuaScript::Destroy() + { + // Assume the script object is clean if there's no Lua state + if (!L) return; + + // remove features + for (int i = 0; i < (int)features.size(); i++) { + Feature *f = features[i]; + delete f; + } + features.clear(); + + // delete environment + lua_close(L); + L = 0; + + loaded = false; + } + + void LuaScript::Reload() + { + Destroy(); + Create(); + } + + LuaScript* LuaScript::GetScriptObject(lua_State *L) + { + lua_getfield(L, LUA_REGISTRYINDEX, "aegisub"); + void *ptr = lua_touserdata(L, -1); + lua_pop(L, 1); + return (LuaScript*)ptr; + } + + int LuaScript::LuaTextExtents(lua_State *L) + { + if (!lua_istable(L, 1)) { + lua_pushstring(L, "First argument to text_extents must be a table"); + lua_error(L); + } + if (!lua_isstring(L, 2)) { + lua_pushstring(L, "Second argument to text_extents must be a string"); + lua_error(L); + } + + lua_pushvalue(L, 1); + AssEntry *et = LuaAssFile::LuaToAssEntry(L); + AssStyle *st = dynamic_cast(et); + lua_pop(L, 1); + if (!st) { + delete et; // Make sure to delete the "live" pointer + lua_pushstring(L, "Not a style entry"); + lua_error(L); + } + + wxString text(lua_tostring(L, 2), wxConvUTF8); + + int width, height, descent, extlead; + if (!CalculateTextExtents(st, text, width, height, descent, extlead)) { + delete st; + lua_pushstring(L, "Some internal error occurred calculating text_extents"); + lua_error(L); + } + delete st; + + lua_pushnumber(L, width); + lua_pushnumber(L, height); + lua_pushnumber(L, descent); + lua_pushnumber(L, extlead); + return 4; + } + + int LuaScript::LuaInclude(lua_State *L) + { + LuaScript *s = GetScriptObject(L); + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "Argument to include must be a string"); + lua_error(L); + return 0; + } + wxString fnames(lua_tostring(L, 1), wxConvUTF8); + + wxFileName fname(fnames); + if (fname.GetDirCount() == 0) { + // filename only + fname = s->include_path.FindAbsoluteValidPath(fnames); + } else if (fname.IsRelative()) { + // relative path + wxFileName sfname(s->GetFilename()); + fname.MakeAbsolute(sfname.GetPath(true)); + } else { + // absolute path, do nothing + } + if (!fname.IsOk() || !fname.FileExists()) { + lua_pushfstring(L, "Could not find Lua script for inclusion: %s", fnames.mb_str(wxConvUTF8)); + lua_error(L); + } + + if (luaL_loadfile(L, fname.GetFullPath().mb_str(wxConvUTF8))) { + lua_pushfstring(L, "An error occurred loading the Lua script file \"%s\":\n\n%s", fname.GetFullPath().mb_str(wxConvUTF8), lua_tostring(L, -1)); + lua_error(L); + return 0; + } + int pretop = lua_gettop(L) - 1; // don't count the function value itself + lua_call(L, 0, LUA_MULTRET); + return lua_gettop(L) - pretop; + } + + + // LuaThreadedCall + + LuaThreadedCall::LuaThreadedCall(lua_State *_L, int _nargs, int _nresults) + : wxThread(wxTHREAD_JOINABLE) + , L(_L) + , nargs(_nargs) + , nresults(_nresults) + { + Create(); + Run(); + } + + wxThread::ExitCode LuaThreadedCall::Entry() + { + int result = lua_pcall(L, nargs, nresults, 0); + + // see if there's a progress sink window to close + lua_getfield(L, LUA_REGISTRYINDEX, "progress_sink"); + if (lua_isuserdata(L, -1)) { + LuaProgressSink *ps = LuaProgressSink::GetObjPointer(L, -1); + // don't bother protecting this with a mutex, it should be safe enough like this + ps->script_finished = true; + // tell wx to run its idle-events now, just to make the progress window notice earlier that we're done + wxWakeUpIdle(); + } + lua_pop(L, 1); + + lua_gc(L, LUA_GCCOLLECT, 0); + return (wxThread::ExitCode)result; + } + + + // LuaFeature + + LuaFeature::LuaFeature(lua_State *_L, ScriptFeatureClass _featureclass, const wxString &_name) + : Feature(_featureclass, _name) + , L(_L) + { + } + + void LuaFeature::RegisterFeature() + { + // get the LuaScript objects + lua_getfield(L, LUA_REGISTRYINDEX, "aegisub"); + LuaScript *s = (LuaScript*)lua_touserdata(L, -1); + lua_pop(L, 1); + + // add the Feature object + s->features.push_back(this); + + // get the index+1 it was pushed into + myid = (int)s->features.size()-1; + + // create table with the functions + // get features table + lua_getfield(L, LUA_REGISTRYINDEX, "features"); + lua_pushvalue(L, -2); + lua_rawseti(L, -2, myid); + lua_pop(L, 1); + } + + void LuaFeature::GetFeatureFunction(int functionid) + { + // get feature table + lua_getfield(L, LUA_REGISTRYINDEX, "features"); + // get this feature's function pointers + lua_rawgeti(L, -1, myid); + // get pointer for validation function + lua_rawgeti(L, -1, functionid); + lua_remove(L, -2); + lua_remove(L, -2); + } + + void LuaFeature::CreateIntegerArray(std::vector &ints) + { + // create an array-style table with an integer vector in it + // leave the new table on top of the stack + lua_newtable(L); + for (int i = 0; i != ints.size(); ++i) { + lua_pushinteger(L, ints[i]+1); + lua_rawseti(L, -2, i+1); + } + } + + void LuaFeature::ThrowError() + { + wxString err(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + wxLogError(err); + } + + + // LuaFeatureMacro + + int LuaFeatureMacro::LuaRegister(lua_State *L) + { + wxString _name(lua_tostring(L, 1), wxConvUTF8); + wxString _description(lua_tostring(L, 2), wxConvUTF8); + const char *_menustring = lua_tostring(L, 3); + MacroMenu _menu = MACROMENU_NONE; + + if (strcmp(_menustring, "edit") == 0) _menu = MACROMENU_EDIT; + else if (strcmp(_menustring, "video") == 0) _menu = MACROMENU_VIDEO; + else if (strcmp(_menustring, "audio") == 0) _menu = MACROMENU_AUDIO; + else if (strcmp(_menustring, "tools") == 0) _menu = MACROMENU_TOOLS; + else if (strcmp(_menustring, "right") == 0) _menu = MACROMENU_RIGHT; + + if (_menu == MACROMENU_NONE) { + lua_pushstring(L, "Error registering macro: Invalid menu name."); + lua_error(L); + } + + LuaFeatureMacro *macro = new LuaFeatureMacro(_name, _description, _menu, L); + + return 0; + } + + LuaFeatureMacro::LuaFeatureMacro(const wxString &_name, const wxString &_description, MacroMenu _menu, lua_State *_L) + : LuaFeature(_L, SCRIPTFEATURE_MACRO, _name) + , FeatureMacro(_name, _description, _menu) + , Feature(SCRIPTFEATURE_MACRO, _name) + { + // new table for containing the functions for this feature + lua_newtable(L); + // store processing function + if (!lua_isfunction(L, 4)) { + lua_pushstring(L, "The macro processing function must be a function"); + lua_error(L); + } + lua_pushvalue(L, 4); + lua_rawseti(L, -2, 1); + // and validation function + lua_pushvalue(L, 5); + no_validate = !lua_isfunction(L, -1); + lua_rawseti(L, -2, 2); + // make the feature known + RegisterFeature(); + // and remove the feature function table again + lua_pop(L, 1); + } + + bool LuaFeatureMacro::Validate(AssFile *subs, std::vector &selected, int active) + { + if (no_validate) + return true; + + GetFeatureFunction(2); // 2 = validation function + + // prepare function call + LuaAssFile *subsobj = new LuaAssFile(L, subs, false, false); + CreateIntegerArray(selected); // selected items + lua_pushinteger(L, -1); // active line + + // do call + LuaThreadedCall call(L, 3, 1); + wxThread::ExitCode code = call.Wait(); + // get result + bool result = !!lua_toboolean(L, -1); + + // clean up stack + lua_pop(L, 1); + + return result; + } + + void LuaFeatureMacro::Process(AssFile *subs, std::vector &selected, int active, wxWindow *progress_parent) + { + GetFeatureFunction(1); // 1 = processing function + + // prepare function call + LuaAssFile *subsobj = new LuaAssFile(L, subs, true, true); + CreateIntegerArray(selected); // selected items + lua_pushinteger(L, -1); // active line + + LuaProgressSink *ps = new LuaProgressSink(L, progress_parent); + ps->SetTitle(GetName()); + + // do call + LuaThreadedCall call(L, 3, 0); + + ps->ShowModal(); + wxThread::ExitCode code = call.Wait(); + if (code) ThrowError(); + + delete ps; + } + + + // LuaFeatureFilter + + LuaFeatureFilter::LuaFeatureFilter(const wxString &_name, const wxString &_description, int merit, lua_State *_L) + : LuaFeature(_L, SCRIPTFEATURE_FILTER, _name) + , FeatureFilter(_name, _description, merit) + , Feature(SCRIPTFEATURE_FILTER, _name) + { + // Works the same as in LuaFeatureMacro + lua_newtable(L); + if (!lua_isfunction(L, 4)) { + lua_pushstring(L, "The filter processing function must be a function"); + lua_error(L); + } + lua_pushvalue(L, 4); + lua_rawseti(L, -2, 1); + lua_pushvalue(L, 5); + has_config = lua_isfunction(L, -1); + lua_rawseti(L, -2, 2); + RegisterFeature(); + lua_pop(L, 1); + } + + void LuaFeatureFilter::Init() + { + // Don't think there's anything to do here... (empty in auto3) + } + + int LuaFeatureFilter::LuaRegister(lua_State *L) + { + wxString _name(lua_tostring(L, 1), wxConvUTF8); + wxString _description(lua_tostring(L, 2), wxConvUTF8); + int _merit = lua_tointeger(L, 3); + + LuaFeatureFilter *filter = new LuaFeatureFilter(_name, _description, _merit, L); + + return 0; + } + + void LuaFeatureFilter::ProcessSubs(AssFile *subs, wxWindow *export_dialog) + { + GetFeatureFunction(1); // 1 = processing function + + // prepare function call + // subtitles (undo doesn't make sense in exported subs, in fact it'll totally break the undo system) + LuaAssFile *subsobj = new LuaAssFile(L, subs, true/*allow modifications*/, false/*disallow undo*/); + // config + if (has_config && config_dialog) { + assert(config_dialog->LuaReadBack(L) == 1); + // TODO, write back stored options here + } + + LuaProgressSink *ps = new LuaProgressSink(L, export_dialog, false); + ps->SetTitle(GetName()); + + // do call + LuaThreadedCall call(L, 2, 0); + + ps->ShowModal(); + wxThread::ExitCode code = call.Wait(); + if (code) ThrowError(); + + delete ps; + } + + ScriptConfigDialog* LuaFeatureFilter::GenerateConfigDialog(wxWindow *parent) + { + if (!has_config) + return 0; + + GetFeatureFunction(2); // 2 = config dialog function + + // prepare function call + // subtitles (don't allow any modifications during dialog creation, ideally the subs aren't even accessed) + LuaAssFile *subsobj = new LuaAssFile(L, AssFile::top, false/*allow modifications*/, false/*disallow undo*/); + // stored options + lua_newtable(L); // TODO, nothing for now + + LuaProgressSink *ps = new LuaProgressSink(L, 0, false); + ps->SetTitle(GetName()); + + // do call + LuaThreadedCall call(L, 2, 1); + + ps->ShowModal(); + wxThread::ExitCode code = call.Wait(); + if (code) ThrowError(); + + delete ps; + + // The config dialog table should now be on stack + return config_dialog = new LuaConfigDialog(L, false); + } + + + // LuaProgressSink + + LuaProgressSink::LuaProgressSink(lua_State *_L, wxWindow *parent, bool allow_config_dialog) + : ProgressSink(parent) + , L(_L) + { + LuaProgressSink **ud = (LuaProgressSink**)lua_newuserdata(L, sizeof(LuaProgressSink*)); + *ud = this; + + // register progress reporting stuff + lua_getglobal(L, "aegisub"); + lua_newtable(L); + + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaSetProgress, 1); + lua_setfield(L, -2, "set"); + + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaSetTask, 1); + lua_setfield(L, -2, "task"); + + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaSetTitle, 1); + lua_setfield(L, -2, "title"); + + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaGetCancelled, 1); + lua_setfield(L, -2, "is_cancelled"); + + lua_setfield(L, -2, "progress"); + + lua_newtable(L); + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaDebugOut, 1); + lua_setfield(L, -2, "out"); + lua_setfield(L, -2, "debug"); + + if (allow_config_dialog) { + lua_newtable(L); + lua_pushvalue(L, -3); + lua_pushcclosure(L, LuaDisplayDialog, 1); + lua_setfield(L, -2, "display"); + lua_setfield(L, -2, "dialog"); + } + + // reference so other objects can also find the progress sink + lua_pushvalue(L, -2); + lua_setfield(L, LUA_REGISTRYINDEX, "progress_sink"); + + lua_pop(L, 2); + } + + LuaProgressSink::~LuaProgressSink() + { + // remove progress reporting stuff + lua_getglobal(L, "aegisub"); + lua_pushnil(L); + lua_setfield(L, -2, "progress"); + lua_pushnil(L); + lua_setfield(L, -2, "debug"); + lua_pop(L, 1); + lua_pushnil(L); + lua_setfield(L, LUA_REGISTRYINDEX, "progress_sink"); + } + + LuaProgressSink* LuaProgressSink::GetObjPointer(lua_State *L, int idx) + { + assert(lua_type(L, idx) == LUA_TUSERDATA); + void *ud = lua_touserdata(L, idx); + return *((LuaProgressSink**)ud); + } + + int LuaProgressSink::LuaSetProgress(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + float progress = lua_tonumber(L, 1); + ps->SetProgress(progress); + return 0; + } + + int LuaProgressSink::LuaSetTask(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + wxString task(lua_tostring(L, 1), wxConvUTF8); + ps->SetTask(task); + return 0; + } + + int LuaProgressSink::LuaSetTitle(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + wxString title(lua_tostring(L, 1), wxConvUTF8); + ps->SetTitle(title); + return 0; + } + + int LuaProgressSink::LuaGetCancelled(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + lua_pushboolean(L, ps->cancelled); + return 1; + } + + int LuaProgressSink::LuaDebugOut(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + wxString msg(lua_tostring(L, 1), wxConvUTF8); + ps->AddDebugOutput(msg); + return 0; + } + + int LuaProgressSink::LuaDisplayDialog(lua_State *L) + { + LuaProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); + + // Check that two arguments were actually given + // If only one, add another empty table for buttons + if (lua_gettop(L) == 1) { + lua_newtable(L); + } + // If more than two, remove the excess + if (lua_gettop(L) > 2) { + lua_settop(L, 2); + } + + // Send the "show dialog" event + // See comments in auto4_base.h for more info on this synchronisation + ShowConfigDialogEvent evt; + + LuaConfigDialog dlg(L, true); // magically creates the config dialog structure etc + evt.config_dialog = &dlg; + + wxSemaphore sema(0, 1); + evt.sync_sema = &sema; + + ps->AddPendingEvent(evt); + + sema.Wait(); + + // more magic: puts two values on stack: button pushed and table with control results + return dlg.LuaReadBack(L); + } + + + // Factory class for Lua scripts + // Not declared in header, since it doesn't need to be accessed from outside + // except through polymorphism + class LuaScriptFactory : public ScriptFactory { + public: + LuaScriptFactory() + { + engine_name = _T("Lua"); + filename_pattern = _T("*.lua"); + Register(this); + } + + virtual Script* Produce(const wxString &filename) const + { + // Just check if file extension is .lua + // Reject anything else + if (filename.Right(4).Lower() == _T(".lua")) { + return new LuaScript(filename); + } else { + return 0; + } + } + }; + LuaScriptFactory *_script_factory; + +}; + +void Initialise_Auto4Lua() +{ + Automation4::_script_factory = new Automation4::LuaScriptFactory; +} diff --git a/core/auto4_lua.h b/core/auto4_lua.h new file mode 100644 index 000000000..ad6813976 --- /dev/null +++ b/core/auto4_lua.h @@ -0,0 +1,244 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#pragma once + +#ifndef _AUTO4_LUA_H +#define _AUTO4_LUA_H + +#include "auto4_base.h" +#include +#include +#include +#include + +class wxWindow; + +namespace Automation4 { + + // Provides access to an AssFile object (and all lines contained) for a Lua script + class LuaAssFile { + private: + AssFile *ass; + lua_State *L; + + bool can_modify; + bool can_set_undo; + void CheckAllowModify(); // throws an error if modification is disallowed + + // keep a cursor of last accessed item to avoid walking over the entire file on every access + std::list::iterator last_entry_ptr; + int last_entry_id; + void GetAssEntry(int n); // set last_entry_ptr to point to item n + + static int ObjectIndexRead(lua_State *L); + static int ObjectIndexWrite(lua_State *L); + static int ObjectGetLen(lua_State *L); + static int ObjectDelete(lua_State *L); + static int ObjectDeleteRange(lua_State *L); + static int ObjectAppend(lua_State *L); + static int ObjectInsert(lua_State *L); + static int ObjectGarbageCollect(lua_State *L); + + static int LuaParseTagData(lua_State *L); + static int LuaUnparseTagData(lua_State *L); + static int LuaParseKaraokeData(lua_State *L); + static int LuaSetUndoPoint(lua_State *L); + + static LuaAssFile *GetObjPointer(lua_State *L, int idx); + + ~LuaAssFile(); + public: + static void AssEntryToLua(lua_State *L, AssEntry *e); // makes a Lua representation of AssEntry and places on the top of the stack + static AssEntry *LuaToAssEntry(lua_State *L); // assumes a Lua representation of AssEntry on the top of the stack, and creates an AssEntry object of it + + LuaAssFile(lua_State *_L, AssFile *_ass, bool _can_modify, bool _can_set_undo); + }; + + + class LuaConfigDialog; + + // Provides progress UI and control functions for a Lua script + class LuaProgressSink : public ProgressSink { + private: + lua_State *L; + + static int LuaSetProgress(lua_State *L); + static int LuaSetTask(lua_State *L); + static int LuaSetTitle(lua_State *L); + static int LuaGetCancelled(lua_State *L); + static int LuaDebugOut(lua_State *L); + static int LuaDisplayDialog(lua_State *L); + + public: + LuaProgressSink(lua_State *_L, wxWindow *parent, bool allow_config_dialog = true); + virtual ~LuaProgressSink(); + + static LuaProgressSink* GetObjPointer(lua_State *L, int idx); + }; + + + // Provides Config UI functions for a Lua script + class LuaConfigDialogControl { + public: + wxControl *cw; // control window + wxString name, hint; + int x, y, width, height; + + virtual wxControl *Create(wxWindow *parent) = 0; + virtual void ControlReadBack() = 0; + virtual void LuaReadBack(lua_State *L) = 0; + + LuaConfigDialogControl(lua_State *L); + }; + + class LuaConfigDialog : public ScriptConfigDialog { + private: + std::vector controls; + std::vector buttons; + bool use_buttons; + + class ButtonEventHandler : public wxEvtHandler { + public: + int *button_pushed; + void OnButtonPush(wxCommandEvent &evt); + }; + + ButtonEventHandler *button_event; + int button_pushed; + + protected: + wxWindow* CreateWindow(wxWindow *parent); + + public: + LuaConfigDialog(lua_State *_L, bool include_buttons); + virtual ~LuaConfigDialog(); + int LuaReadBack(lua_State *L); // read back internal structure to lua structures + + void ReadBack(); // from auto4 base + }; + + + // Second base-class for Lua implemented Features + class LuaFeature : public virtual Feature { + protected: + lua_State *L; + int myid; + + LuaFeature(lua_State *_L, ScriptFeatureClass _featureclass, const wxString &_name); + + void RegisterFeature(); + + void GetFeatureFunction(int functionid); + void CreateIntegerArray(std::vector &ints); + void ThrowError(); + }; + + + // Class of Lua scripts + class LuaScript : public Script { + friend class LuaFeature; + + private: + lua_State *L; + + void Create(); // load script and create internal structures etc. + void Destroy(); // destroy internal structures, unreg features and delete environment + + static LuaScript* GetScriptObject(lua_State *L); + + static int LuaTextExtents(lua_State *L); + static int LuaInclude(lua_State *L); + + public: + LuaScript(const wxString &filename); + virtual ~LuaScript(); + + virtual void Reload(); + }; + + + // A single call to a Lua function, run inside a separate thread. + // This object should be created on the stack in the function that does the call. + class LuaThreadedCall : public wxThread { + private: + lua_State *L; + int nargs; + int nresults; + public: + LuaThreadedCall(lua_State *_L, int _nargs, int _nresults); + virtual ExitCode Entry(); + }; + + + // Implementation of the Macro Feature for Lua scripts + class LuaFeatureMacro : public FeatureMacro, LuaFeature { + private: + bool no_validate; + protected: + LuaFeatureMacro(const wxString &_name, const wxString &_description, MacroMenu _menu, lua_State *_L); + public: + static int LuaRegister(lua_State *L); + virtual ~LuaFeatureMacro() { } + + virtual bool Validate(AssFile *subs, std::vector &selected, int active); + virtual void Process(AssFile *subs, std::vector &selected, int active, wxWindow *progress_parent); + }; + + + // Implementation of the Export Filter Feature for Lua scripts + class LuaFeatureFilter : public FeatureFilter, LuaFeature { + private: + bool has_config; + LuaConfigDialog *config_dialog; + + protected: + LuaFeatureFilter(const wxString &_name, const wxString &_description, int merit, lua_State *_L); + + ScriptConfigDialog* GenerateConfigDialog(wxWindow *parent); + + void Init(); + public: + static int LuaRegister(lua_State *L); + + void ProcessSubs(AssFile *subs, wxWindow *export_dialog); + }; + +}; + +// More or less dummy-function to make sure auto4_lua.cpp is linked in +void Initialise_Auto4Lua(); + +#endif diff --git a/core/auto4_lua_assfile.cpp b/core/auto4_lua_assfile.cpp new file mode 100644 index 000000000..c7ded085a --- /dev/null +++ b/core/auto4_lua_assfile.cpp @@ -0,0 +1,954 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#include "auto4_lua.h" +#include "ass_dialogue.h" +#include "ass_style.h" +#include "ass_file.h" +#include "ass_override.h" +#include +#include +#include +#include + +namespace Automation4 { + + // LuaAssFile + + void LuaAssFile::CheckAllowModify() + { + if (can_modify) + return; + lua_pushstring(L, "Attempt to modify subtitles in read-only feature context."); + lua_error(L); + } + + void LuaAssFile::AssEntryToLua(lua_State *L, AssEntry *e) + { + lua_newtable(L); + + wxString section(e->group); + lua_pushstring(L, section.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "section"); + + wxString raw(e->GetEntryData()); + lua_pushstring(L, raw.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "raw"); + + if (raw.Trim().IsEmpty()) { + lua_pushstring(L, "clear"); + + } else if (raw[0] == _T(';')) { + // "text" field, same as "raw" but with semicolon stripped + wxString text(raw, 1, raw.size()-1); + lua_pushstring(L, text.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + + lua_pushstring(L, "comment"); + + } else if (raw[0] == _T('[')) { + lua_pushstring(L, "head"); + + } else if (section.Lower() == _T("[script info]")) { + // assumed "info" class + + // first "key" + wxString key = raw.BeforeFirst(_T(':')); + lua_pushstring(L, key.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "key"); + + // then "value" + wxString value = raw.AfterFirst(_T(':')); + lua_pushstring(L, value.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "value"); + + lua_pushstring(L, "info"); + + } else if (raw.Left(7).Lower() == _T("format:")) { + + // TODO: parse the format line; just use a tokenizer + + lua_pushstring(L, "format"); + + } else if (e->GetType() == ENTRY_DIALOGUE) { + AssDialogue *dia = e->GetAsDialogue(e); + + lua_pushboolean(L, (int)dia->Comment); + lua_setfield(L, -2, "comment"); + + lua_pushnumber(L, dia->Layer); + lua_setfield(L, -2, "layer"); + + lua_pushnumber(L, dia->Start.GetMS()); + lua_setfield(L, -2, "start_time"); + lua_pushnumber(L, dia->End.GetMS()); + lua_setfield(L, -2, "end_time"); + + lua_pushstring(L, dia->Style.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "style"); + lua_pushstring(L, dia->Actor.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "actor"); + + lua_pushnumber(L, dia->MarginL); + lua_setfield(L, -2, "margin_l"); + lua_pushnumber(L, dia->MarginR); + lua_setfield(L, -2, "margin_r"); + lua_pushnumber(L, dia->MarginV); // duplicating MarginV to margin_t and margin_b here + lua_setfield(L, -2, "margin_t"); + lua_pushnumber(L, dia->MarginV); + lua_setfield(L, -2, "margin_b"); + + lua_pushstring(L, dia->Effect.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "effect"); + + lua_pushstring(L, ""); // tentative AS5 field + lua_setfield(L, -2, "userdata"); + + lua_pushstring(L, dia->Text.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + + lua_pushstring(L, "dialogue"); + + } else if (e->GetType() == ENTRY_STYLE) { + AssStyle *sty = e->GetAsStyle(e); + + lua_pushstring(L, sty->name.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "name"); + + lua_pushstring(L, sty->font.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "fontname"); + lua_pushnumber(L, sty->fontsize); + lua_setfield(L, -2, "fontsize"); + + lua_pushstring(L, sty->primary.GetASSFormatted(true).mb_str(wxConvUTF8)); + lua_setfield(L, -2, "color1"); + lua_pushstring(L, sty->secondary.GetASSFormatted(true).mb_str(wxConvUTF8)); + lua_setfield(L, -2, "color2"); + lua_pushstring(L, sty->outline.GetASSFormatted(true).mb_str(wxConvUTF8)); + lua_setfield(L, -2, "color3"); + lua_pushstring(L, sty->shadow.GetASSFormatted(true).mb_str(wxConvUTF8)); + lua_setfield(L, -2, "color4"); + + lua_pushboolean(L, (int)sty->bold); + lua_setfield(L, -2, "bold"); + lua_pushboolean(L, (int)sty->italic); + lua_setfield(L, -2, "italic"); + lua_pushboolean(L, (int)sty->underline); + lua_setfield(L, -2, "underline"); + lua_pushboolean(L, (int)sty->strikeout); + lua_setfield(L, -2, "strikeout"); + + lua_pushnumber(L, sty->scalex); + lua_setfield(L, -2, "scale_x"); + lua_pushnumber(L, sty->scaley); + lua_setfield(L, -2, "scale_y"); + + lua_pushnumber(L, sty->spacing); + lua_setfield(L, -2, "spacing"); + + lua_pushnumber(L, sty->angle); + lua_setfield(L, -2, "angle"); + + lua_pushnumber(L, sty->borderstyle); + lua_setfield(L, -2, "borderstyle"); + lua_pushnumber(L, sty->outline_w); + lua_setfield(L, -2, "outline"); + lua_pushnumber(L, sty->shadow_w); + lua_setfield(L, -2, "shadow"); + + lua_pushnumber(L, sty->alignment); + lua_setfield(L, -2, "align"); + + lua_pushnumber(L, sty->MarginL); + lua_setfield(L, -2, "margin_l"); + lua_pushnumber(L, sty->MarginR); + lua_setfield(L, -2, "margin_r"); + lua_pushnumber(L, sty->MarginV); // duplicating MarginV to margin_t and margin_b here + lua_setfield(L, -2, "margin_t"); + lua_pushnumber(L, sty->MarginV); + lua_setfield(L, -2, "margin_b"); + + lua_pushnumber(L, sty->encoding); + lua_setfield(L, -2, "encoding"); + + lua_pushnumber(L, 2); // From STS.h: "0: window, 1: video, 2: undefined (~window)" + lua_setfield(L, -2, "relative_to"); + + lua_pushboolean(L, false); // vertical writing, tentative AS5 field + lua_setfield(L, -2, "vertical"); + + lua_pushstring(L, "style"); + + } else { + lua_pushstring(L, "unknown"); + } + // store class of item; last thing done for each class specific code must be pushing the class name + lua_setfield(L, -2, "class"); + } + + AssEntry *LuaAssFile::LuaToAssEntry(lua_State *L) + { + // assume an assentry table is on the top of the stack + // convert it to a real AssEntry object, and pop the table from the stack + + if (!lua_istable(L, -1)) { + lua_pushstring(L, "Can't convert a non-table value to AssEntry"); + lua_error(L); + return 0; + } + + lua_getfield(L, -1, "class"); + if (!lua_isstring(L, -1)) { + lua_pushstring(L, "Table lacks 'class' field, can't convert to AssEntry"); + lua_error(L); + return 0; + } + wxString lclass(lua_tostring(L, -1), wxConvUTF8); + lclass.MakeLower(); + lua_pop(L, 1); + + AssEntry *result; + +#define GETSTRING(varname, fieldname, lineclass) \ + lua_getfield(L, -1, fieldname); \ + if (!lua_isstring(L, -1)) { \ + lua_pushstring(L, "Invalid string '" fieldname "' field in '" lineclass "' class subtitle line"); \ + lua_error(L); \ + return 0; \ + } \ + wxString varname (lua_tostring(L, -1), wxConvUTF8); \ + lua_pop(L, 1); +#define GETFLOAT(varname, fieldname, lineclass) \ + lua_getfield(L, -1, fieldname); \ + if (!lua_isnumber(L, -1)) { \ + lua_pushstring(L, "Invalid number '" fieldname "' field in '" lineclass "' class subtitle line"); \ + lua_error(L); \ + return 0; \ + } \ + float varname = lua_tonumber(L, -1); \ + lua_pop(L, 1); +#define GETINT(varname, fieldname, lineclass) \ + lua_getfield(L, -1, fieldname); \ + if (!lua_isnumber(L, -1)) { \ + lua_pushstring(L, "Invalid number '" fieldname "' field in '" lineclass "' class subtitle line"); \ + lua_error(L); \ + return 0; \ + } \ + int varname = lua_tointeger(L, -1); \ + lua_pop(L, 1); +#define GETBOOL(varname, fieldname, lineclass) \ + lua_getfield(L, -1, fieldname); \ + if (!lua_isboolean(L, -1)) { \ + lua_pushstring(L, "Invalid boolean '" fieldname "' field in '" lineclass "' class subtitle line"); \ + lua_error(L); \ + return 0; \ + } \ + bool varname = !!lua_toboolean(L, -1); \ + lua_pop(L, 1); + + GETSTRING(section, "section", "common") + + if (lclass == _T("clear")) { + result = new AssEntry(_T("")); + result->group = section; + + } else if (lclass == _T("comment")) { + GETSTRING(raw, "text", "comment") + raw.Prepend(_T(";")); + result = new AssEntry(raw); + result->group = section; + + } else if (lclass == _T("head")) { + result = new AssEntry(section); + result->group = section; + + } else if (lclass == _T("info")) { + GETSTRING(key, "key", "info") + GETSTRING(value, "value", "info") + result = new AssEntry(wxString::Format(_T("%s: %s"), key.c_str(), value.c_str())); + result->group = _T("[Script Info]"); // just so it can be read correctly back + + } else if (lclass == _T("format")) { + // ohshi- ... + // *FIXME* maybe ignore the actual data and just put some default stuff based on section? + result = new AssEntry(_T("Format: Auto4,Is,Broken")); + result->group = section; + + } else if (lclass == _T("style")) { + GETSTRING(name, "name", "style") + GETSTRING(fontname, "fontname", "style") + GETFLOAT(fontsize, "fontsize", "style") + GETSTRING(color1, "color1", "style") + GETSTRING(color2, "color2", "style") + GETSTRING(color3, "color3", "style") + GETSTRING(color4, "color4", "style") + GETBOOL(bold, "bold", "style") + GETBOOL(italic, "italic", "style") + GETBOOL(underline, "underline", "style") + GETBOOL(strikeout, "strikeout", "style") + GETFLOAT(scale_x, "scale_x", "style") + GETFLOAT(scale_y, "scale_y", "style") + GETINT(spacing, "spacing", "style") + GETFLOAT(angle, "angle", "style") + GETINT(borderstyle, "borderstyle", "style") + GETFLOAT(outline, "outline", "style") + GETFLOAT(shadow, "shadow", "style") + GETINT(align, "align", "style") + GETINT(margin_l, "margin_l", "style") + GETINT(margin_r, "margin_r", "style") + GETINT(margin_t, "margin_t", "style") + //GETINT(margin_b, "margin_b", "style") // skipping for now, since it's not used anyway + GETINT(encoding, "encoding", "style") + // leaving out relative_to and vertical + + AssStyle *sty = new AssStyle(); + sty->name = name; + sty->font = fontname; + sty->fontsize = fontsize; + sty->primary.ParseASS(color1); + sty->secondary.ParseASS(color2); + sty->outline.ParseASS(color3); + sty->shadow.ParseASS(color4); + sty->bold = bold; + sty->italic = italic; + sty->underline = underline; + sty->strikeout = strikeout; + sty->scalex = scale_x; + sty->scaley = scale_y; + sty->spacing = spacing; + sty->angle = angle; + sty->borderstyle = borderstyle; + sty->outline_w = outline; + sty->shadow_w = shadow; + sty->alignment = align; + sty->MarginL = margin_l; + sty->MarginR = margin_r; + sty->MarginV = margin_t; + sty->encoding = encoding; + sty->UpdateData(); + + result = sty; + + } else if (lclass == _T("styleex")) { + lua_pushstring(L, "Found line with class 'styleex' which is not supported. Wait until AS5 is a reality."); + lua_error(L); + return 0; + + } else if (lclass == _T("dialogue")) { + GETBOOL(comment, "comment", "dialogue") + GETINT(layer, "layer", "dialogue") + GETINT(start_time, "start_time", "dialogue") + GETINT(end_time, "end_time", "dialogue") + GETSTRING(style, "style", "dialogue") + GETSTRING(actor, "actor", "dialogue") + GETINT(margin_l, "margin_l", "dialogue") + GETINT(margin_r, "margin_r", "dialogue") + GETINT(margin_t, "margin_t", "dialogue") + //GETINT(margin_b, "margin_b", "dialogue") // skipping for now, since it's not used anyway + GETSTRING(effect, "effect", "dialogue") + //GETSTRING(userdata, "userdata", "dialogue") + GETSTRING(text, "text", "dialogue") + + AssDialogue *dia = new AssDialogue(); + dia->Comment = comment; + dia->Layer = layer; + dia->Start.SetMS(start_time); + dia->End.SetMS(end_time); + dia->Style = style; + dia->Actor = actor; + dia->MarginL = margin_l; + dia->MarginR = margin_r; + dia->MarginV = margin_t; + dia->Effect = effect; + dia->Text = text; + dia->UpdateData(); + + result = dia; + + } else { + lua_pushfstring(L, "Found line with unknown class: %s", lclass.mb_str(wxConvUTF8)); + lua_error(L); + return 0; + } + +#undef GETSTRING +#undef GETFLOAT +#undef GETINT +#undef GETBOOL + + //lua_pop(L, 1); // the function shouldn't eat the table it converted + return result; + } + + void LuaAssFile::GetAssEntry(int n) + { + entryIter e; + if (n < last_entry_id/2) { + // fastest to search from start + e = ass->Line.begin(); + last_entry_id = n; + while (n-- > 0) e++; + last_entry_ptr = e; + + } else if (last_entry_id + n > last_entry_id + ((int)ass->Line.size() - last_entry_id)/2) { + // fastest to search from end + int i = (int)ass->Line.size(); + e = ass->Line.end(); + last_entry_id = n; + while (i-- > n) e--; + last_entry_ptr = e; + + } else if (last_entry_id > n) { + // search backwards from last_entry_id + e = last_entry_ptr; + while (n < last_entry_id) e--, last_entry_id--; + last_entry_ptr = e; + + } else { + // search forwards from last_entry_id + e = last_entry_ptr; + // reqid and last_entry_id might be equal here, make sure the loop will still work + while (n > last_entry_id) e++, last_entry_id++; + last_entry_ptr = e; + } + } + + int LuaAssFile::ObjectIndexRead(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, 1); + + switch (lua_type(L, 2)) { + + case LUA_TNUMBER: + { + // read an indexed AssEntry + + // get requested index + int reqid = lua_tointeger(L, 2); + if (reqid <= 0 || reqid > (int)laf->ass->Line.size()) { + lua_pushfstring(L, "Requested out-of-range line from subtitle file: %d", reqid); + lua_error(L); + return 0; + } + + laf->GetAssEntry(reqid-1); + laf->AssEntryToLua(L, *laf->last_entry_ptr); + return 1; + } + + case LUA_TSTRING: + { + // either return n or a function doing further stuff + const char *idx = lua_tostring(L, 2); + + if (strcmp(idx, "n") == 0) { + // get number of items + lua_pushnumber(L, laf->ass->Line.size()); + return 1; + + } else if (strcmp(idx, "delete") == 0) { + // make a "delete" function + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectDelete, 1); + return 1; + + } else if (strcmp(idx, "deleterange") == 0) { + // make a "deleterange" function + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectDeleteRange, 1); + return 1; + + } else if (strcmp(idx, "insert") == 0) { + // make an "insert" function + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectInsert, 1); + return 1; + + } else if (strcmp(idx, "append") == 0) { + // make an "append" function + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectAppend, 1); + return 1; + + } else { + // idiot + lua_pushfstring(L, "Invalid indexing in Subtitle File object: '%s'", idx); + lua_error(L); + // should never return + } + assert(false); + } + + default: + { + // crap, user is stupid! + lua_pushfstring(L, "Attempt to index a Subtitle File object with value of type '%s'.", lua_typename(L, lua_type(L, 2))); + lua_error(L); + } + } + + assert(false); + return 0; + } + + int LuaAssFile::ObjectIndexWrite(lua_State *L) + { + // instead of implementing everything twice, just call the other modification-functions from here + // after modifying the stack to match their expectations + + if (!lua_isnumber(L, 2)) { + lua_pushstring(L, "Attempt to write to non-numeric index in subtitle index"); + lua_error(L); + return 0; + } + + LuaAssFile *laf = GetObjPointer(L, 1); + laf->CheckAllowModify(); + + int n = lua_tointeger(L, 2); + + if (n < 0) { + // insert line so new index is n + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectInsert, 1); + lua_pushinteger(L, -n); + lua_pushvalue(L, 3); + lua_call(L, 2, 0); + return 0; + + } else if (n == 0) { + // append line to list + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectAppend, 1); + lua_pushvalue(L, 3); + lua_call(L, 1, 0); + return 0; + + } else { + // replace line at index n or delete + if (!lua_isnil(L, 3)) { + // insert + AssEntry *e = LuaToAssEntry(L); + laf->GetAssEntry(n-1); + delete *laf->last_entry_ptr; + *laf->last_entry_ptr = e; + return 0; + + } else { + // delete + lua_pushvalue(L, 1); + lua_pushcclosure(L, ObjectDelete, 1); + lua_pushvalue(L, 2); + lua_call(L, 1, 0); + return 0; + + } + } + } + + int LuaAssFile::ObjectGetLen(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, 1); + lua_pushnumber(L, laf->ass->Line.size()); + return 1; + } + + int LuaAssFile::ObjectDelete(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + + laf->CheckAllowModify(); + + // get number of items to delete + int itemcount = lua_gettop(L); + std::vector ids; + ids.reserve(itemcount); + + // sort the item id's so we can delete from last to first to preserve original numbering + while (itemcount > 0) { + if (!lua_isnumber(L, itemcount)) { + lua_pushstring(L, "Attempt to delete non-numeric line id from Subtitle Object"); + lua_error(L); + return 0; + } + int n = lua_tointeger(L, itemcount); + if (n > (int)laf->ass->Line.size() || n < 1) { + lua_pushstring(L, "Attempt to delete out of range line id from Subtitle Object"); + lua_error(L); + return 0; + } + ids.push_back(n-1); // make C-style line ids + --itemcount; + } + std::sort(ids.begin(), ids.end()); + + // now delete the id's backwards + // start with the last one, to initialise things + laf->GetAssEntry(ids.back()); + // get an iterator to it, and increase last_entry_ptr so it'll still be valid after deletion, and point to the right index + entryIter e = laf->last_entry_ptr++; + laf->ass->Line.erase(e); + int n = laf->last_entry_id; + for (int i = (int)ids.size()-2; i >= 0; --i) { + int id = ids[i]; + while (id > n--) laf->last_entry_ptr--; + e = laf->last_entry_ptr++; + delete *e; + laf->ass->Line.erase(e); + } + laf->last_entry_id = n; + + return 0; + } + + int LuaAssFile::ObjectDeleteRange(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + + laf->CheckAllowModify(); + + if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) { + lua_pushstring(L, "Non-numeric argument given to deleterange"); + lua_error(L); + return 0; + } + + int a = lua_tointeger(L, 1), b = lua_tointeger(L, 2); + + if (a < 1) a = 1; + if (b > (int)laf->ass->Line.size()) b = (int)laf->ass->Line.size(); + + if (b < a) return 0; + + if (a == b) { + laf->GetAssEntry(a-1); + entryIter e = laf->last_entry_ptr++; + delete *e; + laf->ass->Line.erase(e); + return 0; + } + + entryIter ai, bi; + laf->GetAssEntry(a-1); + ai = laf->last_entry_ptr; + laf->GetAssEntry(b-1); + bi = laf->last_entry_ptr; + laf->last_entry_ptr++; + + laf->ass->Line.erase(ai, bi); + + return 0; + } + + int LuaAssFile::ObjectAppend(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + + laf->CheckAllowModify(); + + int n = lua_gettop(L); + + if (laf->last_entry_ptr != laf->ass->Line.begin()) { + laf->last_entry_ptr--; + laf->last_entry_id--; + } + + for (int i = 1; i <= n; i++) { + lua_pushvalue(L, i); + AssEntry *e = LuaToAssEntry(L); + laf->ass->Line.push_back(e); + } + + return 0; + } + + int LuaAssFile::ObjectInsert(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + + laf->CheckAllowModify(); + + if (!lua_isnumber(L, 1)) { + lua_pushstring(L, "Can't insert at non-numeric index"); + lua_error(L); + return 0; + } + + int n = lua_gettop(L); + + laf->GetAssEntry(lua_tonumber(L, 1)-1); + + for (int i = 2; i <= n; i++) { + lua_pushvalue(L, i); + AssEntry *e = LuaToAssEntry(L); + lua_pop(L, 1); + laf->ass->Line.insert(laf->last_entry_ptr, e); + laf->last_entry_id++; + } + + return 0; + } + + int LuaAssFile::ObjectGarbageCollect(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, 1); + delete laf; + wxLogDebug(_T(">>gc<< Garbage collected LuaAssFile")); + return 0; + } + + int LuaAssFile::LuaParseTagData(lua_State *L) + { + lua_newtable(L); + // TODO + return 1; + } + + int LuaAssFile::LuaUnparseTagData(lua_State *L) + { + lua_pushstring(L, ""); + // TODO + return 1; + } + + int LuaAssFile::LuaParseKaraokeData(lua_State *L) + { + AssEntry *e = LuaToAssEntry(L); + if (e->GetType() != ENTRY_DIALOGUE) { + delete e; + lua_pushstring(L, "Attempt to create karaoke table from non-dialogue subtitle line"); + lua_error(L); + return 0; + } + + AssDialogue *dia = e->GetAsDialogue(e); + dia->ParseASSTags(); + + int kcount = 0; + int kdur = 0; + int ktime = 0; + wxString ktag = _T(""); + wxString ktext = _T(""); + wxString ktext_stripped = _T(""); + + lua_newtable(L); + + for (int i = 0; i < (int)dia->Blocks.size(); i++) { + AssDialogueBlock *block = dia->Blocks[i]; + + switch (block->type) { + + case BLOCK_BASE: + assert(block->type != BLOCK_BASE); + break; + + case BLOCK_PLAIN: + ktext += block->text; + ktext_stripped += block->text; + break; + + case BLOCK_DRAWING: + // a drawing is regarded as a kind of control code here, so it's just stripped away + ktext += block->text; + break; + + case BLOCK_OVERRIDE: { + bool brackets_open = false; + AssDialogueBlockOverride *ovr = block->GetAsOverride(block); + + for (int j = 0; j < (int)ovr->Tags.size(); j++) { + AssOverrideTag *tag = ovr->Tags[j]; + + if (tag->IsValid() && tag->Name.Mid(0,2).CmpNoCase(_T("\\k")) == 0) { + // karaoke tag + if (brackets_open) { + ktext += _T("}"); + brackets_open = false; + } + + // store to lua + lua_newtable(L); + lua_pushnumber(L, kdur); + lua_setfield(L, -2, "duration"); + lua_pushnumber(L, ktime); + lua_setfield(L, -2, "start_time"); + lua_pushnumber(L, ktime+kdur); + lua_setfield(L, -2, "end_time"); + lua_pushstring(L, ktag.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "tag"); + lua_pushstring(L, ktext.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text_stripped"); + lua_rawseti(L, -2, kcount); + + // prepare new syllable + kcount++; + ktag = tag->Name.Mid(1); + // check if it's a "set time" tag, special handling for that (depends on previous syllable duration) + if (ktag == _T("kt")) { + ktime = tag->Params[0]->AsInt() * 10; + kdur = 0; + } else { + ktime += kdur; // duration of previous syllable + kdur = tag->Params[0]->AsInt() * 10; + } + ktext.clear(); + ktext_stripped.clear(); + + } else { + // not karaoke tag + if (!brackets_open) { + ktext += _T("{"); + brackets_open = true; + } + ktext += tag->ToString(); + } + + } + + if (brackets_open) { + ktext += _T("}"); + brackets_open = false; + } + + break;} + + } + + } + + dia->ClearBlocks(); + + // store final syllable/block to lua + lua_newtable(L); + lua_pushnumber(L, kdur); + lua_setfield(L, -2, "duration"); + lua_pushnumber(L, ktime); + lua_setfield(L, -2, "start_time"); + lua_pushnumber(L, ktime+kdur); + lua_setfield(L, -2, "end_time"); + lua_pushstring(L, ktag.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "tag"); + lua_pushstring(L, ktext.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text_stripped"); + lua_rawseti(L, -2, kcount); + + delete dia; + return 1; + } + + int LuaAssFile::LuaSetUndoPoint(lua_State *L) + { + LuaAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + if (!laf->can_set_undo) { + lua_pushstring(L, "Attempt to set an undo point in a context without undo-support."); + lua_error(L); + return 0; + } + + wxString description; + if (lua_isstring(L, 1)) { + description = wxString(lua_tostring(L, 1), wxConvUTF8); + lua_pop(L, 1); + } + AssFile::FlagAsModified(description); + + laf->ass = AssFile::top; // make sure we're still working on the most recent undo point + return 0; + } + + LuaAssFile *LuaAssFile::GetObjPointer(lua_State *L, int idx) + { + assert(lua_type(L, idx) == LUA_TUSERDATA); + void *ud = lua_touserdata(L, idx); + return *((LuaAssFile**)ud); + } + + LuaAssFile::~LuaAssFile() + { + } + + LuaAssFile::LuaAssFile(lua_State *_L, AssFile *_ass, bool _can_modify, bool _can_set_undo) + : ass(_ass) + , L(_L) + , can_modify(_can_modify) + , can_set_undo(_can_set_undo) + { + // prepare cursor + last_entry_ptr = ass->Line.begin(); + last_entry_id = 0; + + // prepare userdata object + void *ud = lua_newuserdata(L, sizeof(LuaAssFile*)); + *((LuaAssFile**)ud) = this; + + // make the metatable + lua_newtable(L); + lua_pushcfunction(L, ObjectIndexRead); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, ObjectIndexWrite); + lua_setfield(L, -2, "__newindex"); + lua_pushcfunction(L, ObjectGetLen); + lua_setfield(L, -2, "__len"); + lua_pushcfunction(L, ObjectGarbageCollect); + lua_setfield(L, -2, "__gc"); + lua_setmetatable(L, -2); + + // register misc functions + // assume the "aegisub" global table exists + lua_getglobal(L, "aegisub"); + assert(lua_type(L, -2) == LUA_TUSERDATA); + lua_pushvalue(L, -2); // the userdata object + lua_pushcclosure(L, LuaParseTagData, 1); + lua_setfield(L, -2, "parse_tag_data"); + assert(lua_type(L, -2) == LUA_TUSERDATA); + lua_pushvalue(L, -2); + lua_pushcclosure(L, LuaUnparseTagData, 1); + lua_setfield(L, -2, "unparse_tag_data"); + assert(lua_type(L, -2) == LUA_TUSERDATA); + lua_pushvalue(L, -2); + lua_pushcclosure(L, LuaParseKaraokeData, 1); + lua_setfield(L, -2, "parse_karaoke_data"); + assert(lua_type(L, -2) == LUA_TUSERDATA); + lua_pushvalue(L, -2); + lua_pushcclosure(L, LuaSetUndoPoint, 1); + lua_setfield(L, -2, "set_undo_point"); + lua_pop(L, 1); + } + +}; diff --git a/core/auto4_lua_dialog.cpp b/core/auto4_lua_dialog.cpp new file mode 100644 index 000000000..27f59c5f3 --- /dev/null +++ b/core/auto4_lua_dialog.cpp @@ -0,0 +1,592 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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 +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +#include "auto4_lua.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Automation4 { + + + // LuaConfigDialogControl + + LuaConfigDialogControl::LuaConfigDialogControl(lua_State *L) + { + // Assume top of stack is a control table (don't do checking) + + lua_getfield(L, -1, "name"); + if (lua_isstring(L, -1)) { + name = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + name = _T(""); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "x"); + if (lua_isnumber(L, -1)) { + x = lua_tointeger(L, -1); + if (x < 0) x = 0; + } else { + x = 0; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "y"); + if (lua_isnumber(L, -1)) { + y = lua_tointeger(L, -1); + if (y < 0) y = 0; + } else { + y = 0; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "width"); + if (lua_isnumber(L, -1)) { + width = lua_tointeger(L, -1); + if (width < 1) width = 1; + } else { + width = 1; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "height"); + if (lua_isnumber(L, -1)) { + height = lua_tointeger(L, -1); + if (height < 1) height = 1; + } else { + height = 1; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "hint"); + if (lua_isstring(L, -1)) { + hint = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + hint = _T(""); + } + lua_pop(L, 1); + + wxLogDebug(_T("created control: '%s', (%d,%d)(%d,%d), '%s'"), name.c_str(), x, y, width, height, hint.c_str()); + } + + namespace LuaControl { + + // Label + + class Label : public LuaConfigDialogControl { + public: + wxString label; + + Label(lua_State *L) + : LuaConfigDialogControl(L) + { + lua_getfield(L, -1, "label"); + label = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxStaticText(parent, -1, label); + } + + void ControlReadBack() + { + // Nothing here + } + + void LuaReadBack(lua_State *L) + { + // Label doesn't produce output, so let it be nil + lua_pushnil(L); + } + }; + + + // Basic edit + + class Edit : public LuaConfigDialogControl { + public: + wxString text; + + Edit(lua_State *L) + : LuaConfigDialogControl(L) + { + lua_getfield(L, -1, "text"); + text = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0); + } + + void ControlReadBack() + { + text = ((wxTextCtrl*)cw)->GetValue(); + } + + void LuaReadBack(lua_State *L) + { + lua_pushstring(L, text.mb_str(wxConvUTF8)); + } + + }; + + + // Multiline edit + + class Textbox : public Edit { + public: + + Textbox(lua_State *L) + : Edit(L) + { + // Nothing more + } + + wxControl *Create(wxWindow *parent) + { + cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE); + cw->SetMinSize(wxSize(0, 30)); + return cw; + } + + }; + + + // Integer only edit + + class IntEdit : public Edit { + public: + int value; + bool hasspin; + int min, max; + + IntEdit(lua_State *L) + : Edit(L) + { + lua_getfield(L, -1, "value"); + value = lua_tointeger(L, -1); + lua_pop(L, 1); + + hasspin = false; + + lua_getfield(L, -1, "min"); + if (!lua_isnumber(L, -1)) + goto nospin; + min = lua_tointeger(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "max"); + if (!lua_isnumber(L, -1)) + goto nospin; + max = lua_tointeger(L, -1); + lua_pop(L, 1); + + hasspin = true; +nospin: + if (!hasspin) { + lua_pop(L, 1); + } + } + + typedef wxValidator IntTextValidator; // TODO + wxControl *Create(wxWindow *parent) + { + if (hasspin) { + return cw = new wxSpinCtrl(parent, -1, wxString::Format(_T("%d"), value), wxDefaultPosition, wxDefaultSize, min, max, value); + } else { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0, IntTextValidator()); + } + } + + void ControlReadBack() + { + if (hasspin) { + value = ((wxSpinCtrl*)cw)->GetValue(); + } else { + long newval; + text = ((wxTextCtrl*)cw)->GetValue(); + if (text.ToLong(&newval)) { + value = newval; + } + } + } + + void LuaReadBack(lua_State *L) + { + lua_pushinteger(L, value); + } + + }; + + + // Float only edit + + class FloatEdit : public Edit { + public: + float value; + // FIXME: Can't support spin button atm + + FloatEdit(lua_State *L) + : Edit(L) + { + lua_getfield(L, -1, "value"); + value = lua_tointeger(L, -1); + lua_pop(L, 1); + + // TODO: spin button support + } + + typedef wxValidator FloatTextValidator; + wxControl *Create(wxWindow *parent) + { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0, FloatTextValidator()); + } + + void ControlReadBack() + { + double newval; + text = ((wxTextCtrl*)cw)->GetValue(); + if (text.ToDouble(&newval)) { + value = newval; + } + } + + void LuaReadBack(lua_State *L) + { + lua_pushnumber(L, value); + } + + }; + + + // Dropdown + + class Dropdown : public LuaConfigDialogControl { + public: + wxArrayString items; + wxString value; + + Dropdown(lua_State *L) + : LuaConfigDialogControl(L) + { + lua_getfield(L, -1, "value"); + value = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + + lua_getfield(L, -1, "items"); + lua_pushnil(L); + while (lua_next(L, -2)) { + if (lua_isstring(L, -1)) { + items.Add(wxString(lua_tostring(L, -1), wxConvUTF8)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxComboBox(parent, -1, value, wxDefaultPosition, wxDefaultSize, items, wxCB_READONLY); + } + + void ControlReadBack() + { + value = ((wxComboBox*)cw)->GetValue(); + } + + void LuaReadBack(lua_State *L) + { + lua_pushstring(L, value.mb_str(wxConvUTF8)); + } + + }; + + + // Checkbox + + class Checkbox : public LuaConfigDialogControl { + public: + wxString label; + bool value; + + Checkbox(lua_State *L) + : LuaConfigDialogControl(L) + { + lua_getfield(L, -1, "label"); + label = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + + lua_getfield(L, -1, "value"); + value = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxCheckBox(parent, -1, label); + } + + void ControlReadBack() + { + value = ((wxCheckBox*)cw)->GetValue(); + } + + void LuaReadBack(lua_State *L) + { + lua_pushboolean(L, value); + } + + }; + + }; + + + // LuaConfigDialog + + LuaConfigDialog::LuaConfigDialog(lua_State *L, bool include_buttons) + : use_buttons(include_buttons) + { + wxLogDebug(_T("creating LuaConfigDialog, this addr is %p"), this); + button_pushed = 0; + if (include_buttons) { + + if (!lua_istable(L, -1)) + // Just to avoid deeper indentation... + goto skipbuttons; + // Iterate over items in table + lua_pushnil(L); // initial key + while (lua_next(L, -2)) { + // Simply skip invalid items... FIXME, warn here? + if (lua_isstring(L, -1)) { + wxString s(lua_tostring(L, -1), wxConvUTF8); + buttons.push_back(s); + } + lua_pop(L, 1); + } +skipbuttons: + lua_pop(L, 1); + } + + // assume top of stack now contains a dialog table + if (!lua_istable(L, -1)) { + lua_pushstring(L, "Cannot create config dialog from something non-table"); + lua_error(L); + assert(false); + } + + // Ok, so there is a table with controls + lua_pushnil(L); // initial key + while (lua_next(L, -2)) { + if (lua_istable(L, -1)) { + // Get control class + lua_getfield(L, -1, "class"); + if (!lua_isstring(L, -1)) + goto badcontrol; + wxString controlclass(lua_tostring(L, -1), wxConvUTF8); + controlclass.LowerCase(); + lua_pop(L, 1); + + LuaConfigDialogControl *ctl; + + // Check control class and create relevant control + if (controlclass == _T("label")) { + ctl = new LuaControl::Label(L); + } else if (controlclass == _T("edit")) { + ctl = new LuaControl::Edit(L); + } else if (controlclass == _T("intedit")) { + ctl = new LuaControl::IntEdit(L); + } else if (controlclass == _T("floatedit")) { + ctl = new LuaControl::FloatEdit(L); + } else if (controlclass == _T("textbox")) { + ctl = new LuaControl::Textbox(L); + } else if (controlclass == _T("dropdown")) { + ctl = new LuaControl::Dropdown(L); + } else if (controlclass == _T("checkbox")) { + ctl = new LuaControl::Checkbox(L); + } else if (controlclass == _T("color")) { + // FIXME + ctl = new LuaControl::Edit(L); + } else if (controlclass == _T("coloralpha")) { + // FIXME + ctl = new LuaControl::Edit(L); + } else if (controlclass == _T("alpha")) { + // FIXME + ctl = new LuaControl::Edit(L); + } else { + goto badcontrol; + } + + controls.push_back(ctl); + + } else { +badcontrol: + // not a control... + // FIXME, better error reporting? + lua_pushstring(L, "bad control table entry"); + lua_error(L); + } + lua_pop(L, 1); + } + } + + LuaConfigDialog::~LuaConfigDialog() + { + for (size_t i = 0; i < controls.size(); ++i) + delete controls[i]; + } + + wxWindow* LuaConfigDialog::CreateWindow(wxWindow *parent) + { + wxWindow *w = new wxPanel(parent); + wxGridBagSizer *s = new wxGridBagSizer(4, 4); + + for (size_t i = 0; i < controls.size(); ++i) { + LuaConfigDialogControl *c = controls[i]; + c->Create(w); + if (dynamic_cast(c)) { + s->Add(c->cw, wxGBPosition(c->y, c->x), wxGBSpan(c->height, c->width), wxALIGN_CENTRE_VERTICAL|wxALIGN_LEFT); + } else { + s->Add(c->cw, wxGBPosition(c->y, c->x), wxGBSpan(c->height, c->width), wxEXPAND); + } + } + + if (use_buttons) { + wxStdDialogButtonSizer *bs = new wxStdDialogButtonSizer(); + if (buttons.size() > 0) { + wxLogDebug(_T("creating user buttons")); + for (size_t i = 0; i < buttons.size(); ++i) { + wxLogDebug(_T("button '%s' gets id %d"), buttons[i].c_str(), 1001+(wxWindowID)i); + bs->Add(new wxButton(w, 1001+(wxWindowID)i, buttons[i])); + } + } else { + wxLogDebug(_T("creating default buttons")); + bs->Add(new wxButton(w, wxID_OK)); + bs->Add(new wxButton(w, wxID_CANCEL)); + } + bs->Realize(); + + button_event = new ButtonEventHandler(); + button_event->button_pushed = &button_pushed; + // passing button_event as userdata because wx will then delete it + w->Connect(wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(LuaConfigDialog::ButtonEventHandler::OnButtonPush), button_event, button_event); + wxLogDebug(_T("set event handler, this addr is %p"), this); + + wxBoxSizer *ms = new wxBoxSizer(wxVERTICAL); + ms->Add(s, 0, wxBOTTOM, 5); + ms->Add(bs); + w->SetSizerAndFit(ms); + } else { + w->SetSizerAndFit(s); + } + + return w; + } + + int LuaConfigDialog::LuaReadBack(lua_State *L) + { + // First read back which button was pressed, if any + if (use_buttons) { + wxLogDebug(_T("reading back button_pushed")); + int btn = button_pushed; + if (btn == 0) { + wxLogDebug(_T("was zero, cancelled")); + // Always cancel/closed + lua_pushboolean(L, 0); + } else { + wxLogDebug(_T("nonzero, something else: %d"), btn); + if (buttons.size() > 0) { + wxLogDebug(_T("user button: %s"), buttons[btn-1].c_str()); + // button_pushed is index+1 to reserve 0 for Cancel + lua_pushstring(L, buttons[btn-1].mb_str(wxConvUTF8)); + } else { + wxLogDebug(_T("default button, must be Ok")); + // Cancel case already covered, must be Ok then + lua_pushboolean(L, 1); + } + } + } + + // Then read controls back + lua_newtable(L); + for (size_t i = 0; i < controls.size(); ++i) { + controls[i]->LuaReadBack(L); + lua_setfield(L, -2, controls[i]->name.mb_str(wxConvUTF8)); + } + + if (use_buttons) { + return 2; + } else { + return 1; + } + } + + void LuaConfigDialog::ReadBack() + { + for (size_t i = 0; i < controls.size(); ++i) { + controls[i]->ControlReadBack(); + } + } + + void LuaConfigDialog::ButtonEventHandler::OnButtonPush(wxCommandEvent &evt) + { + // Let button_pushed == 0 mean "cancelled", such that pushing Cancel or closing the dialog + // will both result in button_pushed == 0 + if (evt.GetId() == wxID_OK) { + wxLogDebug(_T("was wxID_OK")); + *button_pushed = 1; + } else if (evt.GetId() == wxID_CANCEL) { + wxLogDebug(_T("was wxID_CANCEL")); + *button_pushed = 0; + } else { + wxLogDebug(_T("was user button")); + // Therefore, when buttons are numbered from 1001 to 1000+n, make sure to set it to i+1 + *button_pushed = evt.GetId() - 1000; + evt.SetId(wxID_OK); // hack to make sure the dialog will be closed + } + wxLogDebug(_T("button_pushed set to %d"), *button_pushed); + evt.Skip(); + } + +}; diff --git a/core/changelog.txt b/core/changelog.txt index d8d50a861..376875443 100644 --- a/core/changelog.txt +++ b/core/changelog.txt @@ -4,6 +4,10 @@ Please visit http://aegisub.net to download latest version = 1.11 beta - 2006.xx.xx =========================== - New Aegisub logo. (AMZ) +- Automation 4 has replaced Automation 3, see the help file for more details (jfs) + o Automation 4 Lua uses Lua 5.1 instead of 5.0, meaning some new language features + o It is now possible to write macros that manipulates subtitles directly + o Scripts have full access to the entire subtitle file, not just the "Events" section - Support reading SSA/ASS files with intermixed V4 and V4+ Styles sections (jfs) - Fixed loading of sections with unexpected cases. (AMZ) - Changes to Audio Spectrum: (jfs)