1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "common/system.h"
24 #include "common/translation.h"
25 #include "gui/message.h"
26 #include "sci/sci.h"
27 #include "sci/console.h"
28 #include "sci/event.h"
29 #include "sci/engine/kernel.h"
30 #include "sci/engine/seg_manager.h"
31 #include "sci/engine/state.h"
32 #include "sci/graphics/cache.h"
33 #include "sci/graphics/compare.h"
34 #include "sci/graphics/controls32.h"
35 #include "sci/graphics/font.h"
36 #include "sci/graphics/screen.h"
37 #include "sci/graphics/text32.h"
38 
39 namespace Sci {
GfxControls32(SegManager * segMan,GfxCache * cache,GfxText32 * text)40 GfxControls32::GfxControls32(SegManager *segMan, GfxCache *cache, GfxText32 *text) :
41 	_segMan(segMan),
42 	_gfxCache(cache),
43 	_gfxText32(text),
44 	_overwriteMode(false),
45 	_nextCursorFlashTick(0),
46 	// SSCI used a memory handle for a ScrollWindow object as ID. We use a
47 	// simple numeric handle instead.
48 	_nextScrollWindowId(10000) {}
49 
~GfxControls32()50 GfxControls32::~GfxControls32() {
51 	ScrollWindowMap::iterator it;
52 	for (it = _scrollWindows.begin(); it != _scrollWindows.end(); ++it)
53 		delete it->_value;
54 }
55 
56 #pragma mark -
57 #pragma mark Text input control
58 
kernelEditText(const reg_t controlObject)59 reg_t GfxControls32::kernelEditText(const reg_t controlObject) {
60 	SegManager *segMan = _segMan;
61 
62 	TextEditor editor;
63 	reg_t textObject = readSelector(_segMan, controlObject, SELECTOR(text));
64 	editor.text = _segMan->getString(textObject);
65 	editor.foreColor = readSelectorValue(_segMan, controlObject, SELECTOR(fore));
66 	editor.backColor = readSelectorValue(_segMan, controlObject, SELECTOR(back));
67 	editor.skipColor = readSelectorValue(_segMan, controlObject, SELECTOR(skip));
68 	editor.fontId = readSelectorValue(_segMan, controlObject, SELECTOR(font));
69 	editor.maxLength = readSelectorValue(_segMan, controlObject, SELECTOR(width));
70 	editor.bitmap = readSelector(_segMan, controlObject, SELECTOR(bitmap));
71 	editor.cursorCharPosition = 0;
72 	editor.cursorIsDrawn = false;
73 	editor.borderColor = readSelectorValue(_segMan, controlObject, SELECTOR(borderColor));
74 
75 	reg_t titleObject = readSelector(_segMan, controlObject, SELECTOR(title));
76 
77 	int16 titleHeight = 0;
78 	GuiResourceId titleFontId = readSelectorValue(_segMan, controlObject, SELECTOR(titleFont));
79 	if (!titleObject.isNull()) {
80 		GfxFont *titleFont = _gfxCache->getFont(titleFontId);
81 		titleHeight += _gfxText32->scaleUpHeight(titleFont->getHeight()) + 1;
82 		if (editor.borderColor != -1) {
83 			titleHeight += 2;
84 		}
85 	}
86 
87 	int16 width = 0;
88 	int16 height = titleHeight;
89 
90 	GfxFont *editorFont = _gfxCache->getFont(editor.fontId);
91 	height += _gfxText32->scaleUpHeight(editorFont->getHeight()) + 1;
92 	_gfxText32->setFont(editor.fontId);
93 	int16 emSize = _gfxText32->getCharWidth('M', true);
94 	width += editor.maxLength * emSize + 1;
95 	if (editor.borderColor != -1) {
96 		width += 4;
97 		height += 2;
98 	}
99 
100 	Common::Rect editorPlaneRect(width, height);
101 	editorPlaneRect.translate(readSelectorValue(_segMan, controlObject, SELECTOR(x)), readSelectorValue(_segMan, controlObject, SELECTOR(y)));
102 
103 	reg_t planeObj = readSelector(_segMan, controlObject, SELECTOR(plane));
104 	Plane *sourcePlane = g_sci->_gfxFrameout->getVisiblePlanes().findByObject(planeObj);
105 	if (sourcePlane == nullptr) {
106 		sourcePlane = g_sci->_gfxFrameout->getPlanes().findByObject(planeObj);
107 		if (sourcePlane == nullptr) {
108 			error("Could not find plane %04x:%04x", PRINT_REG(planeObj));
109 		}
110 	}
111 	editorPlaneRect.translate(sourcePlane->_gameRect.left, sourcePlane->_gameRect.top);
112 
113 	editor.textRect = Common::Rect(2, titleHeight + 2, width - 1, height - 1);
114 	editor.width = width;
115 
116 	if (editor.bitmap.isNull()) {
117 		TextAlign alignment = (TextAlign)readSelectorValue(_segMan, controlObject, SELECTOR(mode));
118 
119 		if (titleObject.isNull()) {
120 			bool dimmed = readSelectorValue(_segMan, controlObject, SELECTOR(dimmed));
121 			editor.bitmap = _gfxText32->createFontBitmap(width, height, editor.textRect, editor.text, editor.foreColor, editor.backColor, editor.skipColor, editor.fontId, alignment, editor.borderColor, dimmed, true, false);
122 		} else {
123 			error("Titled bitmaps are not known to be used by any game. Please submit a bug report with details about the game you were playing and what you were doing that triggered this error. Thanks!");
124 		}
125 	}
126 
127 	drawCursor(editor);
128 
129 	Plane *plane = new Plane(editorPlaneRect, kPlanePicTransparent);
130 	plane->changePic();
131 	g_sci->_gfxFrameout->addPlane(plane);
132 
133 	CelInfo32 celInfo;
134 	celInfo.type = kCelTypeMem;
135 	celInfo.bitmap = editor.bitmap;
136 
137 	ScreenItem *screenItem = new ScreenItem(plane->_object, celInfo, Common::Point(), ScaleInfo());
138 	plane->_screenItemList.add(screenItem);
139 
140 	// frameOut must be called after the screen item is created, and before it
141 	// is updated at the end of the event loop, otherwise it has both created
142 	// and updated flags set which crashes the engine (updates are handled
143 	// before creations, but the screen item is not in the correct state for an
144 	// update)
145 	g_sci->_gfxFrameout->frameOut(true);
146 
147 	EventManager *eventManager = g_sci->getEventManager();
148 	bool clearTextOnInput = true;
149 	bool textChanged = false;
150 	for (;;) {
151 		// We peek here because the last event needs to be allowed to dispatch a
152 		// second time to the normal event handling system. In SSCI, the event
153 		// is always consumed and then the last event just gets posted back to
154 		// the event manager for reprocessing, but instead, we only remove the
155 		// event from the queue *after* we have determined it is not a
156 		// defocusing event
157 		const SciEvent event = eventManager->getSciEvent(kSciEventAny | kSciEventPeek);
158 
159 		bool focused = true;
160 		// SSCI did not have a QUIT event, but we do, so we have to handle it
161 		if (event.type == kSciEventQuit) {
162 			focused = false;
163 		} else if (event.type == kSciEventMousePress && !editorPlaneRect.contains(event.mousePosSci)) {
164 			focused = false;
165 		} else if (event.type == kSciEventKeyDown) {
166 			switch (event.character) {
167 			case kSciKeyEsc:
168 			case kSciKeyUp:
169 			case kSciKeyDown:
170 			case kSciKeyTab:
171 			case kSciKeyShiftTab:
172 			case kSciKeyEnter:
173 				focused = false;
174 				break;
175 			}
176 		}
177 
178 		if (!focused) {
179 			break;
180 		}
181 
182 		// Consume the event now that we know it is not one of the defocusing
183 		// events above
184 		if (event.type != kSciEventNone)
185 			eventManager->getSciEvent(kSciEventAny);
186 
187 		// In SSCI, the font and bitmap were reset here on each iteration
188 		// through the loop, but this is not necessary since control is not
189 		// yielded back to the VM until input is received, which means there is
190 		// nothing that could modify the GfxText32's state with a different font
191 		// in the meantime
192 
193 		bool shouldDeleteChar = false;
194 		bool shouldRedrawText = false;
195 		uint16 lastCursorPosition = editor.cursorCharPosition;
196 		if (event.type == kSciEventKeyDown) {
197 			switch (event.character) {
198 			case kSciKeyLeft:
199 				clearTextOnInput = false;
200 				if (editor.cursorCharPosition > 0) {
201 					--editor.cursorCharPosition;
202 				}
203 				break;
204 
205 			case kSciKeyRight:
206 				clearTextOnInput = false;
207 				if (editor.cursorCharPosition < editor.text.size()) {
208 					++editor.cursorCharPosition;
209 				}
210 				break;
211 
212 			case kSciKeyHome:
213 				clearTextOnInput = false;
214 				editor.cursorCharPosition = 0;
215 				break;
216 
217 			case kSciKeyEnd:
218 				clearTextOnInput = false;
219 				editor.cursorCharPosition = editor.text.size();
220 				break;
221 
222 			case kSciKeyInsert:
223 				clearTextOnInput = false;
224 				// Redrawing also changes the cursor rect to reflect the new
225 				// insertion mode
226 				shouldRedrawText = true;
227 				_overwriteMode = !_overwriteMode;
228 				break;
229 
230 			case kSciKeyDelete:
231 				clearTextOnInput = false;
232 				if (editor.cursorCharPosition < editor.text.size()) {
233 					shouldDeleteChar = true;
234 				}
235 				break;
236 
237 			case kSciKeyBackspace:
238 				clearTextOnInput = false;
239 				shouldDeleteChar = true;
240 				if (editor.cursorCharPosition > 0) {
241 					--editor.cursorCharPosition;
242 				}
243 				break;
244 
245 			case kSciKeyEtx:
246 				editor.text.clear();
247 				editor.cursorCharPosition = 0;
248 				shouldRedrawText = true;
249 				break;
250 
251 			default: {
252 				if (event.character >= 20 && event.character < 257) {
253 					if (clearTextOnInput) {
254 						clearTextOnInput = false;
255 						editor.text.clear();
256 					}
257 
258 					if (
259 						(_overwriteMode && editor.cursorCharPosition < editor.maxLength) ||
260 						(editor.text.size() < editor.maxLength && _gfxText32->getCharWidth(event.character, true) + _gfxText32->getStringWidth(editor.text) < editor.textRect.width())
261 					) {
262 						if (_overwriteMode && editor.cursorCharPosition < editor.text.size()) {
263 							editor.text.setChar(event.character, editor.cursorCharPosition);
264 						} else {
265 							editor.text.insertChar(event.character, editor.cursorCharPosition);
266 						}
267 
268 						++editor.cursorCharPosition;
269 						shouldRedrawText = true;
270 					}
271 				}
272 			}
273 			}
274 		}
275 
276 		if (shouldDeleteChar) {
277 			shouldRedrawText = true;
278 			if (editor.cursorCharPosition < editor.text.size()) {
279 				editor.text.deleteChar(editor.cursorCharPosition);
280 			}
281 		}
282 
283 		if (shouldRedrawText) {
284 			eraseCursor(editor);
285 			_gfxText32->erase(editor.textRect, true);
286 			_gfxText32->drawTextBox(editor.text);
287 			drawCursor(editor);
288 			textChanged = true;
289 			screenItem->_updated = g_sci->_gfxFrameout->getScreenCount();
290 		} else if (editor.cursorCharPosition != lastCursorPosition) {
291 			eraseCursor(editor);
292 			drawCursor(editor);
293 			screenItem->_updated = g_sci->_gfxFrameout->getScreenCount();
294 		} else {
295 			flashCursor(editor);
296 			screenItem->_updated = g_sci->_gfxFrameout->getScreenCount();
297 		}
298 
299 		g_sci->_gfxFrameout->frameOut(true);
300 		g_sci->_gfxFrameout->throttle();
301 	}
302 
303 	g_sci->_gfxFrameout->deletePlane(*plane);
304 	if (readSelectorValue(segMan, controlObject, SELECTOR(frameOut))) {
305 		g_sci->_gfxFrameout->frameOut(true);
306 	}
307 
308 	_segMan->freeBitmap(editor.bitmap);
309 
310 	if (textChanged) {
311 		editor.text.trim();
312 		SciArray &string = *_segMan->lookupArray(textObject);
313 		string.fromString(editor.text);
314 	}
315 
316 	return make_reg(0, textChanged);
317 }
318 
drawCursor(TextEditor & editor)319 void GfxControls32::drawCursor(TextEditor &editor) {
320 	if (!editor.cursorIsDrawn) {
321 		editor.cursorRect.left = editor.textRect.left + _gfxText32->getTextWidth(editor.text, 0, editor.cursorCharPosition);
322 
323 		const int16 scaledFontHeight = _gfxText32->scaleUpHeight(_gfxText32->_font->getHeight());
324 
325 		// SSCI branched on borderColor here but the two branches appeared to be
326 		// identical, differing only because the compiler decided to be
327 		// differently clever when optimising multiplication in each branch
328 		if (_overwriteMode) {
329 			editor.cursorRect.top = editor.textRect.top;
330 			editor.cursorRect.setHeight(scaledFontHeight);
331 		} else {
332 			editor.cursorRect.top = editor.textRect.top + scaledFontHeight - 1;
333 			editor.cursorRect.setHeight(1);
334 		}
335 
336 		const char currentChar = editor.cursorCharPosition < editor.text.size() ? editor.text[editor.cursorCharPosition] : ' ';
337 		editor.cursorRect.setWidth(_gfxText32->getCharWidth(currentChar, true));
338 
339 		_gfxText32->invertRect(editor.bitmap, editor.width, editor.cursorRect, editor.foreColor, editor.backColor, true);
340 
341 		editor.cursorIsDrawn = true;
342 	}
343 
344 	_nextCursorFlashTick = g_sci->getTickCount() + 30;
345 }
346 
eraseCursor(TextEditor & editor)347 void GfxControls32::eraseCursor(TextEditor &editor) {
348 	if (editor.cursorIsDrawn) {
349 		_gfxText32->invertRect(editor.bitmap, editor.width, editor.cursorRect, editor.foreColor, editor.backColor, true);
350 		editor.cursorIsDrawn = false;
351 	}
352 
353 	_nextCursorFlashTick = g_sci->getTickCount() + 30;
354 }
355 
flashCursor(TextEditor & editor)356 void GfxControls32::flashCursor(TextEditor &editor) {
357 	if (g_sci->getTickCount() > _nextCursorFlashTick) {
358 		_gfxText32->invertRect(editor.bitmap, editor.width, editor.cursorRect, editor.foreColor, editor.backColor, true);
359 
360 		editor.cursorIsDrawn = !editor.cursorIsDrawn;
361 		_nextCursorFlashTick = g_sci->getTickCount() + 30;
362 	}
363 }
364 
365 #pragma mark -
366 #pragma mark Scrollable window control
367 
ScrollWindow(SegManager * segMan,const Common::Rect & gameRect,const Common::Point & position,const reg_t plane,const uint8 defaultForeColor,const uint8 defaultBackColor,const GuiResourceId defaultFontId,const TextAlign defaultAlignment,const int16 defaultBorderColor,const uint16 maxNumEntries)368 ScrollWindow::ScrollWindow(SegManager *segMan, const Common::Rect &gameRect, const Common::Point &position, const reg_t plane, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries) :
369 	_segMan(segMan),
370 	_gfxText32(segMan, g_sci->_gfxCache),
371 	_maxNumEntries(maxNumEntries),
372 	_firstVisibleChar(0),
373 	_topVisibleLine(0),
374 	_lastVisibleChar(0),
375 	_bottomVisibleLine(0),
376 	_numLines(0),
377 	_numVisibleLines(0),
378 	_plane(plane),
379 	_foreColor(defaultForeColor),
380 	_backColor(defaultBackColor),
381 	_borderColor(defaultBorderColor),
382 	_fontId(defaultFontId),
383 	_alignment(defaultAlignment),
384 	_visible(false),
385 	_position(position),
386 	_screenItem(nullptr),
387 	_nextEntryId(1) {
388 
389 	_entries.reserve(maxNumEntries);
390 
391 	_gfxText32.setFont(_fontId);
392 	_pointSize = _gfxText32._font->getHeight();
393 
394 	const uint16 scriptWidth = g_sci->_gfxFrameout->getScriptWidth();
395 	const uint16 scriptHeight = g_sci->_gfxFrameout->getScriptHeight();
396 
397 	Common::Rect bitmapRect(gameRect);
398 	mulinc(bitmapRect, Ratio(_gfxText32._xResolution, scriptWidth), Ratio(_gfxText32._yResolution, scriptHeight));
399 
400 	_textRect.left = 2;
401 	_textRect.top = 2;
402 	_textRect.right = bitmapRect.width() - 2;
403 	_textRect.bottom = bitmapRect.height() - 2;
404 
405 	uint8 skipColor = 0;
406 	while (skipColor == _foreColor || skipColor == _backColor) {
407 		skipColor++;
408 	}
409 
410 	assert(bitmapRect.width() > 0 && bitmapRect.height() > 0);
411 	_bitmap = _gfxText32.createFontBitmap(bitmapRect.width(), bitmapRect.height(), _textRect, "", _foreColor, _backColor, skipColor, _fontId, _alignment, _borderColor, false, false, false);
412 
413 	debugC(1, kDebugLevelGraphics, "New ScrollWindow: textRect size: %d x %d, bitmap: %04x:%04x", _textRect.width(), _textRect.height(), PRINT_REG(_bitmap));
414 }
415 
~ScrollWindow()416 ScrollWindow::~ScrollWindow() {
417 	_segMan->freeBitmap(_bitmap);
418 	// _screenItem will be deleted by GfxFrameout
419 }
420 
where() const421 Ratio ScrollWindow::where() const {
422 	return Ratio(_topVisibleLine, MAX(_numLines, 1));
423 }
424 
show()425 void ScrollWindow::show() {
426 	if (_visible) {
427 		return;
428 	}
429 
430 	if (_screenItem == nullptr) {
431 		CelInfo32 celInfo;
432 		celInfo.type = kCelTypeMem;
433 		celInfo.bitmap = _bitmap;
434 
435 		_screenItem = new ScreenItem(_plane, celInfo, _position, ScaleInfo());
436 	}
437 
438 	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(_plane);
439 
440 	if (plane == nullptr) {
441 		error("[ScrollWindow::show]: Plane %04x:%04x not found", PRINT_REG(_plane));
442 	}
443 
444 	plane->_screenItemList.add(_screenItem);
445 
446 	_visible = true;
447 }
448 
hide()449 void ScrollWindow::hide() {
450 	if (!_visible) {
451 		return;
452 	}
453 
454 	g_sci->_gfxFrameout->deleteScreenItem(*_screenItem, _plane);
455 	_screenItem = nullptr;
456 	g_sci->_gfxFrameout->frameOut(true);
457 
458 	_visible = false;
459 }
460 
add(const Common::String & text,const GuiResourceId fontId,const int16 foreColor,const TextAlign alignment,const bool scrollTo)461 reg_t ScrollWindow::add(const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo) {
462 	if (_entries.size() == _maxNumEntries) {
463 		ScrollWindowEntry removedEntry = _entries.remove_at(0);
464 		_text.erase(0, removedEntry.text.size());
465 		// `_firstVisibleChar` will be reset shortly if `scrollTo` is true, so
466 		// there is no reason to update it
467 		if (!scrollTo) {
468 			_firstVisibleChar -= removedEntry.text.size();
469 		}
470 	}
471 
472 	_entries.push_back(ScrollWindowEntry());
473 	ScrollWindowEntry &entry = _entries.back();
474 
475 	// In SSCI, the line ID was a memory handle for the string of this line. We
476 	// use a numeric ID instead.
477 	entry.id = make_reg(0, _nextEntryId++);
478 
479 	if (_nextEntryId > _maxNumEntries) {
480 		_nextEntryId = 1;
481 	}
482 
483 	// In SSCI, this was updated after _text was updated, which meant there was
484 	// an extra unnecessary subtraction operation (subtracting `entry.text`
485 	// size)
486 	if (scrollTo) {
487 		_firstVisibleChar = _text.size();
488 	}
489 
490 	fillEntry(entry, text, fontId, foreColor, alignment);
491 	_text += entry.text;
492 
493 	computeLineIndices();
494 	update(true);
495 
496 	return entry.id;
497 }
498 
fillEntry(ScrollWindowEntry & entry,const Common::String & text,const GuiResourceId fontId,const int16 foreColor,const TextAlign alignment)499 void ScrollWindow::fillEntry(ScrollWindowEntry &entry, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment) {
500 	entry.alignment = alignment;
501 	entry.foreColor = foreColor;
502 	entry.fontId = fontId;
503 
504 	Common::String formattedText;
505 
506 	// NB: There are inconsistencies here.
507 	// If there is a multi-line entry with non-default properties, and it
508 	// is only partially displayed, it may not be displayed right, since the
509 	// property directives are only added to the first line.
510 	// (Verified by trying this in SSCI SQ6 with a custom ScrollWindowAdd call.)
511 	//
512 	// The converse is also a potential issue (but unverified), where lines
513 	// with properties -1 can inherit properties from the previously rendered
514 	// line instead of the defaults.
515 
516 	// SSCI added "|s<lineIndex>|" here, but |s| is not a valid control code, so
517 	// it just always ended up getting skipped by the text rendering code
518 	if (entry.fontId != -1) {
519 		formattedText += Common::String::format("|f%d|", entry.fontId);
520 	}
521 	if (entry.foreColor != -1) {
522 		formattedText += Common::String::format("|c%d|", entry.foreColor);
523 	}
524 	if (entry.alignment != -1) {
525 		formattedText += Common::String::format("|a%d|", entry.alignment);
526 	}
527 	formattedText += text;
528 	entry.text = formattedText;
529 }
530 
modify(const reg_t id,const Common::String & text,const GuiResourceId fontId,const int16 foreColor,const TextAlign alignment,const bool scrollTo)531 reg_t ScrollWindow::modify(const reg_t id, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo) {
532 
533 	EntriesList::iterator it = _entries.begin();
534 	uint firstCharLocation = 0;
535 	for ( ; it != _entries.end(); ++it) {
536 		if (it->id == id) {
537 			break;
538 		}
539 		firstCharLocation += it->text.size();
540 	}
541 
542 	if (it == _entries.end()) {
543 		return make_reg(0, 0);
544 	}
545 
546 	ScrollWindowEntry &entry = *it;
547 
548 	uint oldTextLength = entry.text.size();
549 
550 	fillEntry(entry, text, fontId, foreColor, alignment);
551 	_text.replace(firstCharLocation, oldTextLength, entry.text);
552 
553 	if (scrollTo) {
554 		_firstVisibleChar = firstCharLocation;
555 	}
556 
557 	computeLineIndices();
558 	update(true);
559 
560 	return entry.id;
561 }
562 
upArrow()563 void ScrollWindow::upArrow() {
564 	if (_topVisibleLine == 0) {
565 		return;
566 	}
567 
568 	_topVisibleLine--;
569 	_bottomVisibleLine--;
570 
571 	if (_bottomVisibleLine - _topVisibleLine + 1 < _numVisibleLines) {
572 		_bottomVisibleLine = _numLines - 1;
573 	}
574 
575 	_firstVisibleChar = _startsOfLines[_topVisibleLine];
576 	_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
577 
578 	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
579 
580 	Common::String lineText(_text.c_str() + _startsOfLines[_topVisibleLine], _text.c_str() + _startsOfLines[_topVisibleLine + 1] - 1);
581 
582 	debugC(3, kDebugLevelGraphics, "ScrollWindow::upArrow: top: %d, bottom: %d, num: %d, numvis: %d, lineText: %s", _topVisibleLine, _bottomVisibleLine, _numLines, _numVisibleLines, lineText.c_str());
583 
584 	_gfxText32.scrollLine(lineText, _numVisibleLines, _foreColor, _alignment, _fontId, kScrollUp);
585 
586 	if (_visible) {
587 		assert(_screenItem);
588 
589 		_screenItem->update();
590 		g_sci->_gfxFrameout->frameOut(true);
591 	}
592 }
593 
downArrow()594 void ScrollWindow::downArrow() {
595 	if (_topVisibleLine + 1 >= _numLines) {
596 		return;
597 	}
598 
599 	_topVisibleLine++;
600 	_bottomVisibleLine++;
601 
602 	if (_bottomVisibleLine + 1 >= _numLines) {
603 		_bottomVisibleLine = _numLines - 1;
604 	}
605 
606 	_firstVisibleChar = _startsOfLines[_topVisibleLine];
607 	_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
608 
609 	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
610 
611 	Common::String lineText;
612 	if (_bottomVisibleLine - _topVisibleLine + 1 == _numVisibleLines) {
613 		lineText = Common::String(_text.c_str() + _startsOfLines[_bottomVisibleLine], _text.c_str() + _startsOfLines[_bottomVisibleLine + 1] - 1);
614 	} else {
615 		// scroll in empty string
616 	}
617 
618 	debugC(3, kDebugLevelGraphics, "ScrollWindow::downArrow: top: %d, bottom: %d, num: %d, numvis: %d, lineText: %s", _topVisibleLine, _bottomVisibleLine, _numLines, _numVisibleLines, lineText.c_str());
619 
620 
621 	_gfxText32.scrollLine(lineText, _numVisibleLines, _foreColor, _alignment, _fontId, kScrollDown);
622 
623 	if (_visible) {
624 		assert(_screenItem);
625 
626 		_screenItem->update();
627 		g_sci->_gfxFrameout->frameOut(true);
628 	}
629 }
630 
go(const Ratio location)631 void ScrollWindow::go(const Ratio location) {
632 	const int line = (location * _numLines).toInt();
633 	if (line < 0 || line > _numLines) {
634 		error("Index is Out of Range in ScrollWindow");
635 	}
636 
637 	_firstVisibleChar = _startsOfLines[line];
638 	update(true);
639 
640 	// HACK:
641 	// It usually isn't possible to set _topVisibleLine >= _numLines, and so
642 	// update() doesn't. However, in this case we should set _topVisibleLine
643 	// past the end. This is clearly visible in Phantasmagoria when dragging
644 	// the slider in the About dialog to the very end. The slider ends up lower
645 	// than where it can be moved by scrolling down with the arrows.
646 	if (location.isOne()) {
647 		_topVisibleLine = _numLines;
648 	}
649 }
650 
home()651 void ScrollWindow::home() {
652 	if (_firstVisibleChar == 0) {
653 		return;
654 	}
655 
656 	_firstVisibleChar = 0;
657 	update(true);
658 }
659 
end()660 void ScrollWindow::end() {
661 	if (_bottomVisibleLine + 1 >= _numLines) {
662 		return;
663 	}
664 
665 	int line = _numLines - _numVisibleLines;
666 	if (line < 0) {
667 		line = 0;
668 	}
669 	_firstVisibleChar = _startsOfLines[line];
670 	update(true);
671 }
672 
pageUp()673 void ScrollWindow::pageUp() {
674 	if (_topVisibleLine == 0) {
675 		return;
676 	}
677 
678 	_topVisibleLine -= _numVisibleLines;
679 	if (_topVisibleLine < 0) {
680 		_topVisibleLine = 0;
681 	}
682 
683 	_firstVisibleChar = _startsOfLines[_topVisibleLine];
684 	update(true);
685 }
686 
pageDown()687 void ScrollWindow::pageDown() {
688 	if (_topVisibleLine + 1 >= _numLines) {
689 		return;
690 	}
691 
692 	_topVisibleLine += _numVisibleLines;
693 	if (_topVisibleLine + 1 >= _numLines) {
694 		_topVisibleLine = _numLines - 1;
695 	}
696 
697 	_firstVisibleChar = _startsOfLines[_topVisibleLine];
698 	update(true);
699 }
700 
computeLineIndices()701 void ScrollWindow::computeLineIndices() {
702 	_gfxText32.setFont(_fontId);
703 	// Unlike SSCI, foreColor and alignment are not set since these properties
704 	// do not affect the width of lines
705 
706 	if (_gfxText32._font->getHeight() != _pointSize) {
707 		error("Illegal font size font = %d pointSize = %d, should be %d.", _fontId, _gfxText32._font->getHeight(), _pointSize);
708 	}
709 
710 	Common::Rect lineRect(0, 0, _textRect.width(), _pointSize + 3);
711 
712 	_startsOfLines.clear();
713 
714 	// SSCI had a 1000-line limit; we do not enforce any limit since we use
715 	// dynamic containers
716 	for (uint charIndex = 0; charIndex < _text.size(); ) {
717 		_startsOfLines.push_back(charIndex);
718 		charIndex += _gfxText32.getTextCount(_text, charIndex, lineRect, false);
719 	}
720 
721 	_numLines = _startsOfLines.size();
722 
723 	_startsOfLines.push_back(_text.size());
724 
725 	_lastVisibleChar = _gfxText32.getTextCount(_text, 0, _fontId, _textRect, false) - 1;
726 
727 	_bottomVisibleLine = 0;
728 	while (
729 		_bottomVisibleLine < _numLines - 1 &&
730 		_startsOfLines[_bottomVisibleLine + 1] < _lastVisibleChar
731 	) {
732 		++_bottomVisibleLine;
733 	}
734 
735 	_numVisibleLines = _bottomVisibleLine + 1;
736 }
737 
update(const bool doFrameOut)738 void ScrollWindow::update(const bool doFrameOut) {
739 	_topVisibleLine = 0;
740 	while (
741 		_topVisibleLine < _numLines - 1 &&
742 		_firstVisibleChar >= _startsOfLines[_topVisibleLine + 1]
743 	) {
744 		++_topVisibleLine;
745 	}
746 
747 	_bottomVisibleLine = _topVisibleLine + _numVisibleLines - 1;
748 	if (_bottomVisibleLine >= _numLines) {
749 		_bottomVisibleLine = _numLines - 1;
750 	}
751 
752 	_firstVisibleChar = _startsOfLines[_topVisibleLine];
753 
754 	if (_bottomVisibleLine >= 0) {
755 		_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
756 	} else {
757 		_lastVisibleChar = -1;
758 	}
759 
760 	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
761 
762 	_gfxText32.erase(_textRect, false);
763 	_gfxText32.drawTextBox(_visibleText);
764 
765 	if (_visible) {
766 		assert(_screenItem);
767 
768 		_screenItem->update();
769 		if (doFrameOut) {
770 			g_sci->_gfxFrameout->frameOut(true);
771 		}
772 	}
773 }
774 
makeScrollWindow(const Common::Rect & gameRect,const Common::Point & position,const reg_t planeObj,const uint8 defaultForeColor,const uint8 defaultBackColor,const GuiResourceId defaultFontId,const TextAlign defaultAlignment,const int16 defaultBorderColor,const uint16 maxNumEntries)775 reg_t GfxControls32::makeScrollWindow(const Common::Rect &gameRect, const Common::Point &position, const reg_t planeObj, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries) {
776 
777 	ScrollWindow *scrollWindow = new ScrollWindow(_segMan, gameRect, position, planeObj, defaultForeColor, defaultBackColor, defaultFontId, defaultAlignment, defaultBorderColor, maxNumEntries);
778 
779 	const uint16 id = _nextScrollWindowId++;
780 	_scrollWindows[id] = scrollWindow;
781 	return make_reg(0, id);
782 }
783 
getScrollWindow(const reg_t id)784 ScrollWindow *GfxControls32::getScrollWindow(const reg_t id) {
785 	ScrollWindowMap::iterator it;
786 	it = _scrollWindows.find(id.toUint16());
787 	if (it == _scrollWindows.end())
788 		error("Invalid ScrollWindow ID");
789 
790 	return it->_value;
791 }
792 
destroyScrollWindow(const reg_t id)793 void GfxControls32::destroyScrollWindow(const reg_t id) {
794 	ScrollWindow *scrollWindow = getScrollWindow(id);
795 	scrollWindow->hide();
796 	_scrollWindows.erase(id.getOffset());
797 	delete scrollWindow;
798 }
799 
800 #pragma mark -
801 #pragma mark Message box
802 
showMessageBox(const Common::String & message,const char * const okLabel,const char * const altLabel,const int16 okValue,const int16 altValue)803 int16 GfxControls32::showMessageBox(const Common::String &message, const char *const okLabel, const char *const altLabel, const int16 okValue, const int16 altValue) {
804 	GUI::MessageDialog dialog(message, okLabel, altLabel);
805 	return (dialog.runModal() == GUI::kMessageOK) ? okValue : altValue;
806 }
807 
kernelMessageBox(const Common::String & message,const Common::String & title,const uint16 style)808 reg_t GfxControls32::kernelMessageBox(const Common::String &message, const Common::String &title, const uint16 style) {
809 	if (g_engine) {
810 		g_engine->pauseEngine(true);
811 	}
812 
813 	int16 result;
814 
815 	switch (style & 0xF) {
816 	case kMessageBoxOK:
817 		result = showMessageBox(message, _("OK"), NULL, 1, 1);
818 	break;
819 	case kMessageBoxYesNo:
820 		result = showMessageBox(message, _("Yes"), _("No"), 6, 7);
821 	break;
822 	default:
823 		error("Unsupported MessageBox style 0x%x", style & 0xF);
824 	}
825 
826 	if (g_engine) {
827 		g_engine->pauseEngine(false);
828 	}
829 
830 	return make_reg(0, result);
831 }
832 
833 } // End of namespace Sci
834