OpenTTD Source 15.0-beta2
textfile_gui.cpp
Go to the documentation of this file.
1/*
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/>.
6 */
7
10#include "stdafx.h"
11#include "core/backup_type.hpp"
12#include "fileio_func.h"
13#include "fontcache.h"
14#include "gfx_type.h"
15#include "gfx_func.h"
16#include "string_func.h"
18#include "textfile_gui.h"
19#include "dropdown_type.h"
20#include "dropdown_func.h"
21#include "gfx_layout.h"
22#include "debug.h"
23#include "openttd.h"
24
25#include "widgets/misc_widget.h"
26
27#include "table/strings.h"
28#include "table/control_codes.h"
29
30#if defined(WITH_ZLIB)
31#include <zlib.h>
32#endif
33
34#if defined(WITH_LIBLZMA)
35#include <lzma.h>
36#endif
37
38#include <regex>
39
40#include "safeguards.h"
41
45 NWidget(WWT_CLOSEBOX, COLOUR_MAUVE),
46 NWidget(WWT_PUSHARROWBTN, COLOUR_MAUVE, WID_TF_NAVBACK), SetFill(0, 1), SetMinimalSize(15, 1), SetArrowWidgetTypeTip(AWV_DECREASE, STR_TEXTFILE_NAVBACK_TOOLTIP),
47 NWidget(WWT_PUSHARROWBTN, COLOUR_MAUVE, WID_TF_NAVFORWARD), SetFill(0, 1), SetMinimalSize(15, 1), SetArrowWidgetTypeTip(AWV_INCREASE, STR_TEXTFILE_NAVFORWARD_TOOLTIP),
48 NWidget(WWT_CAPTION, COLOUR_MAUVE, WID_TF_CAPTION), SetToolTip(STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
49 NWidget(WWT_TEXTBTN, COLOUR_MAUVE, WID_TF_WRAPTEXT), SetStringTip(STR_TEXTFILE_WRAP_TEXT, STR_TEXTFILE_WRAP_TEXT_TOOLTIP),
50 NWidget(WWT_DEFSIZEBOX, COLOUR_MAUVE),
53 NWidget(WWT_PANEL, COLOUR_MAUVE),
55 /* As this widget can be toggled, it needs to be a multiplier of FS_MONO. So add a spacer that ensures this. */
58 NWidget(NWID_SPACER), SetFill(1, 1), SetResize(1, 0),
59 NWidget(WWT_DROPDOWN, COLOUR_MAUVE, WID_TF_JUMPLIST), SetStringTip(STR_TEXTFILE_JUMPLIST, STR_TEXTFILE_JUMPLIST_TOOLTIP), SetFill(1, 0), SetResize(1, 0),
60 NWidget(NWID_SPACER), SetFill(1, 1), SetResize(1, 0),
74 NWidget(WWT_RESIZEBOX, COLOUR_MAUVE),
76};
77
80 WDP_CENTER, "textfile", 630, 460,
82 {},
84);
85
86TextfileWindow::TextfileWindow(TextfileType file_type) : Window(_textfile_desc), file_type(file_type)
87{
88 /* Init of nested tree is deferred.
89 * TextfileWindow::ConstructWindow must be called by the inheriting window. */
90}
91
92void TextfileWindow::ConstructWindow()
93{
94 this->CreateNestedTree();
97 this->GetWidget<NWidgetCore>(WID_TF_CAPTION)->SetStringTip(STR_TEXTFILE_README_CAPTION + this->file_type, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
98 this->GetWidget<NWidgetStacked>(WID_TF_SEL_JUMPLIST)->SetDisplayedPlane(SZSP_HORIZONTAL);
99 this->FinishInitNested(this->file_type);
100
103 this->hscroll->SetStepSize(10); // Speed up horizontal scrollbar
104}
105
111{
112 uint height = 0;
114 for (auto &line : this->lines) {
115 line.top = height;
116 height++;
117 line.bottom = height;
118 }
119 } else {
120 int max_width = this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WidgetDimensions::scaled.frametext.Horizontal();
121 for (auto &line : this->lines) {
122 line.top = height;
123 height += GetStringHeight(line.text, max_width, FS_MONO) / GetCharacterHeight(FS_MONO);
124 line.bottom = height;
125 }
126 }
127
128 return height;
129}
130
131uint TextfileWindow::GetContentHeight()
132{
133 if (this->lines.empty()) return 0;
134 return this->lines.back().bottom;
135}
136
137/* virtual */ void TextfileWindow::UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize)
138{
139 switch (widget) {
142
143 size.height = 4 * resize.height + WidgetDimensions::scaled.frametext.Vertical(); // At least 4 lines are visible.
144 size.width = std::max(200u, size.width); // At least 200 pixels wide.
145 break;
146 }
147}
148
150void TextfileWindow::SetupScrollbars(bool force_reflow)
151{
153 /* Reflow is mandatory if text wrapping is on */
154 uint height = this->ReflowContent();
155 this->vscroll->SetCount(ClampTo<uint16_t>(height));
156 this->hscroll->SetCount(0);
157 } else {
158 uint height = force_reflow ? this->ReflowContent() : this->GetContentHeight();
159 this->vscroll->SetCount(ClampTo<uint16_t>(height));
160 this->hscroll->SetCount(this->max_length);
161 }
162
164}
165
166
168static const std::regex _markdown_link_regex{"\\[(.+?)\\]\\((.+?)\\)", std::regex_constants::ECMAScript | std::regex_constants::optimize};
169
171enum class HyperlinkType : uint8_t {
172 Internal,
173 Web,
174 File,
175 Unknown,
176};
177
185static HyperlinkType ClassifyHyperlink(const std::string &destination, bool trusted)
186{
187 if (destination.empty()) return HyperlinkType::Unknown;
188 if (destination.starts_with("#")) return HyperlinkType::Internal;
189
190 /* Only allow external / internal links for sources we trust. */
191 if (!trusted) return HyperlinkType::Unknown;
192
193 if (destination.starts_with("http://")) return HyperlinkType::Web;
194 if (destination.starts_with("https://")) return HyperlinkType::Web;
195 if (destination.starts_with("./")) return HyperlinkType::File;
197}
198
205static std::string MakeAnchorSlug(const std::string &line)
206{
207 std::string r = "#";
208 uint state = 0;
209 for (char c : line) {
210 if (state == 0) {
211 /* State 0: Skip leading hashmarks and spaces. */
212 if (c == '#') continue;
213 if (c == ' ') continue;
214 state = 1;
215 }
216 if (state == 2) {
217 /* State 2: Wait for a non-space/dash character.
218 * When found, output a dash and that character. */
219 if (c == ' ' || c == '-') continue;
220 r += '-';
221 state = 1;
222 }
223 if (state == 1) {
224 /* State 1: Normal text.
225 * Lowercase alphanumerics,
226 * spaces and dashes become dashes,
227 * everything else is removed. */
228 if (isalnum(c)) {
229 r += tolower(c);
230 } else if (c == ' ' || c == '-') {
231 state = 2;
232 }
233 }
234 }
235
236 return r;
237}
238
245void TextfileWindow::FindHyperlinksInMarkdown(Line &line, size_t line_index)
246{
247 std::string::const_iterator last_match_end = line.text.cbegin();
248 std::string fixed_line;
249 StringBuilder builder(fixed_line);
250
251 std::sregex_iterator matcher{ line.text.cbegin(), line.text.cend(), _markdown_link_regex};
252 while (matcher != std::sregex_iterator()) {
253 std::smatch match = *matcher;
254
255 Hyperlink &link = this->links.emplace_back(line_index, 0, 0, match[2].str());
256
257 HyperlinkType link_type = ClassifyHyperlink(link.destination, this->trusted);
258 StringControlCode link_colour;
259 switch (link_type) {
261 link_colour = SCC_GREEN;
262 break;
264 link_colour = SCC_LTBLUE;
265 break;
267 link_colour = SCC_LTBROWN;
268 break;
269 default:
270 /* Don't make other link types fancy as they aren't handled (yet). */
271 link_colour = SCC_CONTROL_END;
272 break;
273 }
274
275 if (link_colour != SCC_CONTROL_END) {
276 /* Format the link to look like a link. */
277 builder += std::string_view(last_match_end, match[0].first);
278 link.begin = fixed_line.length();
279 builder.PutUtf8(SCC_PUSH_COLOUR);
280 builder.PutUtf8(link_colour);
281 builder += match[1].str();
282 link.end = fixed_line.length();
283 builder.PutUtf8(SCC_POP_COLOUR);
284 last_match_end = match[0].second;
285 }
286
287 /* Find next link. */
288 ++matcher;
289 }
290 if (last_match_end == line.text.cbegin()) return; // nothing found
291
292 /* Add remaining text on line. */
293 fixed_line += std::string(last_match_end, line.text.cend());
294
295 /* Overwrite original line text with "fixed" line text. */
296 line.text = std::move(fixed_line);
297}
298
305{
306 if (this->links.empty()) return nullptr;
307
308 /* Which line was clicked. */
310 size_t line_index;
311 size_t subline;
313 auto it = std::ranges::find_if(this->lines, [clicked_row](const Line &l) { return l.top <= clicked_row && l.bottom > clicked_row; });
314 if (it == this->lines.cend()) return nullptr;
315 line_index = it - this->lines.cbegin();
316 subline = clicked_row - it->top;
317 Debug(misc, 4, "TextfileWindow check hyperlink: clicked_row={}, line_index={}, line.top={}, subline={}", clicked_row, line_index, it->top, subline);
318 } else {
319 line_index = clicked_row / GetCharacterHeight(FS_MONO);
320 subline = 0;
321 }
322
323 /* Find hyperlinks in this line. */
324 std::vector<const Hyperlink *> found_links;
325 for (const auto &link : this->links) {
326 if (link.line == line_index) found_links.push_back(&link);
327 }
328 if (found_links.empty()) return nullptr;
329
330 /* Build line layout to figure out character position that was clicked. */
331 uint window_width = IsWidgetLowered(WID_TF_WRAPTEXT) ? this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WidgetDimensions::scaled.frametext.Horizontal() : INT_MAX;
332 Layouter layout(this->lines[line_index].text, window_width, FS_MONO);
333 assert(subline < layout.size());
334 ptrdiff_t char_index = layout.GetCharAtPosition(pt.x - WidgetDimensions::scaled.frametext.left, subline);
335 if (char_index < 0) return nullptr;
336 Debug(misc, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index, subline, (int)char_index);
337
338 /* Found character index in line, check if any links are at that position. */
339 for (const Hyperlink *link : found_links) {
340 Debug(misc, 4, "Checking link from char {} to {}", link->begin, link->end);
341 if (static_cast<size_t>(char_index) >= link->begin && static_cast<size_t>(char_index) < link->end) {
342 Debug(misc, 4, "Returning link with destination: {}", link->destination);
343 return link;
344 }
345 }
346
347 return nullptr;
348}
349
355void TextfileWindow::AppendHistory(const std::string &filepath)
356{
357 this->history.erase(this->history.begin() + this->history_pos + 1, this->history.end());
359 this->history.emplace_back(filepath, 0);
362 this->history_pos = this->history.size() - 1;
363}
364
372
379{
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;
383
385 this->history_pos += delta;
386
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);
391 }
392
393 this->SetWidgetDisabledState(WID_TF_NAVFORWARD, this->history_pos + 1 >= this->history.size());
395 this->GetScrollbar(WID_TF_VSCROLLBAR)->SetPosition(this->history[this->history_pos].scrollpos);
397 this->SetDirty();
398}
399
400/* virtual */ void TextfileWindow::OnHyperlinkClick(const Hyperlink &link)
401{
402 switch (ClassifyHyperlink(link.destination, this->trusted)) {
404 {
405 auto it = std::ranges::find(this->link_anchors, link.destination, &Hyperlink::destination);
406 if (it != this->link_anchors.cend()) {
407 this->AppendHistory(this->filepath);
408 this->ScrollToLine(it->line);
410 }
411 break;
412 }
413
415 OpenBrowser(link.destination);
416 break;
417
419 this->NavigateToFile(link.destination, 0);
420 break;
421
422 default:
423 /* Do nothing */
424 break;
425 }
426}
427
434void TextfileWindow::NavigateToFile(std::string newfile, size_t line)
435{
436 /* Double-check that the file link begins with ./ as a relative path. */
437 if (!newfile.starts_with("./")) return;
438
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) {
443 newpath.clear();
444 } else {
445 newpath.erase(pos + 1);
446 }
447
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('#');
450 std::string anchor;
451 if (anchor_pos != std::string::npos) {
452 anchor = newfile.substr(anchor_pos);
453 newfile.erase(anchor_pos);
454 }
455
456 /* Now the anchor is gone, check if this is a markdown or textfile. */
457 if (!StrEndsWithIgnoreCase(newfile, ".md") && !StrEndsWithIgnoreCase(newfile, ".txt")) return;
458
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;
464 }
465 }
466
467 /* Paste the two together and check file exists. */
468 newpath = newpath + newfile;
469 if (!FioCheckFileExists(newpath, NO_DIRECTORY)) return;
470
471 /* Update history. */
472 this->AppendHistory(newpath);
473
474 /* Load the new file. */
475 this->filepath = newpath;
476 this->filename = newpath.substr(newpath.find_last_of(PATHSEP) + 1);
477
478 this->LoadTextfile(this->filepath, NO_DIRECTORY);
479
482
483 if (anchor.empty() || line != 0) {
484 this->ScrollToLine(line);
485 } else {
486 auto anchor_dest = std::ranges::find(this->link_anchors, anchor, &Hyperlink::destination);
487 if (anchor_dest != this->link_anchors.cend()) {
488 this->ScrollToLine(anchor_dest->line);
490 } else {
491 this->ScrollToLine(0);
492 }
493 }
494}
495
497{
498 this->link_anchors.clear();
499
500 if (StrEndsWithIgnoreCase(this->filename, ".md")) this->AfterLoadMarkdown();
501
502 if (this->GetWidget<NWidgetStacked>(WID_TF_SEL_JUMPLIST)->SetDisplayedPlane(this->jumplist.empty() ? SZSP_HORIZONTAL : 0)) this->ReInit();
503}
504
509{
510 for (size_t line_index = 0; line_index < this->lines.size(); ++line_index) {
511 Line &line = this->lines[line_index];
512
513 /* Find and mark all hyperlinks in the line. */
514 this->FindHyperlinksInMarkdown(line, line_index);
515
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(line_index, 0, 0, MakeAnchorSlug(line.text));
521 }
522 }
523}
524
525/* virtual */ void TextfileWindow::OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count)
526{
527 switch (widget) {
528 case WID_TF_WRAPTEXT:
530 this->InvalidateData();
531 break;
532
533 case WID_TF_JUMPLIST: {
534 DropDownList list;
535 for (size_t line : this->jumplist) {
536 list.push_back(MakeDropDownListStringItem(GetString(STR_TEXTFILE_JUMPLIST_ITEM, this->lines[line].text), (int)line));
537 }
538 ShowDropDownList(this, std::move(list), -1, widget);
539 break;
540 }
541
542 case WID_TF_NAVBACK:
543 this->NavigateHistory(-1);
544 break;
545
547 this->NavigateHistory(+1);
548 break;
549
550 case WID_TF_BACKGROUND: {
551 const Hyperlink *link = this->GetHyperlink(pt);
552 if (link != nullptr) this->OnHyperlinkClick(*link);
553 break;
554 }
555 }
556}
557
558/* virtual */ bool TextfileWindow::OnTooltip([[maybe_unused]] Point pt, WidgetID widget, TooltipCloseCondition close_cond)
559{
560 if (widget != WID_TF_BACKGROUND) return false;
561
562 const Hyperlink *link = this->GetHyperlink(pt);
563 if (link == nullptr) return false;
564
565 GuiShowTooltips(this, GetEncodedString(STR_JUST_RAW_STRING, link->destination), close_cond);
566
567 return true;
568}
569
570/* virtual */ void TextfileWindow::DrawWidget(const Rect &r, WidgetID widget) const
571{
572 if (widget != WID_TF_BACKGROUND) return;
573
574 Rect fr = r.Shrink(WidgetDimensions::scaled.frametext);
575
576 DrawPixelInfo new_dpi;
577 if (!FillDrawPixelInfo(&new_dpi, fr)) return;
578 AutoRestoreBackup dpi_backup(_cur_dpi, &new_dpi);
579
580 /* Draw content (now coordinates given to DrawString* are local to the new clipping region). */
581 fr = fr.Translate(-fr.left, -fr.top);
582 int line_height = GetCharacterHeight(FS_MONO);
583
584 if (!IsWidgetLowered(WID_TF_WRAPTEXT)) fr = ScrollRect(fr, *this->hscroll, 1);
585
586 int pos = this->vscroll->GetPosition();
587 int cap = this->vscroll->GetCapacity();
588
589 for (auto &line : this->lines) {
590 if (line.bottom < pos) continue;
591 if (line.top > pos + cap) break;
592
593 int y_offset = (line.top - pos) * line_height;
595 DrawStringMultiLine(fr.left, fr.right, y_offset, fr.bottom, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
596 } else {
597 DrawString(fr.left, fr.right, y_offset, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
598 }
599 }
600}
601
602/* virtual */ void TextfileWindow::OnResize()
603{
605 this->hscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND, WidgetDimensions::scaled.framerect.Horizontal());
606
607 this->SetupScrollbars(false);
608}
609
610/* virtual */ void TextfileWindow::OnInvalidateData([[maybe_unused]] int data, [[maybe_unused]] bool gui_scope)
611{
612 if (!gui_scope) return;
613
614 this->SetupScrollbars(true);
615}
616
618{
619 if (widget != WID_TF_JUMPLIST) return;
620
621 this->ScrollToLine(index);
622}
623
624void TextfileWindow::ScrollToLine(size_t line)
625{
627 int newpos;
628 if (this->IsWidgetLowered(WID_TF_WRAPTEXT)) {
629 newpos = this->lines[line].top;
630 } else {
631 newpos = static_cast<int>(line);
632 }
633 sb->SetPosition(std::min(newpos, sb->GetCount() - sb->GetCapacity()));
634 this->SetDirty();
635}
636
637/* virtual */ void TextfileWindow::Reset()
638{
639 this->search_iterator = 0;
640}
641
643{
644 return FS_MONO;
645}
646
647/* virtual */ std::optional<std::string_view> TextfileWindow::NextString()
648{
649 if (this->search_iterator >= this->lines.size()) return std::nullopt;
650
651 return this->lines[this->search_iterator++].text;
652}
653
654/* virtual */ bool TextfileWindow::Monospace()
655{
656 return true;
657}
658
659/* virtual */ void TextfileWindow::SetFontNames([[maybe_unused]] FontCacheSettings *settings, [[maybe_unused]] const char *font_name, [[maybe_unused]] const void *os_data)
660{
661#if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
662 settings->mono.font = font_name;
663 settings->mono.os_handle = os_data;
664#endif
665}
666
667#if defined(WITH_ZLIB)
668
677static std::vector<char> Gunzip(std::span<char> input)
678{
679 static const int BLOCKSIZE = 8192;
680 std::vector<char> output;
681
682 z_stream z;
683 memset(&z, 0, sizeof(z));
684 z.next_in = reinterpret_cast<Bytef *>(input.data());
685 z.avail_in = static_cast<uInt>(input.size());
686
687 /* window size = 15, add 32 to enable gzip or zlib header processing */
688 int res = inflateInit2(&z, 15 + 32);
689 /* Z_BUF_ERROR just means we need more space */
690 while (res == Z_OK || (res == Z_BUF_ERROR && z.avail_out == 0)) {
691 /* When we get here, we're either just starting, or
692 * inflate is out of output space - allocate more */
693 z.avail_out += BLOCKSIZE;
694 output.resize(output.size() + BLOCKSIZE);
695 z.next_out = reinterpret_cast<Bytef *>(output.data() + output.size() - z.avail_out);
696 res = inflate(&z, Z_FINISH);
697 }
698
699 inflateEnd(&z);
700 if (res != Z_STREAM_END) return {};
701
702 output.resize(output.size() - z.avail_out);
703 return output;
704}
705#endif
706
707#if defined(WITH_LIBLZMA)
708
717static std::vector<char> Xunzip(std::span<char> input)
718{
719 static const int BLOCKSIZE = 8192;
720 std::vector<char> output;
721
722 lzma_stream z = LZMA_STREAM_INIT;
723 z.next_in = reinterpret_cast<uint8_t *>(input.data());
724 z.avail_in = input.size();
725
726 int res = lzma_auto_decoder(&z, UINT64_MAX, LZMA_CONCATENATED);
727 /* Z_BUF_ERROR just means we need more space */
728 while (res == LZMA_OK || (res == LZMA_BUF_ERROR && z.avail_out == 0)) {
729 /* When we get here, we're either just starting, or
730 * inflate is out of output space - allocate more */
731 z.avail_out += BLOCKSIZE;
732 output.resize(output.size() + BLOCKSIZE);
733 z.next_out = reinterpret_cast<uint8_t *>(output.data() + output.size() - z.avail_out);
734 res = lzma_code(&z, LZMA_FINISH);
735 }
736
737 lzma_end(&z);
738 if (res != LZMA_STREAM_END) return {};
739
740 output.resize(output.size() - z.avail_out);
741 return output;
742}
743#endif
744
745
749/* virtual */ void TextfileWindow::LoadTextfile(const std::string &textfile, Subdirectory dir)
750{
751 this->lines.clear();
752 this->jumplist.clear();
753
754 if (this->GetWidget<NWidgetStacked>(WID_TF_SEL_JUMPLIST)->SetDisplayedPlane(SZSP_HORIZONTAL)) this->ReInit();
755
756 if (textfile.empty()) return;
757
758 /* Get text from file */
759 size_t filesize;
760 auto handle = FioFOpenFile(textfile, "rb", dir, &filesize);
761 if (!handle.has_value()) return;
762 /* Early return on empty files. */
763 if (filesize == 0) return;
764
765 std::vector<char> buf;
766 buf.resize(filesize);
767 size_t read = fread(buf.data(), 1, buf.size(), *handle);
768
769 if (read != buf.size()) return;
770
771#if defined(WITH_ZLIB)
772 /* In-place gunzip */
773 if (textfile.ends_with(".gz")) buf = Gunzip(buf);
774#endif
775
776#if defined(WITH_LIBLZMA)
777 /* In-place xunzip */
778 if (textfile.ends_with(".xz")) buf = Xunzip(buf);
779#endif
780
781 if (buf.empty()) return;
782
783 std::string_view sv_buf(buf.data(), buf.size());
784
785 /* Check for the byte-order-mark, and skip it if needed. */
786 if (sv_buf.starts_with("\ufeff")) sv_buf.remove_prefix(3);
787
788 /* Update the filename. */
789 this->filepath = textfile;
790 this->filename = this->filepath.substr(this->filepath.find_last_of(PATHSEP) + 1);
791 /* If it's the first file being loaded, add to history. */
792 if (this->history.empty()) this->history.emplace_back(this->filepath, 0);
793
794 /* Process the loaded text into lines, and do any further parsing needed. */
795 this->LoadText(sv_buf);
796}
797
805void TextfileWindow::LoadText(std::string_view buf)
806{
808 this->lines.clear();
809
810 /* Split the string on newlines. */
811 std::string_view p(text);
812 int row = 0;
813 auto next = p.find_first_of('\n');
814 while (next != std::string_view::npos) {
815 this->lines.emplace_back(row, p.substr(0, next));
816 p.remove_prefix(next + 1);
817
818 row++;
819 next = p.find_first_of('\n');
820 }
821 this->lines.emplace_back(row, p);
822
823 /* Calculate maximum text line length. */
824 uint max_length = 0;
825 for (auto &line : this->lines) {
826 max_length = std::max(max_length, GetStringBoundingBox(line.text, FS_MONO).width);
827 }
828 this->max_length = max_length;
829
830 this->AfterLoadText();
831
832 CheckForMissingGlyphs(true, this);
833
834 /* The font may have changed when searching for glyphs, so ensure widget sizes are updated just in case. */
835 this->ReInit();
836}
837
845std::optional<std::string> GetTextfile(TextfileType type, Subdirectory dir, const std::string &filename)
846{
847 static const char * const prefixes[] = {
848 "readme",
849 "changelog",
850 "license",
851 };
852 static_assert(lengthof(prefixes) == TFT_CONTENT_END);
853
854 /* Only the generic text file types allowed for this function */
855 if (type >= TFT_CONTENT_END) return std::nullopt;
856
857 std::string_view prefix = prefixes[type];
858
859 if (filename.empty()) return std::nullopt;
860
861 auto slash = filename.find_last_of(PATHSEPCHAR);
862 if (slash == std::string::npos) return std::nullopt;
863
864 std::string_view base_path(filename.data(), slash + 1);
865
866 static const std::initializer_list<std::string_view> extensions{
867 "txt",
868 "md",
869#if defined(WITH_ZLIB)
870 "txt.gz",
871 "md.gz",
872#endif
873#if defined(WITH_LIBLZMA)
874 "txt.xz",
875 "md.xz",
876#endif
877 };
878
879 for (auto &extension : extensions) {
880 std::string file_path = fmt::format("{}{}_{}.{}", base_path, prefix, GetCurrentLanguageIsoCode(), extension);
881 if (FioCheckFileExists(file_path, dir)) return file_path;
882
883 file_path = fmt::format("{}{}_{:.2s}.{}", base_path, prefix, GetCurrentLanguageIsoCode(), extension);
884 if (FioCheckFileExists(file_path, dir)) return file_path;
885
886 file_path = fmt::format("{}{}.{}", base_path, prefix, extension);
887 if (FioCheckFileExists(file_path, dir)) return file_path;
888 }
889 return std::nullopt;
890}
Class for backupping variables and making sure they are restored later.
void PutUtf8(char32_t c)
Append UTF.8 char.
The layouter performs all the layout work.
Definition gfx_layout.h:138
ptrdiff_t GetCharAtPosition(int x, size_t line_index) const
Get the character that is at a pixel position in the first line of the layouted text.
Scrollbar data structure.
size_type GetCapacity() const
Gets the number of visible elements of the scrollbar.
void SetCount(size_t num)
Sets the number of elements in the list.
bool SetPosition(size_type position)
Sets the position of the first visible element.
void SetCapacityFromWidget(Window *w, WidgetID widget, int padding=0)
Set capacity of visible elements from the size and resize properties of a widget.
Definition widget.cpp:2523
size_type GetCount() const
Gets the number of elements in the list.
void SetStepSize(size_t stepsize)
Set the distance to scroll when using the buttons or the wheel.
size_type GetPosition() const
Gets the position of the first visible element in the list.
Compose data into a growing std::string.
RectPadding frametext
Padding inside frame with text.
Definition window_gui.h:41
static WidgetDimensions scaled
Widget dimensions scaled for current zoom level.
Definition window_gui.h:29
static const WidgetDimensions unscaled
Unscaled widget dimensions.
Definition window_gui.h:94
Control codes that are embedded in the translation strings.
StringControlCode
List of string control codes used for string formatting, displaying, and by strgen to generate the la...
Functions related to debugging.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
Definition debug.h:37
void ShowDropDownList(Window *w, DropDownList &&list, int selected, WidgetID button, uint width, bool instant_close, bool persist)
Show a drop down list.
Definition dropdown.cpp:407
Functions related to the drop down widget.
Types related to the drop down widget.
std::vector< std::unique_ptr< const DropDownListItem > > DropDownList
A drop down list is a collection of drop down list items.
bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:121
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:242
Functions for Standard In/Out file operations.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
@ NO_DIRECTORY
A path without any base directory.
fluid_settings_t * settings
FluidSynth settings handle.
int GetCharacterHeight(FontSize size)
Get height of a character for a given font size.
Definition fontcache.cpp:77
Functions to read fonts from files and cache them.
int GetStringHeight(std::string_view str, int maxw, FontSize fontsize)
Calculates height of string (in pixels).
Definition gfx.cpp:705
Dimension GetStringBoundingBox(std::string_view str, FontSize start_fontsize)
Return the string dimension in pixels.
Definition gfx.cpp:852
int DrawString(int left, int right, int top, std::string_view str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
Draw string, possibly truncated to make it fit in its allocated space.
Definition gfx.cpp:658
int DrawStringMultiLine(int left, int right, int top, int bottom, std::string_view str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
Draw string, possibly over multiple lines.
Definition gfx.cpp:775
bool FillDrawPixelInfo(DrawPixelInfo *n, int left, int top, int width, int height)
Set up a clipping area for only drawing into a certain area.
Definition gfx.cpp:1519
Functions related to the gfx engine.
Functions related to laying out the texts.
Types related to the graphics and/or input devices.
FontSize
Available font sizes.
Definition gfx_type.h:244
@ FS_MONO
Index of the monospaced font in the font tables.
Definition gfx_type.h:248
@ SA_TOP
Top align the text.
Definition gfx_type.h:382
@ SA_LEFT
Left align the text.
Definition gfx_type.h:377
constexpr NWidgetPart SetFill(uint16_t fill_x, uint16_t fill_y)
Widget part function for setting filling.
constexpr NWidgetPart SetPIP(uint8_t pre, uint8_t inter, uint8_t post)
Widget part function for setting a pre/inter/post spaces.
constexpr NWidgetPart SetScrollbar(WidgetID index)
Attach a scrollbar to a widget.
constexpr NWidgetPart SetStringTip(StringID string, StringID tip={})
Widget part function for setting the string and tooltip.
constexpr NWidgetPart SetMinimalSize(int16_t x, int16_t y)
Widget part function for setting the minimal size.
constexpr NWidgetPart NWidget(WidgetType tp, Colours col, WidgetID idx=-1)
Widget part function for starting a new 'real' widget.
constexpr NWidgetPart SetToolTip(StringID tip)
Widget part function for setting tooltip and clearing the widget data.
constexpr NWidgetPart EndContainer()
Widget part function for denoting the end of a container (horizontal, vertical, WWT_FRAME,...
constexpr NWidgetPart SetArrowWidgetTypeTip(ArrowWidgetValues widget_type, StringID tip={})
Widget part function for setting the arrow widget type and tooltip.
constexpr NWidgetPart SetMinimalTextLines(uint8_t lines, uint8_t spacing, FontSize size=FS_NORMAL)
Widget part function for setting the minimal text lines.
constexpr NWidgetPart SetResize(int16_t dx, int16_t dy)
Widget part function for setting the resize step.
void SetDirty() const
Mark entire window as dirty (in need of re-paint)
Definition window.cpp:945
void GuiShowTooltips(Window *parent, EncodedString &&text, TooltipCloseCondition close_tooltip)
Shows a tooltip.
Definition misc_gui.cpp:688
Types related to the misc widgets.
@ WID_TF_JUMPLIST
List to jump around the file.
Definition misc_widget.h:54
@ WID_TF_BACKGROUND
Panel to draw the textfile on.
Definition misc_widget.h:56
@ WID_TF_NAVBACK
Navigate back button.
Definition misc_widget.h:51
@ WID_TF_WRAPTEXT
Whether or not to wrap the text.
Definition misc_widget.h:53
@ WID_TF_NAVFORWARD
Navigate forward button.
Definition misc_widget.h:52
@ WID_TF_CAPTION
The caption of the window.
Definition misc_widget.h:50
@ WID_TF_SEL_JUMPLIST
Selection to display the jump list or not.
Definition misc_widget.h:55
@ WID_TF_HSCROLLBAR
Horizontal scrollbar to scroll through the textfile left-to-right.
Definition misc_widget.h:58
@ WID_TF_VSCROLLBAR
Vertical scrollbar to scroll through the textfile up-and-down.
Definition misc_widget.h:57
Some generic types.
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:271
static void StrMakeValid(T &dst, const char *str, const char *last, StringValidationSettings settings)
Copies the valid (UTF-8) characters from str up to last to the dst.
Definition string.cpp:123
bool StrEndsWithIgnoreCase(std::string_view str, const std::string_view suffix)
Check whether the given string ends with the given suffix, ignoring case.
Definition string.cpp:342
Compose strings from textual and binary data.
Functions related to low-level strings.
@ ReplaceWithQuestionMark
Replace the unknown/bad bits with question marks.
@ AllowNewline
Allow newlines; replaces '\r ' with ' ' during processing.
@ ReplaceTabCrNlWithSpace
Replace tabs ('\t'), carriage returns ('\r') and newlines (' ') with spaces.
const char * GetCurrentLanguageIsoCode()
Get the ISO language code of the currently loaded language.
Definition strings.cpp:2248
void CheckForMissingGlyphs(bool base_font, MissingGlyphSearcher *searcher)
Check whether the currently loaded language pack uses characters that the currently loaded font does ...
Definition strings.cpp:2352
EncodedString GetEncodedString(StringID str)
Encode a string with no parameters into an encoded string.
Definition strings.cpp:91
std::string GetString(StringID string)
Resolve the given StringID into a std::string with formatting but no parameters.
Definition strings.cpp:424
Class to backup a specific variable and restore it upon destruction of this object to prevent stack v...
Dimensions (a width and height) of a rectangle in 2D.
Data about how and where to blit pixels.
Definition gfx_type.h:158
Settings for the four different fonts.
Definition fontcache.h:200
Partial widget specification to allow NWidgets to be written nested.
Coordinates of a point in 2D.
constexpr uint Horizontal() const
Get total horizontal padding of RectPadding.
constexpr uint Vertical() const
Get total vertical padding of RectPadding.
Specification of a rectangle with absolute coordinates of all edges.
Rect Shrink(int s) const
Copy and shrink Rect by s pixels.
Rect Translate(int x, int y) const
Copy and translate Rect by x,y pixels.
int bottom
Bottom scroll position in visual lines.
int top
Top scroll position in visual lines.
std::string text
Contents of the line.
Scrollbar * hscroll
Horizontal scrollbar.
uint ReflowContent()
Get the total height of the content displayed in this window, if wrapping is disabled.
void OnInvalidateData(int data=0, bool gui_scope=true) override
Some data on this window has become invalid.
void Reset() override
Reset the search, i.e.
uint search_iterator
Iterator for the font check search.
void AppendHistory(const std::string &filepath)
Append the new location to the history, so the user can go back.
void OnDropdownSelect(WidgetID widget, int index) override
A dropdown option associated to this window has been selected.
bool Monospace() override
Whether to search for a monospace font or not.
const Hyperlink * GetHyperlink(Point pt) const
Get the hyperlink at the given position.
void NavigateToFile(std::string newfile, size_t line)
Navigate to the requested file.
std::optional< std::string_view > NextString() override
Get the next string to search through.
std::vector< Line > lines
#text, split into lines in a table with lines.
void LoadText(std::string_view buf)
Load a text into the textfile viewer.
bool OnTooltip(Point pt, WidgetID widget, TooltipCloseCondition close_cond) override
Event to display a custom tooltip.
void OnClick(Point pt, WidgetID widget, int click_count) override
A click with the left mouse button has been made on the window.
void UpdateHistoryScrollpos()
Update the scroll position to the current, so we can restore there if we go back.
virtual void AfterLoadText()
Post-processing after the text is loaded.
std::string filepath
Full path to the filename.
TextfileType file_type
Type of textfile to view.
void AfterLoadMarkdown()
Post-processing of markdown files.
void SetupScrollbars(bool force_reflow)
Set scrollbars to the right lengths.
FontSize DefaultSize() override
Get the default (font) size of the string.
void DrawWidget(const Rect &r, WidgetID widget) const override
Draw the contents of a nested widget.
size_t history_pos
Position in browsing history (for forward movement).
std::vector< Hyperlink > link_anchors
Anchor names of headings that can be linked to.
void SetFontNames(FontCacheSettings *settings, const char *font_name, const void *os_data) override
Set the right font names.
void NavigateHistory(int delta)
Navigate through the history, either forward or backward.
virtual void OnHyperlinkClick(const Hyperlink &link)
Handle the clicking on a hyperlink.
std::vector< HistoryEntry > history
Browsing history in this window.
void UpdateWidgetSize(WidgetID widget, Dimension &size, const Dimension &padding, Dimension &fill, Dimension &resize) override
Update size and resize step of a widget in the window.
std::vector< Hyperlink > links
Clickable links in lines.
uint max_length
Maximum length of unwrapped text line.
std::string filename
Filename of the textfile.
Scrollbar * vscroll
Vertical scrollbar.
virtual void LoadTextfile(const std::string &textfile, Subdirectory dir)
Loads the textfile text from file and setup lines.
void OnResize() override
Called after the window got resized.
std::vector< size_t > jumplist
Table of contents list, line numbers.
void FindHyperlinksInMarkdown(Line &line, size_t line_index)
Find any hyperlinks in a given line.
High level window description.
Definition window_gui.h:168
Data structure for an opened window.
Definition window_gui.h:274
void ReInit(int rx=0, int ry=0, bool reposition=false)
Re-initialize a window, and optionally change its size.
Definition window.cpp:957
void FinishInitNested(WindowNumber window_number=0)
Perform the second part of the initialization of a nested widget tree.
Definition window.cpp:1736
void InvalidateData(int data=0, bool gui_scope=true)
Mark this window's data as invalid (in need of re-computing)
Definition window.cpp:3164
ResizeInfo resize
Resize information.
Definition window_gui.h:315
void DisableWidget(WidgetID widget_index)
Sets a widget to disabled.
Definition window_gui.h:392
void CreateNestedTree()
Perform the first part of the initialization of a nested widget tree.
Definition window.cpp:1726
bool IsWidgetLowered(WidgetID widget_index) const
Gets the lowered state of a widget.
Definition window_gui.h:492
void EnableWidget(WidgetID widget_index)
Sets a widget to Enabled.
Definition window_gui.h:401
int GetRowFromWidget(int clickpos, WidgetID widget, int padding, int line_height=-1) const
Compute the row of a widget that a user clicked in.
Definition window.cpp:212
const Scrollbar * GetScrollbar(WidgetID widnum) const
Return the Scrollbar to a widget index.
Definition window.cpp:313
void SetWidgetDisabledState(WidgetID widget_index, bool disab_stat)
Sets the enabled/disabled status of a widget.
Definition window_gui.h:382
int height
Height of the window (number of pixels down in y direction)
Definition window_gui.h:313
void ToggleWidgetLoweredState(WidgetID widget_index)
Invert the lowered/raised status of a widget.
Definition window_gui.h:451
HyperlinkType
Types of link we support in markdown files.
@ File
Link to a local file.
@ Unknown
Unknown link.
@ Internal
Internal link, or "anchor" in HTML language.
@ Web
Link to an external website.
static constexpr NWidgetPart _nested_textfile_widgets[]
Widgets for the textfile window.
static std::vector< char > Gunzip(std::span< char > input)
Do an in-memory gunzip operation.
static std::vector< char > Xunzip(std::span< char > input)
Do an in-memory xunzip operation.
static WindowDesc _textfile_desc(WDP_CENTER, "textfile", 630, 460, WC_TEXTFILE, WC_NONE, {}, _nested_textfile_widgets)
Window definition for the textfile window.
std::optional< std::string > GetTextfile(TextfileType type, Subdirectory dir, const std::string &filename)
Search a textfile file next to the given content.
static HyperlinkType ClassifyHyperlink(const std::string &destination, bool trusted)
Classify the type of hyperlink the destination describes.
static const std::regex _markdown_link_regex
Regular expression that searches for Markdown links.
static std::string MakeAnchorSlug(const std::string &line)
Create a valid slug for the anchor.
GUI functions related to textfiles.
TextfileType
Additional text files accompanying Tar archives.
Rect ScrollRect(Rect r, const Scrollbar &sb, int resize_step)
Apply 'scroll' to a rect to be drawn in.
Definition widget.cpp:2540
@ WWT_PUSHARROWBTN
Normal push-button (no toggle button) with arrow caption.
@ NWID_SPACER
Invisible widget that takes some space.
Definition widget_type.h:69
@ NWID_HORIZONTAL
Horizontal container.
Definition widget_type.h:65
@ WWT_TEXTBTN
(Toggle) Button with text
Definition widget_type.h:45
@ WWT_PANEL
Simple depressed panel.
Definition widget_type.h:40
@ WWT_CAPTION
Window caption (window title between closebox and stickybox)
Definition widget_type.h:51
@ NWID_VSCROLLBAR
Vertical scrollbar.
Definition widget_type.h:75
@ NWID_VERTICAL
Vertical container.
Definition widget_type.h:67
@ WWT_CLOSEBOX
Close box (at top-left of a window)
Definition widget_type.h:59
@ NWID_HSCROLLBAR
Horizontal scrollbar.
Definition widget_type.h:74
@ WWT_RESIZEBOX
Resize box (normally at bottom-right of a window)
Definition widget_type.h:58
@ WWT_DEFSIZEBOX
Default window size box (at top-right of a window, between WWT_SHADEBOX and WWT_STICKYBOX)
Definition widget_type.h:55
@ WWT_DROPDOWN
Drop down list.
Definition widget_type.h:60
@ NWID_SELECTION
Stacked widgets, only one visible at a time (eg in a panel with tabs).
Definition widget_type.h:70
@ SZSP_HORIZONTAL
Display plane with zero size vertically, and filling and resizing horizontally.
@ AWV_DECREASE
Arrow to the left or in case of RTL to the right.
Definition widget_type.h:21
@ AWV_INCREASE
Arrow to the right or in case of RTL to the left.
Definition widget_type.h:22
@ WDP_CENTER
Center the window.
Definition window_gui.h:146
int WidgetID
Widget ID.
Definition window_type.h:20
@ WC_NONE
No window, redirects to WC_MAIN_WINDOW.
Definition window_type.h:47
@ WC_TEXTFILE
textfile; Window numbers: