Merge branch 'folding' into feature

This commit is contained in:
arch1t3cht 2022-07-27 15:27:04 +02:00
commit 11b5a80618
23 changed files with 858 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

@ -40,6 +40,7 @@
#include "ass_karaoke.h"
#include "ass_style.h"
#include "compat.h"
#include "fold_controller.h"
#include <libaegisub/exception.h>
#include <libaegisub/log.h>
@ -100,6 +101,16 @@ namespace {
return ret;
}
template<typename T>
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<T *>(lua_touserdata(L, -1));
lua_pop(L, 1);
}
using namespace Automation4;
template<int (LuaAssFile::*closure)(lua_State *)>
int closure_wrapper(lua_State *L)
@ -181,6 +192,10 @@ namespace Automation4 {
set_field(L, "text", dia->Text);
// preserve the folds
*static_cast<FoldInfo*>(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<uint32_t> new_ids;

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,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<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 +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;
}
}

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 {
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<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,53 @@ 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;
}
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<AssDialogue *> 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<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

@ -15,6 +15,7 @@
// Aegisub Project http://www.aegisub.org/
#include "flyweight_hash.h"
#include "wx/event.h"
#include <memory>
#include <string>
@ -68,6 +69,9 @@ public:
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; }

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

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

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

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

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

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

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