1 // Copyright (c) 2006, Rodrigo Braz Monteiro
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 //   * Redistributions of source code must retain the above copyright notice,
8 //     this list of conditions and the following disclaimer.
9 //   * Redistributions in binary form must reproduce the above copyright notice,
10 //     this list of conditions and the following disclaimer in the documentation
11 //     and/or other materials provided with the distribution.
12 //   * Neither the name of the Aegisub Group nor the names of its contributors
13 //     may be used to endorse or promote products derived from this software
14 //     without specific prior written permission.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 // POSSIBILITY OF SUCH DAMAGE.
27 //
28 // Aegisub Project http://www.aegisub.org/
29 
30 #include "base_grid.h"
31 
32 #include "include/aegisub/context.h"
33 #include "include/aegisub/hotkey.h"
34 #include "include/aegisub/menu.h"
35 
36 #include "ass_dialogue.h"
37 #include "ass_file.h"
38 #include "audio_box.h"
39 #include "compat.h"
40 #include "grid_column.h"
41 #include "options.h"
42 #include "project.h"
43 #include "utils.h"
44 #include "selection_controller.h"
45 #include "subs_controller.h"
46 #include "video_controller.h"
47 
48 #include <libaegisub/util.h>
49 
50 #include <algorithm>
51 
52 #include <wx/dcbuffer.h>
53 #include <wx/menu.h>
54 #include <wx/scrolbar.h>
55 #include <wx/sizer.h>
56 
57 enum {
58 	GRID_SCROLLBAR = 1730,
59 	MENU_SHOW_COL = 1250 // Needs 15 IDs after this
60 };
61 
BaseGrid(wxWindow * parent,agi::Context * context)62 BaseGrid::BaseGrid(wxWindow* parent, agi::Context *context)
63 : wxWindow(parent, -1, wxDefaultPosition, wxDefaultSize, wxWANTS_CHARS | wxSUNKEN_BORDER)
64 , scrollBar(new wxScrollBar(this, GRID_SCROLLBAR, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL))
65 , context(context)
66 , columns(GetGridColumns())
67 , columns_visible(OPT_GET("Subtitle/Grid/Column")->GetListBool())
68 , seek_listener(context->videoController->AddSeekListener(&BaseGrid::OnSeek, this))
69 {
70 	scrollBar->SetScrollbar(0,10,100,10);
71 
72 	auto scrollbarpositioner = new wxBoxSizer(wxHORIZONTAL);
73 	scrollbarpositioner->AddStretchSpacer();
74 	scrollbarpositioner->Add(scrollBar, 0, wxEXPAND, 0);
75 
76 	SetSizerAndFit(scrollbarpositioner);
77 
78 	SetBackgroundStyle(wxBG_STYLE_PAINT);
79 
80 	for (size_t i : agi::util::range(std::min(columns_visible.size(), columns.size()))) {
81 		if (!columns_visible[i])
82 			columns[i]->SetVisible(false);
83 	}
84 
85 	UpdateStyle();
86 	OnHighlightVisibleChange(*OPT_GET("Subtitle/Grid/Highlight Subtitles in Frame"));
87 
88 	connections = agi::signal::make_vector({
89 		context->ass->AddCommitListener(&BaseGrid::OnSubtitlesCommit, this),
90 
91 		context->selectionController->AddActiveLineListener(&BaseGrid::OnActiveLineChanged, this),
92 		context->selectionController->AddSelectionListener([&]{ Refresh(false); }),
93 
94 		OPT_SUB("Subtitle/Grid/Font Face", &BaseGrid::UpdateStyle, this),
95 		OPT_SUB("Subtitle/Grid/Font Size", &BaseGrid::UpdateStyle, this),
96 		OPT_SUB("Colour/Subtitle Grid/Active Border", &BaseGrid::UpdateStyle, this),
97 		OPT_SUB("Colour/Subtitle Grid/Background/Background", &BaseGrid::UpdateStyle, this),
98 		OPT_SUB("Colour/Subtitle Grid/Background/Comment", &BaseGrid::UpdateStyle, this),
99 		OPT_SUB("Colour/Subtitle Grid/Background/Inframe", &BaseGrid::UpdateStyle, this),
100 		OPT_SUB("Colour/Subtitle Grid/Background/Selected Comment", &BaseGrid::UpdateStyle, this),
101 		OPT_SUB("Colour/Subtitle Grid/Background/Selection", &BaseGrid::UpdateStyle, this),
102 		OPT_SUB("Colour/Subtitle Grid/Collision", &BaseGrid::UpdateStyle, this),
103 		OPT_SUB("Colour/Subtitle Grid/Header", &BaseGrid::UpdateStyle, this),
104 		OPT_SUB("Colour/Subtitle Grid/Left Column", &BaseGrid::UpdateStyle, this),
105 		OPT_SUB("Colour/Subtitle Grid/Lines", &BaseGrid::UpdateStyle, this),
106 		OPT_SUB("Colour/Subtitle Grid/Selection", &BaseGrid::UpdateStyle, this),
107 		OPT_SUB("Colour/Subtitle Grid/Standard", &BaseGrid::UpdateStyle, this),
108 
109 		OPT_SUB("Subtitle/Grid/Highlight Subtitles in Frame", &BaseGrid::OnHighlightVisibleChange, this),
110 		OPT_SUB("Subtitle/Grid/Hide Overrides", [&](agi::OptionValue const&) { Refresh(false); }),
111 	});
112 
113 	Bind(wxEVT_CONTEXT_MENU, &BaseGrid::OnContextMenu, this);
114 }
115 
~BaseGrid()116 BaseGrid::~BaseGrid() { }
117 
BEGIN_EVENT_TABLE(BaseGrid,wxWindow)118 BEGIN_EVENT_TABLE(BaseGrid,wxWindow)
119 	EVT_PAINT(BaseGrid::OnPaint)
120 	EVT_SIZE(BaseGrid::OnSize)
121 	EVT_COMMAND_SCROLL(GRID_SCROLLBAR,BaseGrid::OnScroll)
122 	EVT_MOUSE_EVENTS(BaseGrid::OnMouseEvent)
123 	EVT_KEY_DOWN(BaseGrid::OnKeyDown)
124 	EVT_CHAR_HOOK(BaseGrid::OnCharHook)
125 	EVT_MENU_RANGE(MENU_SHOW_COL,MENU_SHOW_COL+15,BaseGrid::OnShowColMenu)
126 END_EVENT_TABLE()
127 
128 void BaseGrid::OnSubtitlesCommit(int type) {
129 	if (type == AssFile::COMMIT_NEW || type & AssFile::COMMIT_ORDER || type & AssFile::COMMIT_DIAG_ADDREM)
130 		UpdateMaps();
131 
132 	if (type & AssFile::COMMIT_DIAG_META) {
133 		SetColumnWidths();
134 		Refresh(false);
135 		return;
136 	}
137 	if (type & AssFile::COMMIT_DIAG_TIME)
138 		Refresh(false);
139 	else if (type & AssFile::COMMIT_DIAG_TEXT) {
140 		for (auto const& rect : text_refresh_rects)
141 			RefreshRect(rect, false);
142 	}
143 }
144 
OnShowColMenu(wxCommandEvent & event)145 void BaseGrid::OnShowColMenu(wxCommandEvent &event) {
146 	int item = event.GetId() - MENU_SHOW_COL;
147 	bool new_value = !columns_visible[item];
148 
149 	columns_visible.resize(columns.size(), true);
150 	columns_visible[item] = new_value;
151 	OPT_SET("Subtitle/Grid/Column")->SetListBool(columns_visible);
152 	columns[item]->SetVisible(new_value);
153 
154 	SetColumnWidths();
155 
156 	Refresh(false);
157 }
158 
OnHighlightVisibleChange(agi::OptionValue const & opt)159 void BaseGrid::OnHighlightVisibleChange(agi::OptionValue const& opt) {
160 	if (opt.GetBool())
161 		seek_listener.Unblock();
162 	else
163 		seek_listener.Block();
164 }
165 
UpdateStyle()166 void BaseGrid::UpdateStyle() {
167 	wxString fontname = FontFace("Subtitle/Grid");
168 	if (fontname.empty()) fontname = "Tahoma";
169 	font.SetFaceName(fontname);
170 	font.SetPointSize(OPT_GET("Subtitle/Grid/Font Size")->GetInt());
171 	font.SetWeight(wxFONTWEIGHT_NORMAL);
172 
173 	wxClientDC dc(this);
174 	dc.SetFont(font);
175 
176 	// Set line height
177 	lineHeight = dc.GetCharHeight() + 4;
178 
179 	// Set row brushes
180 	row_colors.Default.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Background")->GetColor()));
181 	row_colors.Header.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Header")->GetColor()));
182 	row_colors.Selection.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Selection")->GetColor()));
183 	row_colors.Comment.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Comment")->GetColor()));
184 	row_colors.Visible.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Inframe")->GetColor()));
185 	row_colors.SelectedComment.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Background/Selected Comment")->GetColor()));
186 	row_colors.LeftCol.SetColour(to_wx(OPT_GET("Colour/Subtitle Grid/Left Column")->GetColor()));
187 
188 	SetColumnWidths();
189 
190 	AdjustScrollbar();
191 	Refresh(false);
192 }
193 
UpdateMaps()194 void BaseGrid::UpdateMaps() {
195 	index_line_map.clear();
196 
197 	for (auto& curdiag : context->ass->Events)
198 		index_line_map.push_back(&curdiag);
199 
200 	SetColumnWidths();
201 	AdjustScrollbar();
202 	Refresh(false);
203 }
204 
OnActiveLineChanged(AssDialogue * new_active)205 void BaseGrid::OnActiveLineChanged(AssDialogue *new_active) {
206 	if (new_active) {
207 		if (new_active->Row != active_row)
208 			MakeRowVisible(new_active->Row);
209 		extendRow = active_row = new_active->Row;
210 		Refresh(false);
211 	}
212 	else
213 		active_row = -1;
214 }
215 
MakeRowVisible(int row)216 void BaseGrid::MakeRowVisible(int row) {
217 	int h = GetClientSize().GetHeight();
218 
219 	if (row < yPos + 1)
220 		ScrollTo(row - 1);
221 	else if (row > yPos + h/lineHeight - 3)
222 		ScrollTo(row - h/lineHeight + 3);
223 }
224 
SelectRow(int row,bool addToSelected,bool select)225 void BaseGrid::SelectRow(int row, bool addToSelected, bool select) {
226 	if (row < 0 || (size_t)row >= index_line_map.size()) return;
227 
228 	AssDialogue *line = index_line_map[row];
229 
230 	if (!addToSelected) {
231 		context->selectionController->SetSelectedSet(Selection{line});
232 		return;
233 	}
234 
235 	bool selected = !!context->selectionController->GetSelectedSet().count(line);
236 	if (select != selected) {
237 		auto selection = context->selectionController->GetSelectedSet();
238 		if (select)
239 			selection.insert(line);
240 		else
241 			selection.erase(line);
242 		context->selectionController->SetSelectedSet(std::move(selection));
243 	}
244 }
245 
OnSeek()246 void BaseGrid::OnSeek() {
247 	int lines = GetClientSize().GetHeight() / lineHeight + 1;
248 	lines = mid(0, lines, GetRows() - yPos);
249 
250 	auto it = begin(visible_rows);
251 	for (int i : boost::irange(yPos, yPos + lines)) {
252 		if (IsDisplayed(index_line_map[i])) {
253 			if (it == end(visible_rows) || *it != i) {
254 				Refresh(false);
255 				return;
256 			}
257 			++it;
258 		}
259 	}
260 	if (it != end(visible_rows))
261 		Refresh(false);
262 }
263 
OnPaint(wxPaintEvent &)264 void BaseGrid::OnPaint(wxPaintEvent &) {
265 	// Find which columns need to be repainted
266 	std::vector<char> paint_columns;
267 	paint_columns.resize(columns.size(), false);
268 	bool any = false;
269 	for (wxRegionIterator region(GetUpdateRegion()); region; ++region) {
270 		wxRect updrect = region.GetRect();
271 		int x = 0;
272 		for (size_t i : agi::util::range(columns.size())) {
273 			int width = columns[i]->Width();
274 			if (width && updrect.x < x + width && updrect.x + updrect.width > x) {
275 				paint_columns[i] = true;
276 				any = true;
277 			}
278 			x += width;
279 		}
280 	}
281 
282 	if (!any) return;
283 
284 	int w = 0;
285 	int h = 0;
286 	GetClientSize(&w,&h);
287 	w -= scrollBar->GetSize().GetWidth();
288 
289 	wxAutoBufferedPaintDC dc(this);
290 	dc.SetFont(font);
291 
292 	dc.SetBackground(row_colors.Default);
293 	dc.Clear();
294 
295 	// Draw labels
296 	dc.SetPen(*wxTRANSPARENT_PEN);
297 	dc.SetBrush(row_colors.LeftCol);
298 	dc.DrawRectangle(0, lineHeight, columns[0]->Width(), h-lineHeight);
299 
300 	// Row colors
301 	wxColour text_standard(to_wx(OPT_GET("Colour/Subtitle Grid/Standard")->GetColor()));
302 	wxColour text_selection(to_wx(OPT_GET("Colour/Subtitle Grid/Selection")->GetColor()));
303 	wxColour text_collision(to_wx(OPT_GET("Colour/Subtitle Grid/Collision")->GetColor()));
304 
305 	// First grid row
306 	wxPen grid_pen(to_wx(OPT_GET("Colour/Subtitle Grid/Lines")->GetColor()));
307 	dc.SetPen(grid_pen);
308 	dc.DrawLine(0, 0, w, 0);
309 	dc.SetPen(*wxTRANSPARENT_PEN);
310 
311 	auto paint_text = [&](wxString const& str, int x, int y, int col) {
312 		int left = x + 4;
313 		if (columns[col]->Centered()) {
314 			wxSize ext = dc.GetTextExtent(str);
315 			left += (columns[col]->Width() - 6 - ext.GetWidth()) / 2;
316 		}
317 
318 		dc.DrawText(str, left, y + 2);
319 	};
320 
321 	// Paint header
322 	{
323 		dc.SetTextForeground(text_standard);
324 		dc.SetBrush(row_colors.Header);
325 		dc.DrawRectangle(0, 0, w, lineHeight);
326 
327 		int x = 0;
328 		for (size_t i : agi::util::range(columns.size())) {
329 			if (paint_columns[i])
330 				paint_text(columns[i]->Header(), x, 0, i);
331 			x += columns[i]->Width();
332 		}
333 
334 		dc.SetPen(grid_pen);
335 		dc.DrawLine(0, lineHeight, w, lineHeight);
336 	}
337 
338 	// Paint the rows
339 	const int drawPerScreen = h/lineHeight + 1;
340 	const int nDraw = mid(0, drawPerScreen, GetRows() - yPos);
341 	const int grid_x = columns[0]->Width();
342 
343 	const auto active_line = context->selectionController->GetActiveLine();
344 	auto const& selection = context->selectionController->GetSelectedSet();
345 	visible_rows.clear();
346 
347 	for (int i : agi::util::range(nDraw)) {
348 		wxBrush color = row_colors.Default;
349 		AssDialogue *curDiag = index_line_map[i + yPos];
350 
351 		bool inSel = !!selection.count(curDiag);
352 		if (inSel && curDiag->Comment)
353 			color = row_colors.SelectedComment;
354 		else if (inSel)
355 			color = row_colors.Selection;
356 		else if (curDiag->Comment)
357 			color = row_colors.Comment;
358 
359 		if (OPT_GET("Subtitle/Grid/Highlight Subtitles in Frame")->GetBool() && IsDisplayed(curDiag)) {
360 			if (color == row_colors.Default)
361 				color = row_colors.Visible;
362 			visible_rows.push_back(i + yPos);
363 		}
364 		dc.SetBrush(color);
365 
366 		// Draw row background color
367 		if (color != row_colors.Default) {
368 			dc.SetPen(*wxTRANSPARENT_PEN);
369 			dc.DrawRectangle(grid_x, (i + 1) * lineHeight + 1, w, lineHeight);
370 		}
371 
372 		if (active_line != curDiag && curDiag->CollidesWith(active_line))
373 			dc.SetTextForeground(text_collision);
374 		else if (inSel)
375 			dc.SetTextForeground(text_selection);
376 		else
377 			dc.SetTextForeground(text_standard);
378 
379 		// Draw text
380 		int x = 0;
381 		int y = (i + 1) * lineHeight;
382 		for (size_t j : agi::util::range(columns.size())) {
383 			if (paint_columns[j])
384 				columns[j]->Paint(dc, x, y, curDiag, context);
385 			x += columns[j]->Width();
386 		}
387 
388 		// Draw grid
389 		dc.SetPen(grid_pen);
390 		dc.DrawLine(0, y + lineHeight, w , y + lineHeight);
391 		dc.SetPen(*wxTRANSPARENT_PEN);
392 	}
393 
394 	// Draw grid columns
395 	{
396 		int maxH = (nDraw + 1) * lineHeight;
397 		int x = 0;
398 		dc.SetPen(grid_pen);
399 		for (auto const& column : columns) {
400 			x += column->Width();
401 			if (x < w)
402 				dc.DrawLine(x, 0, x, maxH);
403 		}
404 		dc.DrawLine(0, 0, 0, maxH);
405 		dc.DrawLine(w, 0, w, maxH);
406 	}
407 
408 	if (active_line && active_line->Row >= yPos && active_line->Row < yPos + nDraw) {
409 		dc.SetPen(wxPen(to_wx(OPT_GET("Colour/Subtitle Grid/Active Border")->GetColor())));
410 		dc.SetBrush(*wxTRANSPARENT_BRUSH);
411 		dc.DrawRectangle(0, (active_line->Row - yPos + 1) * lineHeight, w, lineHeight + 1);
412 	}
413 }
414 
OnSize(wxSizeEvent &)415 void BaseGrid::OnSize(wxSizeEvent &) {
416 	AdjustScrollbar();
417 	Refresh(false);
418 }
419 
OnScroll(wxScrollEvent & event)420 void BaseGrid::OnScroll(wxScrollEvent &event) {
421 	int newPos = event.GetPosition();
422 	if (yPos != newPos) {
423 		context->ass->Properties.scroll_position = yPos = newPos;
424 		Refresh(false);
425 	}
426 }
427 
OnMouseEvent(wxMouseEvent & event)428 void BaseGrid::OnMouseEvent(wxMouseEvent &event) {
429 	int h = GetClientSize().GetHeight();
430 	bool shift = event.ShiftDown();
431 	bool alt = event.AltDown();
432 	bool ctrl = event.CmdDown();
433 
434 	// Row that mouse is over
435 	bool click = event.LeftDown();
436 	bool dclick = event.LeftDClick();
437 	int row = event.GetY() / lineHeight + yPos - 1;
438 	if (holding && !click)
439 		row = mid(0, row, GetRows()-1);
440 	AssDialogue *dlg = GetDialogue(row);
441 	if (!dlg) row = 0;
442 
443 	if (event.ButtonDown() && OPT_GET("Subtitle/Grid/Focus Allow")->GetBool())
444 		SetFocus();
445 
446 	if (holding) {
447 		if (!event.LeftIsDown()) {
448 			if (dlg)
449 				MakeRowVisible(row);
450 			holding = false;
451 			ReleaseMouse();
452 		}
453 		else {
454 			// Only scroll if the mouse has moved to a different row to avoid
455 			// scrolling on sloppy clicks
456 			if (row != extendRow) {
457 				if (row <= yPos)
458 					ScrollTo(yPos - 3);
459 				// When dragging down we give a 3 row margin to make it easier
460 				// to see what's going on, but we don't want to scroll down if
461 				// the user clicks on the bottom row and drags up
462 				else if (row > yPos + h / lineHeight - (row > extendRow ? 3 : 1))
463 					ScrollTo(yPos + 3);
464 			}
465 		}
466 	}
467 	else if (click && dlg) {
468 		holding = true;
469 		CaptureMouse();
470 	}
471 
472 	if ((click || holding || dclick) && dlg) {
473 		int old_extend = extendRow;
474 
475 		// SetActiveLine will scroll the grid if the row is only half-visible,
476 		// but we don't want to scroll until the mouse moves or the button is
477 		// released, to avoid selecting multiple lines on a click
478 		int old_y_pos = yPos;
479 		context->selectionController->SetActiveLine(dlg);
480 		ScrollTo(old_y_pos);
481 		extendRow = row;
482 
483 		auto const& selection = context->selectionController->GetSelectedSet();
484 
485 		// Toggle selected
486 		if (click && ctrl && !shift && !alt) {
487 			bool isSel = !!selection.count(dlg);
488 			if (isSel && selection.size() == 1) return;
489 			SelectRow(row, true, !isSel);
490 			return;
491 		}
492 
493 		// Normal click
494 		if ((click || dclick) && !shift && !ctrl && !alt) {
495 			if (dclick) {
496 				context->audioBox->ScrollToActiveLine();
497 				context->videoController->JumpToTime(dlg->Start);
498 			}
499 			SelectRow(row, false);
500 			return;
501 		}
502 
503 		// Change active line only
504 		if (click && !shift && !ctrl && alt)
505 			return;
506 
507 		// Block select
508 		if ((click && shift && !alt) || holding) {
509 			extendRow = old_extend;
510 			int i1 = row;
511 			int i2 = extendRow;
512 
513 			if (i1 > i2)
514 				std::swap(i1, i2);
515 
516 			// Toggle each
517 			Selection newsel;
518 			if (ctrl) newsel = selection;
519 			for (int i = i1; i <= i2; i++)
520 				newsel.insert(GetDialogue(i));
521 			context->selectionController->SetSelectedSet(std::move(newsel));
522 			return;
523 		}
524 
525 		return;
526 	}
527 
528 	// Mouse wheel
529 	if (event.GetWheelRotation() != 0) {
530 		if (ForwardMouseWheelEvent(this, event)) {
531 			int step = shift ? h / lineHeight - 2 : 3;
532 			ScrollTo(yPos - step * event.GetWheelRotation() / event.GetWheelDelta());
533 		}
534 		return;
535 	}
536 
537 	event.Skip();
538 }
539 
OnContextMenu(wxContextMenuEvent & evt)540 void BaseGrid::OnContextMenu(wxContextMenuEvent &evt) {
541 	wxPoint pos = evt.GetPosition();
542 	if (pos == wxDefaultPosition || ScreenToClient(pos).y > lineHeight) {
543 		if (!context_menu) context_menu = menu::GetMenu("grid_context", context);
544 		menu::OpenPopupMenu(context_menu.get(), this);
545 	}
546 	else {
547 		wxMenu menu;
548 		for (size_t i : agi::util::range(columns.size())) {
549 			if (columns[i]->CanHide())
550 				menu.Append(MENU_SHOW_COL + i, columns[i]->Description(), "", wxITEM_CHECK)->Check(columns[i]->Visible());
551 		}
552 		PopupMenu(&menu);
553 	}
554 }
555 
ScrollTo(int y)556 void BaseGrid::ScrollTo(int y) {
557 	int nextY = mid(0, y, GetRows() - 1);
558 	if (yPos != nextY) {
559 		context->ass->Properties.scroll_position = yPos = nextY;
560 		scrollBar->SetThumbPosition(yPos);
561 		Refresh(false);
562 	}
563 }
564 
AdjustScrollbar()565 void BaseGrid::AdjustScrollbar() {
566 	wxSize clientSize = GetClientSize();
567 	wxSize scrollbarSize = scrollBar->GetSize();
568 
569 	scrollBar->Freeze();
570 	scrollBar->SetSize(clientSize.GetWidth() - scrollbarSize.GetWidth(), 0, scrollbarSize.GetWidth(), clientSize.GetHeight());
571 
572 	if (GetRows() <= 1) {
573 		scrollBar->Enable(false);
574 		scrollBar->Thaw();
575 		return;
576 	}
577 
578 	if (!scrollBar->IsEnabled())
579 		scrollBar->Enable(true);
580 
581 	int drawPerScreen = clientSize.GetHeight() / lineHeight;
582 	int rows = GetRows();
583 
584 	context->ass->Properties.scroll_position = yPos = mid(0, yPos, rows - 1);
585 
586 	scrollBar->SetScrollbar(yPos, drawPerScreen, rows + drawPerScreen - 1, drawPerScreen - 2, true);
587 	scrollBar->Thaw();
588 }
589 
SetColumnWidths()590 void BaseGrid::SetColumnWidths() {
591 	int w, h;
592 	GetClientSize(&w, &h);
593 
594 	// DC for text extents test
595 	wxClientDC dc(this);
596 	dc.SetFont(font);
597 
598 	text_refresh_rects.clear();
599 	int x = 0;
600 
601 	WidthHelper helper{dc, std::unordered_map<boost::flyweight<std::string>, int>{}};
602 	for (auto const& column : columns) {
603 		column->UpdateWidth(context, helper);
604 		if (column->Width() && column->RefreshOnTextChange())
605 			text_refresh_rects.emplace_back(x, 0, column->Width(), h);
606 		x += column->Width();
607 	}
608 }
609 
GetDialogue(int n) const610 AssDialogue *BaseGrid::GetDialogue(int n) const {
611 	if (static_cast<size_t>(n) >= index_line_map.size()) return nullptr;
612 	return index_line_map[n];
613 }
614 
IsDisplayed(const AssDialogue * line) const615 bool BaseGrid::IsDisplayed(const AssDialogue *line) const {
616 	if (!context->project->VideoProvider()) return false;
617 	int frame = context->videoController->GetFrameN();
618 	return context->project->Timecodes().FrameAtTime(line->Start, agi::vfr::START) <= frame
619 		&& context->project->Timecodes().FrameAtTime(line->End, agi::vfr::END) >= frame;
620 }
621 
OnCharHook(wxKeyEvent & event)622 void BaseGrid::OnCharHook(wxKeyEvent &event) {
623 	if (hotkey::check("Subtitle Grid", context, event))
624 		return;
625 
626 	int key = event.GetKeyCode();
627 
628 	if (key == WXK_UP || key == WXK_DOWN ||
629 		key == WXK_PAGEUP || key == WXK_PAGEDOWN ||
630 		key == WXK_HOME || key == WXK_END)
631 	{
632 		event.Skip();
633 		return;
634 	}
635 
636 	hotkey::check("Audio", context, event);
637 }
638 
OnKeyDown(wxKeyEvent & event)639 void BaseGrid::OnKeyDown(wxKeyEvent &event) {
640 	int w,h;
641 	GetClientSize(&w, &h);
642 
643 	int key = event.GetKeyCode();
644 	bool ctrl = event.CmdDown();
645 	bool alt = event.AltDown();
646 	bool shift = event.ShiftDown();
647 
648 	int dir = 0;
649 	int step = 1;
650 	if (key == WXK_UP) dir = -1;
651 	else if (key == WXK_DOWN) dir = 1;
652 	else if (key == WXK_PAGEUP) {
653 		dir = -1;
654 		step = h / lineHeight - 2;
655 	}
656 	else if (key == WXK_PAGEDOWN) {
657 		dir = 1;
658 		step = h / lineHeight - 2;
659 	}
660 	else if (key == WXK_HOME) {
661 		dir = -1;
662 		step = GetRows();
663 	}
664 	else if (key == WXK_END) {
665 		dir = 1;
666 		step = GetRows();
667 	}
668 
669 	if (!dir) {
670 		event.Skip();
671 		return;
672 	}
673 
674 	auto active_line = context->selectionController->GetActiveLine();
675 	int old_extend = extendRow;
676 	int next = mid(0, (active_line ? active_line->Row : 0) + dir * step, GetRows() - 1);
677 	context->selectionController->SetActiveLine(GetDialogue(next));
678 
679 	// Move selection
680 	if (!ctrl && !shift && !alt) {
681 		SelectRow(next);
682 		return;
683 	}
684 
685 	// Move active only
686 	if (alt && !shift && !ctrl)
687 		return;
688 
689 	// Shift-selection
690 	if (shift && !ctrl && !alt) {
691 		extendRow = old_extend;
692 		// Set range
693 		int begin = next;
694 		int end = extendRow;
695 		if (end < begin)
696 			std::swap(begin, end);
697 
698 		// Select range
699 		Selection newsel;
700 		for (int i = begin; i <= end; i++)
701 			newsel.insert(GetDialogue(i));
702 
703 		context->selectionController->SetSelectedSet(std::move(newsel));
704 
705 		MakeRowVisible(next);
706 		return;
707 	}
708 }
709 
SetByFrame(bool state)710 void BaseGrid::SetByFrame(bool state) {
711 	if (byFrame == state) return;
712 	byFrame = state;
713 	for (auto& column : columns)
714 		column->SetByFrame(byFrame);
715 	SetColumnWidths();
716 	Refresh(false);
717 }
718