diff --git a/src/command/video.cpp b/src/command/video.cpp index fde138f28..8299f0981 100644 --- a/src/command/video.cpp +++ b/src/command/video.cpp @@ -32,6 +32,8 @@ #include "command.h" #include "../ass_dialogue.h" +#include "../ass_file.h" +#include "../ass_style.h" #include "../async_video_provider.h" #include "../compat.h" #include "../dialog_detached_video.h" @@ -45,6 +47,7 @@ #include "../options.h" #include "../project.h" #include "../selection_controller.h" +#include "../text_selection_controller.h" #include "../utils.h" #include "../video_controller.h" #include "../video_display.h" @@ -54,16 +57,20 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include namespace { using cmd::Command; + using namespace boost::adaptors; struct validator_video_loaded : public Command { CMD_TYPE(COMMAND_VALIDATE) @@ -692,6 +699,253 @@ struct video_play_line final : public validator_video_loaded { } }; +struct parsed_line { + AssDialogue *line; + std::vector> blocks; + + parsed_line(AssDialogue *line) : line(line), blocks(line->ParseTags()) { } + parsed_line(parsed_line&& r) = default; + + const AssOverrideTag *find_tag(int blockn, std::string const& tag_name, std::string const& alt) const { + for (auto ovr : blocks | sliced(0, blockn + 1) | reversed | agi::of_type()) { + for (auto const& tag : ovr->Tags | reversed) { + if (tag.Name == tag_name || tag.Name == alt) + return &tag; + } + } + return nullptr; + } + + template + T get_value(int blockn, T initial, std::string const& tag_name, std::string const& alt = "") const { + auto tag = find_tag(blockn, tag_name, alt); + if (tag) + return tag->Params[0].template Get(initial); + return initial; + } + + int block_at_pos(int pos) const { + auto const& text = line->Text.get(); + int n = 0; + int max = text.size() - 1; + bool in_block = false; + + for (int i = 0; i <= max; ++i) { + if (text[i] == '{') { + if (!in_block && i > 0 && pos >= 0) + ++n; + in_block = true; + } + else if (text[i] == '}' && in_block) { + in_block = false; + if (pos > 0 && (i + 1 == max || text[i + 1] != '{')) + n++; + } + else if (!in_block) { + if (--pos == 0) + return n + (i < max && text[i + 1] == '{'); + } + } + + return n - in_block; + } + + int set_tag(std::string const& tag, std::string const& value, int norm_pos, int orig_pos) { + int blockn = block_at_pos(norm_pos); + + AssDialogueBlockPlain *plain = nullptr; + AssDialogueBlockOverride *ovr = nullptr; + while (blockn >= 0 && !plain && !ovr) { + AssDialogueBlock *block = blocks[blockn].get(); + switch (block->GetType()) { + case AssBlockType::PLAIN: + plain = static_cast(block); + break; + case AssBlockType::DRAWING: + --blockn; + break; + case AssBlockType::COMMENT: + --blockn; + orig_pos = line->Text.get().rfind('{', orig_pos); + break; + case AssBlockType::OVERRIDE: + ovr = static_cast(block); + break; + } + } + + // If we didn't hit a suitable block for inserting the override just put + // it at the beginning of the line + if (blockn < 0) + orig_pos = 0; + + std::string insert(tag + value); + int shift = insert.size(); + if (plain || blockn < 0) { + line->Text = line->Text.get().substr(0, orig_pos) + "{" + insert + "}" + line->Text.get().substr(orig_pos); + shift += 2; + blocks = line->ParseTags(); + } + else if (ovr) { + std::string alt; + if (tag == "\\c") alt = "\\1c"; + // Remove old of same + bool found = false; + for (size_t i = 0; i < ovr->Tags.size(); i++) { + std::string const& name = ovr->Tags[i].Name; + if (tag == name || alt == name) { + shift -= ((std::string)ovr->Tags[i]).size(); + if (found) { + ovr->Tags.erase(ovr->Tags.begin() + i); + i--; + } + else { + ovr->Tags[i].Params[0].Set(value); + found = true; + } + } + } + if (!found) + ovr->AddTag(insert); + + line->UpdateText(blocks); + } + else + assert(false); + + return shift; + } +}; + +int normalize_pos(std::string const& text, int pos) { + int plain_len = 0; + bool in_block = false; + + for (int i = 0, max = text.size() - 1; i < pos && i <= max; ++i) { + if (text[i] == '{') + in_block = true; + if (!in_block) + ++plain_len; + if (text[i] == '}' && in_block) + in_block = false; + } + + return plain_len; +} + +template +void update_lines(const agi::Context *c, wxString const& undo_msg, Func&& f) { + const auto active_line = c->selectionController->GetActiveLine(); + const int sel_start = c->textSelectionController->GetSelectionStart(); + const int sel_end = c->textSelectionController->GetSelectionEnd(); + const int norm_sel_start = normalize_pos(active_line->Text, sel_start); + const int norm_sel_end = normalize_pos(active_line->Text, sel_end); + int active_sel_shift = 0; + + for (const auto line : c->selectionController->GetSelectedSet()) { + int shift = f(line, sel_start, sel_end, norm_sel_start, norm_sel_end); + if (line == active_line) + active_sel_shift = shift; + } + + auto const& sel = c->selectionController->GetSelectedSet(); + c->ass->Commit(undo_msg, AssFile::COMMIT_DIAG_TEXT, -1, sel.size() == 1 ? *sel.begin() : nullptr); + if (active_sel_shift != 0) + c->textSelectionController->SetSelection(sel_start + active_sel_shift, sel_end + active_sel_shift); +} + +static void video_set_color_from_cursor(agi::Context *c, const char *tag, const char *alt) { + Vector2D coords = c->videoDisplay->GetMousePosition(); + wxImage img = get_image(c, false); + agi::Color new_color( + img.GetRed(coords.X(), coords.Y()), + img.GetGreen(coords.X(), coords.Y()), + img.GetBlue(coords.X(), coords.Y()) + ); + + agi::Color initial_color; + const auto active_line = c->selectionController->GetActiveLine(); + const int sel_start = c->textSelectionController->GetSelectionStart(); + const int sel_end = c->textSelectionController->GetSelectionStart(); + const int norm_sel_start = normalize_pos(active_line->Text, sel_start); + + auto const& sel = c->selectionController->GetSelectedSet(); + using line_info = std::pair; + std::vector lines; + for (auto line : sel) { + AssStyle const* const style = c->ass->GetStyle(line->Style); + + parsed_line parsed(line); + int blockn = parsed.block_at_pos(norm_sel_start); + + agi::Color color = parsed.get_value(blockn, color, tag, alt); + + if (line == active_line) + initial_color = color; + + lines.emplace_back(color, std::move(parsed)); + } + + int active_shift = 0; + int commit_id = -1; + { + for (auto& line : lines) { + int shift = line.second.set_tag(tag, new_color.GetAssOverrideFormatted(), norm_sel_start, sel_start); + + if (line.second.line == active_line) + active_shift = shift; + } + + commit_id = c->ass->Commit(_("set color"), AssFile::COMMIT_DIAG_TEXT, commit_id, sel.size() == 1 ? *sel.begin() : nullptr); + if (active_shift) + c->textSelectionController->SetSelection(sel_start + active_shift, sel_start + active_shift); + } +} + +struct video_set_color_primary final : public validator_video_loaded { + CMD_NAME("video/frame/set_color_primary") + STR_MENU("Set primary fill color") + STR_DISP("Set primary fill color") + STR_HELP("Sets the primary fill color (\\c) at the text cursor position from the pixel at the mouse cursor position") + + void operator()(agi::Context *c) override { + video_set_color_from_cursor(c, "\\c", "\\1c"); + } +}; + +struct video_set_color_secondary final : public validator_video_loaded { + CMD_NAME("video/frame/set_color_secondary") + STR_MENU("Set secondary fill color") + STR_DISP("Set secondary fill color") + STR_HELP("Sets the secondary fill color (\\2c) at the text cursor position from the pixel at the mouse cursor position") + + void operator()(agi::Context *c) override { + video_set_color_from_cursor(c, "\\2c", ""); + } +}; + +struct video_set_color_outline final : public validator_video_loaded { + CMD_NAME("video/frame/set_color_outline") + STR_MENU("Set outline color") + STR_DISP("Set outline color") + STR_HELP("Sets the outline color (\\3c) at the text cursor position from the pixel at the mouse cursor position") + + void operator()(agi::Context *c) override { + video_set_color_from_cursor(c, "\\3c", ""); + } +}; + +struct video_set_color_shadow final : public validator_video_loaded { + CMD_NAME("video/frame/set_color_shadow") + STR_MENU("Set shadow color") + STR_DISP("Set shadow color") + STR_HELP("Sets the shadow color (\\4c) at the text cursor position from the pixel at the mouse cursor position") + + void operator()(agi::Context *c) override { + video_set_color_from_cursor(c, "\\4c", ""); + } +}; + struct video_show_overscan final : public validator_video_loaded { CMD_NAME("video/show_overscan") STR_MENU("Show &Overscan Mask") @@ -839,6 +1093,10 @@ namespace cmd { reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); diff --git a/src/libresrc/default_menu.json b/src/libresrc/default_menu.json index 2007f3eb5..5d2ff39bc 100644 --- a/src/libresrc/default_menu.json +++ b/src/libresrc/default_menu.json @@ -224,6 +224,11 @@ { "command" : "app/log" } ], "video_context" : [ + { "command" : "video/frame/set_color_primary"}, + { "command" : "video/frame/set_color_secondary"}, + { "command" : "video/frame/set_color_outline"}, + { "command" : "video/frame/set_color_shadow"}, + {}, { "command" : "video/frame/save" }, { "command" : "video/frame/copy" }, {},