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.cpp Code related to textfiles. */
11 #include "core/math_func.hpp"
12 #include "fileio_func.h"
19 #include "widgets/misc_widget.h"
21 #include "table/strings.h"
23 #if defined(WITH_ZLIB)
27 #if defined(WITH_LZMA)
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
),
39 NWidget(NWID_HORIZONTAL
),
40 NWidget(WWT_PANEL
, COLOUR_MAUVE
, WID_TF_BACKGROUND
), SetMinimalSize(200, 125), SetResize(1, 12), SetScrollbar(WID_TF_VSCROLLBAR
),
42 NWidget(NWID_VERTICAL
),
43 NWidget(NWID_VSCROLLBAR
, COLOUR_MAUVE
, WID_TF_VSCROLLBAR
),
46 NWidget(NWID_HORIZONTAL
),
47 NWidget(NWID_HSCROLLBAR
, COLOUR_MAUVE
, WID_TF_HSCROLLBAR
),
48 NWidget(WWT_RESIZEBOX
, COLOUR_MAUVE
),
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(
60 _nested_textfile_widgets
, lengthof(_nested_textfile_widgets
),
65 /** Stream template struct. */
66 template <TextfileDesc::Format FMT
>
69 /** Stream loop function results. */
71 STREAM_OK
, ///< success, decoding in progress
72 STREAM_END
, ///< success, decoding finished
73 STREAM_ERROR
, ///< error
78 /** Zlib stream struct. */
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
)) {
104 if (z
->avail_out
== 0) return STREAM_OK
;
111 static inline void end (z_stream
*z
)
113 int r
= inflateEnd (z
);
118 #endif /* WITH_ZLIB */
122 /** LZMA stream struct. */
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
)
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;
172 z
->next_in
= &(*input
)[0];
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;
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
;
202 assert (z
.avail_out
> 0);
207 if (z
.avail_in
== 0 && filesize
!= 0
208 && !fill_buffer (&input
, handle
, &filesize
, &z
)) {
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
);
224 z
.avail_out
= BLOCKSIZE
;
228 /* Finish decoding. */
229 stream
<FMT
>::end (&z
);
231 if (r
!= STREAM_END
) {
236 /* Compute total size. */
237 size_t total
= alloc
- z
.avail_out
;
245 /* Append null terminator if required. */
246 if (z
.avail_out
== 0) {
247 if (output
[total
- 1] == '\n') {
250 output
= xrealloc (output
, alloc
+ 1);
253 output
[total
] = '\0';
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
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();
272 char *text
= xmalloc (filesize
+ 1);
273 size_t read
= fread (text
, 1, filesize
, handle
);
275 if (read
!= filesize
) {
279 text
[filesize
] = '\0';
284 #if defined(WITH_ZLIB)
286 char *text
= stream_unzip
<FORMAT_GZ
> (handle
, filesize
, len
);
292 #if defined(WITH_LZMA)
294 char *text
= stream_unzip
<FORMAT_XZ
> (handle
, filesize
, len
);
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
);
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;
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. */
337 this->lines
.push_back (p
);
338 p
= strchr (p
, '\n');
339 if (p
== NULL
) break;
341 /* Break on last line if the file does end with a newline. */
342 if (p
== this->text
+ filesize
) break;
346 /* virtual */ TextfileWindow::~TextfileWindow()
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
;
360 for (uint i
= 0; i
< this->lines
.size(); i
++) {
361 height
+= GetStringHeight(this->lines
[i
], max_width
, FS_MONO
);
367 /* virtual */ void TextfileWindow::UpdateWidgetSize(int widget
, Dimension
*size
, const Dimension
&padding
, Dimension
*fill
, Dimension
*resize
)
370 case WID_TF_BACKGROUND
:
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.
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);
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
)
400 case WID_TF_WRAPTEXT
:
401 this->ToggleWidgetLoweredState(WID_TF_WRAPTEXT
);
402 this->SetupScrollbars();
403 this->InvalidateData();
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
;
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
);
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
[] = {
466 assert_compile(lengthof(prefixes
) == TFT_END
);
468 if (filename
== NULL
) {
469 this->format
= FORMAT_END
;
473 const char *slash
= strrchr (filename
, PATHSEPCHAR
);
475 this->format
= FORMAT_END
;
479 size_t alloc_size
= (slash
- filename
)
481 + 9 // longest prefix length ("changelog")
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
[] = {
493 #if defined(WITH_ZLIB)
496 #if defined(WITH_LZMA)
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
;
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
;
517 buf
.truncate (base_length
);
518 buf
.append_fmt (".%s", exts
[i
]);
519 if (FioCheckFileExists (buf
.c_str(), dir
)) {
520 this->format
= (Format
) i
;
526 assert (this->path
.get() == NULL
);
527 this->format
= FORMAT_END
;