Translations update
[openttd/fttd.git] / src / textfile.cpp
blob7e0441024a0c5598e1db77c6848b29dcd3c22219
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 */
8 /** @file textfile.cpp Code related to textfiles. */
10 #include "stdafx.h"
11 #include "core/math_func.hpp"
12 #include "fileio_func.h"
13 #include "font.h"
14 #include "gfx_type.h"
15 #include "gfx_func.h"
16 #include "string.h"
17 #include "textfile.h"
19 #include "widgets/misc_widget.h"
21 #include "table/strings.h"
23 #if defined(WITH_ZLIB)
24 #include <zlib.h>
25 #endif
27 #if defined(WITH_LZMA)
28 #include <lzma.h>
29 #endif
31 /** Widgets for the textfile window. */
32 static const NWidgetPart _nested_textfile_widgets[] = {
33 NWidget(NWID_HORIZONTAL),
34 NWidget(WWT_CLOSEBOX, COLOUR_MAUVE),
35 NWidget(WWT_CAPTION, COLOUR_MAUVE, WID_TF_CAPTION), SetDataTip(STR_NULL, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
36 NWidget(WWT_TEXTBTN, COLOUR_MAUVE, WID_TF_WRAPTEXT), SetDataTip(STR_TEXTFILE_WRAP_TEXT, STR_TEXTFILE_WRAP_TEXT_TOOLTIP),
37 NWidget(WWT_DEFSIZEBOX, COLOUR_MAUVE),
38 EndContainer(),
39 NWidget(NWID_HORIZONTAL),
40 NWidget(WWT_PANEL, COLOUR_MAUVE, WID_TF_BACKGROUND), SetMinimalSize(200, 125), SetResize(1, 12), SetScrollbar(WID_TF_VSCROLLBAR),
41 EndContainer(),
42 NWidget(NWID_VERTICAL),
43 NWidget(NWID_VSCROLLBAR, COLOUR_MAUVE, WID_TF_VSCROLLBAR),
44 EndContainer(),
45 EndContainer(),
46 NWidget(NWID_HORIZONTAL),
47 NWidget(NWID_HSCROLLBAR, COLOUR_MAUVE, WID_TF_HSCROLLBAR),
48 NWidget(WWT_RESIZEBOX, COLOUR_MAUVE),
49 EndContainer(),
52 /** Window preferences for the textfile window */
53 static WindowDesc::Prefs _textfile_prefs ("textfile");
55 /** Window definition for the textfile window */
56 static const WindowDesc _textfile_desc(
57 WDP_CENTER, 630, 460,
58 WC_TEXTFILE, WC_NONE,
60 _nested_textfile_widgets, lengthof(_nested_textfile_widgets),
61 &_textfile_prefs
65 /** Stream template struct. */
66 template <TextfileDesc::Format FMT>
67 struct stream;
69 /** Stream loop function results. */
70 enum StreamResult {
71 STREAM_OK, ///< success, decoding in progress
72 STREAM_END, ///< success, decoding finished
73 STREAM_ERROR, ///< error
76 #ifdef WITH_ZLIB
78 /** Zlib stream struct. */
79 template <>
80 struct stream <TextfileDesc::FORMAT_GZ> {
81 typedef z_stream stream_type;
83 static inline void construct (z_stream *z)
85 memset (z, 0, sizeof(z_stream));
88 static inline bool init (z_stream *z)
90 /* Window size is 15, plus flag 32 for automatic header detection. */
91 return inflateInit2 (z, 15 + 32) == Z_OK;
94 static inline StreamResult loop (z_stream *z, bool finish)
96 switch (inflate (z, finish ? Z_FINISH : Z_NO_FLUSH)) {
97 case Z_OK:
98 return STREAM_OK;
100 case Z_STREAM_END:
101 return STREAM_END;
103 case Z_BUF_ERROR:
104 if (z->avail_out == 0) return STREAM_OK;
105 /* fall through */
106 default:
107 return STREAM_ERROR;
111 static inline void end (z_stream *z)
113 int r = inflateEnd (z);
114 assert (r == Z_OK);
118 #endif /* WITH_ZLIB */
120 #ifdef WITH_LZMA
122 /** LZMA stream struct. */
123 template <>
124 struct stream <TextfileDesc::FORMAT_XZ> {
125 typedef lzma_stream stream_type;
127 static inline void construct (lzma_stream *z)
129 memset (z, 0, sizeof(lzma_stream));
132 static inline bool init (lzma_stream *z)
134 return lzma_auto_decoder (z, UINT64_MAX, LZMA_CONCATENATED) == LZMA_OK;
137 static inline StreamResult loop (lzma_stream *z, bool finish)
139 switch (lzma_code (z, finish ? LZMA_FINISH : LZMA_RUN)) {
140 case LZMA_OK: return STREAM_OK;
141 case LZMA_STREAM_END: return STREAM_END;
142 default: return STREAM_ERROR;
146 static inline void end (lzma_stream *z)
148 lzma_end (z);
152 #endif /* WITH_LZMA */
155 * Read in data from file and update stream.
156 * @param input Buffer to read data into.
157 * @param handle Handle of file to read from.
158 * @param remaining Remaining size of file to read, updated on return.
159 * @param z Stream to update.
160 * @return Whether (some) data could be read.
162 template <uint N, typename Z>
163 static bool fill_buffer (byte (*input) [N], FILE *handle, size_t *remaining, Z *z)
165 assert (z->avail_in == 0);
167 size_t read = fread (input, 1, min (*remaining, N), handle);
168 if (read == 0) return false;
170 *remaining -= read;
171 z->avail_in = read;
172 z->next_in = &(*input)[0];
173 return true;
177 * Read in a compressed file.
178 * @tparam FMT Stream format type.
179 * @param handle Handle of file to read.
180 * @param filesize Size of file to read.
181 * @param len Pointer to store decompressed length on success, excluding
182 * appended null char.
183 * @return Newly allocated storage with the decompressed contents of the file.
185 template <TextfileDesc::Format FMT>
186 static char *stream_unzip (FILE *handle, size_t filesize, size_t *len)
188 static const uint BLOCKSIZE = 4096;
189 byte input [1024];
191 /* Initialise stream. */
192 typename stream<FMT>::stream_type z;
193 stream<FMT>::construct (&z);
194 if (!fill_buffer (&input, handle, &filesize, &z)) return NULL;
195 if (!stream<FMT>::init (&z)) return NULL;
197 /* Assume output will be at least as big as input, and align up. */
198 size_t alloc = Align (filesize + z.avail_in, BLOCKSIZE);
199 char *output = xmalloc (alloc);
200 z.next_out = (byte*)output;
201 z.avail_out = alloc;
202 assert (z.avail_out > 0);
204 /* Decode loop. */
205 int r;
206 for (;;) {
207 if (z.avail_in == 0 && filesize != 0
208 && !fill_buffer (&input, handle, &filesize, &z)) {
209 r = STREAM_ERROR;
210 break;
213 assert (z.avail_out != 0);
214 r = stream<FMT>::loop (&z, filesize == 0);
215 if (r != STREAM_OK) break;
217 if (z.avail_out == 0) {
218 assert (z.next_out == (byte*)(output + alloc));
219 size_t new_alloc = alloc +
220 Align (filesize + z.avail_in, BLOCKSIZE);
221 output = xrealloc (output, new_alloc);
222 z.next_out = (byte*)(output + alloc);
223 alloc = new_alloc;
224 z.avail_out = BLOCKSIZE;
228 /* Finish decoding. */
229 stream<FMT>::end (&z);
231 if (r != STREAM_END) {
232 free (output);
233 return NULL;
236 /* Compute total size. */
237 size_t total = alloc - z.avail_out;
238 if (total == 0) {
239 /* No output? */
240 free (output);
241 return NULL;
243 *len = total;
245 /* Append null terminator if required. */
246 if (z.avail_out == 0) {
247 if (output[total - 1] == '\n') {
248 total--;
249 } else {
250 output = xrealloc (output, alloc + 1);
253 output[total] = '\0';
254 return output;
258 * Read in the text file represented by this description.
259 * @param len pointer to store file length on success, excluding appended 0
260 * @return pointer to newly allocated storage with the contents of the file on success, or NULL on error
262 char *TextfileDesc::read (size_t *len) const
264 size_t filesize;
265 FILE *handle = FioFOpenFile (this->path.get(), "rb", this->dir, &filesize);
266 if (handle == NULL) return NULL;
268 switch (this->format) {
269 default: NOT_REACHED();
271 case FORMAT_RAW: {
272 char *text = xmalloc (filesize + 1);
273 size_t read = fread (text, 1, filesize, handle);
274 fclose (handle);
275 if (read != filesize) {
276 free (text);
277 return NULL;
279 text[filesize] = '\0';
280 *len = filesize;
281 return text;
284 #if defined(WITH_ZLIB)
285 case FORMAT_GZ: {
286 char *text = stream_unzip<FORMAT_GZ> (handle, filesize, len);
287 fclose (handle);
288 return text;
290 #endif
292 #if defined(WITH_LZMA)
293 case FORMAT_XZ: {
294 char *text = stream_unzip<FORMAT_XZ> (handle, filesize, len);
295 fclose (handle);
296 return text;
298 #endif
302 TextfileWindow::TextfileWindow (const TextfileDesc &txt) :
303 Window (&_textfile_desc), file_type (txt.type),
304 vscroll (NULL), hscroll (NULL), text (NULL), lines ()
306 this->CreateNestedTree();
307 this->vscroll = this->GetScrollbar(WID_TF_VSCROLLBAR);
308 this->hscroll = this->GetScrollbar(WID_TF_HSCROLLBAR);
309 this->InitNested();
310 this->GetWidget<NWidgetCore>(WID_TF_CAPTION)->SetDataTip(STR_TEXTFILE_README_CAPTION + txt.type, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
312 this->hscroll->SetStepSize(10); // Speed up horizontal scrollbar
313 this->vscroll->SetStepSize(FONT_HEIGHT_MONO);
315 if (!txt.valid()) return;
317 this->lines.clear();
319 size_t filesize;
320 this->text = txt.read (&filesize);
321 if (!this->text) return;
323 /* Replace tabs and line feeds with a space since str_validate removes those. */
324 for (char *p = this->text; *p != '\0'; p++) {
325 if (*p == '\t' || *p == '\r') *p = ' ';
328 /* Check for the byte-order-mark, and skip it if needed. */
329 char *p = this->text;
330 if (strncmp ("\xEF\xBB\xBF", p, 3) == 0) p += 3;
332 /* Make sure the string is a valid UTF-8 sequence. */
333 str_validate (p, this->text + filesize, SVS_REPLACE_WITH_QUESTION_MARK | SVS_ALLOW_NEWLINE);
335 /* Split the string on newlines. */
336 for (;;) {
337 this->lines.push_back (p);
338 p = strchr (p, '\n');
339 if (p == NULL) break;
340 *(p++) = '\0';
341 /* Break on last line if the file does end with a newline. */
342 if (p == this->text + filesize) break;
346 /* virtual */ TextfileWindow::~TextfileWindow()
348 free(this->text);
352 * Get the total height of the content displayed in this window, if wrapping is disabled.
353 * @return the height in pixels
355 uint TextfileWindow::GetContentHeight()
357 int max_width = this->GetWidget<NWidgetCore>(WID_TF_BACKGROUND)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMERECT_RIGHT;
359 uint height = 0;
360 for (uint i = 0; i < this->lines.size(); i++) {
361 height += GetStringHeight(this->lines[i], max_width, FS_MONO);
364 return height;
367 /* virtual */ void TextfileWindow::UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
369 switch (widget) {
370 case WID_TF_BACKGROUND:
371 resize->height = 1;
373 size->height = 4 * resize->height + TOP_SPACING + BOTTOM_SPACING; // At least 4 lines are visible.
374 size->width = max(200u, size->width); // At least 200 pixels wide.
375 break;
379 /** Set scrollbars to the right lengths. */
380 void TextfileWindow::SetupScrollbars()
382 if (IsWidgetLowered(WID_TF_WRAPTEXT)) {
383 this->vscroll->SetCount(this->GetContentHeight());
384 this->hscroll->SetCount(0);
385 } else {
386 uint max_length = 0;
387 for (uint i = 0; i < this->lines.size(); i++) {
388 max_length = max(max_length, GetStringBoundingBox(this->lines[i], FS_MONO).width);
390 this->vscroll->SetCount(this->lines.size() * FONT_HEIGHT_MONO);
391 this->hscroll->SetCount(max_length + WD_FRAMETEXT_LEFT + WD_FRAMETEXT_RIGHT);
394 this->SetWidgetDisabledState(WID_TF_HSCROLLBAR, IsWidgetLowered(WID_TF_WRAPTEXT));
397 /* virtual */ void TextfileWindow::OnClick(Point pt, int widget, int click_count)
399 switch (widget) {
400 case WID_TF_WRAPTEXT:
401 this->ToggleWidgetLoweredState(WID_TF_WRAPTEXT);
402 this->SetupScrollbars();
403 this->InvalidateData();
404 break;
408 void TextfileWindow::DrawWidget (BlitArea *dpi, const Rect &r, int widget) const
410 if (widget != WID_TF_BACKGROUND) return;
412 const int x = r.left + WD_FRAMETEXT_LEFT;
413 const int y = r.top + WD_FRAMETEXT_TOP;
414 const int right = r.right - WD_FRAMETEXT_RIGHT;
415 const int bottom = r.bottom - WD_FRAMETEXT_BOTTOM;
417 BlitArea new_dpi;
418 if (!InitBlitArea (dpi, &new_dpi, x, y, right - x + 1, bottom - y + 1)) return;
420 /* Draw content (now coordinates given to DrawString* are local to the new clipping region). */
421 int line_height = FONT_HEIGHT_MONO;
422 int y_offset = -this->vscroll->GetPosition();
424 for (uint i = 0; i < this->lines.size(); i++) {
425 if (IsWidgetLowered(WID_TF_WRAPTEXT)) {
426 y_offset = DrawStringMultiLine (&new_dpi, 0, right - x, y_offset, bottom - y, this->lines[i], TC_WHITE, SA_TOP | SA_LEFT, false, FS_MONO);
427 } else {
428 DrawString (&new_dpi, -this->hscroll->GetPosition(), right - x, y_offset, this->lines[i], TC_WHITE, SA_TOP | SA_LEFT, false, FS_MONO);
429 y_offset += line_height; // margin to previous element
434 /* virtual */ void TextfileWindow::OnResize()
436 this->vscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND, TOP_SPACING + BOTTOM_SPACING);
437 this->hscroll->SetCapacityFromWidget(this, WID_TF_BACKGROUND);
439 this->SetupScrollbars();
442 void TextfileWindow::GlyphSearcher::Reset()
444 this->iter = this->begin;
447 const char *TextfileWindow::GlyphSearcher::NextString()
449 return (this->iter == this->end) ? NULL : *(this->iter++);
453 * Search a textfile file next to the given content.
454 * @param type The type of the textfile to search for.
455 * @param dir The subdirectory to search in.
456 * @param filename The filename of the content to look for.
458 TextfileDesc::TextfileDesc (TextfileType type, Subdirectory dir, const char *filename)
459 : type(type), dir(dir)
461 static const char * const prefixes[] = {
462 "readme",
463 "changelog",
464 "license",
466 assert_compile(lengthof(prefixes) == TFT_END);
468 if (filename == NULL) {
469 this->format = FORMAT_END;
470 return;
473 const char *slash = strrchr (filename, PATHSEPCHAR);
474 if (slash == NULL) {
475 this->format = FORMAT_END;
476 return;
479 size_t alloc_size = (slash - filename)
480 + 1 // slash
481 + 9 // longest prefix length ("changelog")
482 + 1 // underscore
483 + 7 // longest possible language length
484 + 9; // ".txt.ext" and null terminator
486 this->path.reset (xmalloc (alloc_size));
487 stringb buf (alloc_size, this->path.get());
488 buf.fmt ("%.*s%s", (int)(slash - filename + 1), filename, prefixes[type]);
489 size_t base_length = buf.length();
491 static const char * const exts[] = {
492 "txt",
493 #if defined(WITH_ZLIB)
494 "txt.gz",
495 #endif
496 #if defined(WITH_LZMA)
497 "txt.xz",
498 #endif
500 assert_compile (lengthof(exts) == FORMAT_END);
502 for (size_t i = 0; i < lengthof(exts); i++) {
503 buf.truncate (base_length);
504 buf.append_fmt ("_%s.%s", GetCurrentLanguageIsoCode(), exts[i]);
505 if (FioCheckFileExists (buf.c_str(), dir)) {
506 this->format = (Format) i;
507 return;
510 buf.truncate (base_length);
511 buf.append_fmt ("_%.2s.%s", GetCurrentLanguageIsoCode(), exts[i]);
512 if (FioCheckFileExists (buf.c_str(), dir)) {
513 this->format = (Format) i;
514 return;
517 buf.truncate (base_length);
518 buf.append_fmt (".%s", exts[i]);
519 if (FioCheckFileExists (buf.c_str(), dir)) {
520 this->format = (Format) i;
521 return;
525 this->path.reset();
526 assert (this->path.get() == NULL);
527 this->format = FORMAT_END;