OpenTTD Source 20241224-master-gf74b0cf984
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"
17#include "textfile_gui.h"
18#include "dropdown_type.h"
19#include "dropdown_func.h"
20#include "gfx_layout.h"
21#include "debug.h"
22#include "openttd.h"
23
24#include "widgets/misc_widget.h"
25
26#include "table/strings.h"
27#include "table/control_codes.h"
28
29#if defined(WITH_ZLIB)
30#include <zlib.h>
31#endif
32
33#if defined(WITH_LIBLZMA)
34#include <lzma.h>
35#endif
36
37#include <regex>
38
39#include "safeguards.h"
40
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),
52 NWidget(WWT_PANEL, COLOUR_MAUVE),
54 /* As this widget can be toggled, it needs to be a multiplier of FS_MONO. So add a spacer that ensures this. */
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),
73 NWidget(WWT_RESIZEBOX, COLOUR_MAUVE),
75};
76
79 WDP_CENTER, "textfile", 630, 460,
81 0,
83);
84
85TextfileWindow::TextfileWindow(TextfileType file_type) : Window(_textfile_desc), file_type(file_type)
86{
87 /* Init of nested tree is deferred.
88 * TextfileWindow::ConstructWindow must be called by the inheriting window. */
89}
90
91void TextfileWindow::ConstructWindow()
92{
93 this->CreateNestedTree();
98 this->FinishInitNested(this->file_type);
99
102 this->hscroll->SetStepSize(10); // Speed up horizontal scrollbar
103}
104
110{
111 uint height = 0;
113 for (auto &line : this->lines) {
114 line.top = height;
115 height++;
116 line.bottom = height;
117 }
118 } else {
120 for (auto &line : this->lines) {
121 line.top = height;
123 line.bottom = height;
124 }
125 }
126
127 return height;
128}
129
130uint TextfileWindow::GetContentHeight()
131{
132 if (this->lines.empty()) return 0;
133 return this->lines.back().bottom;
134}
135
136/* virtual */ void TextfileWindow::UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize)
137{
138 switch (widget) {
141
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.
144 break;
145 }
146}
147
149void TextfileWindow::SetupScrollbars(bool force_reflow)
150{
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);
156 } else {
157 uint height = force_reflow ? this->ReflowContent() : this->GetContentHeight();
158 this->vscroll->SetCount(ClampTo<uint16_t>(height));
159 this->hscroll->SetCount(this->max_length);
160 }
161
163}
164
165
167static const std::regex _markdown_link_regex{"\\[(.+?)\\]\\((.+?)\\)", std::regex_constants::ECMAScript | std::regex_constants::optimize};
168
170enum class HyperlinkType {
171 Internal,
172 Web,
173 File,
174 Unknown,
175};
176
184static HyperlinkType ClassifyHyperlink(const std::string &destination, bool trusted)
185{
186 if (destination.empty()) return HyperlinkType::Unknown;
187 if (destination.starts_with("#")) return HyperlinkType::Internal;
188
189 /* Only allow external / internal links for sources we trust. */
190 if (!trusted) return HyperlinkType::Unknown;
191
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;
196}
197
204static std::string MakeAnchorSlug(const std::string &line)
205{
206 std::string r = "#";
207 uint state = 0;
208 for (char c : line) {
209 if (state == 0) {
210 /* State 0: Skip leading hashmarks and spaces. */
211 if (c == '#') continue;
212 if (c == ' ') continue;
213 state = 1;
214 }
215 if (state == 2) {
216 /* State 2: Wait for a non-space/dash character.
217 * When found, output a dash and that character. */
218 if (c == ' ' || c == '-') continue;
219 r += '-';
220 state = 1;
221 }
222 if (state == 1) {
223 /* State 1: Normal text.
224 * Lowercase alphanumerics,
225 * spaces and dashes become dashes,
226 * everything else is removed. */
227 if (isalnum(c)) {
228 r += tolower(c);
229 } else if (c == ' ' || c == '-') {
230 state = 2;
231 }
232 }
233 }
234
235 return r;
236}
237
244void TextfileWindow::FindHyperlinksInMarkdown(Line &line, size_t line_index)
245{
246 std::string::const_iterator last_match_end = line.text.cbegin();
247 std::string fixed_line;
248 char ccbuf[5];
249
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;
253
254 Hyperlink link;
255 link.line = line_index;
256 link.destination = match[2].str();
257 this->links.push_back(link);
258
261 switch (link_type) {
263 link_colour = SCC_GREEN;
264 break;
266 link_colour = SCC_LTBLUE;
267 break;
269 link_colour = SCC_LTBROWN;
270 break;
271 default:
272 /* Don't make other link types fancy as they aren't handled (yet). */
273 link_colour = SCC_CONTROL_END;
274 break;
275 }
276
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;
287 }
288
289 /* Find next link. */
290 ++matcher;
291 }
292 if (last_match_end == line.text.cbegin()) return; // nothing found
293
294 /* Add remaining text on line. */
295 fixed_line += std::string(last_match_end, line.text.cend());
296
297 /* Overwrite original line text with "fixed" line text. */
298 line.text = fixed_line;
299}
300
307{
308 if (this->links.empty()) return;
309
310 /* Which line was clicked. */
312 size_t line_index;
313 size_t subline;
315 auto it = std::ranges::find_if(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);
320 } else {
321 line_index = clicked_row / GetCharacterHeight(FS_MONO);
322 subline = 0;
323 }
324
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);
329 }
330 if (found_links.empty()) return;
331
332 /* Build line layout to figure out character position that was clicked. */
334 Layouter layout(this->lines[line_index].text, window_width, FS_MONO);
335 assert(subline < layout.size());
337 if (char_index < 0) return;
338 Debug(misc, 4, "TextfileWindow check hyperlink click: line={}, subline={}, char_index={}", line_index, subline, (int)char_index);
339
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);
346 return;
347 }
348 }
349}
350
356void TextfileWindow::AppendHistory(const std::string &filepath)
357{
358 this->history.erase(this->history.begin() + this->history_pos + 1, this->history.end());
360 this->history.push_back(HistoryEntry{ filepath, 0 });
363 this->history_pos = this->history.size() - 1;
364}
365
373
380{
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;
384
386 this->history_pos += delta;
387
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);
392 }
393
394 this->SetWidgetDisabledState(WID_TF_NAVFORWARD, this->history_pos + 1 >= this->history.size());
396 this->GetScrollbar(WID_TF_VSCROLLBAR)->SetPosition(this->history[this->history_pos].scrollpos);
398 this->SetDirty();
399}
400
401/* virtual */ void TextfileWindow::OnHyperlinkClick(const Hyperlink &link)
402{
403 switch (ClassifyHyperlink(link.destination, this->trusted)) {
405 {
406 auto it = std::ranges::find(this->link_anchors, link.destination, &Hyperlink::destination);
407 if (it != this->link_anchors.cend()) {
408 this->AppendHistory(this->filepath);
409 this->ScrollToLine(it->line);
411 }
412 break;
413 }
414
416 OpenBrowser(link.destination);
417 break;
418
420 this->NavigateToFile(link.destination, 0);
421 break;
422
423 default:
424 /* Do nothing */
425 break;
426 }
427}
428
435void TextfileWindow::NavigateToFile(std::string newfile, size_t line)
436{
437 /* Double-check that the file link begins with ./ as a relative path. */
438 if (!newfile.starts_with("./")) return;
439
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) {
444 newpath.clear();
445 } else {
446 newpath.erase(pos + 1);
447 }
448
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('#');
451 std::string anchor;
452 if (anchor_pos != std::string::npos) {
453 anchor = newfile.substr(anchor_pos);
454 newfile.erase(anchor_pos);
455 }
456
457 /* Now the anchor is gone, check if this is a markdown or textfile. */
458 if (!StrEndsWithIgnoreCase(newfile, ".md") && !StrEndsWithIgnoreCase(newfile, ".txt")) return;
459
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;
465 }
466 }
467
468 /* Paste the two together and check file exists. */
471
472 /* Update history. */
473 this->AppendHistory(newpath);
474
475 /* Load the new file. */
476 this->filepath = newpath;
477 this->filename = newpath.substr(newpath.find_last_of(PATHSEP) + 1);
478
479 this->LoadTextfile(this->filepath, NO_DIRECTORY);
480
483
484 if (anchor.empty() || line != 0) {
485 this->ScrollToLine(line);
486 } else {
487 auto anchor_dest = std::ranges::find(this->link_anchors, anchor, &Hyperlink::destination);
488 if (anchor_dest != this->link_anchors.cend()) {
489 this->ScrollToLine(anchor_dest->line);
491 } else {
492 this->ScrollToLine(0);
493 }
494 }
495}
496
498{
499 this->link_anchors.clear();
500
501 if (StrEndsWithIgnoreCase(this->filename, ".md")) this->AfterLoadMarkdown();
502
503 if (this->GetWidget<NWidgetStacked>(WID_TF_SEL_JUMPLIST)->SetDisplayedPlane(this->jumplist.empty() ? SZSP_HORIZONTAL : 0)) this->ReInit();
504}
505
510{
511 for (size_t line_index = 0; line_index < this->lines.size(); ++line_index) {
512 Line &line = this->lines[line_index];
513
514 /* Find and mark all hyperlinks in the line. */
516
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) });
522 }
523 }
524}
525
526/* virtual */ void TextfileWindow::OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count)
527{
528 switch (widget) {
529 case WID_TF_WRAPTEXT:
531 this->InvalidateData();
532 break;
533
534 case WID_TF_JUMPLIST: {
535 DropDownList list;
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));
539 }
540 ShowDropDownList(this, std::move(list), -1, widget);
541 break;
542 }
543
544 case WID_TF_NAVBACK:
545 this->NavigateHistory(-1);
546 break;
547
549 this->NavigateHistory(+1);
550 break;
551
553 this->CheckHyperlinkClick(pt);
554 break;
555 }
556}
557
558/* virtual */ void TextfileWindow::DrawWidget(const Rect &r, WidgetID widget) const
559{
560 if (widget != WID_TF_BACKGROUND) return;
561
562 Rect fr = r.Shrink(WidgetDimensions::scaled.frametext);
563
565 if (!FillDrawPixelInfo(&new_dpi, fr)) return;
567
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);
571
573
574 int pos = this->vscroll->GetPosition();
575 int cap = this->vscroll->GetCapacity();
576
577 for (auto &line : this->lines) {
578 if (line.bottom < pos) continue;
579 if (line.top > pos + cap) break;
580
581 int y_offset = (line.top - pos) * line_height;
583 DrawStringMultiLine(fr.left, fr.right, y_offset, fr.bottom, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
584 } else {
585 DrawString(fr.left, fr.right, y_offset, line.text, line.colour, SA_TOP | SA_LEFT, false, FS_MONO);
586 }
587 }
588}
589
590/* virtual */ void TextfileWindow::OnResize()
591{
593 this->hscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND, WidgetDimensions::scaled.framerect.Horizontal());
594
595 this->SetupScrollbars(false);
596}
597
598/* virtual */ void TextfileWindow::OnInvalidateData([[maybe_unused]] int data, [[maybe_unused]] bool gui_scope)
599{
600 if (!gui_scope) return;
601
602 this->SetupScrollbars(true);
603}
604
606{
607 if (widget != WID_TF_JUMPLIST) return;
608
609 this->ScrollToLine(index);
610}
611
612void TextfileWindow::ScrollToLine(size_t line)
613{
615 int newpos;
616 if (this->IsWidgetLowered(WID_TF_WRAPTEXT)) {
617 newpos = this->lines[line].top;
618 } else {
619 newpos = static_cast<int>(line);
620 }
621 sb->SetPosition(std::min(newpos, sb->GetCount() - sb->GetCapacity()));
622 this->SetDirty();
623}
624
625/* virtual */ void TextfileWindow::Reset()
626{
627 this->search_iterator = 0;
628}
629
631{
632 return FS_MONO;
633}
634
635/* virtual */ std::optional<std::string_view> TextfileWindow::NextString()
636{
637 if (this->search_iterator >= this->lines.size()) return std::nullopt;
638
639 return this->lines[this->search_iterator++].text;
640}
641
642/* virtual */ bool TextfileWindow::Monospace()
643{
644 return true;
645}
646
647/* virtual */ void TextfileWindow::SetFontNames([[maybe_unused]] FontCacheSettings *settings, [[maybe_unused]] const char *font_name, [[maybe_unused]] const void *os_data)
648{
649#if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
650 settings->mono.font = font_name;
651 settings->mono.os_handle = os_data;
652#endif
653}
654
655#if defined(WITH_ZLIB)
656
665static std::vector<char> Gunzip(std::span<char> input)
666{
667 static const int BLOCKSIZE = 8192;
668 std::vector<char> output;
669
670 z_stream z;
671 memset(&z, 0, sizeof(z));
672 z.next_in = reinterpret_cast<Bytef *>(input.data());
673 z.avail_in = static_cast<uInt>(input.size());
674
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);
685 }
686
687 inflateEnd(&z);
688 if (res != Z_STREAM_END) return {};
689
690 output.resize(output.size() - z.avail_out);
691 return output;
692}
693#endif
694
695#if defined(WITH_LIBLZMA)
696
705static std::vector<char> Xunzip(std::span<char> input)
706{
707 static const int BLOCKSIZE = 8192;
708 std::vector<char> output;
709
710 lzma_stream z = LZMA_STREAM_INIT;
711 z.next_in = reinterpret_cast<uint8_t *>(input.data());
712 z.avail_in = input.size();
713
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);
723 }
724
725 lzma_end(&z);
726 if (res != LZMA_STREAM_END) return {};
727
728 output.resize(output.size() - z.avail_out);
729 return output;
730}
731#endif
732
733
737/* virtual */ void TextfileWindow::LoadTextfile(const std::string &textfile, Subdirectory dir)
738{
739 this->lines.clear();
740 this->jumplist.clear();
741
742 if (this->GetWidget<NWidgetStacked>(WID_TF_SEL_JUMPLIST)->SetDisplayedPlane(SZSP_HORIZONTAL)) this->ReInit();
743
744 if (textfile.empty()) return;
745
746 /* Get text from file */
747 size_t filesize;
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;
752
753 std::vector<char> buf;
754 buf.resize(filesize);
755 size_t read = fread(buf.data(), 1, buf.size(), *handle);
756
757 if (read != buf.size()) return;
758
759#if defined(WITH_ZLIB)
760 /* In-place gunzip */
761 if (textfile.ends_with(".gz")) buf = Gunzip(buf);
762#endif
763
764#if defined(WITH_LIBLZMA)
765 /* In-place xunzip */
766 if (textfile.ends_with(".xz")) buf = Xunzip(buf);
767#endif
768
769 if (buf.empty()) return;
770
771 std::string_view sv_buf(buf.data(), buf.size());
772
773 /* Check for the byte-order-mark, and skip it if needed. */
774 if (sv_buf.starts_with("\ufeff")) sv_buf.remove_prefix(3);
775
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 });
781
782 /* Process the loaded text into lines, and do any further parsing needed. */
783 this->LoadText(sv_buf);
784}
785
793void TextfileWindow::LoadText(std::string_view buf)
794{
796 this->lines.clear();
797
798 /* Split the string on newlines. */
799 std::string_view p(text);
800 int row = 0;
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);
805
806 row++;
807 next = p.find_first_of('\n');
808 }
809 this->lines.emplace_back(row, p);
810
811 /* Calculate maximum text line length. */
812 uint max_length = 0;
813 for (auto &line : this->lines) {
814 max_length = std::max(max_length, GetStringBoundingBox(line.text, FS_MONO).width);
815 }
816 this->max_length = max_length;
817
818 this->AfterLoadText();
819
820 CheckForMissingGlyphs(true, this);
821
822 /* The font may have changed when searching for glyphs, so ensure widget sizes are updated just in case. */
823 this->ReInit();
824}
825
833std::optional<std::string> GetTextfile(TextfileType type, Subdirectory dir, const std::string &filename)
834{
835 static const char * const prefixes[] = {
836 "readme",
837 "changelog",
838 "license",
839 };
840 static_assert(lengthof(prefixes) == TFT_CONTENT_END);
841
842 /* Only the generic text file types allowed for this function */
843 if (type >= TFT_CONTENT_END) return std::nullopt;
844
845 std::string_view prefix = prefixes[type];
846
847 if (filename.empty()) return std::nullopt;
848
849 auto slash = filename.find_last_of(PATHSEPCHAR);
850 if (slash == std::string::npos) return std::nullopt;
851
852 std::string_view base_path(filename.data(), slash + 1);
853
854 static const std::initializer_list<std::string_view> extensions{
855 "txt",
856 "md",
857#if defined(WITH_ZLIB)
858 "txt.gz",
859 "md.gz",
860#endif
861#if defined(WITH_LIBLZMA)
862 "txt.xz",
863 "md.xz",
864#endif
865 };
866
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;
870
871 file_path = fmt::format("{}{}_{:.2s}.{}", base_path, prefix, GetCurrentLanguageIsoCode(), extension);
872 if (FioCheckFileExists(file_path, dir)) return file_path;
873
874 file_path = fmt::format("{}{}.{}", base_path, prefix, extension);
875 if (FioCheckFileExists(file_path, dir)) return file_path;
876 }
877 return std::nullopt;
878}
Class for backupping variables and making sure they are restored later.
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:2451
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.
RectPadding frametext
Padding inside frame with text.
Definition window_gui.h:43
static WidgetDimensions scaled
Widget dimensions scaled for current zoom level.
Definition window_gui.h:28
static const WidgetDimensions unscaled
Unscaled widget dimensions.
Definition window_gui.h:96
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,...)
Ouptut 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:404
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:704
Dimension GetStringBoundingBox(std::string_view str, FontSize start_fontsize)
Return the string dimension in pixels.
Definition gfx.cpp:851
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:657
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:774
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:1548
Functions related to the gfx engine.
Functions related to laying out the texts.
Types related to the graphics and/or input devices.
@ SA_TOP
Top align the text.
Definition gfx_type.h:348
@ SA_LEFT
Left align the text.
Definition gfx_type.h:343
FontSize
Available font sizes.
Definition gfx_type.h:208
@ FS_MONO
Index of the monospaced font in the font tables.
Definition gfx_type.h:212
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 SetDataTip(uint32_t data, StringID tip)
Widget part function for setting the data 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 EndContainer()
Widget part function for denoting the end of a container (horizontal, vertical, WWT_FRAME,...
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:940
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:280
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:107
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:321
size_t Utf8Encode(T buf, char32_t c)
Encode a unicode character and place it in the buffer.
Definition string.cpp:460
Functions related to low-level strings.
@ SVS_REPLACE_TAB_CR_NL_WITH_SPACE
Replace tabs ('\t'), carriage returns ('\r') and newlines (' ') with spaces.
Definition string_type.h:54
@ SVS_ALLOW_NEWLINE
Allow newlines; replaces '\r ' with ' ' during processing.
Definition string_type.h:47
@ SVS_REPLACE_WITH_QUESTION_MARK
Replace the unknown/bad bits with question marks.
Definition string_type.h:46
const char * GetCurrentLanguageIsoCode()
Get the ISO language code of the currently loaded language.
Definition strings.cpp:2183
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:2291
void SetDParamStr(size_t n, const char *str)
This function is used to "bind" a C string to a OpenTTD dparam slot.
Definition strings.cpp:371
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:157
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.
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 CheckHyperlinkClick(Point pt)
Check if the user clicked on a hyperlink, and handle it if so.
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.
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.
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:159
Data structure for an opened window.
Definition window_gui.h:273
void ReInit(int rx=0, int ry=0, bool reposition=false)
Re-initialize a window, and optionally change its size.
Definition window.cpp:952
void FinishInitNested(WindowNumber window_number=0)
Perform the second part of the initialization of a nested widget tree.
Definition window.cpp:1733
void InvalidateData(int data=0, bool gui_scope=true)
Mark this window's data as invalid (in need of re-computing)
Definition window.cpp:3159
ResizeInfo resize
Resize information.
Definition window_gui.h:314
void DisableWidget(WidgetID widget_index)
Sets a widget to disabled.
Definition window_gui.h:397
void CreateNestedTree()
Perform the first part of the initialization of a nested widget tree.
Definition window.cpp:1723
bool IsWidgetLowered(WidgetID widget_index) const
Gets the lowered state of a widget.
Definition window_gui.h:497
void EnableWidget(WidgetID widget_index)
Sets a widget to Enabled.
Definition window_gui.h:406
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:213
const NWID * GetWidget(WidgetID widnum) const
Get the nested widget with number widnum from the nested widget tree.
Definition window_gui.h:977
const Scrollbar * GetScrollbar(WidgetID widnum) const
Return the Scrollbar to a widget index.
Definition window.cpp:314
void SetWidgetDisabledState(WidgetID widget_index, bool disab_stat)
Sets the enabled/disabled status of a widget.
Definition window_gui.h:387
int height
Height of the window (number of pixels down in y direction)
Definition window_gui.h:312
void ToggleWidgetLoweredState(WidgetID widget_index)
Invert the lowered/raised status of a widget.
Definition window_gui.h:456
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, 0, _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.
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.
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:2468
@ 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:31
@ AWV_INCREASE
Arrow to the right or in case of RTL to the left.
Definition widget_type.h:32
@ WWT_PUSHARROWBTN
Normal push-button (no toggle button) with arrow caption.
@ NWID_SPACER
Invisible widget that takes some space.
Definition widget_type.h:79
@ NWID_HORIZONTAL
Horizontal container.
Definition widget_type.h:75
@ WWT_TEXTBTN
(Toggle) Button with text
Definition widget_type.h:55
@ WWT_PANEL
Simple depressed panel.
Definition widget_type.h:50
@ WWT_CAPTION
Window caption (window title between closebox and stickybox)
Definition widget_type.h:61
@ NWID_VSCROLLBAR
Vertical scrollbar.
Definition widget_type.h:85
@ NWID_VERTICAL
Vertical container.
Definition widget_type.h:77
@ WWT_CLOSEBOX
Close box (at top-left of a window)
Definition widget_type.h:69
@ NWID_HSCROLLBAR
Horizontal scrollbar.
Definition widget_type.h:84
@ WWT_RESIZEBOX
Resize box (normally at bottom-right of a window)
Definition widget_type.h:68
@ WWT_DEFSIZEBOX
Default window size box (at top-right of a window, between WWT_SHADEBOX and WWT_STICKYBOX)
Definition widget_type.h:65
@ WWT_DROPDOWN
Drop down list.
Definition widget_type.h:70
@ NWID_SELECTION
Stacked widgets, only one visible at a time (eg in a panel with tabs).
Definition widget_type.h:80
@ WDP_CENTER
Center the window.
Definition window_gui.h:148
int WidgetID
Widget ID.
Definition window_type.h:18
@ WC_NONE
No window, redirects to WC_MAIN_WINDOW.
Definition window_type.h:45
@ WC_TEXTFILE
textfile; Window numbers: