Merge branch 'folding' into feature

This commit is contained in:
arch1t3cht 2023-01-27 02:55:50 +01:00
commit 79a0655eb8
10 changed files with 198 additions and 165 deletions

View file

@ -246,6 +246,39 @@ uint32_t AssFile::AddExtradata(std::string const& key, std::string const& value)
return next_extradata_id++; // return old value, then post-increment
}
void AssFile::SetExtradataValue(AssDialogue& line, std::string const& key, std::string const& value, bool del) {
std::vector<uint32_t> id_list = line.ExtradataIds;
std::vector<bool> to_erase(id_list.size());
bool dirty = false;
bool found = false;
std::vector<ExtradataEntry> entry_list = GetExtradata(id_list);
for (int i = entry_list.size() - 1; i >= 0; i--) {
if (entry_list[i].key == key) {
if (!del && entry_list[i].value == value) {
found = true;
} else {
to_erase[i] = true;
dirty = true;
}
}
}
// The key is already set, we don't need to change anything
if (found && !dirty)
return;
for (int i = id_list.size() - 1; i >= 0; i--) {
if (to_erase[i])
id_list.erase(id_list.begin() + i, id_list.begin() + i + 1);
}
if (!del && !found)
id_list.push_back(AddExtradata(key, value));
line.ExtradataIds = id_list;
}
namespace {
struct extradata_id_cmp {
bool operator()(ExtradataEntry const& e, uint32_t id) {

View file

@ -85,7 +85,6 @@ struct ProjectProperties {
int active_row = 0;
int ar_mode = 0;
int video_position = 0;
std::vector<LineFold> folds;
};
class AssFile {
@ -93,6 +92,8 @@ class AssFile {
agi::signal::Signal<int, const AssDialogue*> AnnounceCommit;
agi::signal::Signal<int, const AssDialogue*> AnnouncePreCommit;
agi::signal::Signal<AssFileCommit> PushState;
void SetExtradataValue(AssDialogue& line, std::string const& key, std::string const& value, bool del);
public:
/// The lines in the file
std::vector<AssInfo> Info;
@ -144,6 +145,10 @@ public:
uint32_t AddExtradata(std::string const& key, std::string const& value);
/// Fetch all extradata entries from a list of IDs
std::vector<ExtradataEntry> GetExtradata(std::vector<uint32_t> const& id_list) const;
/// Set an extradata kex:value pair for a dialogue line, clearing previous values for this key if necessary
void SetExtradataValue(AssDialogue& line, std::string const& key, std::string const& value) { SetExtradataValue(line, key, value, false); };
/// Delete any extradata values for the given key
void DeleteExtradataValue(AssDialogue& line, std::string const& key) { SetExtradataValue(line, key, "", true); };
/// Remove unreferenced extradata entries
void CleanExtradata();
@ -178,7 +183,7 @@ public:
/// Extradata entries were added/modified/removed
COMMIT_EXTRADATA = 0x100,
/// Folds were added or removed
COMMIT_FOLD = 0x200,
COMMIT_FOLD = COMMIT_EXTRADATA,
};
DEFINE_SIGNAL_ADDERS(AnnouncePreCommit, AddPreCommitListener)

View file

@ -24,7 +24,6 @@
#include <libaegisub/ass/uuencode.h>
#include <libaegisub/make_unique.h>
#include <libaegisub/split.h>
#include <libaegisub/util.h>
#include <algorithm>
@ -40,8 +39,7 @@ class AssParser::HeaderToProperty {
using field = boost::variant<
std::string ProjectProperties::*,
int ProjectProperties::*,
double ProjectProperties::*,
std::vector<LineFold> ProjectProperties::*
double ProjectProperties::*
>;
std::unordered_map<std::string, field> fields;
@ -60,7 +58,6 @@ 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},
@ -83,29 +80,6 @@ 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,7 +40,6 @@
#include "ass_karaoke.h"
#include "ass_style.h"
#include "compat.h"
#include "fold_controller.h"
#include <libaegisub/exception.h>
#include <libaegisub/log.h>
@ -101,22 +100,6 @@ namespace {
return ret;
}
template<typename T>
bool get_userdata_field(lua_State *L, const char *name, const char *line_class, T *target, bool required)
{
lua_getfield(L, -1, name);
if (!lua_isuserdata(L, -1)) {
if (!required) {
lua_pop(L, 1);
return false;
}
throw bad_field("userdata", name, line_class);
}
*target = *static_cast<T *>(lua_touserdata(L, -1));
lua_pop(L, 1);
return true;
}
using namespace Automation4;
template<int (LuaAssFile::*closure)(lua_State *)>
int closure_wrapper(lua_State *L)
@ -198,10 +181,6 @@ namespace Automation4 {
set_field(L, "text", std::string(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)) {
@ -322,9 +301,6 @@ 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");
if (!get_userdata_field(L, "_foldinfo", "dialogue", &dia->Fold, false)) {
dia->Fold = FoldInfo();
}
std::vector<uint32_t> new_ids;

View file

@ -502,7 +502,7 @@ void BaseGrid::OnMouseEvent(wxMouseEvent &event) {
CaptureMouse();
}
if (columns[col]->OnMouseEvent(dlg, context, event)) {
if (dlg && columns[col]->OnMouseEvent(dlg, context, event)) {
return;
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>>
// 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
@ -24,9 +24,10 @@
#include <algorithm>
#include <unordered_map>
#include <libaegisub/log.h>
#include <libaegisub/split.h>
#include <libaegisub/util.h>
static int next_fold_id = 0;
const char *folds_key = "_aegi_folddata";
FoldController::FoldController(agi::Context *c)
: context(c)
@ -35,12 +36,12 @@ FoldController::FoldController(agi::Context *c)
bool FoldController::CanAddFold(AssDialogue& start, AssDialogue& end) {
if (start.Fold.exists || end.Fold.exists) {
if (start.Fold.valid || end.Fold.valid) {
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) {
if (it->Fold.valid) {
folddepth += it->Fold.side ? -1 : 1;
}
if (folddepth < 0) {
@ -51,17 +52,24 @@ bool FoldController::CanAddFold(AssDialogue& start, AssDialogue& end) {
}
void FoldController::RawAddFold(AssDialogue& start, AssDialogue& end, bool collapsed) {
int id = next_fold_id++;
int id = ++max_fold_id;
context->ass->SetExtradataValue(start, folds_key, agi::format("0;%d;%d", int(collapsed), id));
context->ass->SetExtradataValue(end, folds_key, agi::format("1;%d;%d", int(collapsed), id));
}
start.Fold.exists = true;
start.Fold.collapsed = collapsed;
start.Fold.id = id;
start.Fold.side = false;
void FoldController::UpdateLineExtradata(AssDialogue &line) {
if (line.Fold.extraExists)
context->ass->SetExtradataValue(line, folds_key, agi::format("%d;%d;%d", int(line.Fold.side), int(line.Fold.collapsed), int(line.Fold.id)));
else
context->ass->DeleteExtradataValue(line, folds_key);
}
end.Fold.exists = true;
end.Fold.collapsed = collapsed;
end.Fold.id = id;
end.Fold.side = true;
void FoldController::InvalidateLineFold(AssDialogue &line) {
line.Fold.valid = false;
if (++line.Fold.invalidCount > 100) {
line.Fold.extraExists = false;
UpdateLineExtradata(line);
}
}
void FoldController::AddFold(AssDialogue& start, AssDialogue& end, bool collapsed) {
@ -73,10 +81,11 @@ void FoldController::AddFold(AssDialogue& start, AssDialogue& end, bool collapse
bool FoldController::DoForAllFolds(bool action(AssDialogue& line)) {
for (AssDialogue& line : context->ass->Events) {
if (line.Fold.exists) {
if (action(line)) {
if (line.Fold.valid) {
bool result = action(line);
UpdateLineExtradata(line);
if (result)
return true;
}
}
}
return false;
@ -84,34 +93,10 @@ bool FoldController::DoForAllFolds(bool action(AssDialogue& line)) {
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();
UpdateFoldInfo();
}
}
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.
//
@ -119,17 +104,56 @@ void FoldController::MakeFoldsFromFile() {
// 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)) {
if (line->Fold.parent != nullptr && !(line->Fold.valid && !line->Fold.side)) {
line = line->Fold.parent;
}
if (!line->Fold.visited && action(*line)) {
return true;
if (!line->Fold.visited) {
bool result = action(*line);
UpdateLineExtradata(*line);
if (result)
return true;
}
line->Fold.visited = true;
}
return false;
}
void FoldController::UpdateFoldInfo() {
ReadFromExtradata();
FixFolds();
LinkFolds();
}
void FoldController::ReadFromExtradata() {
max_fold_id = 0;
for (auto line = context->ass->Events.begin(); line != context->ass->Events.end(); line++) {
line->Fold.extraExists = false;
for (auto const& extra : context->ass->GetExtradata(line->ExtradataIds)) {
if (extra.key == folds_key) {
std::vector<std::string> fields;
agi::Split(fields, extra.value, ';');
if (fields.size() != 3)
break;
int side;
int collapsed;
if (!agi::util::try_parse(fields[0], &side)) break;
if (!agi::util::try_parse(fields[1], &collapsed)) break;
if (!agi::util::try_parse(fields[2], &line->Fold.id)) break;
line->Fold.side = side;
line->Fold.collapsed = collapsed;
line->Fold.extraExists = true;
max_fold_id = std::max(max_fold_id, line->Fold.id);
break;
}
}
line->Fold.valid = line->Fold.extraExists;
}
}
void FoldController::FixFolds() {
// Stack of which folds we've desended into so far
std::vector<AssDialogue *> foldStack;
@ -142,15 +166,28 @@ void FoldController::FixFolds() {
// fold data with this ID is skipped and deleted.
std::unordered_map<int, bool> completedFolds;
// Map iteratively applied to all id's.
// Once some fold has been completely found, subsequent markers found with the same id will be mapped to this new id.
std::unordered_map<int, int> idRemap;
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.extraExists) {
bool needs_update = false;
while (idRemap.count(line->Fold.id)) {
line->Fold.id = idRemap[line->Fold.id];
needs_update = true;
}
if (completedFolds.count(line->Fold.id)) { // Duplicate entry - try to start a new one
idRemap[line->Fold.id] = ++max_fold_id;
line->Fold.id = idRemap[line->Fold.id];
needs_update = true;
}
if (!line->Fold.side) {
if (foldHeads.count(line->Fold.id)) { // Duplicate entry
line->Fold.exists = false;
InvalidateLineFold(*line);
} else {
foldHeads[line->Fold.id] = &*line;
foldStack.push_back(&*line);
@ -160,7 +197,7 @@ void FoldController::FixFolds() {
// 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;
InvalidateLineFold(*line);
} else {
// We found a fold. Now we need to see if the stack matches.
// We scan our stack for the counterpart of the fold.
@ -176,12 +213,15 @@ void FoldController::FixFolds() {
// 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;
InvalidateLineFold(*foldStack[j]);
foldStack.pop_back();
}
// Sync the found fold and pop the stack
line->Fold.collapsed = foldStack[i]->Fold.collapsed;
if (line->Fold.collapsed != foldStack[i]->Fold.collapsed) {
line->Fold.collapsed = foldStack[i]->Fold.collapsed;
needs_update = true;
}
foldStack.pop_back();
found = true;
@ -190,25 +230,30 @@ void FoldController::FixFolds() {
}
if (!found) {
completedFolds[line->Fold.id] = true;
line->Fold.exists = false;
InvalidateLineFold(*line);
}
}
}
if (needs_update) {
UpdateLineExtradata(*line);
}
}
}
// All remaining lines are invalid
for (AssDialogue *line : foldStack) {
line->Fold.exists = false;
line->Fold.valid = false;
if (++line->Fold.invalidCount > 100) {
line->Fold.extraExists = false;
UpdateLineExtradata(*line);
}
}
LinkFolds();
}
void FoldController::LinkFolds() {
std::vector<AssDialogue *> foldStack;
AssDialogue *lastVisible = nullptr;
context->ass->Properties.folds.clear();
maxdepth = 0;
@ -217,7 +262,7 @@ void FoldController::LinkFolds() {
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.visible = highestFolded > (int) foldStack.size();
line->Fold.visited = false;
line->Fold.visibleRow = visibleRow;
@ -228,26 +273,20 @@ void FoldController::LinkFolds() {
lastVisible = &*line;
visibleRow++;
}
if (line->Fold.exists && !line->Fold.side) {
if (line->Fold.valid && !line->Fold.side) {
foldStack.push_back(&*line);
if (!line->Fold.collapsed && highestFolded == foldStack.size()) {
if (!line->Fold.collapsed && highestFolded == (int) foldStack.size()) {
highestFolded++;
}
if (foldStack.size() > maxdepth) {
if ((int) 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,
});
if (line->Fold.valid && line->Fold.side) {
line->Fold.counterpart = foldStack.back();
(*foldStack.rbegin())->Fold.counterpart = &*line;
if (highestFolded >= foldStack.size()) {
if (highestFolded >= (int) foldStack.size()) {
highestFolded = foldStack.size();
}
@ -260,9 +299,9 @@ int FoldController::GetMaxDepth() {
return maxdepth;
}
bool FoldController::ActionHasFold(AssDialogue& line) { return line.Fold.exists; }
bool FoldController::ActionHasFold(AssDialogue& line) { return line.Fold.valid; }
bool FoldController::ActionClearFold(AssDialogue& line) { line.Fold.exists = false; return false; }
bool FoldController::ActionClearFold(AssDialogue& line) { line.Fold.extraExists = false; line.Fold.valid = false; return false; }
bool FoldController::ActionOpenFold(AssDialogue& line) { line.Fold.collapsed = false; return false; }

View file

@ -1,4 +1,4 @@
// Copyright (c) 2022, arch1t3cht <arch1t3cht@gmail.com>>
// 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
@ -23,6 +23,8 @@
namespace agi { struct Context; }
extern const char *folds_key;
/// 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)
@ -30,55 +32,58 @@ namespace agi { struct Context; }
/// 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.
/// In order for folds to be preserved while adding or deleting lines and work nicely with operations like copy/paste,
/// they need to be stored as extradata. Furthermore, in order for the subtitle grid and fold management commands to efficiently
/// navigate the folds, we cache some information on the fold after each commit.
///
/// 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.
/// A fold descriptor for a line is an extradata field of the form <direction>;<collapsed>;<id>, where
/// direction is 0 if this line starts a fold, and 1 if the line ends one
/// collapsed is 1 if the fold is collapsed and 0 otherwise
/// id is a unique id pairing this fold with its counterpart
/// Part of the data for an AssDialogue object, describing folds starting or ending at this line.
class FoldInfo {
// Base data describing the folds:w
// Cached, parsed versions of the contents of the extradata entry
/// Whether a fold starts or ends at the line. All other fields are only valid if this is true.
bool exists = false;
/// Whether there is some extradata entry on folds here
bool extraExists = false;
/// Whether a fold starts or ends at the line. The following three fields are only valid if this is true.
bool valid = false;
/// The id
int id = 0;
/// 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;
/// Increased when there's an extradata entry in here that turned out to be invalid.
/// Once this hits some threshold, the extradata entry is deleted.
/// We don't delete it immediately to allow cut/pasting fold delimiters around.
int invalidCount = 0;
/// 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;
int visibleRow = -1;
friend class FoldController;
public:
bool hasFold() const { return exists; }
bool hasFold() const { return valid; }
bool isFolded() const { return collapsed; }
bool isEnd() const { return side; }
@ -95,6 +100,7 @@ class FoldController {
agi::Context *context;
agi::signal::Connection pre_commit_listener;
int maxdepth = 0;
int max_fold_id = 0;
bool CanAddFold(AssDialogue& start, AssDialogue& end);
@ -106,8 +112,6 @@ class FoldController {
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);
@ -120,10 +124,25 @@ class FoldController {
static bool ActionToggleFold(AssDialogue& line);
/// Updates the line's extradata entry from the values in FoldInfo. Used after actions like toggling folds.
void UpdateLineExtradata(AssDialogue& line);
/// Sets valid = false and increases the invalidCounter, deleting the extradata if necessary
void InvalidateLineFold(AssDialogue &line);
/// After lines have been added or deleted, this ensures consistency again. Run with every relevant commit.
/// Performs the three actions below in order.
void UpdateFoldInfo();
/// Parses the extradata of all lines and sets the respective lines in the FoldInfo.
/// Also deduplicates extradata entries and mangles fold id's when necessary.
void ReadFromExtradata();
/// Ensures consistency by making sure every fold has two delimiters and folds are properly nested.
/// Cleans up extradata entries if they've been invalid for long enough.
void FixFolds();
/// If the fold base dataa is valid, sets up all the cached links in the FoldData
/// Once the fold base data is valid, sets up all the cached links in the FoldData.
void LinkFolds();
public:

View file

@ -93,7 +93,7 @@
{ "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

@ -96,7 +96,7 @@
{ "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

@ -109,19 +109,6 @@ struct Writer {
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);
}
void WriteIfNotEmpty(const char *key, std::string const& value) {