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 "fileio_func.h"
12 #include "fontcache.h"
15 #include "string_func.h"
16 #include "textfile_gui.h"
18 #include "widgets/misc_widget.h"
20 #include "table/strings.h"
22 #if defined(WITH_ZLIB)
26 #if defined(WITH_LIBLZMA)
30 #include "safeguards.h"
32 /** Widgets for the textfile window. */
33 static const NWidgetPart _nested_textfile_widgets
[] = {
34 NWidget(NWID_HORIZONTAL
),
35 NWidget(WWT_CLOSEBOX
, COLOUR_MAUVE
),
36 NWidget(WWT_CAPTION
, COLOUR_MAUVE
, WID_TF_CAPTION
), SetDataTip(STR_NULL
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
37 NWidget(WWT_TEXTBTN
, COLOUR_MAUVE
, WID_TF_WRAPTEXT
), SetDataTip(STR_TEXTFILE_WRAP_TEXT
, STR_TEXTFILE_WRAP_TEXT_TOOLTIP
),
38 NWidget(WWT_DEFSIZEBOX
, COLOUR_MAUVE
),
40 NWidget(NWID_HORIZONTAL
),
41 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_TF_BACKGROUND
), SetMinimalSize(200, 125), SetResize(1, 12), SetScrollbar(WID_TF_VSCROLLBAR
),
43 NWidget(NWID_VERTICAL
),
44 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_TF_VSCROLLBAR
),
47 NWidget(NWID_HORIZONTAL
),
48 NWidget(NWID_HSCROLLBAR
, COLOUR_MAUVE
, WID_TF_HSCROLLBAR
),
49 NWidget(WWT_RESIZEBOX
, COLOUR_MAUVE
),
53 /** Window definition for the textfile window */
54 static WindowDesc
_textfile_desc(
55 WDP_CENTER
, "textfile", 630, 460,
58 _nested_textfile_widgets
, lengthof(_nested_textfile_widgets
)
61 TextfileWindow::TextfileWindow(TextfileType file_type
) : Window(&_textfile_desc
), file_type(file_type
)
63 this->CreateNestedTree();
64 this->vscroll
= this->GetScrollbar(WID_TF_VSCROLLBAR
);
65 this->hscroll
= this->GetScrollbar(WID_TF_HSCROLLBAR
);
66 this->FinishInitNested(file_type
);
67 this->GetWidget
<NWidgetCore
>(WID_TF_CAPTION
)->SetDataTip(STR_TEXTFILE_README_CAPTION
+ file_type
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
);
69 this->hscroll
->SetStepSize(10); // Speed up horizontal scrollbar
72 /* virtual */ TextfileWindow::~TextfileWindow()
78 * Get the total height of the content displayed in this window, if wrapping is disabled.
79 * @return the height in pixels
81 uint
TextfileWindow::ReflowContent()
84 if (!IsWidgetLowered(WID_TF_WRAPTEXT
)) {
85 for (auto &line
: this->lines
) {
91 int max_width
= this->GetWidget
<NWidgetCore
>(WID_TF_BACKGROUND
)->current_x
- WD_FRAMETEXT_LEFT
- WD_FRAMERECT_RIGHT
;
92 for (auto &line
: this->lines
) {
94 height
+= GetStringHeight(line
.text
, max_width
, FS_MONO
) / FONT_HEIGHT_MONO
;
102 uint
TextfileWindow::GetContentHeight()
104 if (this->lines
.size() == 0) return 0;
105 return this->lines
.back().bottom
;
108 /* virtual */ void TextfileWindow::UpdateWidgetSize(int widget
, Dimension
*size
, const Dimension
&padding
, Dimension
*fill
, Dimension
*resize
)
111 case WID_TF_BACKGROUND
:
112 resize
->height
= FONT_HEIGHT_MONO
;
114 size
->height
= 4 * resize
->height
+ TOP_SPACING
+ BOTTOM_SPACING
; // At least 4 lines are visible.
115 size
->width
= std::max(200u, size
->width
); // At least 200 pixels wide.
120 /** Set scrollbars to the right lengths. */
121 void TextfileWindow::SetupScrollbars(bool force_reflow
)
123 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
124 /* Reflow is mandatory if text wrapping is on */
125 uint height
= this->ReflowContent();
126 this->vscroll
->SetCount(std::min
<uint
>(UINT16_MAX
, height
));
127 this->hscroll
->SetCount(0);
129 uint height
= force_reflow
? this->ReflowContent() : this->GetContentHeight();
130 this->vscroll
->SetCount(std::min
<uint
>(UINT16_MAX
, height
));
131 this->hscroll
->SetCount(this->max_length
+ WD_FRAMETEXT_LEFT
+ WD_FRAMETEXT_RIGHT
);
134 this->SetWidgetDisabledState(WID_TF_HSCROLLBAR
, IsWidgetLowered(WID_TF_WRAPTEXT
));
137 /* virtual */ void TextfileWindow::OnClick(Point pt
, int widget
, int click_count
)
140 case WID_TF_WRAPTEXT
:
141 this->ToggleWidgetLoweredState(WID_TF_WRAPTEXT
);
142 this->InvalidateData();
147 /* virtual */ void TextfileWindow::DrawWidget(const Rect
&r
, int widget
) const
149 if (widget
!= WID_TF_BACKGROUND
) return;
151 const int x
= r
.left
+ WD_FRAMETEXT_LEFT
;
152 const int y
= r
.top
+ WD_FRAMETEXT_TOP
;
153 const int right
= r
.right
- WD_FRAMETEXT_RIGHT
;
154 const int bottom
= r
.bottom
- WD_FRAMETEXT_BOTTOM
;
156 DrawPixelInfo new_dpi
;
157 if (!FillDrawPixelInfo(&new_dpi
, x
, y
, right
- x
+ 1, bottom
- y
+ 1)) return;
158 DrawPixelInfo
*old_dpi
= _cur_dpi
;
161 /* Draw content (now coordinates given to DrawString* are local to the new clipping region). */
162 int line_height
= FONT_HEIGHT_MONO
;
163 int pos
= this->vscroll
->GetPosition();
164 int cap
= this->vscroll
->GetCapacity();
166 for (auto &line
: this->lines
) {
167 if (line
.bottom
< pos
) continue;
168 if (line
.top
> pos
+ cap
) break;
170 int y_offset
= (line
.top
- pos
) * line_height
;
171 if (IsWidgetLowered(WID_TF_WRAPTEXT
)) {
172 DrawStringMultiLine(0, right
- x
, y_offset
, bottom
- y
, line
.text
, TC_WHITE
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
174 DrawString(-this->hscroll
->GetPosition(), right
- x
, y_offset
, line
.text
, TC_WHITE
, SA_TOP
| SA_LEFT
, false, FS_MONO
);
181 /* virtual */ void TextfileWindow::OnResize()
183 this->vscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
, TOP_SPACING
+ BOTTOM_SPACING
);
184 this->hscroll
->SetCapacityFromWidget(this, WID_TF_BACKGROUND
);
186 this->SetupScrollbars(false);
189 /* virtual */ void TextfileWindow::OnInvalidateData(int data
, bool gui_scope
)
191 if (!gui_scope
) return;
193 this->SetupScrollbars(true);
196 /* virtual */ void TextfileWindow::Reset()
198 this->search_iterator
= 0;
201 /* virtual */ FontSize
TextfileWindow::DefaultSize()
206 /* virtual */ const char *TextfileWindow::NextString()
208 if (this->search_iterator
>= this->lines
.size()) return nullptr;
210 return this->lines
[this->search_iterator
++].text
;
213 /* virtual */ bool TextfileWindow::Monospace()
218 /* virtual */ void TextfileWindow::SetFontNames(FreeTypeSettings
*settings
, const char *font_name
, const void *os_data
)
220 #if defined(WITH_FREETYPE) || defined(_WIN32) || defined(WITH_COCOA)
221 settings
->mono
.font
= font_name
;
222 settings
->mono
.os_handle
= os_data
;
226 #if defined(WITH_ZLIB)
229 * Do an in-memory gunzip operation. This works on a raw deflate stream,
230 * or a file with gzip or zlib header.
231 * @param bufp A pointer to a buffer containing the input data. This
232 * buffer will be freed and replaced by a buffer containing
233 * the uncompressed data.
234 * @param sizep A pointer to the buffer size. Before the call, the value
235 * pointed to should contain the size of the input buffer.
236 * After the call, it contains the size of the uncompressed
239 * When decompressing fails, *bufp is set to nullptr and *sizep to 0. The
240 * compressed buffer passed in is still freed in this case.
242 static void Gunzip(byte
**bufp
, size_t *sizep
)
244 static const int BLOCKSIZE
= 8192;
246 size_t alloc_size
= 0;
250 memset(&z
, 0, sizeof(z
));
252 z
.avail_in
= (uInt
)*sizep
;
254 /* window size = 15, add 32 to enable gzip or zlib header processing */
255 res
= inflateInit2(&z
, 15 + 32);
256 /* Z_BUF_ERROR just means we need more space */
257 while (res
== Z_OK
|| (res
== Z_BUF_ERROR
&& z
.avail_out
== 0)) {
258 /* When we get here, we're either just starting, or
259 * inflate is out of output space - allocate more */
260 alloc_size
+= BLOCKSIZE
;
261 z
.avail_out
+= BLOCKSIZE
;
262 buf
= ReallocT(buf
, alloc_size
);
263 z
.next_out
= buf
+ alloc_size
- z
.avail_out
;
264 res
= inflate(&z
, Z_FINISH
);
270 if (res
== Z_STREAM_END
) {
272 *sizep
= alloc_size
- z
.avail_out
;
274 /* Something went wrong */
282 #if defined(WITH_LIBLZMA)
285 * Do an in-memory xunzip operation. This works on a .xz or (legacy)
287 * @param bufp A pointer to a buffer containing the input data. This
288 * buffer will be freed and replaced by a buffer containing
289 * the uncompressed data.
290 * @param sizep A pointer to the buffer size. Before the call, the value
291 * pointed to should contain the size of the input buffer.
292 * After the call, it contains the size of the uncompressed
295 * When decompressing fails, *bufp is set to nullptr and *sizep to 0. The
296 * compressed buffer passed in is still freed in this case.
298 static void Xunzip(byte
**bufp
, size_t *sizep
)
300 static const int BLOCKSIZE
= 8192;
302 size_t alloc_size
= 0;
303 lzma_stream z
= LZMA_STREAM_INIT
;
309 res
= lzma_auto_decoder(&z
, UINT64_MAX
, LZMA_CONCATENATED
);
310 /* Z_BUF_ERROR just means we need more space */
311 while (res
== LZMA_OK
|| (res
== LZMA_BUF_ERROR
&& z
.avail_out
== 0)) {
312 /* When we get here, we're either just starting, or
313 * inflate is out of output space - allocate more */
314 alloc_size
+= BLOCKSIZE
;
315 z
.avail_out
+= BLOCKSIZE
;
316 buf
= ReallocT(buf
, alloc_size
);
317 z
.next_out
= buf
+ alloc_size
- z
.avail_out
;
318 res
= lzma_code(&z
, LZMA_FINISH
);
324 if (res
== LZMA_STREAM_END
) {
326 *sizep
= alloc_size
- z
.avail_out
;
328 /* Something went wrong */
338 * Loads the textfile text from file and setup #lines.
340 /* virtual */ void TextfileWindow::LoadTextfile(const char *textfile
, Subdirectory dir
)
342 if (textfile
== nullptr) return;
346 /* Get text from file */
348 FILE *handle
= FioFOpenFile(textfile
, "rb", dir
, &filesize
);
349 if (handle
== nullptr) return;
351 this->text
= ReallocT(this->text
, filesize
);
352 size_t read
= fread(this->text
, 1, filesize
, handle
);
355 if (read
!= filesize
) return;
357 #if defined(WITH_ZLIB) || defined(WITH_LIBLZMA)
358 const char *suffix
= strrchr(textfile
, '.');
359 if (suffix
== nullptr) return;
362 #if defined(WITH_ZLIB)
363 /* In-place gunzip */
364 if (strcmp(suffix
, ".gz") == 0) Gunzip((byte
**)&this->text
, &filesize
);
367 #if defined(WITH_LIBLZMA)
368 /* In-place xunzip */
369 if (strcmp(suffix
, ".xz") == 0) Xunzip((byte
**)&this->text
, &filesize
);
372 if (!this->text
) return;
374 /* Add space for trailing \0 */
375 this->text
= ReallocT(this->text
, filesize
+ 1);
376 this->text
[filesize
] = '\0';
378 /* Replace tabs and line feeds with a space since StrMakeValidInPlace removes those. */
379 for (char *p
= this->text
; *p
!= '\0'; p
++) {
380 if (*p
== '\t' || *p
== '\r') *p
= ' ';
383 /* Check for the byte-order-mark, and skip it if needed. */
384 char *p
= this->text
+ (strncmp(u8
"\ufeff", this->text
, 3) == 0 ? 3 : 0);
386 /* Make sure the string is a valid UTF-8 sequence. */
387 StrMakeValidInPlace(p
, this->text
+ filesize
, SVS_REPLACE_WITH_QUESTION_MARK
| SVS_ALLOW_NEWLINE
);
389 /* Split the string on newlines. */
391 this->lines
.emplace_back(row
, p
);
392 for (; *p
!= '\0'; p
++) {
395 this->lines
.emplace_back(++row
, p
+ 1);
399 /* Calculate maximum text line length. */
401 for (auto &line
: this->lines
) {
402 max_length
= std::max(max_length
, GetStringBoundingBox(line
.text
, FS_MONO
).width
);
404 this->max_length
= max_length
;
406 CheckForMissingGlyphs(true, this);
410 * Search a textfile file next to the given content.
411 * @param type The type of the textfile to search for.
412 * @param dir The subdirectory to search in.
413 * @param filename The filename of the content to look for.
414 * @return The path to the textfile, \c nullptr otherwise.
416 const char *GetTextfile(TextfileType type
, Subdirectory dir
, const char *filename
)
418 static const char * const prefixes
[] = {
423 static_assert(lengthof(prefixes
) == TFT_END
);
425 const char *prefix
= prefixes
[type
];
427 if (filename
== nullptr) return nullptr;
429 static char file_path
[MAX_PATH
];
430 strecpy(file_path
, filename
, lastof(file_path
));
432 char *slash
= strrchr(file_path
, PATHSEPCHAR
);
433 if (slash
== nullptr) return nullptr;
435 static const char * const exts
[] = {
437 #if defined(WITH_ZLIB)
440 #if defined(WITH_LIBLZMA)
445 for (size_t i
= 0; i
< lengthof(exts
); i
++) {
446 seprintf(slash
+ 1, lastof(file_path
), "%s_%s.%s", prefix
, GetCurrentLanguageIsoCode(), exts
[i
]);
447 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
449 seprintf(slash
+ 1, lastof(file_path
), "%s_%.2s.%s", prefix
, GetCurrentLanguageIsoCode(), exts
[i
]);
450 if (FioCheckFileExists(file_path
, dir
)) return file_path
;
452 seprintf(slash
+ 1, lastof(file_path
), "%s.%s", prefix
, exts
[i
]);
453 if (FioCheckFileExists(file_path
, dir
)) return file_path
;