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