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 "widgets/dropdown_type.h"
19 #include "gfx_layout.h"
23 #include "widgets/misc_widget.h"
25 #include "table/strings.h"
26 #include "table/control_codes.h"
28 #if defined(WITH_ZLIB)
32 #if defined(WITH_LIBLZMA)
38 #include "safeguards.h"
40 /** Widgets for the textfile window. */
41 static constexpr NWidgetPart _nested_textfile_widgets
[] = {
42 NWidget(NWID_HORIZONTAL
),
43 NWidget(WWT_CLOSEBOX
, COLOUR_MAUVE
),
44 NWidget(WWT_PUSHARROWBTN
, COLOUR_MAUVE
, WID_TF_NAVBACK
), SetFill(0, 1), SetMinimalSize(15, 1), SetDataTip(AWV_DECREASE
, STR_TEXTFILE_NAVBACK_TOOLTIP
),
45 NWidget(WWT_PUSHARROWBTN
, COLOUR_MAUVE
, WID_TF_NAVFORWARD
), SetFill(0, 1), SetMinimalSize(15, 1), SetDataTip(AWV_INCREASE
, STR_TEXTFILE_NAVFORWARD_TOOLTIP
),
46 NWidget(WWT_CAPTION
, COLOUR_MAUVE
, WID_TF_CAPTION
), SetDataTip(STR_NULL
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
47 NWidget(WWT_TEXTBTN
, COLOUR_MAUVE
, WID_TF_WRAPTEXT
), SetDataTip(STR_TEXTFILE_WRAP_TEXT
, STR_TEXTFILE_WRAP_TEXT_TOOLTIP
),
48 NWidget(WWT_DEFSIZEBOX
, COLOUR_MAUVE
),
50 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_TF_SEL_JUMPLIST
),
51 NWidget(WWT_PANEL
, COLOUR_MAUVE
),
52 NWidget(NWID_HORIZONTAL
), SetPIP(WidgetDimensions::unscaled
.frametext
.left
, 0, WidgetDimensions::unscaled
.frametext
.right
),
53 /* As this widget can be toggled, it needs to be a multiplier of FS_MONO. So add a spacer that ensures this. */
54 NWidget(NWID_SPACER
), SetMinimalSize(1, 0), SetMinimalTextLines(2, 0, FS_MONO
),
55 NWidget(NWID_VERTICAL
),
56 NWidget(NWID_SPACER
), SetFill(1, 1), SetResize(1, 0),
57 NWidget(WWT_DROPDOWN
, COLOUR_MAUVE
, WID_TF_JUMPLIST
), SetDataTip(STR_TEXTFILE_JUMPLIST
, STR_TEXTFILE_JUMPLIST_TOOLTIP
), SetFill(1, 0), SetResize(1, 0),
58 NWidget(NWID_SPACER
), SetFill(1, 1), SetResize(1, 0),
63 NWidget(NWID_HORIZONTAL
),
64 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_TF_BACKGROUND
), SetMinimalSize(200, 125), SetResize(1, 12), SetScrollbar(WID_TF_VSCROLLBAR
),
66 NWidget(NWID_VERTICAL
),
67 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_TF_VSCROLLBAR
),
70 NWidget(NWID_HORIZONTAL
),
71 NWidget(NWID_HSCROLLBAR
, COLOUR_MAUVE
, WID_TF_HSCROLLBAR
),
72 NWidget(WWT_RESIZEBOX
, COLOUR_MAUVE
),
76 /** Window definition for the textfile window */
77 static WindowDesc
_textfile_desc(__FILE__
, __LINE__
,
78 WDP_CENTER
, "textfile", 630, 460,
81 std::begin(_nested_textfile_widgets
), std::end(_nested_textfile_widgets
)
84 TextfileWindow::TextfileWindow(TextfileType file_type
) : Window(&_textfile_desc
), file_type(file_type
)
86 /* Init of nested tree is deferred.
87 * TextfileWindow::ConstructWindow must be called by the inheriting window. */
90 void TextfileWindow::ConstructWindow()
92 this->CreateNestedTree();
93 this->vscroll
= this->GetScrollbar(WID_TF_VSCROLLBAR
);
94 this->hscroll
= this->GetScrollbar(WID_TF_HSCROLLBAR
);
95 this->GetWidget
<NWidgetCore
>(WID_TF_CAPTION
)->SetDataTip(STR_TEXTFILE_README_CAPTION
+ this->file_type
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
);
96 this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(SZSP_HORIZONTAL
);
97 this->FinishInitNested(this->file_type
);
99 this->DisableWidget(WID_TF_NAVBACK
);
100 this->DisableWidget(WID_TF_NAVFORWARD
);
101 this->hscroll
->SetStepSize(10); // Speed up horizontal scrollbar
105 * Get the total height of the content displayed in this window, if wrapping is disabled.
106 * @return the height in pixels
108 uint
TextfileWindow::ReflowContent()
111 if (!IsWidgetLowered(WID_TF_WRAPTEXT
)) {
112 for (auto &line
: this->lines
) {
115 line
.bottom
= height
;
118 int max_width
= this->GetWidget
<NWidgetCore
>(WID_TF_BACKGROUND
)->current_x
- WidgetDimensions::scaled
.frametext
.Horizontal();
119 for (auto &line
: this->lines
) {
121 height
+= GetStringHeight(line
.text
, max_width
, FS_MONO
) / GetCharacterHeight(FS_MONO
);
122 line
.bottom
= height
;
129 uint
TextfileWindow::GetContentHeight()
131 if (this->lines
.empty()) return 0;
132 return this->lines
.back().bottom
;
135 /* virtual */ void TextfileWindow::UpdateWidgetSize(WidgetID widget
, Dimension
*size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
*fill
, [[maybe_unused
]] Dimension
*resize
)
138 case WID_TF_BACKGROUND
:
139 resize
->height
= GetCharacterHeight(FS_MONO
);
141 size
->height
= 4 * resize
->height
+ WidgetDimensions::scaled
.frametext
.Vertical(); // At least 4 lines are visible.
142 size
->width
= std::max(200u, size
->width
); // At least 200 pixels wide.
147 /** Set scrollbars to the right lengths. */
148 void TextfileWindow::SetupScrollbars(bool force_reflow
)
150 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
151 /* Reflow is mandatory if text wrapping is on */
152 uint height
= this->ReflowContent();
153 this->vscroll
->SetCount(ClampTo
<uint16_t>(height
));
154 this->hscroll
->SetCount(0);
156 uint height
= force_reflow
? this->ReflowContent() : this->GetContentHeight();
157 this->vscroll
->SetCount(ClampTo
<uint16_t>(height
));
158 this->hscroll
->SetCount(this->max_length
+ WidgetDimensions::scaled
.frametext
.Horizontal());
161 this->SetWidgetDisabledState(WID_TF_HSCROLLBAR
, IsWidgetLowered(WID_TF_WRAPTEXT
));
165 /** Regular expression that searches for Markdown links. */
166 static const std::regex _markdown_link_regex
{"\\[(.+?)\\]\\((.+?)\\)", std::regex_constants::ECMAScript
| std::regex_constants::optimize
};
168 /** Types of link we support in markdown files. */
169 enum class HyperlinkType
{
170 Internal
, ///< Internal link, or "anchor" in HTML language.
171 Web
, ///< Link to an external website.
172 File
, ///< Link to a local file.
173 Unknown
, ///< Unknown link.
177 * Classify the type of hyperlink the destination describes.
179 * @param destination The hyperlink destination.
180 * @param trusted Whether we trust the content of this file.
181 * @return HyperlinkType The classification of the link.
183 static HyperlinkType
ClassifyHyperlink(const std::string
&destination
, bool trusted
)
185 if (destination
.empty()) return HyperlinkType::Unknown
;
186 if (destination
.starts_with("#")) return HyperlinkType::Internal
;
188 /* Only allow external / internal links for sources we trust. */
189 if (!trusted
) return HyperlinkType::Unknown
;
191 if (destination
.starts_with("http://")) return HyperlinkType::Web
;
192 if (destination
.starts_with("https://")) return HyperlinkType::Web
;
193 if (destination
.starts_with("./")) return HyperlinkType::File
;
194 return HyperlinkType::Unknown
;
198 * Create a valid slug for the anchor.
200 * @param line The line to create the slug for.
201 * @return std::string The slug.
203 static std::string
MakeAnchorSlug(const std::string
&line
)
207 for (char c
: line
) {
209 /* State 0: Skip leading hashmarks and spaces. */
210 if (c
== '#') continue;
211 if (c
== ' ') continue;
215 /* State 2: Wait for a non-space/dash character.
216 * When found, output a dash and that character. */
217 if (c
== ' ' || c
== '-') continue;
222 /* State 1: Normal text.
223 * Lowercase alphanumerics,
224 * spaces and dashes become dashes,
225 * everything else is removed. */
228 } else if (c
== ' ' || c
== '-') {
238 * Find any hyperlinks in a given line.
240 * @param line The line to search for hyperlinks.
241 * @param line_index The index of the line.
243 void TextfileWindow::FindHyperlinksInMarkdown(Line
&line
, size_t line_index
)
245 std::string::const_iterator last_match_end
= line
.text
.cbegin();
246 std::string fixed_line
;
249 std::sregex_iterator matcher
{ line
.text
.cbegin(), line
.text
.cend(), _markdown_link_regex
};
250 while (matcher
!= std::sregex_iterator()) {
251 std::smatch match
= *matcher
;
254 link
.line
= line_index
;
255 link
.destination
= match
[2].str();
256 this->links
.push_back(link
);
258 HyperlinkType link_type
= ClassifyHyperlink(link
.destination
, this->trusted
);
259 StringControlCode link_colour
;
261 case HyperlinkType::Internal
:
262 link_colour
= SCC_GREEN
;
264 case HyperlinkType::Web
:
265 link_colour
= SCC_LTBLUE
;
267 case HyperlinkType::File
:
268 link_colour
= SCC_LTBROWN
;
271 /* Don't make other link types fancy as they aren't handled (yet). */
272 link_colour
= SCC_CONTROL_END
;
276 if (link_colour
!= SCC_CONTROL_END
) {
277 /* Format the link to look like a link. */
278 fixed_line
+= std::string(last_match_end
, match
[0].first
);
279 this->links
.back().begin
= fixed_line
.length();
280 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, SCC_PUSH_COLOUR
));
281 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, link_colour
));
282 fixed_line
+= match
[1].str();
283 this->links
.back().end
= fixed_line
.length();
284 fixed_line
+= std::string(ccbuf
, Utf8Encode(ccbuf
, SCC_POP_COLOUR
));
285 last_match_end
= match
[0].second
;
288 /* Find next link. */
291 if (last_match_end
== line
.text
.cbegin()) return; // nothing found
293 /* Add remaining text on line. */
294 fixed_line
+= std::string(last_match_end
, line
.text
.cend());
296 /* Overwrite original line text with "fixed" line text. */
297 line
.text
= fixed_line
;
301 * Check if the user clicked on a hyperlink, and handle it if so.
303 * @param pt The loation the user clicked.
305 void TextfileWindow::CheckHyperlinkClick(Point pt
)
307 if (this->links
.empty()) return;
309 /* Which line was clicked. */
310 const int clicked_row
= this->GetRowFromWidget(pt
.y
, WID_TF_BACKGROUND
, WidgetDimensions::scaled
.frametext
.top
, GetCharacterHeight(FS_MONO
)) + this->GetScrollbar(WID_TF_VSCROLLBAR
)->GetPosition();
313 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
314 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
; });
315 if (it
== this->lines
.cend()) return;
316 line_index
= it
- this->lines
.cbegin();
317 subline
= clicked_row
- it
->top
;
318 Debug(misc
, 4, "TextfileWindow check hyperlink: clicked_row={}, line_index={}, line.top={}, subline={}", clicked_row
, line_index
, it
->top
, subline
);
320 line_index
= clicked_row
/ GetCharacterHeight(FS_MONO
);
324 /* Find hyperlinks in this line. */
325 std::vector
<Hyperlink
> found_links
;
326 for (const auto &link
: this->links
) {
327 if (link
.line
== line_index
) found_links
.push_back(link
);
329 if (found_links
.empty()) return;
331 /* Build line layout to figure out character position that was clicked. */
332 uint window_width
= IsWidgetLowered(WID_TF_WRAPTEXT
) ? this->GetWidget
<NWidgetCore
>(WID_TF_BACKGROUND
)->current_x
- WidgetDimensions::scaled
.frametext
.Horizontal() : INT_MAX
;
333 Layouter
layout(this->lines
[line_index
].text
, window_width
, this->lines
[line_index
].colour
, FS_MONO
);
334 assert(subline
< layout
.size());
335 ptrdiff_t char_index
= layout
.GetCharAtPosition(pt
.x
- WidgetDimensions::scaled
.frametext
.left
, subline
);
336 if (char_index
< 0) return;
337 Debug(misc
, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index
, subline
, (int)char_index
);
339 /* Found character index in line, check if any links are at that position. */
340 for (const auto &link
: found_links
) {
341 Debug(misc
, 4, "Checking link from char {} to {}", link
.begin
, link
.end
);
342 if ((size_t)char_index
>= link
.begin
&& (size_t)char_index
< link
.end
) {
343 Debug(misc
, 4, "Activating link with destination: {}", link
.destination
);
344 this->OnHyperlinkClick(link
);
351 * Append the new location to the history, so the user can go back.
353 * @param filepath The location the user is navigating to.
355 void TextfileWindow::AppendHistory(const std::string
&filepath
)
357 this->history
.erase(this->history
.begin() + this->history_pos
+ 1, this->history
.end());
358 this->UpdateHistoryScrollpos();
359 this->history
.push_back(HistoryEntry
{ filepath
, 0 });
360 this->EnableWidget(WID_TF_NAVBACK
);
361 this->DisableWidget(WID_TF_NAVFORWARD
);
362 this->history_pos
= this->history
.size() - 1;
366 * Update the scroll position to the current, so we can restore there if we go back.
368 void TextfileWindow::UpdateHistoryScrollpos()
370 this->history
[this->history_pos
].scrollpos
= this->GetScrollbar(WID_TF_VSCROLLBAR
)->GetPosition();
374 * Navigate through the history, either forward or backward.
376 * @param delta The direction to navigate.
378 void TextfileWindow::NavigateHistory(int delta
)
380 if (delta
== 0) return;
381 if (delta
< 0 && static_cast<int>(this->history_pos
) < -delta
) return;
382 if (delta
> 0 && this->history_pos
+ delta
>= this->history
.size()) return;
384 this->UpdateHistoryScrollpos();
385 this->history_pos
+= delta
;
387 if (this->history
[this->history_pos
].filepath
!= this->filepath
) {
388 this->filepath
= this->history
[this->history_pos
].filepath
;
389 this->filename
= this->filepath
.substr(this->filepath
.find_last_of(PATHSEP
) + 1);
390 this->LoadTextfile(this->filepath
, NO_DIRECTORY
);
393 this->SetWidgetDisabledState(WID_TF_NAVFORWARD
, this->history_pos
+ 1 >= this->history
.size());
394 this->SetWidgetDisabledState(WID_TF_NAVBACK
, this->history_pos
== 0);
395 this->GetScrollbar(WID_TF_VSCROLLBAR
)->SetPosition(this->history
[this->history_pos
].scrollpos
);
396 this->GetScrollbar(WID_TF_HSCROLLBAR
)->SetPosition(0);
400 /* virtual */ void TextfileWindow::OnHyperlinkClick(const Hyperlink
&link
)
402 switch (ClassifyHyperlink(link
.destination
, this->trusted
)) {
403 case HyperlinkType::Internal
:
405 auto it
= std::find_if(this->link_anchors
.cbegin(), this->link_anchors
.cend(), [&](const Hyperlink
&other
) { return link
.destination
== other
.destination
; });
406 if (it
!= this->link_anchors
.cend()) {
407 this->AppendHistory(this->filepath
);
408 this->ScrollToLine(it
->line
);
409 this->UpdateHistoryScrollpos();
414 case HyperlinkType::Web
:
415 OpenBrowser(link
.destination
);
418 case HyperlinkType::File
:
419 this->NavigateToFile(link
.destination
, 0);
429 * Navigate to the requested file.
431 * @param newfile The file to navigate to.
432 * @param line The line to scroll to.
434 void TextfileWindow::NavigateToFile(std::string newfile
, size_t line
)
436 /* Double-check that the file link begins with ./ as a relative path. */
437 if (!newfile
.starts_with("./")) return;
439 /* Get the path portion of the current file path. */
440 std::string newpath
= this->filepath
;
441 size_t pos
= newpath
.find_last_of(PATHSEPCHAR
);
442 if (pos
== std::string::npos
) {
445 newpath
.erase(pos
+ 1);
448 /* Check and remove for anchor in link. Do this before we find the filename, as people might have a / after the hash. */
449 size_t anchor_pos
= newfile
.find_first_of('#');
451 if (anchor_pos
!= std::string::npos
) {
452 anchor
= newfile
.substr(anchor_pos
);
453 newfile
.erase(anchor_pos
);
456 /* Now the anchor is gone, check if this is a markdown or textfile. */
457 if (!StrEndsWithIgnoreCase(newfile
, ".md") && !StrEndsWithIgnoreCase(newfile
, ".txt")) return;
459 /* Convert link destination to acceptable local filename (replace forward slashes with correct path separator). */
460 newfile
= newfile
.substr(2);
461 if (PATHSEPCHAR
!= '/') {
462 for (char &c
: newfile
) {
463 if (c
== '/') c
= PATHSEPCHAR
;
467 /* Paste the two together and check file exists. */
468 newpath
= newpath
+ newfile
;
469 if (!FioCheckFileExists(newpath
, NO_DIRECTORY
)) return;
471 /* Update history. */
472 this->AppendHistory(newpath
);
474 /* Load the new file. */
475 this->filepath
= newpath
;
476 this->filename
= newpath
.substr(newpath
.find_last_of(PATHSEP
) + 1);
478 this->LoadTextfile(this->filepath
, NO_DIRECTORY
);
480 this->GetScrollbar(WID_TF_HSCROLLBAR
)->SetPosition(0);
481 this->GetScrollbar(WID_TF_VSCROLLBAR
)->SetPosition(0);
483 if (anchor
.empty() || line
!= 0) {
484 this->ScrollToLine(line
);
486 auto anchor_dest
= std::find_if(this->link_anchors
.cbegin(), this->link_anchors
.cend(), [&](const Hyperlink
&other
) { return anchor
== other
.destination
; });
487 if (anchor_dest
!= this->link_anchors
.cend()) {
488 this->ScrollToLine(anchor_dest
->line
);
489 this->UpdateHistoryScrollpos();
491 this->ScrollToLine(0);
496 /* virtual */ void TextfileWindow::AfterLoadText()
498 this->link_anchors
.clear();
500 if (StrEndsWithIgnoreCase(this->filename
, ".md")) this->AfterLoadMarkdown();
502 if (this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(this->jumplist
.empty() ? SZSP_HORIZONTAL
: 0)) this->ReInit();
506 * Post-processing of markdown files.
508 void TextfileWindow::AfterLoadMarkdown()
510 for (size_t line_index
= 0; line_index
< this->lines
.size(); ++line_index
) {
511 Line
&line
= this->lines
[line_index
];
513 /* Find and mark all hyperlinks in the line. */
514 this->FindHyperlinksInMarkdown(line
, line_index
);
516 /* All lines beginning with # are headings. */
517 if (!line
.text
.empty() && line
.text
[0] == '#') {
518 this->jumplist
.push_back(line_index
);
519 this->lines
[line_index
].colour
= TC_GOLD
;
520 this->link_anchors
.emplace_back(Hyperlink
{ line_index
, 0, 0, MakeAnchorSlug(line
.text
) });
525 /* virtual */ void TextfileWindow::OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
)
528 case WID_TF_WRAPTEXT
:
529 this->ToggleWidgetLoweredState(WID_TF_WRAPTEXT
);
530 this->InvalidateData();
533 case WID_TF_JUMPLIST
: {
535 for (size_t line
: this->jumplist
) {
536 SetDParamStr(0, this->lines
[line
].text
);
537 list
.push_back(std::make_unique
<DropDownListStringItem
>(STR_TEXTFILE_JUMPLIST_ITEM
, (int)line
, false));
539 ShowDropDownList(this, std::move(list
), -1, widget
);
544 this->NavigateHistory(-1);
547 case WID_TF_NAVFORWARD
:
548 this->NavigateHistory(+1);
551 case WID_TF_BACKGROUND
:
552 this->CheckHyperlinkClick(pt
);
557 /* virtual */ void TextfileWindow::DrawWidget(const Rect
&r
, WidgetID widget
) const
559 if (widget
!= WID_TF_BACKGROUND
) return;
561 Rect fr
= r
.Shrink(WidgetDimensions::scaled
.frametext
);
563 DrawPixelInfo new_dpi
;
564 if (!FillDrawPixelInfo(&new_dpi
, fr
)) return;
565 AutoRestoreBackup
dpi_backup(_cur_dpi
, &new_dpi
);
567 /* Draw content (now coordinates given to DrawString* are local to the new clipping region). */
568 fr
= fr
.Translate(-fr
.left
, -fr
.top
);
569 int line_height
= GetCharacterHeight(FS_MONO
);
570 int pos
= this->vscroll
->GetPosition();
571 int cap
= this->vscroll
->GetCapacity();
573 for (auto &line
: this->lines
) {
574 if (line
.bottom
< pos
) continue;
575 if (line
.top
> pos
+ cap
) break;
577 int y_offset
= (line
.top
- pos
) * line_height
;
578 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
579 DrawStringMultiLine(0, fr
.right
, y_offset
, fr
.bottom
, line
.text
, line
.colour
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
581 DrawString(-this->hscroll
->GetPosition(), fr
.right
, y_offset
, line
.text
, line
.colour
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
586 /* virtual */ void TextfileWindow::OnResize()
588 this->vscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
, WidgetDimensions::scaled
.frametext
.Vertical());
589 this->hscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
);
591 this->SetupScrollbars(false);
594 /* virtual */ void TextfileWindow::OnInvalidateData([[maybe_unused
]] int data
, [[maybe_unused
]] bool gui_scope
)
596 if (!gui_scope
) return;
598 this->SetupScrollbars(true);
601 void TextfileWindow::OnDropdownSelect(WidgetID widget
, int index
)
603 if (widget
!= WID_TF_JUMPLIST
) return;
605 this->ScrollToLine(index
);
608 void TextfileWindow::ScrollToLine(size_t line
)
610 Scrollbar
*sb
= this->GetScrollbar(WID_TF_VSCROLLBAR
);
612 if (this->IsWidgetLowered(WID_TF_WRAPTEXT
)) {
613 newpos
= this->lines
[line
].top
;
615 newpos
= static_cast<int>(line
);
617 sb
->SetPosition(std::min(newpos
, sb
->GetCount() - sb
->GetCapacity()));
621 /* virtual */ void TextfileWindow::Reset()
623 this->search_iterator
= 0;
626 /* virtual */ FontSize
TextfileWindow::DefaultSize()
631 /* virtual */ std::optional
<std::string_view
> TextfileWindow::NextString()
633 if (this->search_iterator
>= this->lines
.size()) return std::nullopt
;
635 return this->lines
[this->search_iterator
++].text
;
638 /* virtual */ bool TextfileWindow::Monospace()
643 /* virtual */ void TextfileWindow::SetFontNames([[maybe_unused
]] FontCacheSettings
*settings
, [[maybe_unused
]] const char *font_name
, [[maybe_unused
]] const void *os_data
)
645 #if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
646 settings
->mono
.font
= font_name
;
647 settings
->mono
.os_handle
= os_data
;
651 #if defined(WITH_ZLIB)
654 * Do an in-memory gunzip operation. This works on a raw deflate stream,
655 * or a file with gzip or zlib header.
656 * @param bufp A pointer to a buffer containing the input data. This
657 * buffer will be freed and replaced by a buffer containing
658 * the uncompressed data.
659 * @param sizep A pointer to the buffer size. Before the call, the value
660 * pointed to should contain the size of the input buffer.
661 * After the call, it contains the size of the uncompressed
664 * When decompressing fails, *bufp is set to nullptr and *sizep to 0. The
665 * compressed buffer passed in is still freed in this case.
667 static void Gunzip(byte
**bufp
, size_t *sizep
)
669 static const int BLOCKSIZE
= 8192;
671 size_t alloc_size
= 0;
675 memset(&z
, 0, sizeof(z
));
677 z
.avail_in
= (uInt
)*sizep
;
679 /* window size = 15, add 32 to enable gzip or zlib header processing */
680 res
= inflateInit2(&z
, 15 + 32);
681 /* Z_BUF_ERROR just means we need more space */
682 while (res
== Z_OK
|| (res
== Z_BUF_ERROR
&& z
.avail_out
== 0)) {
683 /* When we get here, we're either just starting, or
684 * inflate is out of output space - allocate more */
685 alloc_size
+= BLOCKSIZE
;
686 z
.avail_out
+= BLOCKSIZE
;
687 buf
= ReallocT(buf
, alloc_size
);
688 z
.next_out
= buf
+ alloc_size
- z
.avail_out
;
689 res
= inflate(&z
, Z_FINISH
);
695 if (res
== Z_STREAM_END
) {
697 *sizep
= alloc_size
- z
.avail_out
;
699 /* Something went wrong */
707 #if defined(WITH_LIBLZMA)
710 * Do an in-memory xunzip operation. This works on a .xz or (legacy)
712 * @param bufp A pointer to a buffer containing the input data. This
713 * buffer will be freed and replaced by a buffer containing
714 * the uncompressed data.
715 * @param sizep A pointer to the buffer size. Before the call, the value
716 * pointed to should contain the size of the input buffer.
717 * After the call, it contains the size of the uncompressed
720 * When decompressing fails, *bufp is set to nullptr and *sizep to 0. The
721 * compressed buffer passed in is still freed in this case.
723 static void Xunzip(byte
**bufp
, size_t *sizep
)
725 static const int BLOCKSIZE
= 8192;
727 size_t alloc_size
= 0;
728 lzma_stream z
= LZMA_STREAM_INIT
;
734 res
= lzma_auto_decoder(&z
, UINT64_MAX
, LZMA_CONCATENATED
);
735 /* Z_BUF_ERROR just means we need more space */
736 while (res
== LZMA_OK
|| (res
== LZMA_BUF_ERROR
&& z
.avail_out
== 0)) {
737 /* When we get here, we're either just starting, or
738 * inflate is out of output space - allocate more */
739 alloc_size
+= BLOCKSIZE
;
740 z
.avail_out
+= BLOCKSIZE
;
741 buf
= ReallocT(buf
, alloc_size
);
742 z
.next_out
= buf
+ alloc_size
- z
.avail_out
;
743 res
= lzma_code(&z
, LZMA_FINISH
);
749 if (res
== LZMA_STREAM_END
) {
751 *sizep
= alloc_size
- z
.avail_out
;
753 /* Something went wrong */
763 * Loads the textfile text from file and setup #lines.
765 /* virtual */ void TextfileWindow::LoadTextfile(const std::string
&textfile
, Subdirectory dir
)
768 this->jumplist
.clear();
770 if (this->GetWidget
<NWidgetStacked
>(WID_TF_SEL_JUMPLIST
)->SetDisplayedPlane(SZSP_HORIZONTAL
)) this->ReInit();
772 if (textfile
.empty()) return;
774 /* Get text from file */
776 FILE *handle
= FioFOpenFile(textfile
, "rb", dir
, &filesize
);
777 if (handle
== nullptr) return;
778 /* Early return on empty files. */
779 if (filesize
== 0) return;
781 char *buf
= MallocT
<char>(filesize
);
782 size_t read
= fread(buf
, 1, filesize
, handle
);
785 if (read
!= filesize
) {
790 #if defined(WITH_ZLIB)
791 /* In-place gunzip */
792 if (textfile
.ends_with(".gz")) Gunzip((byte
**)&buf
, &filesize
);
795 #if defined(WITH_LIBLZMA)
796 /* In-place xunzip */
797 if (textfile
.ends_with(".xz")) Xunzip((byte
**)&buf
, &filesize
);
800 if (buf
== nullptr) return;
802 std::string_view
sv_buf(buf
, filesize
);
804 /* Check for the byte-order-mark, and skip it if needed. */
805 if (sv_buf
.starts_with("\ufeff")) sv_buf
.remove_prefix(3);
807 /* Update the filename. */
808 this->filepath
= textfile
;
809 this->filename
= this->filepath
.substr(this->filepath
.find_last_of(PATHSEP
) + 1);
810 /* If it's the first file being loaded, add to history. */
811 if (this->history
.empty()) this->history
.push_back(HistoryEntry
{ this->filepath
, 0 });
813 /* Process the loaded text into lines, and do any further parsing needed. */
814 this->LoadText(sv_buf
);
819 * Load a text into the textfile viewer.
821 * This will split the text into newlines and stores it for fast drawing.
823 * @param buf The text to load.
825 void TextfileWindow::LoadText(std::string_view buf
)
827 std::string text
= StrMakeValid(buf
, SVS_REPLACE_WITH_QUESTION_MARK
| SVS_ALLOW_NEWLINE
| SVS_REPLACE_TAB_CR_NL_WITH_SPACE
);
830 /* Split the string on newlines. */
831 std::string_view
p(text
);
833 auto next
= p
.find_first_of('\n');
834 while (next
!= std::string_view::npos
) {
835 this->lines
.emplace_back(row
, p
.substr(0, next
));
836 p
.remove_prefix(next
+ 1);
839 next
= p
.find_first_of('\n');
841 this->lines
.emplace_back(row
, p
);
843 /* Calculate maximum text line length. */
845 for (auto &line
: this->lines
) {
846 max_length
= std::max(max_length
, GetStringBoundingBox(line
.text
, FS_MONO
).width
);
848 this->max_length
= max_length
;
850 this->AfterLoadText();
852 CheckForMissingGlyphs(true, this);
854 /* The font may have changed when searching for glyphs, so ensure widget sizes are updated just in case. */
859 * Search a textfile file next to the given content.
860 * @param type The type of the textfile to search for.
861 * @param dir The subdirectory to search in.
862 * @param filename The filename of the content to look for.
863 * @return The path to the textfile, \c nullptr otherwise.
865 std::optional
<std::string
> GetTextfile(TextfileType type
, Subdirectory dir
, const std::string
&filename
)
867 static const char * const prefixes
[] = {
872 static_assert(lengthof(prefixes
) == TFT_CONTENT_END
);
874 /* Only the generic text file types allowed for this function */
875 if (type
>= TFT_CONTENT_END
) return std::nullopt
;
877 std::string_view prefix
= prefixes
[type
];
879 if (filename
.empty()) return std::nullopt
;
881 auto slash
= filename
.find_last_of(PATHSEPCHAR
);
882 if (slash
== std::string::npos
) return std::nullopt
;
884 std::string_view
base_path(filename
.data(), slash
+ 1);
886 static const std::initializer_list
<std::string_view
> extensions
{
889 #if defined(WITH_ZLIB)
893 #if defined(WITH_LIBLZMA)
899 for (auto &extension
: extensions
) {
900 std::string file_path
= fmt::format("{}{}_{}.{}", base_path
, prefix
, GetCurrentLanguageIsoCode(), extension
);
901 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
903 file_path
= fmt::format("{}{}_{:.2s}.{}", base_path
, prefix
, GetCurrentLanguageIsoCode(), extension
);
904 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
906 file_path
= fmt::format("{}{}.{}", base_path
, prefix
, extension
);
907 if (FioCheckFileExists(file_path
, dir
)) return file_path
;