diff --git a/build/Aegisub/Aegisub.vcxproj b/build/Aegisub/Aegisub.vcxproj
index 7c2e91466..b1bac423c 100644
--- a/build/Aegisub/Aegisub.vcxproj
+++ b/build/Aegisub/Aegisub.vcxproj
@@ -171,6 +171,7 @@
+
@@ -373,6 +374,7 @@
+
diff --git a/build/Aegisub/Aegisub.vcxproj.filters b/build/Aegisub/Aegisub.vcxproj.filters
index b9a07eebd..2441580aa 100644
--- a/build/Aegisub/Aegisub.vcxproj.filters
+++ b/build/Aegisub/Aegisub.vcxproj.filters
@@ -624,6 +624,9 @@
Features\Resolution resampler
+
+ Main UI\Grid
+
@@ -1181,6 +1184,9 @@
Main UI\Edit box
+
+ Main UI\Grid
+
diff --git a/libaegisub/include/libaegisub/util.h b/libaegisub/include/libaegisub/util.h
index ca92c3edd..f6589e83c 100644
--- a/libaegisub/include/libaegisub/util.h
+++ b/libaegisub/include/libaegisub/util.h
@@ -19,6 +19,7 @@
#include
#include
+#include
#include
#include
#include
@@ -74,5 +75,10 @@ namespace agi {
std::string ErrorString(int error);
+ template
+ auto range(Integer end) -> decltype(boost::irange(0, end)) {
+ return boost::irange(0, end);
+ }
+
} // namespace util
} // namespace agi
diff --git a/src/Makefile b/src/Makefile
index 25a1e001c..919c55abc 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -194,6 +194,7 @@ SRC += \
frame_main.cpp \
gl_text.cpp \
gl_wrap.cpp \
+ grid_column.cpp \
help_button.cpp \
hotkey.cpp \
hotkey_data_view_model.cpp \
diff --git a/src/base_grid.cpp b/src/base_grid.cpp
index 4002a5edb..970501c0b 100644
--- a/src/base_grid.cpp
+++ b/src/base_grid.cpp
@@ -35,24 +35,23 @@
#include "ass_dialogue.h"
#include "ass_file.h"
-#include "ass_style.h"
#include "audio_box.h"
#include "compat.h"
#include "frame_main.h"
+#include "grid_column.h"
#include "options.h"
#include "utils.h"
#include "selection_controller.h"
#include "subs_controller.h"
#include "video_context.h"
-#include "video_slider.h"
+
+#include
#include
#include
-#include
#include
#include
#include
-#include
#include
#include
@@ -65,23 +64,11 @@ enum {
MENU_SHOW_COL = 1250 // Needs 15 IDs after this
};
-namespace std {
- template
- struct hash> {
- size_t operator()(boost::flyweight const& ss) const {
- return hash()(&ss.get());
- }
- };
-}
-
BaseGrid::BaseGrid(wxWindow* parent, agi::Context *context)
: wxWindow(parent, -1, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxSUNKEN_BORDER)
, scrollBar(new wxScrollBar(this, GRID_SCROLLBAR, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL))
-, seek_listener(context->videoController->AddSeekListener(std::bind(&BaseGrid::Refresh, this, false, nullptr)))
-, headerNames({{
- _("#"), _("L"), _("Start"), _("End"), _("CPS"), _("Style"), _("Actor"),
- _("Effect"), _("Left"), _("Right"), _("Vert"), _("Text")
-}})
+, columns(GetGridColumns())
+, seek_listener(context->videoController->AddSeekListener([&] { Refresh(false); }))
, context(context)
{
scrollBar->SetScrollbar(0,10,100,10);
@@ -149,8 +136,8 @@ void BaseGrid::OnSubtitlesCommit(int type) {
if (type & AssFile::COMMIT_DIAG_TIME)
Refresh(false);
else if (type & AssFile::COMMIT_DIAG_TEXT) {
- RefreshRect(wxRect(cps_col_x, 0, cps_col_w, GetClientSize().GetHeight()), false);
- RefreshRect(wxRect(text_col_x, 0, text_col_w, GetClientSize().GetHeight()), false);
+ for (auto const& rect : text_refresh_rects)
+ RefreshRect(rect, false);
}
}
@@ -164,9 +151,9 @@ void BaseGrid::OnSubtitlesSave() {
void BaseGrid::OnShowColMenu(wxCommandEvent &event) {
int item = event.GetId() - MENU_SHOW_COL;
- showCol[item] = !showCol[item];
+ column_shown[item] = !column_shown[item];
- OPT_SET("Subtitle/Grid/Column")->SetListBool(std::vector(std::begin(showCol), std::end(showCol)));
+ OPT_SET("Subtitle/Grid/Column")->SetListBool(std::vector(std::begin(column_shown), std::end(column_shown)));
SetColumnWidths();
Refresh(false);
@@ -203,13 +190,12 @@ void BaseGrid::UpdateStyle() {
// Set column widths
std::vector column_array(OPT_GET("Subtitle/Grid/Column")->GetListBool());
- assert(column_array.size() <= showCol.size());
- boost::copy(column_array, std::begin(showCol));
- for (size_t i : boost::irange(column_array.size(), showCol.size()))
- showCol[i] = true;
+ column_shown.assign(column_array.begin(), column_array.end());
+ column_shown.resize(columns.size(), true);
- for (int i : boost::irange(0, column_count))
- headerWidth[i] = dc.GetTextExtent(headerNames[i]).GetWidth();
+ column_header_widths.resize(columns.size());
+ for (size_t i : agi::util::range(columns.size()))
+ column_header_widths[i] = dc.GetTextExtent(columns[i]->Header()).GetWidth();
SetColumnWidths();
@@ -268,32 +254,29 @@ void BaseGrid::SelectRow(int row, bool addToSelected, bool select) {
}
void BaseGrid::OnPaint(wxPaintEvent &) {
- // Get size and pos
- wxSize cs = GetClientSize();
- cs.SetWidth(cs.GetWidth() - scrollBar->GetSize().GetWidth());
-
// Find which columns need to be repainted
- bool paint_columns[column_count] = {0};
+ std::vector paint_columns;
+ std::vector paint_column_widths;
for (wxRegionIterator region(GetUpdateRegion()); region; ++region) {
wxRect updrect = region.GetRect();
int x = 0;
- for (int i : boost::irange(0, column_count)) {
- if (updrect.x < x + colWidth[i] && updrect.x + updrect.width > x && colWidth[i])
- paint_columns[i] = true;
- x += colWidth[i];
+ for (size_t i : agi::util::range(columns.size())) {
+ if (updrect.x < x + column_widths[i] && updrect.x + updrect.width > x && column_widths[i])
+ paint_columns.push_back(columns[i].get());
+ else
+ paint_columns.push_back(nullptr);
+ x += column_widths[i];
}
}
- wxAutoBufferedPaintDC dc(this);
- DrawImage(dc, paint_columns);
-}
+ if (paint_columns.empty()) return;
-void BaseGrid::DrawImage(wxDC &dc, bool paint_columns[]) {
int w = 0;
int h = 0;
GetClientSize(&w,&h);
w -= scrollBar->GetSize().GetWidth();
+ wxAutoBufferedPaintDC dc(this);
dc.SetFont(font);
dc.SetBackground(row_colors.Default);
@@ -302,12 +285,7 @@ void BaseGrid::DrawImage(wxDC &dc, bool paint_columns[]) {
// Draw labels
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(row_colors.LeftCol);
- dc.DrawRectangle(0,lineHeight,colWidth[0],h-lineHeight);
-
- // Visible lines
- int drawPerScreen = h/lineHeight + 1;
- int nDraw = mid(0,drawPerScreen,GetRows()-yPos);
- int maxH = (nDraw+1) * lineHeight;
+ dc.DrawRectangle(0, lineHeight, column_widths[0], h-lineHeight);
// Row colors
wxColour text_standard(to_wx(OPT_GET("Colour/Subtitle Grid/Standard")->GetColor()));
@@ -320,181 +298,109 @@ void BaseGrid::DrawImage(wxDC &dc, bool paint_columns[]) {
dc.DrawLine(0, 0, w, 0);
dc.SetPen(*wxTRANSPARENT_PEN);
- auto strings = headerNames;
+ auto paint_text = [&](wxString const& str, int x, int y, int col) {
+ wxSize ext = dc.GetTextExtent(str);
- int override_mode = OPT_GET("Subtitle/Grid/Hide Overrides")->GetInt();
- wxString replace_char;
- if (override_mode == 1)
- replace_char = to_wx(OPT_GET("Subtitle/Grid/Hide Overrides Char")->GetString());
+ int top = y + (lineHeight - ext.GetHeight()) / 2;
+ int left = x + 4;
+ if (columns[col]->Centered())
+ left += (column_widths[col] - 6 - ext.GetWidth()) / 2;
+
+ dc.DrawText(str, left, top);
+ };
+
+ // Paint header
+ {
+ dc.SetTextForeground(text_standard);
+ dc.SetBrush(row_colors.Header);
+ dc.DrawRectangle(0, 0, w, lineHeight);
+
+ int x = 0;
+ for (size_t i : agi::util::range(columns.size())) {
+ if (paint_columns[i])
+ paint_text(columns[i]->Header(), x, 0, i);
+ x += column_widths[i];
+ }
+
+ dc.SetPen(grid_pen);
+ dc.DrawLine(0, lineHeight, w, lineHeight);
+ }
+
+ // Paint the rows
+ int drawPerScreen = h/lineHeight + 1;
+ int nDraw = mid(0, drawPerScreen, GetRows() - yPos);
auto active_line = context->selectionController->GetActiveLine();
auto const& selection = context->selectionController->GetSelectedSet();
- for (int i = 0; i < nDraw + 1; i++) {
- int curRow = i + yPos - 1;
- wxBrush curColor = row_colors.Default;
- AssDialogue *curDiag = nullptr;
+ for (int i : agi::util::range(nDraw)) {
+ wxBrush color = row_colors.Default;
+ AssDialogue *curDiag = index_line_map[i + yPos];
- // Header
- if (i == 0) {
- curColor = row_colors.Header;
+ bool inSel = !!selection.count(curDiag);
+ if (inSel && curDiag->Comment)
+ color = row_colors.SelectedComment;
+ else if (inSel)
+ color = row_colors.Selection;
+ else if (curDiag->Comment)
+ color = row_colors.Comment;
+ else if (OPT_GET("Subtitle/Grid/Highlight Subtitles in Frame")->GetBool() && IsDisplayed(curDiag))
+ color = row_colors.Visible;
+
+ if (active_line != curDiag && curDiag->CollidesWith(active_line))
+ dc.SetTextForeground(text_collision);
+ else if (inSel)
+ dc.SetTextForeground(text_selection);
+ else
dc.SetTextForeground(text_standard);
- }
- // Lines
- else if ((curDiag = GetDialogue(curRow))) {
- GetRowStrings(curRow, curDiag, paint_columns, &strings[0], !!override_mode, replace_char);
-
- bool inSel = !!selection.count(curDiag);
- if (inSel && curDiag->Comment)
- curColor = row_colors.SelectedComment;
- else if (inSel)
- curColor = row_colors.Selection;
- else if (curDiag->Comment)
- curColor = row_colors.Comment;
- else if (OPT_GET("Subtitle/Grid/Highlight Subtitles in Frame")->GetBool() && IsDisplayed(curDiag))
- curColor = row_colors.Visible;
-
- if (active_line != curDiag && curDiag->CollidesWith(active_line))
- dc.SetTextForeground(text_collision);
- else if (inSel)
- dc.SetTextForeground(text_selection);
- else
- dc.SetTextForeground(text_standard);
- }
- else {
- assert(false);
- }
// Draw row background color
- if (curColor != row_colors.Default) {
- dc.SetBrush(curColor);
- dc.DrawRectangle(
- (i == 0) ? 0 : colWidth[0], i * lineHeight + 1,
- w, lineHeight);
+ if (color != row_colors.Default) {
+ dc.SetBrush(color);
+ dc.DrawRectangle(column_widths[0], (i + 1) * lineHeight + 1, w, lineHeight);
}
// Draw text
- int dx = 0;
- int dy = i*lineHeight;
- for (int j : boost::irange(0, column_count)) {
- if (colWidth[j] == 0) continue;
+ int x = 0;
+ int y = (i + 1) * lineHeight;
+ for (size_t j : agi::util::range(columns.size())) {
+ if (column_widths[j] == 0) continue;
- if (paint_columns[j]) {
- wxSize ext = dc.GetTextExtent(strings[j]);
-
- int left = dx + 4;
- int top = dy + (lineHeight - ext.GetHeight()) / 2;
-
- // Centered columns
- if (!(j == 5 || j == 6 || j == 7 || j == 11)) {
- left += (colWidth[j] - 6 - ext.GetWidth()) / 2;
- }
-
- dc.DrawText(strings[j], left, top);
- }
- dx += colWidth[j];
+ if (paint_columns[j])
+ paint_text(columns[j]->Value(curDiag, byFrame ? context : nullptr), x, y, j);
+ x += column_widths[j];
}
// Draw grid
- dc.DestroyClippingRegion();
if (curDiag == active_line) {
dc.SetPen(wxPen(to_wx(OPT_GET("Colour/Subtitle Grid/Active Border")->GetColor())));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
- dc.DrawRectangle(0, dy, w, lineHeight + 1);
+ dc.DrawRectangle(0, y, w, lineHeight + 1);
}
else {
dc.SetPen(grid_pen);
- dc.DrawLine(0, dy + lineHeight, w , dy + lineHeight);
+ dc.DrawLine(0, y + lineHeight, w , y + lineHeight);
}
dc.SetPen(*wxTRANSPARENT_PEN);
}
// Draw grid columns
- int dx = 0;
- dc.SetPen(grid_pen);
- for (int i : boost::irange(0, column_count - 1)) {
- dx += colWidth[i];
- dc.DrawLine(dx, 0, dx, maxH);
- }
- dc.DrawLine(0, 0, 0, maxH);
- dc.DrawLine(w-1, 0, w-1, maxH);
-}
-
-void BaseGrid::GetRowStrings(int row, AssDialogue *line, bool *paint_columns, wxString *strings, bool replace, wxString const& rep_char) const {
- if (paint_columns[0]) strings[0] = std::to_wstring(row + 1);
- if (paint_columns[1]) strings[1] = std::to_wstring(line->Layer);
- if (byFrame) {
- if (paint_columns[2]) strings[2] = std::to_wstring(context->videoController->FrameAtTime(line->Start, agi::vfr::START));
- if (paint_columns[3]) strings[3] = std::to_wstring(context->videoController->FrameAtTime(line->End, agi::vfr::END));
- }
- else {
- if (paint_columns[2]) strings[2] = to_wx(line->Start.GetAssFormated());
- if (paint_columns[3]) strings[3] = to_wx(line->End.GetAssFormated());
- }
- if (paint_columns[5]) strings[5] = to_wx(line->Style);
- if (paint_columns[6]) strings[6] = to_wx(line->Actor);
- if (paint_columns[7]) strings[7] = to_wx(line->Effect);
- if (paint_columns[8]) strings[8] = line->Margin[0] ? wxString(std::to_wstring(line->Margin[0])) : wxString();
- if (paint_columns[9]) strings[9] = line->Margin[1] ? wxString(std::to_wstring(line->Margin[1])) : wxString();
- if (paint_columns[10]) strings[10] = line->Margin[2] ? wxString(std::to_wstring(line->Margin[2])) : wxString();
-
- if (paint_columns[4]) {
- int characters = 0;
-
- auto const& text = line->Text.get();
- auto pos = begin(text);
- do {
- auto it = std::find(pos, end(text), '{');
- characters += CharacterCount(pos, it, true);
- if (it == end(text)) break;
-
- pos = std::find(pos, end(text), '}');
- if (pos == end(text)) {
- characters += CharacterCount(it, pos, true);
- break;
- }
- } while (++pos != end(text));
-
- int duration = line->End - line->Start;
- if (duration <= 0 || characters * 1000 / duration >= 1000)
- strings[4] = "";
- else
- strings[4] = std::to_wstring(characters * 1000 / duration);
- }
-
- if (paint_columns[11]) {
- strings[11].clear();
-
- // Show overrides
- if (!replace)
- strings[11] = to_wx(line->Text);
- // Hidden overrides
- else {
- strings[11].reserve(line->Text.get().size());
- size_t start = 0, pos;
- while ((pos = line->Text.get().find('{', start)) != std::string::npos) {
- strings[11] += to_wx(line->Text.get().substr(start, pos - start));
- strings[11] += rep_char;
- start = line->Text.get().find('}', pos);
- if (start != std::string::npos) ++start;
- }
- if (start != std::string::npos)
- strings[11] += to_wx(line->Text.get().substr(start));
+ {
+ int maxH = (nDraw + 1) * lineHeight;
+ int x = 0;
+ dc.SetPen(grid_pen);
+ for (int width : column_widths) {
+ x += width;
+ if (x < w)
+ dc.DrawLine(x, 0, x, maxH);
}
-
- // Cap length and set text
- if (strings[11].size() > 512)
- strings[11] = strings[11].Left(512) + "...";
+ dc.DrawLine(0, 0, 0, maxH);
+ dc.DrawLine(w - 1, 0, w - 1, maxH);
}
}
void BaseGrid::OnSize(wxSizeEvent &) {
AdjustScrollbar();
-
- int w, h;
- GetClientSize(&w, &h);
- colWidth[11] = text_col_w = w - text_col_x;
-
Refresh(false);
}
@@ -625,23 +531,11 @@ void BaseGrid::OnContextMenu(wxContextMenuEvent &evt) {
menu::OpenPopupMenu(context_menu.get(), this);
}
else {
- const wxString strings[] = {
- _("Line Number"),
- _("Layer"),
- _("Start"),
- _("End"),
- _("Characters per Second"),
- _("Style"),
- _("Actor"),
- _("Effect"),
- _("Left"),
- _("Right"),
- _("Vert"),
- };
-
wxMenu menu;
- for (size_t i : boost::irange(0, boost::size(strings)))
- menu.Append(MENU_SHOW_COL + i, strings[i], "", wxITEM_CHECK)->Check(showCol[i]);
+ for (size_t i : agi::util::range(columns.size())) {
+ if (columns[i]->CanHide())
+ menu.Append(MENU_SHOW_COL + i, columns[i]->Description(), "", wxITEM_CHECK)->Check(!!column_shown[i]);
+ }
PopupMenu(&menu);
}
}
@@ -688,94 +582,26 @@ void BaseGrid::SetColumnWidths() {
wxClientDC dc(this);
dc.SetFont(font);
- // O(1) widths
- int marginLen = dc.GetTextExtent("0000").GetWidth();
- int cpsLen = dc.GetTextExtent("999").GetWidth();
+ WidthHelper helper{dc, {}};
- int labelLen = dc.GetTextExtent(std::to_wstring(GetRows())).GetWidth();
- int startLen = 0;
- int endLen = 0;
- if (!byFrame)
- startLen = endLen = dc.GetTextExtent(to_wx(AssTime().GetAssFormated())).GetWidth();
-
- std::unordered_map, int> widths;
- auto get_width = [&](boost::flyweight const& str) -> int {
- if (str.get().empty()) return 0;
- auto it = widths.find(str);
- if (it != end(widths)) return it->second;
- int width = dc.GetTextExtent(to_wx(str)).GetWidth();
- widths[str] = width;
- return width;
- };
-
- // O(n) widths
- bool showMargin[3] = { false, false, false };
- int styleLen = 0;
- int actorLen = 0;
- int effectLen = 0;
- int maxLayer = 0;
- int maxStart = 0;
- int maxEnd = 0;
- for (auto const& diag : context->ass->Events) {
- maxLayer = std::max(maxLayer, diag.Layer);
- actorLen = std::max(actorLen, get_width(diag.Actor));
- styleLen = std::max(styleLen, get_width(diag.Style));
- effectLen = std::max(effectLen, get_width(diag.Effect));
-
- // Margins
- for (int j = 0; j < 3; j++) {
- if (diag.Margin[j])
- showMargin[j] = true;
- }
-
- // Times
- if (byFrame) {
- maxStart = std::max(maxStart, context->videoController->FrameAtTime(diag.Start, agi::vfr::START));
- maxEnd = std::max(maxEnd, context->videoController->FrameAtTime(diag.End, agi::vfr::END));
+ column_widths.clear();
+ for (auto i : agi::util::range(columns.size())) {
+ if (!column_shown[i])
+ column_widths.push_back(0);
+ else {
+ int width = columns[i]->Width(context, helper, byFrame);
+ if (width) // 10 is an arbitrary amount of padding
+ width = 10 + std::max(width, column_header_widths[i]);
+ column_widths.push_back(width);
}
}
- // Finish layer
- int layerLen = maxLayer ? dc.GetTextExtent(std::to_wstring(maxLayer)).GetWidth() : 0;
-
- // Finish times
- if (byFrame) {
- startLen = dc.GetTextExtent(std::to_wstring(maxStart)).GetWidth();
- endLen = dc.GetTextExtent(std::to_wstring(maxEnd)).GetWidth();
+ text_refresh_rects.clear();
+ int x = 0;
+ for (auto i : agi::util::range(columns.size())) {
+ if (columns[i]->RefreshOnTextChange() && column_widths[i])
+ text_refresh_rects.emplace_back(x, 0, column_widths[i], h);
}
-
- // Set column widths
- colWidth[0] = labelLen;
- colWidth[1] = layerLen;
- colWidth[2] = startLen;
- colWidth[3] = endLen;
- colWidth[4] = cpsLen;
- colWidth[5] = styleLen;
- colWidth[6] = actorLen;
- colWidth[7] = effectLen;
- for (int i = 0; i < 3; i++)
- colWidth[i + 8] = showMargin[i] ? marginLen : 0;
- colWidth[11] = 1;
-
- // Hide columns and ensure every visible column is at least as big as its
- // header plus padding
- for (size_t i : boost::irange(0u, showCol.size())) {
- if (!showCol[i])
- colWidth[i] = 0;
- else if (colWidth[i])
- colWidth[i] = 10 + std::max(colWidth[i], headerWidth[i]);
- }
-
- // Set size of last
- int total = std::accumulate(colWidth.begin(), colWidth.end(), 0);
- colWidth[11] = std::max(w - total, 0);
-
- time_cols_x = colWidth[0] + colWidth[1];
- time_cols_w = colWidth[2] + colWidth[3] + colWidth[4];
- cps_col_x = time_cols_x + colWidth[2] + colWidth[3];
- cps_col_w = colWidth[4];
- text_col_x = total;
- text_col_w = colWidth[11];
}
AssDialogue *BaseGrid::GetDialogue(int n) const {
diff --git a/src/base_grid.h b/src/base_grid.h
index 5c6d661e3..726ee726a 100644
--- a/src/base_grid.h
+++ b/src/base_grid.h
@@ -39,10 +39,9 @@ namespace agi {
class OptionValue;
}
class AssDialogue;
+struct GridColumn;
class BaseGrid final : public wxWindow {
- static const int column_count = 12;
-
std::vector connections;
int lineHeight = 1; ///< Height of a line in pixels in the current font
bool holding = false; ///< Is a drag selection in process?
@@ -50,6 +49,13 @@ class BaseGrid final : public wxWindow {
wxScrollBar *scrollBar; ///< The grid's scrollbar
bool byFrame = false; ///< Should times be displayed as frame numbers
+ std::vector> columns;
+ std::vector column_widths;
+ std::vector column_header_widths;
+ std::vector column_shown;
+
+ std::vector text_refresh_rects;
+
/// Cached brushes used for row backgrounds
struct {
wxBrush Default;
@@ -88,24 +94,8 @@ class BaseGrid final : public wxWindow {
void OnSubtitlesSave();
void OnActiveLineChanged(AssDialogue *);
- void DrawImage(wxDC &dc, bool paint_columns[]);
- void GetRowStrings(int row, AssDialogue *line, bool *paint_columns, wxString *strings, bool replace, wxString const& rep_char) const;
-
void ScrollTo(int y);
- std::array colWidth; ///< Width in pixels of each column
- std::array headerWidth; ///< Width in pixels of each column's header
- std::array headerNames;
-
- int time_cols_x; ///< Left edge of the times columns
- int time_cols_w; ///< Width of the two times columns
- int text_col_x; ///< Left edge of the text column
- int text_col_w; ///< Width of the text column
- int cps_col_x; ///< Left edge of the cps column
- int cps_col_w; ///< Width of the cps column
-
- std::array showCol; ///< Column visibility mask (Text can't be hidden)
-
int yPos = 0;
void AdjustScrollbar();
diff --git a/src/grid_column.cpp b/src/grid_column.cpp
new file mode 100644
index 000000000..e71b522f2
--- /dev/null
+++ b/src/grid_column.cpp
@@ -0,0 +1,332 @@
+// Copyright (c) 2014, Thomas Goyne
+//
+// 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 "grid_column.h"
+
+#include "ass_dialogue.h"
+#include "ass_file.h"
+#include "compat.h"
+#include "include/aegisub/context.h"
+#include "options.h"
+#include "utils.h"
+#include "video_context.h"
+
+#include
+
+int WidthHelper::operator()(boost::flyweight const& str) {
+ if (str.get().empty()) return 0;
+ auto it = widths.find(str);
+ if (it != end(widths)) return it->second;
+ int width = dc.GetTextExtent(to_wx(str)).GetWidth();
+ widths[str] = width;
+ return width;
+}
+
+int WidthHelper::operator()(std::string const& str) {
+ return dc.GetTextExtent(to_wx(str)).GetWidth();
+}
+
+int WidthHelper::operator()(wxString const& str) {
+ return dc.GetTextExtent(str).GetWidth();
+}
+
+namespace {
+#define COLUMN_HEADER(value) \
+ private: const wxString header = value; \
+ public: wxString const& Header() const override { return header; }
+#define COLUMN_DESCRIPTION(value) \
+ private: const wxString description = value; \
+ public: wxString const& Description() const override { return description; }
+
+struct GridColumnLineNumber final : GridColumn {
+ COLUMN_HEADER(_("#"))
+ COLUMN_DESCRIPTION(_("Line Number"))
+ bool Centered() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context * = nullptr) const override {
+ return std::to_wstring(d->Row + 1);
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return helper(Value(&c->ass->Events.back()));
+ }
+};
+
+template
+T max_value(T AssDialogueBase::*field, EntryList const& lines) {
+ T value = 0;
+ for (AssDialogue const& line : lines) {
+ if (line.*field > value)
+ value = line.*field;
+ }
+ return value;
+}
+
+struct GridColumnLayer final : GridColumn {
+ COLUMN_HEADER(_("L"))
+ COLUMN_DESCRIPTION(_("Layer"))
+ bool Centered() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ return d->Layer ? wxString(std::to_wstring(d->Layer)) : wxString();
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ int max_layer = max_value(&AssDialogue::Layer, c->ass->Events);
+ return max_layer == 0 ? 0 : helper(std::to_wstring(max_layer));
+ }
+};
+
+struct GridColumnStartTime final : GridColumn {
+ COLUMN_HEADER(_("Start"))
+ COLUMN_DESCRIPTION(_("Start Time"))
+ bool Centered() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *c) const override {
+ if (c)
+ return std::to_wstring(c->videoController->FrameAtTime(d->Start, agi::vfr::START));
+ return to_wx(d->Start.GetAssFormated());
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool by_frame) const override {
+ if (!by_frame)
+ return helper(wxS("0:00:00.00"));
+ int frame = c->videoController->FrameAtTime(max_value(&AssDialogue::Start, c->ass->Events), agi::vfr::START);
+ return helper(std::to_wstring(frame));
+ }
+};
+
+struct GridColumnEndTime final : GridColumn {
+ COLUMN_HEADER(_("End"))
+ COLUMN_DESCRIPTION(_("End Time"))
+ bool Centered() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *c) const override {
+ if (c)
+ return std::to_wstring(c->videoController->FrameAtTime(d->End, agi::vfr::END));
+ return to_wx(d->End.GetAssFormated());
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool by_frame) const override {
+ if (!by_frame)
+ return helper(wxS("0:00:00.00"));
+ int frame = c->videoController->FrameAtTime(max_value(&AssDialogue::End, c->ass->Events), agi::vfr::END);
+ return helper(std::to_wstring(frame));
+ }
+};
+
+template
+int max_width(T AssDialogueBase::*field, EntryList const& lines, WidthHelper &helper) {
+ int w = 0;
+ for (AssDialogue const& line : lines) {
+ auto const& v = line.*field;
+ if (v.get().empty()) continue;
+ int width = helper(v);
+ if (width > w)
+ w = width;
+ }
+ return w;
+}
+
+struct GridColumnStyle final : GridColumn {
+ COLUMN_HEADER(_("Style"))
+ COLUMN_DESCRIPTION(_("Style"))
+ bool Centered() const override { return false; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *c) const override {
+ return to_wx(d->Style);
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return max_width(&AssDialogue::Style, c->ass->Events, helper);
+ }
+};
+
+struct GridColumnEffect final : GridColumn {
+ COLUMN_HEADER(_("Effect"))
+ COLUMN_DESCRIPTION(_("Effect"))
+ bool Centered() const override { return false; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ return to_wx(d->Effect);
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return max_width(&AssDialogue::Effect, c->ass->Events, helper);
+ }
+};
+
+struct GridColumnActor final : GridColumn {
+ COLUMN_HEADER(_("Actor"))
+ COLUMN_DESCRIPTION(_("Actor"))
+ bool Centered() const override { return false; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ return to_wx(d->Actor);
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return max_width(&AssDialogue::Actor, c->ass->Events, helper);
+ }
+};
+
+template
+struct GridColumnMargin : GridColumn {
+ bool Centered() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ return d->Margin[Index] ? wxString(std::to_wstring(d->Margin[Index])) : wxString();
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ int max = 0;
+ for (AssDialogue const& line : c->ass->Events) {
+ if (line.Margin[Index] > max)
+ max = line.Margin[Index];
+ }
+ return max == 0 ? 0 : helper(std::to_wstring(max));
+ }
+};
+
+struct GridColumnMarginLeft final : GridColumnMargin<0> {
+ COLUMN_HEADER(_("Left"))
+ COLUMN_DESCRIPTION(_("Left Margin"))
+};
+
+struct GridColumnMarginRight final : GridColumnMargin<0> {
+ COLUMN_HEADER(_("Right"))
+ COLUMN_DESCRIPTION(_("Right Margin"))
+};
+
+struct GridColumnMarginVert final : GridColumnMargin<0> {
+ COLUMN_HEADER(_("Vert"))
+ COLUMN_DESCRIPTION(_("Vertical Margin"))
+};
+
+struct GridColumnCPS final : GridColumn {
+ COLUMN_HEADER(_("CPS"))
+ COLUMN_DESCRIPTION(_("Characters Per Second"))
+ bool Centered() const override { return true; }
+ bool RefreshOnTextChange() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ int characters = 0;
+
+ auto const& text = d->Text.get();
+ auto pos = begin(text);
+ do {
+ auto it = std::find(pos, end(text), '{');
+ characters += CharacterCount(pos, it, true);
+ if (it == end(text)) break;
+
+ pos = std::find(pos, end(text), '}');
+ if (pos == end(text)) {
+ characters += CharacterCount(it, pos, true);
+ break;
+ }
+ } while (++pos != end(text));
+
+ int duration = d->End - d->Start;
+ if (duration <= 0 || characters * 1000 / duration >= 1000)
+ return wxS("");
+ else
+ return std::to_wstring(characters * 1000 / duration);
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return helper(wxS("999"));
+ }
+};
+
+class GridColumnText final : public GridColumn {
+ int override_mode;
+ wxString replace_char;
+
+ agi::signal::Connection override_mode_connection;
+ agi::signal::Connection replace_char_connection;
+
+public:
+ GridColumnText()
+ : override_mode(OPT_GET("Subtitle/Grid/Hide Overrides")->GetInt())
+ , replace_char(to_wx(OPT_GET("Subtitle/Grid/Hide Overrides Char")->GetString()))
+ , override_mode_connection(OPT_SUB("Subtitle/Grid/Hide Overrides",
+ [&](agi::OptionValue const& v) { override_mode = v.GetInt(); }))
+ , replace_char_connection(OPT_SUB("Subtitle/Grid/Hide Overrides Char",
+ [&](agi::OptionValue const& v) { replace_char = to_wx(v.GetString()); }))
+ {
+ }
+
+ COLUMN_HEADER(_("Text"))
+ COLUMN_DESCRIPTION(_("Text"))
+ bool Centered() const override { return false; }
+ bool CanHide() const override { return false; }
+ bool RefreshOnTextChange() const override { return true; }
+
+ wxString Value(const AssDialogue *d, const agi::Context *) const override {
+ wxString str;
+
+ // Show overrides
+ if (override_mode == 0)
+ str = to_wx(d->Text);
+ // Hidden overrides
+ else {
+ str.reserve(d->Text.get().size());
+ size_t start = 0, pos;
+ while ((pos = d->Text.get().find('{', start)) != std::string::npos) {
+ str += to_wx(d->Text.get().substr(start, pos - start));
+ if (override_mode == 1)
+ str += replace_char;
+ start = d->Text.get().find('}', pos);
+ if (start != std::string::npos) ++start;
+ }
+ if (start != std::string::npos)
+ str += to_wx(d->Text.get().substr(start));
+ }
+
+ // Cap length and set text
+ if (str.size() > 512)
+ str = str.Left(512) + "...";
+ return str;
+ }
+
+ int Width(const agi::Context *c, WidthHelper &helper, bool) const override {
+ return 5000;
+ }
+};
+
+template
+std::unique_ptr make() {
+ return std::unique_ptr(new T);
+}
+
+}
+
+std::vector> GetGridColumns() {
+ std::vector> ret;
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ ret.push_back(make());
+ return ret;
+}
diff --git a/src/grid_column.h b/src/grid_column.h
new file mode 100644
index 000000000..10d8501d3
--- /dev/null
+++ b/src/grid_column.h
@@ -0,0 +1,60 @@
+// Copyright (c) 2014, Thomas Goyne
+//
+// 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
+#include
+#include
+#include
+#include
+
+class AssDialogue;
+class wxClientDC;
+class wxString;
+namespace agi { struct Context; }
+
+namespace std {
+ template
+ struct hash> {
+ size_t operator()(boost::flyweight const& ss) const {
+ return hash()(&ss.get());
+ }
+ };
+}
+
+struct WidthHelper {
+ wxDC &dc;
+ std::unordered_map, int> widths;
+
+ int operator()(boost::flyweight const& str);
+ int operator()(std::string const& str);
+ int operator()(wxString const& str);
+};
+
+struct GridColumn {
+ virtual ~GridColumn() = default;
+
+ virtual bool Centered() const = 0;
+ virtual bool CanHide() const { return true; }
+ virtual bool RefreshOnTextChange() const { return false; }
+
+ virtual wxString const& Header() const = 0;
+ virtual wxString const& Description() const = 0;
+
+ virtual wxString Value(const AssDialogue *d, const agi::Context * = nullptr) const = 0;
+ virtual int Width(const agi::Context *c, WidthHelper &helper, bool by_frame) const = 0;
+};
+
+std::vector> GetGridColumns();
\ No newline at end of file