diff --git a/src/ass_dialogue.h b/src/ass_dialogue.h index 786d675911..fed9c26f50 100644 --- a/src/ass_dialogue.h +++ b/src/ass_dialogue.h @@ -26,9 +26,11 @@ // POSSIBILITY OF SUCH DAMAGE. // // Aegisub Project http://www.aegisub.org/ +#pragma once #include "ass_entry.h" #include "ass_override.h" +#include "fold_controller.h" #include @@ -124,6 +126,9 @@ struct AssDialogueBase { int Row = -1; + /// Data describing line folds starting or ending at this line + FoldInfo Fold; + /// Is this a comment line? bool Comment = false; /// Layer number diff --git a/src/ass_file.cpp b/src/ass_file.cpp index bd06c562e8..743bbfea24 100644 --- a/src/ass_file.cpp +++ b/src/ass_file.cpp @@ -175,6 +175,8 @@ int AssFile::Commit(wxString const& desc, int type, int amend_id, AssDialogue *s event.Row = i++; } + AnnouncePreCommit(type, single_line); + PushState({desc, &amend_id, single_line}); AnnounceCommit(type, single_line); diff --git a/src/ass_file.h b/src/ass_file.h index db848cd1ef..bf5f393982 100644 --- a/src/ass_file.h +++ b/src/ass_file.h @@ -27,6 +27,8 @@ // // Aegisub Project http://www.aegisub.org/ +#pragma once + #include "ass_entry.h" #include @@ -52,6 +54,13 @@ struct ExtradataEntry { std::string value; }; +// Both start and end are inclusive +struct LineFold { + int start; + int end; + bool collapsed; +}; + struct AssFileCommit { wxString const& message; int *commit_id; @@ -76,11 +85,13 @@ struct ProjectProperties { int active_row = 0; int ar_mode = 0; int video_position = 0; + std::vector folds; }; class AssFile { /// A set of changes has been committed to the file (AssFile::COMMITType) agi::signal::Signal AnnounceCommit; + agi::signal::Signal AnnouncePreCommit; agi::signal::Signal PushState; public: /// The lines in the file @@ -166,8 +177,11 @@ class AssFile { COMMIT_DIAG_FULL = COMMIT_DIAG_META | COMMIT_DIAG_TIME | COMMIT_DIAG_TEXT, /// Extradata entries were added/modified/removed COMMIT_EXTRADATA = 0x100, + /// Folds were added or removed + COMMIT_FOLD = 0x200, }; + DEFINE_SIGNAL_ADDERS(AnnouncePreCommit, AddPreCommitListener) DEFINE_SIGNAL_ADDERS(AnnounceCommit, AddCommitListener) DEFINE_SIGNAL_ADDERS(PushState, AddUndoManager) diff --git a/src/ass_parser.cpp b/src/ass_parser.cpp index f55fc3567f..e9e3c78998 100644 --- a/src/ass_parser.cpp +++ b/src/ass_parser.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -39,7 +40,8 @@ class AssParser::HeaderToProperty { using field = boost::variant< std::string ProjectProperties::*, int ProjectProperties::*, - double ProjectProperties::* + double ProjectProperties::*, + std::vector ProjectProperties::* >; std::unordered_map fields; @@ -58,6 +60,7 @@ class AssParser::HeaderToProperty { {"Video Zoom Percent", &ProjectProperties::video_zoom}, {"Scroll Position", &ProjectProperties::scroll_position}, {"Active Line", &ProjectProperties::active_row}, + {"Line Folds", &ProjectProperties::folds}, {"Video Position", &ProjectProperties::video_position}, {"Video AR Mode", &ProjectProperties::ar_mode}, {"Video AR Value", &ProjectProperties::ar_value}, @@ -80,6 +83,29 @@ class AssParser::HeaderToProperty { void operator()(std::string ProjectProperties::*f) const { obj.*f = value; } void operator()(int ProjectProperties::*f) const { try_parse(value, &(obj.*f)); } void operator()(double ProjectProperties::*f) const { try_parse(value, &(obj.*f)); } + void operator()(std::vector ProjectProperties::*f) const { + std::vector folds; + + for (auto foldstr : agi::Split(value, ',')) { + LineFold fold; + std::vector parsed; + agi::Split(parsed, foldstr, ':'); + if (parsed.size() != 3) { + continue; + } + + int collapsed; + try_parse(parsed[0], &fold.start); + try_parse(parsed[1], &fold.end); + try_parse(parsed[2], &collapsed); + fold.collapsed = !!collapsed; + + if (fold.start > 0 && fold.end > fold.start) { + folds.push_back(fold); + } + } + obj.*f = folds; + } } visitor {target->Properties, value}; boost::apply_visitor(visitor, it->second); return true; diff --git a/src/auto4_lua_assfile.cpp b/src/auto4_lua_assfile.cpp index 3aef3b7694..f98e0a35db 100644 --- a/src/auto4_lua_assfile.cpp +++ b/src/auto4_lua_assfile.cpp @@ -40,6 +40,7 @@ #include "ass_karaoke.h" #include "ass_style.h" #include "compat.h" +#include "fold_controller.h" #include #include @@ -100,6 +101,16 @@ namespace { return ret; } + template + void get_userdata_field(lua_State *L, const char *name, const char *line_class, T *target) + { + lua_getfield(L, -1, name); + if (!lua_isuserdata(L, -1)) + throw bad_field("userdata", name, line_class); + *target = *static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + } + using namespace Automation4; template int closure_wrapper(lua_State *L) @@ -181,6 +192,10 @@ namespace Automation4 { set_field(L, "text", dia->Text); + // preserve the folds + *static_cast(lua_newuserdata(L, sizeof(FoldInfo))) = dia->Fold; + lua_setfield(L, -2, "_foldinfo"); + // create extradata table lua_newtable(L); for (auto const& ed : ass->GetExtradata(dia->ExtradataIds)) { @@ -301,6 +316,7 @@ namespace Automation4 { dia->Margin[2] = get_int_field(L, "margin_t", "dialogue"); dia->Effect = get_string_field(L, "effect", "dialogue"); dia->Text = get_string_field(L, "text", "dialogue"); + get_userdata_field(L, "_foldinfo", "dialogue", &dia->Fold); std::vector new_ids; diff --git a/src/base_grid.cpp b/src/base_grid.cpp index 151b15be03..b01a0dac22 100644 --- a/src/base_grid.cpp +++ b/src/base_grid.cpp @@ -37,6 +37,7 @@ #include "ass_file.h" #include "audio_box.h" #include "compat.h" +#include "fold_controller.h" #include "grid_column.h" #include "options.h" #include "project.h" @@ -100,6 +101,8 @@ BaseGrid::BaseGrid(wxWindow* parent, agi::Context *context) OPT_SUB("Colour/Subtitle Grid/Background/Inframe", &BaseGrid::UpdateStyle, this), OPT_SUB("Colour/Subtitle Grid/Background/Selected Comment", &BaseGrid::UpdateStyle, this), OPT_SUB("Colour/Subtitle Grid/Background/Selection", &BaseGrid::UpdateStyle, this), + OPT_SUB("Colour/Subtitle Grid/Background/Open Fold", &BaseGrid::UpdateStyle, this), + OPT_SUB("Colour/Subtitle Grid/Background/Closed Fold", &BaseGrid::UpdateStyle, this), OPT_SUB("Colour/Subtitle Grid/Collision", &BaseGrid::UpdateStyle, this), OPT_SUB("Colour/Subtitle Grid/Header", &BaseGrid::UpdateStyle, this), OPT_SUB("Colour/Subtitle Grid/Left Column", &BaseGrid::UpdateStyle, this), @@ -127,7 +130,7 @@ BEGIN_EVENT_TABLE(BaseGrid,wxWindow) END_EVENT_TABLE() void BaseGrid::OnSubtitlesCommit(int type) { - if (type == AssFile::COMMIT_NEW || type & AssFile::COMMIT_ORDER || type & AssFile::COMMIT_DIAG_ADDREM) + if (type == AssFile::COMMIT_NEW || type & AssFile::COMMIT_ORDER || type & AssFile::COMMIT_DIAG_ADDREM || type & AssFile::COMMIT_FOLD) UpdateMaps(); if (type & AssFile::COMMIT_DIAG_META) { @@ -184,6 +187,8 @@ void BaseGrid::UpdateStyle() { row_colors.Comment.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Comment")->GetColor())); row_colors.Visible.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Inframe")->GetColor())); row_colors.SelectedComment.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Selected Comment")->GetColor())); + row_colors.FoldOpen.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Open Fold")->GetColor())); + row_colors.FoldClosed.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Closed Fold")->GetColor())); row_colors.LeftCol.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Left Column")->GetColor())); SetColumnWidths(); @@ -194,10 +199,14 @@ void BaseGrid::UpdateStyle() { void BaseGrid::UpdateMaps() { index_line_map.clear(); + vis_index_line_map.clear(); for (auto& curdiag : context->ass->Events) index_line_map.push_back(&curdiag); + for (AssDialogue *curdiag = &*context->ass->Events.begin(); curdiag != nullptr; curdiag = curdiag->Fold.getNextVisible()) + vis_index_line_map.push_back(&*curdiag); + SetColumnWidths(); AdjustScrollbar(); Refresh(false); @@ -215,6 +224,10 @@ void BaseGrid::OnActiveLineChanged(AssDialogue *new_active) { } void BaseGrid::MakeRowVisible(int row) { + MakeVisRowVisible(GetDialogue(row)->Fold.getVisibleRow()); +} + +void BaseGrid::MakeVisRowVisible(int row) { int h = GetClientSize().GetHeight(); if (row < yPos + 1) @@ -224,9 +237,9 @@ void BaseGrid::MakeRowVisible(int row) { } void BaseGrid::SelectRow(int row, bool addToSelected, bool select) { - if (row < 0 || (size_t)row >= index_line_map.size()) return; + if (row < 0 || (size_t)row >= vis_index_line_map.size()) return; - AssDialogue *line = index_line_map[row]; + AssDialogue *line = vis_index_line_map[row]; if (!addToSelected) { context->selectionController->SetSelectedSet(Selection{line}); @@ -246,11 +259,11 @@ void BaseGrid::SelectRow(int row, bool addToSelected, bool select) { void BaseGrid::OnSeek() { int lines = GetClientSize().GetHeight() / lineHeight + 1; - lines = mid(0, lines, GetRows() - yPos); + lines = mid(0, lines, GetVisRows() - yPos); auto it = begin(visible_rows); for (int i : boost::irange(yPos, yPos + lines)) { - if (IsDisplayed(index_line_map[i])) { + if (IsDisplayed(vis_index_line_map[i])) { if (it == end(visible_rows) || *it != i) { Refresh(false); return; @@ -338,7 +351,7 @@ void BaseGrid::OnPaint(wxPaintEvent &) { // Paint the rows const int drawPerScreen = h/lineHeight + 1; - const int nDraw = mid(0, drawPerScreen, GetRows() - yPos); + const int nDraw = mid(0, drawPerScreen, GetVisRows() - yPos); const int grid_x = columns[0]->Width(); const auto active_line = context->selectionController->GetActiveLine(); @@ -347,7 +360,7 @@ void BaseGrid::OnPaint(wxPaintEvent &) { for (int i : agi::util::range(nDraw)) { wxBrush color = row_colors.Default; - AssDialogue *curDiag = index_line_map[i + yPos]; + AssDialogue *curDiag = vis_index_line_map[i + yPos]; bool inSel = !!selection.count(curDiag); if (inSel && curDiag->Comment) @@ -362,6 +375,11 @@ void BaseGrid::OnPaint(wxPaintEvent &) { color = row_colors.Visible; visible_rows.push_back(i + yPos); } + + if (curDiag->Fold.hasFold() && !inSel) { + color = curDiag->Fold.isFolded() ? row_colors.FoldClosed : row_colors.FoldOpen; + } + dc.SetBrush(color); // Draw row background color @@ -406,10 +424,10 @@ void BaseGrid::OnPaint(wxPaintEvent &) { dc.DrawLine(w, 0, w, maxH); } - if (active_line && active_line->Row >= yPos && active_line->Row < yPos + nDraw) { + if (active_line && active_line->Fold.getVisibleRow() >= yPos && active_line->Fold.getVisibleRow() < yPos + nDraw) { dc.SetPen(wxPen(to_wx(OPT_GET("Colour/Subtitle Grid/Active Border")->GetColor()))); dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRectangle(0, (active_line->Row - yPos + 1) * lineHeight, w, lineHeight + 1); + dc.DrawRectangle(0, (active_line->Fold.getVisibleRow() - yPos + 1) * lineHeight, w, lineHeight + 1); } } @@ -437,17 +455,28 @@ void BaseGrid::OnMouseEvent(wxMouseEvent &event) { bool dclick = event.LeftDClick(); int row = event.GetY() / lineHeight + yPos - 1; if (holding && !click) - row = mid(0, row, GetRows()-1); - AssDialogue *dlg = GetDialogue(row); + row = mid(0, row, GetVisRows()-1); + AssDialogue *dlg = GetVisDialogue(row); if (!dlg) row = 0; + // Find the column the mouse is over + int colx = event.GetX(); + int col; + for (col = 0; col < columns.size(); col++) { + int w = columns[col]->Width(); + if (colx < w) { + break; + } + colx -= w; + } + if (event.ButtonDown() && OPT_GET("Subtitle/Grid/Focus Allow")->GetBool()) SetFocus(); if (holding) { if (!event.LeftIsDown()) { if (dlg) - MakeRowVisible(row); + MakeVisRowVisible(row); holding = false; ReleaseMouse(); } @@ -470,6 +499,10 @@ void BaseGrid::OnMouseEvent(wxMouseEvent &event) { CaptureMouse(); } + if (columns[col]->OnMouseEvent(dlg, context, event)) { + return; + } + if ((click || holding || dclick) && dlg) { int old_extend = extendRow; @@ -517,7 +550,7 @@ void BaseGrid::OnMouseEvent(wxMouseEvent &event) { // Toggle each Selection newsel; if (ctrl) newsel = selection; - for (int i = i1; i <= i2; i++) + for (int i = VisRowToRow(i1); i <= VisRowToRow(i2); i++) newsel.insert(GetDialogue(i)); context->selectionController->SetSelectedSet(std::move(newsel)); return; @@ -555,7 +588,7 @@ void BaseGrid::OnContextMenu(wxContextMenuEvent &evt) { } void BaseGrid::ScrollTo(int y) { - int nextY = mid(0, y, GetRows() - 1); + int nextY = mid(0, y, GetVisRows() - 1); if (yPos != nextY) { context->ass->Properties.scroll_position = yPos = nextY; scrollBar->SetThumbPosition(yPos); @@ -570,7 +603,7 @@ void BaseGrid::AdjustScrollbar() { scrollBar->Freeze(); scrollBar->SetSize(clientSize.GetWidth() - scrollbarSize.GetWidth(), 0, scrollbarSize.GetWidth(), clientSize.GetHeight()); - if (GetRows() <= 1) { + if (GetVisRows() <= 1) { yPos = 0; scrollBar->Enable(false); scrollBar->Thaw(); @@ -581,7 +614,7 @@ void BaseGrid::AdjustScrollbar() { scrollBar->Enable(true); int drawPerScreen = clientSize.GetHeight() / lineHeight; - int rows = GetRows(); + int rows = GetVisRows(); context->ass->Properties.scroll_position = yPos = mid(0, yPos, rows - 1); @@ -618,6 +651,16 @@ AssDialogue *BaseGrid::GetDialogue(int n) const { return index_line_map[n]; } +AssDialogue *BaseGrid::GetVisDialogue(int n) const { + if (static_cast(n) >= vis_index_line_map.size()) return nullptr; + return vis_index_line_map[n]; +} + +int BaseGrid::VisRowToRow(int n) const { + AssDialogue *d = GetVisDialogue(n); + return d != nullptr ? d->Row : GetRows() - 1; +} + bool BaseGrid::IsDisplayed(const AssDialogue *line) const { if (!context->project->VideoProvider()) return false; int frame = context->videoController->GetFrameN(); @@ -665,11 +708,11 @@ void BaseGrid::OnKeyDown(wxKeyEvent &event) { } else if (key == WXK_HOME) { dir = -1; - step = GetRows(); + step = GetVisRows(); } else if (key == WXK_END) { dir = 1; - step = GetRows(); + step = GetVisRows(); } if (!dir) { @@ -679,8 +722,8 @@ void BaseGrid::OnKeyDown(wxKeyEvent &event) { auto active_line = context->selectionController->GetActiveLine(); int old_extend = extendRow; - int next = mid(0, (active_line ? active_line->Row : 0) + dir * step, GetRows() - 1); - context->selectionController->SetActiveLine(GetDialogue(next)); + int next = mid(0, (active_line ? active_line->Fold.getVisibleRow() : 0) + dir * step, GetVisRows() - 1); + context->selectionController->SetActiveLine(GetVisDialogue(next)); // Move selection if (!ctrl && !shift && !alt) { @@ -703,12 +746,12 @@ void BaseGrid::OnKeyDown(wxKeyEvent &event) { // Select range Selection newsel; - for (int i = begin; i <= end; i++) + for (int i = VisRowToRow(begin); i <= VisRowToRow(end); i++) newsel.insert(GetDialogue(i)); context->selectionController->SetSelectedSet(std::move(newsel)); - MakeRowVisible(next); + MakeVisRowVisible(next); return; } } diff --git a/src/base_grid.h b/src/base_grid.h index 366189588c..9b68b1da30 100644 --- a/src/base_grid.h +++ b/src/base_grid.h @@ -80,9 +80,12 @@ class BaseGrid final : public wxWindow { wxBrush Visible; wxBrush SelectedComment; wxBrush LeftCol; + wxBrush FoldOpen; + wxBrush FoldClosed; } row_colors; std::vector index_line_map; ///< Row number -> dialogue line + std::vector vis_index_line_map; ///< Visible Row number -> dialogue line /// Connection for video seek event. Stored explicitly so that it can be /// blocked if the relevant option is disabled @@ -115,13 +118,22 @@ class BaseGrid final : public wxWindow { void SelectRow(int row, bool addToSelected = false, bool select=true); int GetRows() const { return index_line_map.size(); } + int GetVisRows() const { return vis_index_line_map.size(); } void MakeRowVisible(int row); + void MakeVisRowVisible(int row); /// @brief Get dialogue by index /// @param n Index to look up /// @return Subtitle dialogue line for index, or 0 if invalid index AssDialogue *GetDialogue(int n) const; + /// @brief Get visible dialogue by the displayed row's index + /// @param n Displayed ndex to look up + /// @return Visible ubtitle dialogue line for index, or 0 if invalid index + AssDialogue *GetVisDialogue(int n) const; + + int VisRowToRow(int n) const; + public: BaseGrid(wxWindow* parent, agi::Context *context); ~BaseGrid(); diff --git a/src/command/grid.cpp b/src/command/grid.cpp index d0a9d165b7..d591fd1670 100644 --- a/src/command/grid.cpp +++ b/src/command/grid.cpp @@ -35,6 +35,7 @@ #include "../ass_file.h" #include "../audio_controller.h" #include "../audio_timing.h" +#include "../fold_controller.h" #include "../frame_main.h" #include "../include/aegisub/context.h" #include "../libresrc/libresrc.h" @@ -398,6 +399,123 @@ struct grid_swap final : public Command { } }; +struct grid_fold_create final : public Command { + CMD_NAME("grid/fold/create") + STR_MENU("Create new Fold") + STR_DISP("Create new Fold") + STR_HELP("Create a new fold collapsing the selected lines into a group") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return c->selectionController->GetSelectedSet().size() >= 2; + } + + void operator()(agi::Context *c) override { + auto const& sel = c->selectionController->GetSortedSelection(); + if (sel.size() >= 2) { + c->foldController->AddFold(**sel.begin(), **sel.rbegin(), true); + c->selectionController->SetSelectionAndActive({ *sel.begin() }, *sel.begin()); + } + } +}; + +struct grid_fold_open final : public Command { + CMD_NAME("grid/fold/open") + STR_MENU("Open Folds") + STR_DISP("Open Folds") + STR_HELP("Expand the folds under the selected lines") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return c->foldController->AreFoldsAt(c->selectionController->GetSortedSelection()); + } + + void operator()(agi::Context *c) override { + c->foldController->OpenFoldsAt(c->selectionController->GetSortedSelection()); + } +}; + +struct grid_fold_close final : public Command { + CMD_NAME("grid/fold/close") + STR_MENU("Close Folds") + STR_DISP("Close Folds") + STR_HELP("Collapse the folds around the selected lines") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return c->foldController->AreFoldsAt(c->selectionController->GetSortedSelection()); + } + + void operator()(agi::Context *c) override { + c->foldController->CloseFoldsAt(c->selectionController->GetSortedSelection()); + } +}; + +struct grid_fold_clear final : public Command { + CMD_NAME("grid/fold/clear") + STR_MENU("Clear Folds") + STR_DISP("Clear Folds") + STR_HELP("Remove the folds around the selected lines") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return c->foldController->AreFoldsAt(c->selectionController->GetSortedSelection()); + } + + void operator()(agi::Context *c) override { + c->foldController->ClearFoldsAt(c->selectionController->GetSortedSelection()); + } +}; + +struct grid_fold_toggle final : public Command { + CMD_NAME("grid/fold/toggle") + STR_MENU("Toggle Folds") + STR_DISP("Toggle Folds") + STR_HELP("Open or close the folds around the selected lines") + CMD_TYPE(COMMAND_VALIDATE) + + bool Validate(const agi::Context *c) override { + return c->foldController->AreFoldsAt(c->selectionController->GetSortedSelection()); + } + + void operator()(agi::Context *c) override { + c->foldController->ToggleFoldsAt(c->selectionController->GetSortedSelection()); + } +}; + +struct grid_fold_open_all final : public Command { + CMD_NAME("grid/fold/open_all") + STR_MENU("Open all Folds") + STR_DISP("Open all Folds") + STR_HELP("Open all Folds") + + void operator()(agi::Context *c) override { + c->foldController->OpenAllFolds(); + } +}; + +struct grid_fold_close_all final : public Command { + CMD_NAME("grid/fold/close_all") + STR_MENU("Close all Folds") + STR_DISP("Close all Folds") + STR_HELP("Close all Folds") + + void operator()(agi::Context *c) override { + c->foldController->CloseAllFolds(); + } +}; + +struct grid_fold_clear_all final : public Command { + CMD_NAME("grid/fold/clear_all") + STR_MENU("Clear all Folds") + STR_DISP("Clear all Folds") + STR_HELP("Remove all Folds") + + void operator()(agi::Context *c) override { + c->foldController->ClearAllFolds(); + } +}; + } namespace cmd { @@ -420,6 +538,14 @@ 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()); + reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); reg(agi::make_unique()); diff --git a/src/context.cpp b/src/context.cpp index a2203e0263..df80abb710 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -20,6 +20,7 @@ #include "audio_controller.h" #include "auto4_base.h" #include "dialog_manager.h" +#include "fold_controller.h" #include "initial_line_state.h" #include "options.h" #include "project.h" @@ -40,6 +41,7 @@ Context::Context() , project(make_unique(this)) , local_scripts(make_unique(this)) , selectionController(make_unique(this)) +, foldController(make_unique(this)) , videoController(make_unique(this)) , audioController(make_unique(this)) , initialLineState(make_unique(this)) diff --git a/src/fold_controller.cpp b/src/fold_controller.cpp new file mode 100644 index 0000000000..56de5aaf4f --- /dev/null +++ b/src/fold_controller.cpp @@ -0,0 +1,315 @@ +// Copyright (c) 2022, arch1t3cht > +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#include "fold_controller.h" + +#include "ass_file.h" +#include "include/aegisub/context.h" +#include "format.h" +#include "subs_controller.h" + +#include +#include + +#include + +static int next_fold_id = 0; + +FoldController::FoldController(agi::Context *c) +: context(c) +, pre_commit_listener(c->ass->AddPreCommitListener(&FoldController::FixFoldsPreCommit, this)) +{ } + + +bool FoldController::CanAddFold(AssDialogue& start, AssDialogue& end) { + if (start.Fold.exists || end.Fold.exists) { + return false; + } + int folddepth = 0; + for (auto it = std::next(context->ass->Events.begin(), start.Row); it->Row < end.Row; it++) { + if (it->Fold.exists) { + folddepth += it->Fold.side ? -1 : 1; + } + if (folddepth < 0) { + return false; + } + } + return folddepth == 0; +} + +void FoldController::RawAddFold(AssDialogue& start, AssDialogue& end, bool collapsed) { + int id = next_fold_id++; + + start.Fold.exists = true; + start.Fold.collapsed = collapsed; + start.Fold.id = id; + start.Fold.side = false; + + end.Fold.exists = true; + end.Fold.collapsed = collapsed; + end.Fold.id = id; + end.Fold.side = true; +} + +void FoldController::AddFold(AssDialogue& start, AssDialogue& end, bool collapsed) { + if (CanAddFold(start, end)) { + RawAddFold(start, end, true); + context->ass->Commit(_("add fold"), AssFile::COMMIT_FOLD); + } +} + +bool FoldController::DoForAllFolds(bool action(AssDialogue& line)) { + for (AssDialogue& line : context->ass->Events) { + if (line.Fold.exists) { + if (action(line)) { + return true; + } + } + } + return false; +} + +void FoldController::FixFoldsPreCommit(int type, const AssDialogue *single_line) { + if ((type & (AssFile::COMMIT_FOLD | AssFile::COMMIT_DIAG_ADDREM | AssFile::COMMIT_ORDER)) || type == AssFile::COMMIT_NEW) { + if (type == AssFile::COMMIT_NEW && context->subsController->IsUndoStackEmpty()) { + // This might be the biggest hack in all of this. We want to hook into the FileOpen signal to + // read and apply the folds from the project data, but if we do it naively, this will only happen + // after the first commit has been pushed to the undo stack. Thus, if a user uses Ctrl+Z after opening + // a file, all folds will be cleared. + // Instead, we hook into the first commit which is made after loading a file, right after the undo stack was cleared. + DoForAllFolds(FoldController::ActionClearFold); + MakeFoldsFromFile(); + } + FixFolds(); + } +} + +void FoldController::MakeFoldsFromFile() { + if (context->ass->Properties.folds.empty()) { + return; + } + + int numlines = context->ass->Events.size(); + for (LineFold fold : context->ass->Properties.folds) { + if (fold.start > 0 && fold.start < fold.end && fold.end <= numlines) { + auto opener = std::next(context->ass->Events.begin(), fold.start); + RawAddFold(*opener, *std::next(opener, fold.end - fold.start), fold.collapsed); + } + } +} + + +// For each line in lines, applies action() to the opening delimiter of the innermost fold containing this line. +// Returns true as soon as any action() call returned true. +// +// In general, this can leave the folds in an inconsistent state, so unless action() is read-only this should always +// be followed by a commit. +bool FoldController::DoForFoldsAt(std::vector const& lines, bool action(AssDialogue& line)) { + for (AssDialogue *line : lines) { + if (line->Fold.parent != nullptr && !(line->Fold.exists && !line->Fold.side)) { + line = line->Fold.parent; + } + if (!line->Fold.visited && action(*line)) { + return true; + } + line->Fold.visited = true; + } + return false; +} + +void FoldController::FixFolds() { + // Stack of which folds we've desended into so far + std::vector foldStack; + + // ID's for which we've found starters + std::unordered_map foldHeads; + + // ID's for which we've either found a valid starter and ender, + // or determined that the respective fold is invalid. All further + // fold data with this ID is skipped and deleted. + std::unordered_map completedFolds; + + for (auto line = context->ass->Events.begin(); line != context->ass->Events.end(); line++) { + if (line->Fold.exists) { + if (completedFolds.count(line->Fold.id)) { // Duplicate entry + line->Fold.exists = false; + continue; + } + if (!line->Fold.side) { + if (foldHeads.count(line->Fold.id)) { // Duplicate entry + line->Fold.exists = false; + } else { + foldHeads[line->Fold.id] = &*line; + foldStack.push_back(&*line); + } + } else { + if (!foldHeads.count(line->Fold.id)) { // Non-matching ender + // Deactivate it. Because we can, also push it to completedFolds: + // If its counterpart appears further below, we can delete it right away. + completedFolds[line->Fold.id] = true; + line->Fold.exists = false; + } else { + // We found a fold. Now we need to see if the stack matches. + // We scan our stack for the counterpart of the fold. + // If one exists, we assume all starters above it are invalid. + // If none exists, we assume this ender is invalid. + // If none of these assumptions are true, the folds are probably + // broken beyond repair. + + completedFolds[line->Fold.id] = true; + bool found = false; + for (int i = foldStack.size() - 1; i >= 0; i--) { + if (foldStack[i]->Fold.id == line->Fold.id) { + // Erase all folds further inward + for (int j = foldStack.size() - 1; j > i; j--) { + completedFolds[foldStack[j]->Fold.id] = true; + foldStack[j]->Fold.exists = false; + foldStack.pop_back(); + } + + // Sync the found fold and pop the stack + line->Fold.collapsed = foldStack[i]->Fold.collapsed; + foldStack.pop_back(); + + found = true; + break; + } + } + if (!found) { + completedFolds[line->Fold.id] = true; + line->Fold.exists = false; + } + } + } + } + } + + // All remaining lines are invalid + for (AssDialogue *line : foldStack) { + line->Fold.exists = false; + } + + LinkFolds(); +} + +void FoldController::LinkFolds() { + std::vector foldStack; + AssDialogue *lastVisible = nullptr; + context->ass->Properties.folds.clear(); + + maxdepth = 0; + + int visibleRow = 0; + int highestFolded = 1; + for (auto line = context->ass->Events.begin(); line != context->ass->Events.end(); line++) { + line->Fold.parent = foldStack.empty() ? nullptr : foldStack.back(); + line->Fold.nextVisible = nullptr; + line->Fold.visible = highestFolded > foldStack.size(); + line->Fold.visited = false; + line->Fold.visibleRow = visibleRow; + + if (line->Fold.visible) { + if (lastVisible != nullptr) { + lastVisible->Fold.nextVisible = &*line; + } + lastVisible = &*line; + visibleRow++; + } + if (line->Fold.exists && !line->Fold.side) { + foldStack.push_back(&*line); + if (!line->Fold.collapsed && highestFolded == foldStack.size()) { + highestFolded++; + } + if (foldStack.size() > maxdepth) { + maxdepth = foldStack.size(); + } + } + if (line->Fold.exists && line->Fold.side) { + context->ass->Properties.folds.push_back(LineFold { + foldStack.back()->Row, + line->Row, + line->Fold.collapsed, + }); + + line->Fold.counterpart = foldStack.back(); + (*foldStack.rbegin())->Fold.counterpart = &*line; + + if (highestFolded >= foldStack.size()) { + highestFolded = foldStack.size(); + } + + foldStack.pop_back(); + } + } +} + +int FoldController::GetMaxDepth() { + return maxdepth; +} + +bool FoldController::ActionHasFold(AssDialogue& line) { return line.Fold.exists; } + +bool FoldController::ActionClearFold(AssDialogue& line) { line.Fold.exists = false; return false; } + +bool FoldController::ActionOpenFold(AssDialogue& line) { line.Fold.collapsed = false; return false; } + +bool FoldController::ActionCloseFold(AssDialogue& line) { line.Fold.collapsed = true; return false; } + +bool FoldController::ActionToggleFold(AssDialogue& line) { line.Fold.collapsed = !line.Fold.collapsed; return false; } + + +void FoldController::ClearAllFolds() { + FoldController::DoForAllFolds(FoldController::ActionClearFold); + context->ass->Commit(_("clear all folds"), AssFile::COMMIT_FOLD); +} + +void FoldController::OpenAllFolds() { + FoldController::DoForAllFolds(FoldController::ActionOpenFold); + context->ass->Commit(_("open all folds"), AssFile::COMMIT_FOLD); +} + +void FoldController::CloseAllFolds() { + FoldController::DoForAllFolds(FoldController::ActionCloseFold); + context->ass->Commit(_("close all folds"), AssFile::COMMIT_FOLD); +} + +bool FoldController::HasFolds() { + return FoldController::DoForAllFolds(FoldController::ActionHasFold); +} + +void FoldController::ClearFoldsAt(std::vector const& lines) { + FoldController::DoForFoldsAt(lines, FoldController::ActionClearFold); + context->ass->Commit(_("clear folds"), AssFile::COMMIT_FOLD); +} + +void FoldController::OpenFoldsAt(std::vector const& lines) { + FoldController::DoForFoldsAt(lines, FoldController::ActionOpenFold); + context->ass->Commit(_("open folds"), AssFile::COMMIT_FOLD); +} + +void FoldController::CloseFoldsAt(std::vector const& lines) { + FoldController::DoForFoldsAt(lines, FoldController::ActionCloseFold); + context->ass->Commit(_("close folds"), AssFile::COMMIT_FOLD); +} + +void FoldController::ToggleFoldsAt(std::vector const& lines) { + FoldController::DoForFoldsAt(lines, FoldController::ActionToggleFold); + context->ass->Commit(_("toggle folds"), AssFile::COMMIT_FOLD); +} + +bool FoldController::AreFoldsAt(std::vector const& lines) { + return FoldController::DoForFoldsAt(lines, FoldController::ActionHasFold); +} diff --git a/src/fold_controller.h b/src/fold_controller.h new file mode 100644 index 0000000000..c1adfb8f33 --- /dev/null +++ b/src/fold_controller.h @@ -0,0 +1,173 @@ +// Copyright (c) 2022, arch1t3cht > +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#pragma once + +#include +#include "ass_file.h" + +#include + +namespace agi { struct Context; } + +/// We allow hiding ass lines using cascading folds, each of which collapses a contiguous collection of dialogue lines into a single one. +/// A fold is described by inclusive start and end points of the contiguous set of dialogue line it extends over. +/// An existing fold can be active (collapsed) or inactive (existing, but not collapsed at the moment) +/// A fold may *strictly* contain other folds or be *strictly* contained in other folds, but it may not intersect another fold with +/// an intersection set not equal to one of the two folds. +/// Only one fold may be started or ended at any given line. + +/// Since we need to track how the folds move when lines are inserted or deleted, we need to represent the fold +/// data as part of the individual AssDialogue lines. Hooking into insertion or deletion calls is not possible +/// without extensive restructuring, and also wouldn't interact well with undo/redo functionality. +/// +/// Because of this, we store the data defining folds as part of the AssDialogue lines. We use a pre-commit hook +/// to fix any format violations after changes are made. Furthermore, to be able to traverse the folds more easily, +/// we compute various metadata and set up pointers between the fold parts. + +/// Part of the data for an AssDialogue object, describing folds starting or ending at this line. +class FoldInfo { + // Base data describing the folds:w + + /// Whether a fold starts or ends at the line. All other fields are only valid if this is true. + bool exists = false; + /// Whether the fold is currently collapsed + bool collapsed = false; + /// False if a fold is started here, true otherwise. + bool side = false; + /// A unique ID describing the fold. The other end of the fold has a matching ID and the opposite value for side. + int id = 0; + + // Used in DoForFoldsAt to ensure each line is visited only once + bool visited = false; + + // The following is cached data used for making traversing folds more efficient. These are only valid directly after + // a commit and shouldn't be changed outside of the pre-commit handler. + + /// Whether the line is currently visible + bool visible = true; + + /// If exists is true, this is a pointer to the other line with the given fold id + AssDialogue *counterpart = nullptr; + + /// A pointer to the opener of the innermost fold containing the line, if one exists. + /// If the line starts a fold, this points to the next bigger fold. + AssDialogue *parent = nullptr; + + /// If this line is visible, this points to the next visible line, if one exists + AssDialogue *nextVisible = nullptr; + + /// The row number where this line would appear in the subtitle grid. That is, the ordinary + /// Row value, but with hidden lines skipped. + /// Out of all AssDialogue lines with the same visibleRow, only the one with the lowest Row is shown. + int visibleRow; + + friend class FoldController; + +public: + bool hasFold() const { return exists; } + bool isFolded() const { return collapsed; } + bool isEnd() const { return side; } + + // The following functions are only valid directly after a commit. + // Their behaviour is undefined as soon as any uncommitted change is made to the Events. + AssDialogue *getFoldOpener() const { return parent; } + AssDialogue *getNextVisible() const { return nextVisible; } + int getVisibleRow() const { return visibleRow; } +}; + +#include "ass_dialogue.h" + +class FoldController { + agi::Context *context; + agi::signal::Connection pre_commit_listener; + int maxdepth = 0; + + bool CanAddFold(AssDialogue& start, AssDialogue& end); + + void RawAddFold(AssDialogue& start, AssDialogue& end, bool collapsed); + + bool DoForFoldsAt(std::vector const& lines, bool action(AssDialogue& line)); + + bool DoForAllFolds(bool action(AssDialogue& line)); + + void FixFoldsPreCommit(int type, const AssDialogue *single_line); + + void MakeFoldsFromFile(); + + // These are used for the DoForAllFolds action and should not be used as ordinary getters/setters + + static bool ActionHasFold(AssDialogue& line); + + static bool ActionClearFold(AssDialogue& line); + + static bool ActionOpenFold(AssDialogue& line); + + static bool ActionCloseFold(AssDialogue& line); + + static bool ActionToggleFold(AssDialogue& line); + + /// After lines have been added or deleted, this ensures consistency again. Run with every relevant commit. + void FixFolds(); + + /// If the fold base dataa is valid, sets up all the cached links in the FoldData + void LinkFolds(); + +public: + FoldController(agi::Context *context); + + int GetMaxDepth(); + + // All of the following functions are only valid directly after a commit. + // Their behaviour is undefined as soon as any uncommitted change is made to the Events. + + /// @brief Add a new fold + /// + /// The new fold must not intersect with any existing fold. + /// + /// Calling this method should only cause a commit if the fold was + /// successfully added. + void AddFold(AssDialogue& start, AssDialogue& end, bool collapsed); + + void ClearAllFolds(); + + void OpenAllFolds(); + + void CloseAllFolds(); + + bool HasFolds(); + + /// @brief Remove the folds in which the given lines are contained, if they exist + /// @param lines The lines whose folds should be removed + void ClearFoldsAt(std::vector const& lines); + + /// @brief Open the folds in which the given lines are contained, if they exist + /// @param lines The lines whose folds should be opened + void OpenFoldsAt(std::vector const& lines); + + /// @brief Open or closes the folds in which the given lines are contained, if they exist + /// @param lines The lines whose folds should be opened + void ToggleFoldsAt(std::vector const& lines); + + /// @brief Close the folds in which the given lines are contained, if they exist + /// @param lines The lines whose folds should be closed + void CloseFoldsAt(std::vector const& lines); + + /// @brief Returns whether any of the given lines are contained in folds + /// @param lines The lines + bool AreFoldsAt(std::vector const& lines); + +}; diff --git a/src/grid_column.cpp b/src/grid_column.cpp index 321a09439f..08e165a418 100644 --- a/src/grid_column.cpp +++ b/src/grid_column.cpp @@ -22,6 +22,7 @@ #include "include/aegisub/context.h" #include "options.h" #include "video_controller.h" +#include "fold_controller.h" #include @@ -125,6 +126,53 @@ T max_value(T AssDialogueBase::*field, EntryList const& lines) { return value; } +struct GridColumnFolds final : GridColumn { + COLUMN_HEADER(_(" >")) + COLUMN_DESCRIPTION(_("Folds")) + bool Centered() const override { return false; } + + wxString Value(const AssDialogue *d, const agi::Context *) const override { + std::string value; + if (d->Fold.hasFold()) { + if (!d->Fold.isEnd()) { + value = d->Fold.isFolded() ? ">" : "v"; + } else if (!d->Fold.isFolded()) { + value = "-"; + } + while (d->Fold.getFoldOpener()) { + d = d->Fold.getFoldOpener(); + value = " " + value; + } + } + return " " + value; + } + + bool OnMouseEvent(AssDialogue *d, agi::Context *c, wxMouseEvent &event) const override { + if ((event.LeftDown() || event.LeftDClick()) && !event.ShiftDown() && !event.CmdDown() && !event.AltDown()) { + if (d->Fold.hasFold() && !d->Fold.isEnd()) { + std::vector lines; + lines.push_back(d); + c->foldController->ToggleFoldsAt(lines); + return true; + } + } + return false; + } + + int Width(const agi::Context *c, WidthHelper &helper) const override { + int maxdepth = c->foldController->GetMaxDepth(); + if (maxdepth == 0) { + return 0; + } + std::string maxentry; + for (int i = 0; i < maxdepth; i++) { + maxentry += " "; + } + maxentry += ">"; + return helper(maxentry); + } +}; + struct GridColumnLayer final : GridColumn { COLUMN_HEADER(_("L")) COLUMN_DESCRIPTION(_("Layer")) @@ -409,6 +457,7 @@ std::unique_ptr make() { std::vector> GetGridColumns() { std::vector> ret; ret.push_back(make()); + ret.push_back(make()); ret.push_back(make()); ret.push_back(make()); ret.push_back(make()); diff --git a/src/grid_column.h b/src/grid_column.h index 16b70f2a91..d00d36478c 100644 --- a/src/grid_column.h +++ b/src/grid_column.h @@ -15,6 +15,7 @@ // Aegisub Project http://www.aegisub.org/ #include "flyweight_hash.h" +#include "wx/event.h" #include #include @@ -68,6 +69,9 @@ class GridColumn { virtual wxString const& Description() const = 0; virtual void Paint(wxDC &dc, int x, int y, const AssDialogue *d, const agi::Context *c) const; + // Returns true if the default action should be skipped + virtual bool OnMouseEvent(AssDialogue *d, agi::Context *c, wxMouseEvent &event) const { return false; } + int Width() const { return width; } bool Visible() const { return visible; } diff --git a/src/include/aegisub/context.h b/src/include/aegisub/context.h index 361dcf9c7a..3712a2f7b4 100644 --- a/src/include/aegisub/context.h +++ b/src/include/aegisub/context.h @@ -27,6 +27,7 @@ class Project; class SearchReplaceEngine; class InitialLineState; class SelectionController; +class FoldController; class SubsController; class BaseGrid; class TextSelectionController; @@ -47,6 +48,7 @@ struct Context { std::unique_ptr project; std::unique_ptr local_scripts; std::unique_ptr selectionController; + std::unique_ptr foldController; std::unique_ptr videoController; std::unique_ptr audioController; std::unique_ptr initialLineState; diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index 3cb2b0276e..d60ecc4c44 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -213,7 +213,9 @@ "Comment" : "rgb(216, 222, 245)", "Inframe" : "rgb(255, 253, 234)", "Selected Comment" : "rgb(211, 238, 238)", - "Selection" : "rgb(206, 255, 231)" + "Selection" : "rgb(206, 255, 231)", + "Open Fold" : "rgb(235, 235, 235)", + "Closed Fold" : "rgb(200, 200, 200)" }, "Collision" : "rgb(255,0,0)", "CPS Error" : "rgb(255,0,0)", diff --git a/src/libresrc/default_hotkey.json b/src/libresrc/default_hotkey.json index b9460979e8..34e0ef4864 100644 --- a/src/libresrc/default_hotkey.json +++ b/src/libresrc/default_hotkey.json @@ -263,6 +263,9 @@ "subtitle/select/all" : [ "Ctrl-A" ], + "grid/toggle" : [ + "Enter" + ], "video/frame/next" : [ "Right" ], @@ -359,4 +362,4 @@ "J" ] } -} \ No newline at end of file +} diff --git a/src/libresrc/default_menu.json b/src/libresrc/default_menu.json index 8d2f6c55b9..3521536cd0 100644 --- a/src/libresrc/default_menu.json +++ b/src/libresrc/default_menu.json @@ -20,6 +20,10 @@ {}, { "command" : "audio/save/clip" }, {}, + { "command" : "grid/fold/create" }, + { "command" : "grid/fold/toggle" }, + { "command" : "grid/fold/clear" }, + {}, { "command" : "edit/line/cut" }, { "command" : "edit/line/copy" }, { "command" : "edit/line/paste" }, @@ -86,6 +90,10 @@ { "command" : "edit/line/recombine" }, { "command" : "edit/line/split/by_karaoke" }, {}, + { "command" : "grid/fold/open_all" }, + { "command" : "grid/fold/close_all" }, + { "command" : "grid/fold/clear_all" }, + {}, { "submenu" : "main/subtitle/sort lines", "text" : "Sort All Lines" }, { "submenu" : "main/subtitle/sort selected lines", "text" : "Sort Selected Lines" }, { "command" : "grid/swap" }, diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index 782ead26dc..2e24ccbece 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -213,7 +213,9 @@ "Comment" : "rgb(216, 222, 245)", "Inframe" : "rgb(255, 253, 234)", "Selected Comment" : "rgb(211, 238, 238)", - "Selection" : "rgb(206, 255, 231)" + "Selection" : "rgb(206, 255, 231)", + "Open Fold" : "rgb(235, 235, 235)", + "Closed Fold" : "rgb(200, 200, 200)" }, "Collision" : "rgb(255,0,0)", "CPS Error" : "rgb(255,0,0)", diff --git a/src/libresrc/osx/default_hotkey.json b/src/libresrc/osx/default_hotkey.json index 829adb88e6..136f5bc483 100644 --- a/src/libresrc/osx/default_hotkey.json +++ b/src/libresrc/osx/default_hotkey.json @@ -273,6 +273,9 @@ "subtitle/select/all" : [ "Ctrl-A" ], + "grid/toggle" : [ + "Enter" + ], "video/frame/next" : [ "Right" ], diff --git a/src/libresrc/osx/default_menu.json b/src/libresrc/osx/default_menu.json index 75a9e4a036..2b39283419 100644 --- a/src/libresrc/osx/default_menu.json +++ b/src/libresrc/osx/default_menu.json @@ -20,6 +20,10 @@ {}, { "command" : "audio/save/clip" }, {}, + { "command" : "grid/fold/create" }, + { "command" : "grid/fold/toggle" }, + { "command" : "grid/fold/clear" }, + {}, { "command" : "edit/line/cut" }, { "command" : "edit/line/copy" }, { "command" : "edit/line/paste" }, @@ -89,6 +93,10 @@ { "command" : "edit/line/recombine" }, { "command" : "edit/line/split/by_karaoke" }, {}, + { "command" : "grid/fold/open_all" }, + { "command" : "grid/fold/close_all" }, + { "command" : "grid/fold/clear_all" }, + {}, { "submenu" : "main/subtitle/sort lines", "text" : "Sort All Lines" }, { "submenu" : "main/subtitle/sort selected lines", "text" : "Sort Selected Lines" }, { "command" : "grid/swap" }, diff --git a/src/meson.build b/src/meson.build index 9592cf6e0a..64f0bb50ab 100644 --- a/src/meson.build +++ b/src/meson.build @@ -89,6 +89,7 @@ aegisub_src = files( 'export_fixstyle.cpp', 'export_framerate.cpp', 'fft.cpp', + 'fold_controller.cpp', 'font_file_lister.cpp', 'frame_main.cpp', 'gl_text.cpp', diff --git a/src/preferences.cpp b/src/preferences.cpp index 6f858fc5fb..743c39a0fc 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -285,6 +285,8 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) { p->OptionAdd(grid, _("In frame background"), "Colour/Subtitle Grid/Background/Inframe"); p->OptionAdd(grid, _("Comment background"), "Colour/Subtitle Grid/Background/Comment"); p->OptionAdd(grid, _("Selected comment background"), "Colour/Subtitle Grid/Background/Selected Comment"); + p->OptionAdd(grid, _("Open fold background"), "Colour/Subtitle Grid/Background/Open Fold"); + p->OptionAdd(grid, _("Closed fold background"), "Colour/Subtitle Grid/Background/Closed Fold"); p->OptionAdd(grid, _("Header background"), "Colour/Subtitle Grid/Header"); p->OptionAdd(grid, _("Left Column"), "Colour/Subtitle Grid/Left Column"); p->OptionAdd(grid, _("Active Line Border"), "Colour/Subtitle Grid/Active Border"); diff --git a/src/subtitle_format_ass.cpp b/src/subtitle_format_ass.cpp index 6815c229f8..9117897abd 100644 --- a/src/subtitle_format_ass.cpp +++ b/src/subtitle_format_ass.cpp @@ -108,6 +108,20 @@ struct Writer { WriteIfNotZero("Scroll Position: ", properties.scroll_position); WriteIfNotZero("Active Line: ", properties.active_row); WriteIfNotZero("Video Position: ", properties.video_position); + + std::string foldsdata; + for (LineFold fold : properties.folds) { + if (!foldsdata.empty()) { + foldsdata += ","; + } + foldsdata += std::to_string(fold.start); + foldsdata += ":"; + foldsdata += std::to_string(fold.end); + foldsdata += ":"; + foldsdata += fold.collapsed ? "1" : "0"; + } + + WriteIfNotEmpty("Line Folds: ", foldsdata); } }