Add line folding

Line folds are managed using metadata of AssDialogue elements, and
saved in the project properties. They only affect the appearance of the
subtitle grid, and have no impact on other line operations.
This commit is contained in:
arch1t3cht 2022-07-26 19:55:04 +02:00
parent 1772dd17ae
commit 56cc1a6873
21 changed files with 811 additions and 26 deletions

View file

@ -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 <libaegisub/ass/time.h>
@ -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

View file

@ -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);

View file

@ -27,6 +27,8 @@
//
// Aegisub Project http://www.aegisub.org/
#pragma once
#include "ass_entry.h"
#include <libaegisub/fs_fwd.h>
@ -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<LineFold> folds;
};
class AssFile {
/// A set of changes has been committed to the file (AssFile::COMMITType)
agi::signal::Signal<int, const AssDialogue*> AnnounceCommit;
agi::signal::Signal<int, const AssDialogue*> AnnouncePreCommit;
agi::signal::Signal<AssFileCommit> PushState;
public:
/// The lines in the file
@ -166,8 +177,11 @@ public:
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)

View file

@ -24,6 +24,7 @@
#include <libaegisub/ass/uuencode.h>
#include <libaegisub/make_unique.h>
#include <libaegisub/split.h>
#include <libaegisub/util.h>
#include <algorithm>
@ -39,7 +40,8 @@ class AssParser::HeaderToProperty {
using field = boost::variant<
std::string ProjectProperties::*,
int ProjectProperties::*,
double ProjectProperties::*
double ProjectProperties::*,
std::vector<LineFold> ProjectProperties::*
>;
std::unordered_map<std::string, field> fields;
@ -58,6 +60,7 @@ public:
{"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 @@ public:
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<LineFold> ProjectProperties::*f) const {
std::vector<LineFold> folds;
for (auto foldstr : agi::Split(value, ',')) {
LineFold fold;
std::vector<std::string> 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;

View file

@ -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,8 +455,8 @@ 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;
if (event.ButtonDown() && OPT_GET("Subtitle/Grid/Focus Allow")->GetBool())
@ -447,7 +465,7 @@ void BaseGrid::OnMouseEvent(wxMouseEvent &event) {
if (holding) {
if (!event.LeftIsDown()) {
if (dlg)
MakeRowVisible(row);
MakeVisRowVisible(row);
holding = false;
ReleaseMouse();
}
@ -517,7 +535,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 +573,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 +588,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 +599,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 +636,16 @@ AssDialogue *BaseGrid::GetDialogue(int n) const {
return index_line_map[n];
}
AssDialogue *BaseGrid::GetVisDialogue(int n) const {
if (static_cast<size_t>(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 +693,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 +707,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 +731,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;
}
}

View file

@ -80,9 +80,12 @@ class BaseGrid final : public wxWindow {
wxBrush Visible;
wxBrush SelectedComment;
wxBrush LeftCol;
wxBrush FoldOpen;
wxBrush FoldClosed;
} row_colors;
std::vector<AssDialogue*> index_line_map; ///< Row number -> dialogue line
std::vector<AssDialogue*> 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();

View file

@ -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<grid_move_down>());
reg(agi::make_unique<grid_move_up>());
reg(agi::make_unique<grid_swap>());
reg(agi::make_unique<grid_fold_create>());
reg(agi::make_unique<grid_fold_open>());
reg(agi::make_unique<grid_fold_close>());
reg(agi::make_unique<grid_fold_toggle>());
reg(agi::make_unique<grid_fold_clear>());
reg(agi::make_unique<grid_fold_open_all>());
reg(agi::make_unique<grid_fold_close_all>());
reg(agi::make_unique<grid_fold_clear_all>());
reg(agi::make_unique<grid_tag_cycle_hiding>());
reg(agi::make_unique<grid_tags_hide>());
reg(agi::make_unique<grid_tags_show>());

View file

@ -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<Project>(this))
, local_scripts(make_unique<Automation4::LocalScriptManager>(this))
, selectionController(make_unique<SelectionController>(this))
, foldController(make_unique<FoldController>(this))
, videoController(make_unique<VideoController>(this))
, audioController(make_unique<AudioController>(this))
, initialLineState(make_unique<InitialLineState>(this))

315
src/fold_controller.cpp Normal file
View file

@ -0,0 +1,315 @@
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>>
//
// 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 <algorithm>
#include <unordered_map>
#include <libaegisub/log.h>
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<AssDialogue *> 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<AssDialogue *> foldStack;
// ID's for which we've found starters
std::unordered_map<int, AssDialogue*> 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<int, bool> 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<AssDialogue *> 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 {
.start = foldStack.back()->Row,
.end = line->Row,
.collapsed = 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<AssDialogue *> const& lines) {
FoldController::DoForFoldsAt(lines, FoldController::ActionClearFold);
context->ass->Commit(_("clear folds"), AssFile::COMMIT_FOLD);
}
void FoldController::OpenFoldsAt(std::vector<AssDialogue *> const& lines) {
FoldController::DoForFoldsAt(lines, FoldController::ActionOpenFold);
context->ass->Commit(_("open folds"), AssFile::COMMIT_FOLD);
}
void FoldController::CloseFoldsAt(std::vector<AssDialogue *> const& lines) {
FoldController::DoForFoldsAt(lines, FoldController::ActionCloseFold);
context->ass->Commit(_("close folds"), AssFile::COMMIT_FOLD);
}
void FoldController::ToggleFoldsAt(std::vector<AssDialogue *> const& lines) {
FoldController::DoForFoldsAt(lines, FoldController::ActionToggleFold);
context->ass->Commit(_("toggle folds"), AssFile::COMMIT_FOLD);
}
bool FoldController::AreFoldsAt(std::vector<AssDialogue *> const& lines) {
return FoldController::DoForFoldsAt(lines, FoldController::ActionHasFold);
}

173
src/fold_controller.h Normal file
View file

@ -0,0 +1,173 @@
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>>
//
// 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 <libaegisub/signal.h>
#include "ass_file.h"
#include <vector>
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<AssDialogue *> 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<AssDialogue *> 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<AssDialogue *> 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<AssDialogue *> 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<AssDialogue *> const& lines);
/// @brief Returns whether any of the given lines are contained in folds
/// @param lines The lines
bool AreFoldsAt(std::vector<AssDialogue *> const& lines);
};

View file

@ -22,6 +22,7 @@
#include "include/aegisub/context.h"
#include "options.h"
#include "video_controller.h"
#include "fold_controller.h"
#include <libaegisub/character_count.h>
@ -125,6 +126,41 @@ T max_value(T AssDialogueBase::*field, EntryList<AssDialogue> 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;
}
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 +445,7 @@ std::unique_ptr<GridColumn> make() {
std::vector<std::unique_ptr<GridColumn>> GetGridColumns() {
std::vector<std::unique_ptr<GridColumn>> ret;
ret.push_back(make<GridColumnLineNumber>());
ret.push_back(make<GridColumnFolds>());
ret.push_back(make<GridColumnLayer>());
ret.push_back(make<GridColumnStartTime>());
ret.push_back(make<GridColumnEndTime>());

View file

@ -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> project;
std::unique_ptr<Automation4::ScriptManager> local_scripts;
std::unique_ptr<SelectionController> selectionController;
std::unique_ptr<FoldController> foldController;
std::unique_ptr<VideoController> videoController;
std::unique_ptr<AudioController> audioController;
std::unique_ptr<InitialLineState> initialLineState;

View file

@ -212,7 +212,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)",

View file

@ -263,6 +263,9 @@
"subtitle/select/all" : [
"Ctrl-A"
],
"grid/toggle" : [
"Enter"
],
"video/frame/next" : [
"Right"
],
@ -359,4 +362,4 @@
"J"
]
}
}
}

View file

@ -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" },

View file

@ -212,7 +212,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)",

View file

@ -273,6 +273,9 @@
"subtitle/select/all" : [
"Ctrl-A"
],
"grid/toggle" : [
"Enter"
],
"video/frame/next" : [
"Right"
],

View file

@ -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" },

View file

@ -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',

View file

@ -282,6 +282,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");

View file

@ -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);
}
}