forked from mia/Aegisub
Merge branch 'folding' into feature
This commit is contained in:
commit
11b5a80618
23 changed files with 858 additions and 26 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>());
|
||||
|
|
|
@ -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
315
src/fold_controller.cpp
Normal 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
173
src/fold_controller.h
Normal 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);
|
||||
|
||||
};
|
|
@ -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>());
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -263,6 +263,9 @@
|
|||
"subtitle/select/all" : [
|
||||
"Ctrl-A"
|
||||
],
|
||||
"grid/toggle" : [
|
||||
"Enter"
|
||||
],
|
||||
"video/frame/next" : [
|
||||
"Right"
|
||||
],
|
||||
|
@ -359,4 +362,4 @@
|
|||
"J"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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)",
|
||||
|
|
|
@ -273,6 +273,9 @@
|
|||
"subtitle/select/all" : [
|
||||
"Ctrl-A"
|
||||
],
|
||||
"grid/toggle" : [
|
||||
"Enter"
|
||||
],
|
||||
"video/frame/next" : [
|
||||
"Right"
|
||||
],
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue