2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file textfile_gui.cpp Implementation of textfile window. */
11 #include "core/backup_type.hpp"
12 #include "fileio_func.h"
13 #include "fontcache.h"
16 #include "string_func.h"
17 #include "textfile_gui.h"
18 #include "dropdown_type.h"
19 #include "dropdown_func.h"
20 #include "gfx_layout.h"
24 #include "widgets/misc_widget.h"
26 #include "table/strings.h"
27 #include "table/control_codes.h"
29 #if defined(WITH_ZLIB)
33 #if defined(WITH_LIBLZMA)
39 #include "safeguards.h"
41 /** Widgets for the textfile window. */
42 static constexpr NWidgetPart _nested_textfile_widgets
[] = {
43 NWidget(NWID_HORIZONTAL
),
44 NWidget(WWT_CLOSEBOX
, COLOUR_MAUVE
),
45 NWidget(WWT_PUSHARROWBTN
, COLOUR_MAUVE
, WID_TF_NAVBACK
), SetFill(0, 1), SetMinimalSize(15, 1), SetDataTip(AWV_DECREASE
, STR_TEXTFILE_NAVBACK_TOOLTIP
),
46 NWidget(WWT_PUSHARROWBTN
, COLOUR_MAUVE
, WID_TF_NAVFORWARD
), SetFill(0, 1), SetMinimalSize(15, 1), SetDataTip(AWV_INCREASE
, STR_TEXTFILE_NAVFORWARD_TOOLTIP
),
47 NWidget(WWT_CAPTION
, COLOUR_MAUVE
, WID_TF_CAPTION
), SetDataTip(STR_NULL
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
48 NWidget(WWT_TEXTBTN
, COLOUR_MAUVE
, WID_TF_WRAPTEXT
), SetDataTip(STR_TEXTFILE_WRAP_TEXT
, STR_TEXTFILE_WRAP_TEXT_TOOLTIP
),
49 NWidget(WWT_DEFSIZEBOX
, COLOUR_MAUVE
),
51 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_TF_SEL_JUMPLIST
),
52 NWidget(WWT_PANEL
, COLOUR_MAUVE
),
53 NWidget(NWID_HORIZONTAL
), SetPIP(WidgetDimensions::unscaled
.frametext
.left
, 0, WidgetDimensions::unscaled
.frametext
.right
),
54 /* As this widget can be toggled, it needs to be a multiplier of FS_MONO. So add a spacer that ensures this. */
55 NWidget(NWID_SPACER
), SetMinimalSize(1, 0), SetMinimalTextLines(2, 0, FS_MONO
),
56 NWidget(NWID_VERTICAL
),
57 NWidget(NWID_SPACER
), SetFill(1, 1), SetResize(1, 0),
58 NWidget(WWT_DROPDOWN
, COLOUR_MAUVE
, WID_TF_JUMPLIST
), SetDataTip(STR_TEXTFILE_JUMPLIST
, STR_TEXTFILE_JUMPLIST_TOOLTIP
), SetFill(1, 0), SetResize(1, 0),
59 NWidget(NWID_SPACER
), SetFill(1, 1), SetResize(1, 0),
64 NWidget(NWID_HORIZONTAL
),
65 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_TF_BACKGROUND
), SetMinimalSize(200, 125), SetResize(1, 12), SetScrollbar(WID_TF_VSCROLLBAR
),
67 NWidget(NWID_VERTICAL
),
68 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_TF_VSCROLLBAR
),
71 NWidget(NWID_HORIZONTAL
),
72 NWidget(NWID_HSCROLLBAR
, COLOUR_MAUVE
, WID_TF_HSCROLLBAR
),
73 NWidget(WWT_RESIZEBOX
, COLOUR_MAUVE
),
77 /** Window definition for the textfile window */
78 static WindowDesc
_textfile_desc(
79 WDP_CENTER
, "textfile", 630, 460,
82 _nested_textfile_widgets
85 TextfileWindow::TextfileWindow(TextfileType file_type
) : Window(_textfile_desc
), file_type(file_type
)
87 /* Init of nested tree is deferred.
88 * TextfileWindow::ConstructWindow must be called by the inheriting window. */
91 void TextfileWindow::ConstructWindow()
93 this->CreateNestedTree();
94 this->vscroll
= this->GetScrollbar(WID_TF_VSCROLLBAR
);
95 this->hscroll
= this->GetScrollbar(WID_TF_HSCROLLBAR
);
96 this->GetWidget
<NWidgetCore
>(WID_TF_CAPTION
)->SetDataTip(STR_TEXTFILE_README_CAPTION
+ this->file_type
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
);
97 this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(SZSP_HORIZONTAL
);
98 this->FinishInitNested(this->file_type
);
100 this->DisableWidget(WID_TF_NAVBACK
);
101 this->DisableWidget(WID_TF_NAVFORWARD
);
102 this->hscroll
->SetStepSize(10); // Speed up horizontal scrollbar
106 * Get the total height of the content displayed in this window, if wrapping is disabled.
107 * @return the height in pixels
109 uint
TextfileWindow::ReflowContent()
112 if (!IsWidgetLowered(WID_TF_WRAPTEXT
)) {
113 for (auto &line
: this->lines
) {
116 line
.bottom
= height
;
119 int max_width
= this->GetWidget
<NWidgetCore
>(WID_TF_BACKGROUND
)->current_x
- WidgetDimensions::scaled
.frametext
.Horizontal();
120 for (auto &line
: this->lines
) {
122 height
+= GetStringHeight(line
.text
, max_width
, FS_MONO
) / GetCharacterHeight(FS_MONO
);
123 line
.bottom
= height
;
130 uint
TextfileWindow::GetContentHeight()
132 if (this->lines
.empty()) return 0;
133 return this->lines
.back().bottom
;
136 /* virtual */ void TextfileWindow::UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
)
139 case WID_TF_BACKGROUND
:
140 resize
.height
= GetCharacterHeight(FS_MONO
);
142 size
.height
= 4 * resize
.height
+ WidgetDimensions::scaled
.frametext
.Vertical(); // At least 4 lines are visible.
143 size
.width
= std::max(200u, size
.width
); // At least 200 pixels wide.
148 /** Set scrollbars to the right lengths. */
149 void TextfileWindow::SetupScrollbars(bool force_reflow
)
151 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
152 /* Reflow is mandatory if text wrapping is on */
153 uint height
= this->ReflowContent();
154 this->vscroll
->SetCount(ClampTo
<uint16_t>(height
));
155 this->hscroll
->SetCount(0);
157 uint height
= force_reflow
? this->ReflowContent() : this->GetContentHeight();
158 this->vscroll
->SetCount(ClampTo
<uint16_t>(height
));
159 this->hscroll
->SetCount(this->max_length
);
162 this->SetWidgetDisabledState(WID_TF_HSCROLLBAR
, IsWidgetLowered(WID_TF_WRAPTEXT
));
166 /** Regular expression that searches for Markdown links. */
167 static const std::regex _markdown_link_regex
{"\\[(.+?)\\]\\((.+?)\\)", std::regex_constants::ECMAScript
| std::regex_constants::optimize
};
169 /** Types of link we support in markdown files. */
170 enum class HyperlinkType
{
171 Internal
, ///< Internal link, or "anchor" in HTML language.
172 Web
, ///< Link to an external website.
173 File
, ///< Link to a local file.
174 Unknown
, ///< Unknown link.
178 * Classify the type of hyperlink the destination describes.
180 * @param destination The hyperlink destination.
181 * @param trusted Whether we trust the content of this file.
182 * @return HyperlinkType The classification of the link.
184 static HyperlinkType
ClassifyHyperlink(const std::string
&destination
, bool trusted
)
186 if (destination
.empty()) return HyperlinkType::Unknown
;
187 if (destination
.starts_with("#")) return HyperlinkType::Internal
;
189 /* Only allow external / internal links for sources we trust. */
190 if (!trusted
) return HyperlinkType::Unknown
;
192 if (destination
.starts_with("http://")) return HyperlinkType::Web
;
193 if (destination
.starts_with("https://")) return HyperlinkType::Web
;
194 if (destination
.starts_with("./")) return HyperlinkType::File
;
195 return HyperlinkType::Unknown
;
199 * Create a valid slug for the anchor.
201 * @param line The line to create the slug for.
202 * @return std::string The slug.
204 static std::string
MakeAnchorSlug(const std::string
&line
)
208 for (char c
: line
) {
210 /* State 0: Skip leading hashmarks and spaces. */
211 if (c
== '#') continue;
212 if (c
== ' ') continue;
216 /* State 2: Wait for a non-space/dash character.
217 * When found, output a dash and that character. */
218 if (c
== ' ' || c
== '-') continue;
223 /* State 1: Normal text.
224 * Lowercase alphanumerics,
225 * spaces and dashes become dashes,
226 * everything else is removed. */
229 } else if (c
== ' ' || c
== '-') {
239 * Find any hyperlinks in a given line.
241 * @param line The line to search for hyperlinks.
242 * @param line_index The index of the line.
244 void TextfileWindow::FindHyperlinksInMarkdown(Line
&line
, size_t line_index
)
246 std::string::const_iterator last_match_end
= line
.text
.cbegin();
247 std::string fixed_line
;
250 std::sregex_iterator matcher
{ line
.text
.cbegin(), line
.text
.cend(), _markdown_link_regex
};
251 while (matcher
!= std::sregex_iterator()) {
252 std::smatch match
= *matcher
;
255 link
.line
= line_index
;
256 link
.destination
= match
[2].str();
257 this->links
.push_back(link
);
259 HyperlinkType link_type
= ClassifyHyperlink(link
.destination
, this->trusted
);
260 StringControlCode link_colour
;
262 case HyperlinkType::Internal
:
263 link_colour
= SCC_GREEN
;
265 case HyperlinkType::Web
:
266 link_colour
= SCC_LTBLUE
;
268 case HyperlinkType::File
:
269 link_colour
= SCC_LTBROWN
;
272 /* Don't make other link types fancy as they aren't handled (yet). */
273 link_colour
= SCC_CONTROL_END
;
277 if (link_colour
!= SCC_CONTROL_END
) {
278 /* Format the link to look like a link. */
279 fixed_line
+= std::string(last_match_end
, match
[0].first
);
280 this->links
.back().begin
= fixed_line
.length();
281 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, SCC_PUSH_COLOUR
));
282 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, link_colour
));
283 fixed_line
+= match
[1].str();
284 this->links
.back().end
= fixed_line
.length();
285 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, SCC_POP_COLOUR
));
286 last_match_end
= match
[0].second
;
289 /* Find next link. */
292 if (last_match_end
== line
.text
.cbegin()) return; // nothing found
294 /* Add remaining text on line. */
295 fixed_line
+= std::string(last_match_end
, line
.text
.cend());
297 /* Overwrite original line text with "fixed" line text. */
298 line
.text
= fixed_line
;
302 * Check if the user clicked on a hyperlink, and handle it if so.
304 * @param pt The loation the user clicked.
306 void TextfileWindow::CheckHyperlinkClick(Point pt
)
308 if (this->links
.empty()) return;
310 /* Which line was clicked. */
311 const int clicked_row
= this->GetRowFromWidget(pt
.y
, WID_TF_BACKGROUND
, WidgetDimensions::scaled
.frametext
.top
, GetCharacterHeight(FS_MONO
)) + this->GetScrollbar(WID_TF_VSCROLLBAR
)->GetPosition();
314 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
315 auto it
= std::find_if(std::begin(this->lines
), std::end(this->lines
), [clicked_row
](const Line
&l
) { return l
.top
<= clicked_row
&& l
.bottom
> clicked_row
; });
316 if (it
== this->lines
.cend()) return;
317 line_index
= it
- this->lines
.cbegin();
318 subline
= clicked_row
- it
->top
;
319 Debug(misc
, 4, "TextfileWindow check hyperlink: clicked_row={}, line_index={}, line.top={}, subline={}", clicked_row
, line_index
, it
->top
, subline
);
321 line_index
= clicked_row
/ GetCharacterHeight(FS_MONO
);
325 /* Find hyperlinks in this line. */
326 std::vector
<Hyperlink
> found_links
;
327 for (const auto &link
: this->links
) {
328 if (link
.line
== line_index
) found_links
.push_back(link
);
330 if (found_links
.empty()) return;
332 /* Build line layout to figure out character position that was clicked. */
333 uint window_width
= IsWidgetLowered(WID_TF_WRAPTEXT
) ? this->GetWidget
<NWidgetCore
>(WID_TF_BACKGROUND
)->current_x
- WidgetDimensions::scaled
.frametext
.Horizontal() : INT_MAX
;
334 Layouter
layout(this->lines
[line_index
].text
, window_width
, FS_MONO
);
335 assert(subline
< layout
.size());
336 ptrdiff_t char_index
= layout
.GetCharAtPosition(pt
.x
- WidgetDimensions::scaled
.frametext
.left
, subline
);
337 if (char_index
< 0) return;
338 Debug(misc
, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index
, subline
, (int)char_index
);
340 /* Found character index in line, check if any links are at that position. */
341 for (const auto &link
: found_links
) {
342 Debug(misc
, 4, "Checking link from char {} to {}", link
.begin
, link
.end
);
343 if (static_cast<size_t>(char_index
) >= link
.begin
&& static_cast<size_t>(char_index
) < link
.end
) {
344 Debug(misc
, 4, "Activating link with destination: {}", link
.destination
);
345 this->OnHyperlinkClick(link
);
352 * Append the new location to the history, so the user can go back.
354 * @param filepath The location the user is navigating to.
356 void TextfileWindow::AppendHistory(const std::string
&filepath
)
358 this->history
.erase(this->history
.begin() + this->history_pos
+ 1, this->history
.end());
359 this->UpdateHistoryScrollpos();
360 this->history
.push_back(HistoryEntry
{ filepath
, 0 });
361 this->EnableWidget(WID_TF_NAVBACK
);
362 this->DisableWidget(WID_TF_NAVFORWARD
);
363 this->history_pos
= this->history
.size() - 1;
367 * Update the scroll position to the current, so we can restore there if we go back.
369 void TextfileWindow::UpdateHistoryScrollpos()
371 this->history
[this->history_pos
].scrollpos
= this->GetScrollbar(WID_TF_VSCROLLBAR
)->GetPosition();
375 * Navigate through the history, either forward or backward.
377 * @param delta The direction to navigate.
379 void TextfileWindow::NavigateHistory(int delta
)
381 if (delta
== 0) return;
382 if (delta
< 0 && static_cast<int>(this->history_pos
) < -delta
) return;
383 if (delta
> 0 && this->history_pos
+ delta
>= this->history
.size()) return;
385 this->UpdateHistoryScrollpos();
386 this->history_pos
+= delta
;
388 if (this->history
[this->history_pos
].filepath
!= this->filepath
) {
389 this->filepath
= this->history
[this->history_pos
].filepath
;
390 this->filename
= this->filepath
.substr(this->filepath
.find_last_of(PATHSEP
) + 1);
391 this->LoadTextfile(this->filepath
, NO_DIRECTORY
);
394 this->SetWidgetDisabledState(WID_TF_NAVFORWARD
, this->history_pos
+ 1 >= this->history
.size());
395 this->SetWidgetDisabledState(WID_TF_NAVBACK
, this->history_pos
== 0);
396 this->GetScrollbar(WID_TF_VSCROLLBAR
)->SetPosition(this->history
[this->history_pos
].scrollpos
);
397 this->GetScrollbar(WID_TF_HSCROLLBAR
)->SetPosition(0);
401 /* virtual */ void TextfileWindow::OnHyperlinkClick(const Hyperlink
&link
)
403 switch (ClassifyHyperlink(link
.destination
, this->trusted
)) {
404 case HyperlinkType::Internal
:
406 auto it
= std::find_if(this->link_anchors
.cbegin(), this->link_anchors
.cend(), [&](const Hyperlink
&other
) { return link
.destination
== other
.destination
; });
407 if (it
!= this->link_anchors
.cend()) {
408 this->AppendHistory(this->filepath
);
409 this->ScrollToLine(it
->line
);
410 this->UpdateHistoryScrollpos();
415 case HyperlinkType::Web
:
416 OpenBrowser(link
.destination
);
419 case HyperlinkType::File
:
420 this->NavigateToFile(link
.destination
, 0);
430 * Navigate to the requested file.
432 * @param newfile The file to navigate to.
433 * @param line The line to scroll to.
435 void TextfileWindow::NavigateToFile(std::string newfile
, size_t line
)
437 /* Double-check that the file link begins with ./ as a relative path. */
438 if (!newfile
.starts_with("./")) return;
440 /* Get the path portion of the current file path. */
441 std::string newpath
= this->filepath
;
442 size_t pos
= newpath
.find_last_of(PATHSEPCHAR
);
443 if (pos
== std::string::npos
) {
446 newpath
.erase(pos
+ 1);
449 /* Check and remove for anchor in link. Do this before we find the filename, as people might have a / after the hash. */
450 size_t anchor_pos
= newfile
.find_first_of('#');
452 if (anchor_pos
!= std::string::npos
) {
453 anchor
= newfile
.substr(anchor_pos
);
454 newfile
.erase(anchor_pos
);
457 /* Now the anchor is gone, check if this is a markdown or textfile. */
458 if (!StrEndsWithIgnoreCase(newfile
, ".md") && !StrEndsWithIgnoreCase(newfile
, ".txt")) return;
460 /* Convert link destination to acceptable local filename (replace forward slashes with correct path separator). */
461 newfile
= newfile
.substr(2);
462 if (PATHSEPCHAR
!= '/') {
463 for (char &c
: newfile
) {
464 if (c
== '/') c
= PATHSEPCHAR
;
468 /* Paste the two together and check file exists. */
469 newpath
= newpath
+ newfile
;
470 if (!FioCheckFileExists(newpath
, NO_DIRECTORY
)) return;
472 /* Update history. */
473 this->AppendHistory(newpath
);
475 /* Load the new file. */
476 this->filepath
= newpath
;
477 this->filename
= newpath
.substr(newpath
.find_last_of(PATHSEP
) + 1);
479 this->LoadTextfile(this->filepath
, NO_DIRECTORY
);
481 this->GetScrollbar(WID_TF_HSCROLLBAR
)->SetPosition(0);
482 this->GetScrollbar(WID_TF_VSCROLLBAR
)->SetPosition(0);
484 if (anchor
.empty() || line
!= 0) {
485 this->ScrollToLine(line
);
487 auto anchor_dest
= std::find_if(this->link_anchors
.cbegin(), this->link_anchors
.cend(), [&](const Hyperlink
&other
) { return anchor
== other
.destination
; });
488 if (anchor_dest
!= this->link_anchors
.cend()) {
489 this->ScrollToLine(anchor_dest
->line
);
490 this->UpdateHistoryScrollpos();
492 this->ScrollToLine(0);
497 /* virtual */ void TextfileWindow::AfterLoadText()
499 this->link_anchors
.clear();
501 if (StrEndsWithIgnoreCase(this->filename
, ".md")) this->AfterLoadMarkdown();
503 if (this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(this->jumplist
.empty() ? SZSP_HORIZONTAL
: 0)) this->ReInit();
507 * Post-processing of markdown files.
509 void TextfileWindow::AfterLoadMarkdown()
511 for (size_t line_index
= 0; line_index
< this->lines
.size(); ++line_index
) {
512 Line
&line
= this->lines
[line_index
];
514 /* Find and mark all hyperlinks in the line. */
515 this->FindHyperlinksInMarkdown(line
, line_index
);
517 /* All lines beginning with # are headings. */
518 if (!line
.text
.empty() && line
.text
[0] == '#') {
519 this->jumplist
.push_back(line_index
);
520 this->lines
[line_index
].colour
= TC_GOLD
;
521 this->link_anchors
.emplace_back(Hyperlink
{ line_index
, 0, 0, MakeAnchorSlug(line
.text
) });
526 /* virtual */ void TextfileWindow::OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
)
529 case WID_TF_WRAPTEXT
:
530 this->ToggleWidgetLoweredState(WID_TF_WRAPTEXT
);
531 this->InvalidateData();
534 case WID_TF_JUMPLIST
: {
536 for (size_t line
: this->jumplist
) {
537 SetDParamStr(0, this->lines
[line
].text
);
538 list
.push_back(MakeDropDownListStringItem(STR_TEXTFILE_JUMPLIST_ITEM
, (int)line
));
540 ShowDropDownList(this, std::move(list
), -1, widget
);
545 this->NavigateHistory(-1);
548 case WID_TF_NAVFORWARD
:
549 this->NavigateHistory(+1);
552 case WID_TF_BACKGROUND
:
553 this->CheckHyperlinkClick(pt
);
558 /* virtual */ void TextfileWindow::DrawWidget(const Rect
&r
, WidgetID widget
) const
560 if (widget
!= WID_TF_BACKGROUND
) return;
562 Rect fr
= r
.Shrink(WidgetDimensions::scaled
.frametext
);
564 DrawPixelInfo new_dpi
;
565 if (!FillDrawPixelInfo(&new_dpi
, fr
)) return;
566 AutoRestoreBackup
dpi_backup(_cur_dpi
, &new_dpi
);
568 /* Draw content (now coordinates given to DrawString* are local to the new clipping region). */
569 fr
= fr
.Translate(-fr
.left
, -fr
.top
);
570 int line_height
= GetCharacterHeight(FS_MONO
);
572 if (!IsWidgetLowered(WID_TF_WRAPTEXT
)) fr
= ScrollRect(fr
, *this->hscroll
, 1);
574 int pos
= this->vscroll
->GetPosition();
575 int cap
= this->vscroll
->GetCapacity();
577 for (auto &line
: this->lines
) {
578 if (line
.bottom
< pos
) continue;
579 if (line
.top
> pos
+ cap
) break;
581 int y_offset
= (line
.top
- pos
) * line_height
;
582 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
583 DrawStringMultiLine(fr
.left
, fr
.right
, y_offset
, fr
.bottom
, line
.text
, line
.colour
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
585 DrawString(fr
.left
, fr
.right
, y_offset
, line
.text
, line
.colour
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
590 /* virtual */ void TextfileWindow::OnResize()
592 this->vscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
, WidgetDimensions::scaled
.frametext
.Vertical());
593 this->hscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
, WidgetDimensions::scaled
.framerect
.Horizontal());
595 this->SetupScrollbars(false);
598 /* virtual */ void TextfileWindow::OnInvalidateData([[maybe_unused
]] int data
, [[maybe_unused
]] bool gui_scope
)
600 if (!gui_scope
) return;
602 this->SetupScrollbars(true);
605 void TextfileWindow::OnDropdownSelect(WidgetID widget
, int index
)
607 if (widget
!= WID_TF_JUMPLIST
) return;
609 this->ScrollToLine(index
);
612 void TextfileWindow::ScrollToLine(size_t line
)
614 Scrollbar
*sb
= this->GetScrollbar(WID_TF_VSCROLLBAR
);
616 if (this->IsWidgetLowered(WID_TF_WRAPTEXT
)) {
617 newpos
= this->lines
[line
].top
;
619 newpos
= static_cast<int>(line
);
621 sb
->SetPosition(std::min(newpos
, sb
->GetCount() - sb
->GetCapacity()));
625 /* virtual */ void TextfileWindow::Reset()
627 this->search_iterator
= 0;
630 /* virtual */ FontSize
TextfileWindow::DefaultSize()
635 /* virtual */ std::optional
<std::string_view
> TextfileWindow::NextString()
637 if (this->search_iterator
>= this->lines
.size()) return std::nullopt
;
639 return this->lines
[this->search_iterator
++].text
;
642 /* virtual */ bool TextfileWindow::Monospace()
647 /* virtual */ void TextfileWindow::SetFontNames([[maybe_unused
]] FontCacheSettings
*settings
, [[maybe_unused
]] const char *font_name
, [[maybe_unused
]] const void *os_data
)
649 #if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
650 settings
->mono
.font
= font_name
;
651 settings
->mono
.os_handle
= os_data
;
655 #if defined(WITH_ZLIB)
658 * Do an in-memory gunzip operation. This works on a raw deflate stream,
659 * or a file with gzip or zlib header.
660 * @param input Buffer containing the input data.
661 * @return Decompressed buffer.
663 * When decompressing fails, an empty buffer is returned.
665 static std::vector
<char> Gunzip(std::span
<char> input
)
667 static const int BLOCKSIZE
= 8192;
668 std::vector
<char> output
;
671 memset(&z
, 0, sizeof(z
));
672 z
.next_in
= reinterpret_cast<Bytef
*>(input
.data());
673 z
.avail_in
= static_cast<uInt
>(input
.size());
675 /* window size = 15, add 32 to enable gzip or zlib header processing */
676 int res
= inflateInit2(&z
, 15 + 32);
677 /* Z_BUF_ERROR just means we need more space */
678 while (res
== Z_OK
|| (res
== Z_BUF_ERROR
&& z
.avail_out
== 0)) {
679 /* When we get here, we're either just starting, or
680 * inflate is out of output space - allocate more */
681 z
.avail_out
+= BLOCKSIZE
;
682 output
.resize(output
.size() + BLOCKSIZE
);
683 z
.next_out
= reinterpret_cast<Bytef
*>(&*output
.end() - z
.avail_out
);
684 res
= inflate(&z
, Z_FINISH
);
688 if (res
!= Z_STREAM_END
) return {};
690 output
.resize(output
.size() - z
.avail_out
);
695 #if defined(WITH_LIBLZMA)
698 * Do an in-memory xunzip operation. This works on a .xz or (legacy)
700 * @param input Buffer containing the input data.
701 * @return Decompressed buffer.
703 * When decompressing fails, an empty buffer is returned.
705 static std::vector
<char> Xunzip(std::span
<char> input
)
707 static const int BLOCKSIZE
= 8192;
708 std::vector
<char> output
;
710 lzma_stream z
= LZMA_STREAM_INIT
;
711 z
.next_in
= reinterpret_cast<uint8_t *>(input
.data());
712 z
.avail_in
= input
.size();
714 int res
= lzma_auto_decoder(&z
, UINT64_MAX
, LZMA_CONCATENATED
);
715 /* Z_BUF_ERROR just means we need more space */
716 while (res
== LZMA_OK
|| (res
== LZMA_BUF_ERROR
&& z
.avail_out
== 0)) {
717 /* When we get here, we're either just starting, or
718 * inflate is out of output space - allocate more */
719 z
.avail_out
+= BLOCKSIZE
;
720 output
.resize(output
.size() + BLOCKSIZE
);
721 z
.next_out
= reinterpret_cast<uint8_t *>(&*output
.end() - z
.avail_out
);
722 res
= lzma_code(&z
, LZMA_FINISH
);
726 if (res
!= LZMA_STREAM_END
) return {};
728 output
.resize(output
.size() - z
.avail_out
);
735 * Loads the textfile text from file and setup #lines.
737 /* virtual */ void TextfileWindow::LoadTextfile(const std::string
&textfile
, Subdirectory dir
)
740 this->jumplist
.clear();
742 if (this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(SZSP_HORIZONTAL
)) this->ReInit();
744 if (textfile
.empty()) return;
746 /* Get text from file */
748 auto handle
= FioFOpenFile(textfile
, "rb", dir
, &filesize
);
749 if (!handle
.has_value()) return;
750 /* Early return on empty files. */
751 if (filesize
== 0) return;
753 std::vector
<char> buf
;
754 buf
.resize(filesize
);
755 size_t read
= fread(buf
.data(), 1, buf
.size(), *handle
);
757 if (read
!= buf
.size()) return;
759 #if defined(WITH_ZLIB)
760 /* In-place gunzip */
761 if (textfile
.ends_with(".gz")) buf
= Gunzip(buf
);
764 #if defined(WITH_LIBLZMA)
765 /* In-place xunzip */
766 if (textfile
.ends_with(".xz")) buf
= Xunzip(buf
);
769 if (buf
.empty()) return;
771 std::string_view
sv_buf(buf
.data(), buf
.size());
773 /* Check for the byte-order-mark, and skip it if needed. */
774 if (sv_buf
.starts_with("\ufeff")) sv_buf
.remove_prefix(3);
776 /* Update the filename. */
777 this->filepath
= textfile
;
778 this->filename
= this->filepath
.substr(this->filepath
.find_last_of(PATHSEP
) + 1);
779 /* If it's the first file being loaded, add to history. */
780 if (this->history
.empty()) this->history
.push_back(HistoryEntry
{ this->filepath
, 0 });
782 /* Process the loaded text into lines, and do any further parsing needed. */
783 this->LoadText(sv_buf
);
787 * Load a text into the textfile viewer.
789 * This will split the text into newlines and stores it for fast drawing.
791 * @param buf The text to load.
793 void TextfileWindow::LoadText(std::string_view buf
)
795 std::string text
= StrMakeValid(buf
, SVS_REPLACE_WITH_QUESTION_MARK
| SVS_ALLOW_NEWLINE
| SVS_REPLACE_TAB_CR_NL_WITH_SPACE
);
798 /* Split the string on newlines. */
799 std::string_view
p(text
);
801 auto next
= p
.find_first_of('\n');
802 while (next
!= std::string_view::npos
) {
803 this->lines
.emplace_back(row
, p
.substr(0, next
));
804 p
.remove_prefix(next
+ 1);
807 next
= p
.find_first_of('\n');
809 this->lines
.emplace_back(row
, p
);
811 /* Calculate maximum text line length. */
813 for (auto &line
: this->lines
) {
814 max_length
= std::max(max_length
, GetStringBoundingBox(line
.text
, FS_MONO
).width
);
816 this->max_length
= max_length
;
818 this->AfterLoadText();
820 CheckForMissingGlyphs(true, this);
822 /* The font may have changed when searching for glyphs, so ensure widget sizes are updated just in case. */
827 * Search a textfile file next to the given content.
828 * @param type The type of the textfile to search for.
829 * @param dir The subdirectory to search in.
830 * @param filename The filename of the content to look for.
831 * @return The path to the textfile, \c nullptr otherwise.
833 std::optional
<std::string
> GetTextfile(TextfileType type
, Subdirectory dir
, const std::string
&filename
)
835 static const char * const prefixes
[] = {
840 static_assert(lengthof(prefixes
) == TFT_CONTENT_END
);
842 /* Only the generic text file types allowed for this function */
843 if (type
>= TFT_CONTENT_END
) return std::nullopt
;
845 std::string_view prefix
= prefixes
[type
];
847 if (filename
.empty()) return std::nullopt
;
849 auto slash
= filename
.find_last_of(PATHSEPCHAR
);
850 if (slash
== std::string::npos
) return std::nullopt
;
852 std::string_view
base_path(filename
.data(), slash
+ 1);
854 static const std::initializer_list
<std::string_view
> extensions
{
857 #if defined(WITH_ZLIB)
861 #if defined(WITH_LIBLZMA)
867 for (auto &extension
: extensions
) {
868 std::string file_path
= fmt::format("{}{}_{}.{}", base_path
, prefix
, GetCurrentLanguageIsoCode(), extension
);
869 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
871 file_path
= fmt::format("{}{}_{:.2s}.{}", base_path
, prefix
, GetCurrentLanguageIsoCode(), extension
);
872 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
874 file_path
= fmt::format("{}{}.{}", base_path
, prefix
, extension
);
875 if (FioCheckFileExists(file_path
, dir
)) return file_path
;