[Scummvm-git-logs] scummvm master -> a46f421625b28d4877a3f35e02b394ed1d321bad

bluegr noreply at scummvm.org
Sun Jul 5 22:15:59 UTC 2026


This automated email contains information about 5 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
bebf0971aa NANCY: NANCY10: Play click sound when clicking on inventory buttons
fd77304ce5 NANCY: NANCY10: Cellphone interface fixes
e71d9b4bf5 NANCY: NANCY10: Disable taskbar buttons when a popup is open
11efa8821f NANCY: NANCY10: Cleanup comments
a46f421625 NANCY: NANCY10: Notebook fixes


Commit: bebf0971aa8f3db80411b7f5de423dc8a89d6144
    https://github.com/scummvm/scummvm/commit/bebf0971aa8f3db80411b7f5de423dc8a89d6144
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T01:15:42+03:00

Commit Message:
NANCY: NANCY10: Play click sound when clicking on inventory buttons

Fix #16901

Changed paths:
    engines/nancy/ui/inventorypopup.cpp
    engines/nancy/ui/inventorypopup.h


diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 1aafab3899b..5ceeb619ac2 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -281,6 +281,15 @@ void InventoryPopup::setActiveFilterIndex(uint index) {
 	_activeFilterIndex = index;
 }
 
+void InventoryPopup::playButtonClickSound(const UIButtonRecord &button) {
+	SoundDescription sound = button.clickSound;
+	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND"))
+		return;
+
+	g_nancy->_sound->loadSound(sound);
+	g_nancy->_sound->playSound(sound);
+}
+
 void InventoryPopup::drawSlot(uint slotIndex, int16 itemId) {
 	if (slotIndex >= _uiivData->slotDestRects.size())
 		return;
@@ -386,6 +395,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		if (overClose) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				playButtonClickSound(closeBtn);
 				input.eatMouseInput();
 				close();
 				return;
@@ -489,6 +499,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		drawFilterTab(i, true);
 
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			playButtonClickSound(filter.button);
 			setActiveFilterIndex(i);
 			_currentPage = 0;
 			_scrollPos = 0.0f;
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
index e3d065e5d92..e18896e7a5e 100644
--- a/engines/nancy/ui/inventorypopup.h
+++ b/engines/nancy/ui/inventorypopup.h
@@ -86,6 +86,12 @@ private:
 	void rebuildVisibleList();
 	void setActiveFilterIndex(uint index);
 
+	// Play a popup button's click sound (the filter tabs and the close X),
+	// mirroring the original's per-CUIButton click cue. Falls back to the
+	// shared button-click slot in the popup header when the button carries no
+	// sound of its own.
+	void playButtonClickSound(const UIButtonRecord &button);
+
 	// Apply the current scrollbar position to the page index, clamping
 	// to the number of pages required by the active filter.
 	void updatePageFromScroll();


Commit: fd77304ce5ccf03ca89ea09d01b5fe57f4511ba1
    https://github.com/scummvm/scummvm/commit/fd77304ce5ccf03ca89ea09d01b5fe57f4511ba1
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T01:15:43+03:00

Commit Message:
NANCY: NANCY10: Cellphone interface fixes

- Fix margin and add scroll buttons to help screen - fix #16902
- Hide help button during calls - fix #16903
- Implement auto-dial - fix #16904
- Add missing keypad sounds - fix #16905
- Add back button when reading e-mail messages - fix #16906
- Play click sound when clicking the close button

Changed paths:
    engines/nancy/ui/cellphonepopup.cpp
    engines/nancy/ui/cellphonepopup.h


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 6db57cb5804..1a421580b4e 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -410,7 +410,10 @@ void CellPhonePopup::drawChrome() {
 			: _uiclData->header.normalSrcRect;
 	_drawSurface.blitFrom(_overlayImage, chromeSrc, Common::Point(0, 0));
 	drawCloseButton(_closeButtonHovered);
-	if (!isSubScreenState()) {
+	// The help "?" button lives on the dialer face only. The original hides
+	// it once a call is being placed (the connecting / "We're sorry" screens)
+	// and on every sub-screen that shows its own heading.
+	if (_screenState == kWelcome || _screenState == kDialing) {
 		drawHelpButton(0);
 	}
 	_needsRedraw = true;
@@ -511,14 +514,11 @@ void CellPhonePopup::drawScreenContent() {
 			drawHeading(*_contentHeading);
 		}
 		drawContentView();
-		// The help page is a static small-window blurb with no scroll arrows;
-		// browser / email articles keep the big-screen arrows. Help shows the
-		// Back button in the lower ribbon (returns to the main screen).
-		if (!isHelpContentView()) {
-			drawDirectoryArrows();
-		} else {
-			drawBackLabel();
-		}
+		// Help's Back sits in the lower ribbon (subButtons[0]); the zoomed
+		// articles' Back sits at the bottom of the screen (subButtons[7]).
+		// drawDirectoryArrows() blits whichever scroll pair applies.
+		drawDirectoryArrows();
+		drawBackButton(isHelpContentView() ? 0 : 7);
 		break;
 	}
 
@@ -835,7 +835,11 @@ void CellPhonePopup::drawContentView() {
 	const int lcdTop  = ws.top  - _screenPosition.top;
 	const int lcdW    = ws.width();
 	const int lcdH    = ws.height();
-	const int textTop = 22;                 // clear the heading sprite
+	// The email / browser heading sits inside the zoomed LCD, so the article
+	// text starts below it. The help page's heading is in the title-bar strip
+	// above the small LCD, so its text starts flush with the LCD top (matching
+	// the original's "Help info in small window" placement).
+	const int textTop = isHelpContentView() ? 2 : 22;
 	const int viewH   = MAX(0, lcdH - textTop);
 	const int rowH    = MAX(font->getFontHeight() + 1, 12);
 
@@ -999,10 +1003,11 @@ void CellPhonePopup::drawWelcomeScreen() {
 											ws.destRect.top - chunkOrigin.y));
 }
 
-void CellPhonePopup::drawBackLabel() {
+void CellPhonePopup::drawBackButton(uint subButtonIndex) {
 	// subButtons[0] (original CUIButton 0x10) is the Back button that returns a
-	// sub-screen to the main view; it sits at the left of the lower ribbon.
-	const UICL::ThreeRectWidget &back = _uiclData->subButtons[0];
+	// sub-screen to the main view; subButtons[7] (0x17) is the equivalent Back
+	// button at the bottom of the zoomed email / browser content view.
+	const UICL::ThreeRectWidget &back = _uiclData->subButtons[subButtonIndex];
 	if (back.srcRectIdle.isEmpty() || back.destRect.isEmpty()) {
 		return;
 	}
@@ -1013,9 +1018,9 @@ void CellPhonePopup::drawBackLabel() {
 											back.destRect.top - chunkOrigin.y));
 }
 
-Common::Rect CellPhonePopup::backButtonHitRect() const {
-	// Popup-local hit rect for the Back button (subButtons[0]).
-	Common::Rect r = _uiclData->subButtons[0].destRect;
+Common::Rect CellPhonePopup::backButtonHitRect(uint subButtonIndex) const {
+	// Popup-local hit rect for a Back sub-button.
+	Common::Rect r = _uiclData->subButtons[subButtonIndex].destRect;
 	if (r.isEmpty()) {
 		return r;
 	}
@@ -1024,15 +1029,15 @@ Common::Rect CellPhonePopup::backButtonHitRect() const {
 }
 
 const UICL::ThreeRectWidget &CellPhonePopup::scrollUpButton() const {
-	// Directory uses subButtons[1]; search / email / browser content all
-	// use subButtons[5] (which sits above the taller list LCD area).
-	return _screenState == kDirectory
+	// Directory and help both scroll with the small-LCD arrow pair
+	// (subButtons[1]/[2]); the zoomed email / browser articles use [5]/[6].
+	return (_screenState == kDirectory || isHelpContentView())
 		? _uiclData->subButtons[1]
 		: _uiclData->subButtons[5];
 }
 
 const UICL::ThreeRectWidget &CellPhonePopup::scrollDownButton() const {
-	return _screenState == kDirectory
+	return (_screenState == kDirectory || isHelpContentView())
 		? _uiclData->subButtons[2]
 		: _uiclData->subButtons[6];
 }
@@ -1109,6 +1114,45 @@ void CellPhonePopup::appendDigit(byte slotIndex) {
 	}
 	_dialedNumber += (char)('0' + slotIndex);
 	enterScreenState(kDialing);
+
+	// Auto-dial without a Talk press: a leading '1' is a full 11-digit number;
+	// anything else is a 7-digit local number that dials as soon as it matches
+	// a contact (or reaches 7 digits, ringing through to "We're sorry").
+	if (_noSignal) {
+		return;
+	}
+	const bool longDistance = (_dialedNumber[0] == '1');
+	if (longDistance) {
+		if (_dialedNumber.size() >= 11) {
+			enterScreenState(kPlaceCall);
+		}
+	} else if (findContactByDialBuffer() != -1 || _dialedNumber.size() >= 7) {
+		enterScreenState(kPlaceCall);
+	}
+}
+
+void CellPhonePopup::playDialPadSound(const Common::String &name) {
+	if (name.empty() || name.equalsIgnoreCase("NO SOUND")) {
+		return;
+	}
+	// Dial-pad tones are raw sound filenames, so play them through the phone's
+	// call-sound channel (a single, non-looping cue) instead of the common
+	// sound table, which only holds boot-registered sounds.
+	SoundDescription sound = _uiclData->callSoundTemplate;
+	sound.name = name;
+	sound.numLoops = 1;
+	g_nancy->_sound->loadSound(sound);
+	g_nancy->_sound->playSound(sound);
+}
+
+void CellPhonePopup::playButtonClickSound(const UIButtonRecord &button) {
+	SoundDescription sound = button.clickSound;
+	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND"))
+		return;
+
+	sound.numLoops = 1;
+	g_nancy->_sound->loadSound(sound);
+	g_nancy->_sound->playSound(sound);
 }
 
 bool CellPhonePopup::playSoundIfPresent(const Common::Path &soundName) {
@@ -1526,6 +1570,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		if (overClose) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				playButtonClickSound(closeBtn);
 				input.eatMouseInput();
 				close();
 				return;
@@ -1541,7 +1586,9 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 
 	const Common::Point chunkMouse = mouseToChunkCoords(input.mousePos);
 
-	// Light the up/down arrows on hover in any state that uses them.
+	// Light the up/down arrows on hover in any state that uses them (directory,
+	// link lists, and the content view — help included, which scrolls via the
+	// small-LCD arrow pair).
 	const bool arrowsActive = _screenState == kDirectory || isLinkListMode() ||
 								_screenState == kContentView;
 	const bool overUp = arrowsActive &&
@@ -1782,6 +1829,8 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 			}
 		}
 
+		// scrollUpButton()/scrollDownButton() return the right pair for help
+		// (subButtons[1]/[2]) or the zoomed articles ([5]/[6]).
 		const Common::Rect &upDst = scrollUpButton().destRect;
 		const Common::Rect &downDst = scrollDownButton().destRect;
 
@@ -1804,15 +1853,16 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 				return;
 			}
 		}
+		const bool overUpDown =
+			upDst.contains(chunkMouse) || downDst.contains(chunkMouse);
 
-		// Help draws a real Back button (subButtons[0]); hit-test it so the
-		// visible button and the hotspot line up. Browser / email articles keep
-		// using the wider ribbon-area hotspot.
-		const Common::Rect backHit = isHelpContentView() ? backButtonHitRect() : backLabelHitRect();
+		// Help draws its Back button in the lower ribbon (subButtons[0]); the
+		// zoomed email / browser view draws it at the bottom of the screen
+		// (subButtons[7]). Hit-test the matching button so it lines up with the
+		// visible sprite.
+		const Common::Rect backHit = backButtonHitRect(isHelpContentView() ? 0 : 7);
 		const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
 										chunkMouse.y - _screenPosition.top);
-		const bool overUpDown =
-			upDst.contains(chunkMouse) || downDst.contains(chunkMouse);
 		if (!overUpDown && !backHit.isEmpty() && backHit.contains(popupMouse)) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
@@ -1831,6 +1881,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			playDialPadSound(_uiclData->dialPadSlots[12].soundName);
 			if (!_noSignal) {
 				if (_screenState == kDirectory) {
 					const int contactIdx =
@@ -1872,9 +1923,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
 			const UICL::DialPadSlot &slot = _uiclData->dialPadSlots[newHovered];
 
-			if (!slot.soundName.empty()) {
-				g_nancy->_sound->playSound(slot.soundName);
-			}
+			playDialPadSound(slot.soundName);
 
 			if (newHovered < 10) {
 				if (_screenState == kDirectory || isLinkListMode()) {
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 2e3506027e1..92a7963ed9f 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -110,7 +110,10 @@ private:
 	void drawDirectoryList();
 	void drawDirectoryArrows();
 	void drawWelcomeScreen();
-	void drawBackLabel();
+	// Blit a sub-button's idle sprite at its chunk dest (used for the visible
+	// Back buttons: subButtons[0] on the help page, subButtons[7] in the
+	// zoomed email / browser content view).
+	void drawBackButton(uint subButtonIndex);
 
 	// Generic list renderer used by web / email modes.
 	void drawLinkList();
@@ -157,6 +160,13 @@ private:
 	void resetDialPad();
 	void enterScreenState(ScreenState newState);
 	void appendDigit(byte slotIndex);
+	// Play a dial-pad key's DTMF tone. The name is a raw sound filename, so it
+	// is played through the phone's call-sound channel rather than the common
+	// (boot-registered) sound table.
+	void playDialPadSound(const Common::String &name);
+	// Play a popup button's click sound (the close X), like the inventory
+	// popup. Falls back to the shared button-click slot in the popup header.
+	void playButtonClickSound(const UIButtonRecord &button);
 	bool playSoundIfPresent(const Common::Path &soundName);
 	bool callSoundIsStillPlaying() const;
 	void triggerContactCallSceneChange(uint contactIndex);
@@ -193,8 +203,8 @@ private:
 	bool isContactVisible(const UICL::Contact &c) const;
 	// Popup-local rect of the Back hotspot in directory mode.
 	Common::Rect backLabelHitRect() const;
-	// Popup-local rect of the visible Back button (subButtons[0]).
-	Common::Rect backButtonHitRect() const;
+	// Popup-local rect of a visible Back sub-button (subButtons[index]).
+	Common::Rect backButtonHitRect(uint subButtonIndex) const;
 	// Move the directory selection by delta, scrolling as needed.
 	void moveDirectorySelection(int delta);
 


Commit: e71d9b4bf5f6d32c672cb7e91cd09739bb31c172
    https://github.com/scummvm/scummvm/commit/e71d9b4bf5f6d32c672cb7e91cd09739bb31c172
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T01:15:45+03:00

Commit Message:
NANCY: NANCY10: Disable taskbar buttons when a popup is open

Fix #16910

Changed paths:
    engines/nancy/state/scene.cpp
    engines/nancy/ui/taskbar.cpp
    engines/nancy/ui/taskbar.h


diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index fd29e0d44f9..a361dec0cb9 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1420,7 +1420,18 @@ void Scene::handleInput() {
 	// skipped while the textbox is in open mode (it visually covers the
 	// buttons, so they should not receive hover/clicks).
 	if (!_activeMovie) {
-		if (_taskbar && !_textbox.isFullMode()) {
+		// While a Nancy 10+ popup (inventory / notebook / cellphone /
+		// conversation) is open, the original disables the entire taskbar —
+		// every button, including MENU and HELP. Skip the taskbar input so it
+		// neither hovers nor reacts to clicks until the popup is closed.
+		const bool popupOpen = g_nancy->getGameType() >= kGameTypeNancy10 &&
+								!activePopupConfinement().isEmpty();
+		if (_taskbar) {
+			// Grey out the whole taskbar while a popup is open (matches the
+			// original); restored automatically once the popup closes.
+			_taskbar->setPopupLockout(popupOpen);
+		}
+		if (_taskbar && !_textbox.isFullMode() && !popupOpen) {
 			// MENU and HELP leave gameplay entirely, which would cut off the
 			// taskbar click sound. The original defers the transition until that
 			// sound finishes, so we hold the click here and only switch state
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index 7cb2fce445b..bdca7818376 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -128,6 +128,10 @@ Taskbar::ButtonState Taskbar::restingState(uint index) const {
 	if (index >= TASK::kNumButtons) {
 		return kButtonIdle;
 	}
+	// While a popup is open every button is disabled.
+	if (_popupLockout) {
+		return kButtonDisabled;
+	}
 	if (!_enabled[index]) {
 		return kButtonDisabled;
 	}
@@ -203,6 +207,26 @@ void Taskbar::toggleButton(uint index, bool enabled) {
 	}
 }
 
+void Taskbar::setPopupLockout(bool locked) {
+	if (_popupLockout == locked) {
+		return;
+	}
+	_popupLockout = locked;
+
+	// Repaint every button in its new resting state (all disabled while locked,
+	// back to idle/badge when the popup closes) and drop any lingering hover.
+	_hoveredButton = -1;
+	auto *taskData = GetEngineData(TASK);
+	if (!taskData) {
+		return;
+	}
+	for (uint i = 0; i < TASK::kNumButtons; ++i) {
+		if (isButtonSlotUsed(taskData->buttons[i])) {
+			drawButton(i, restingState(i));
+		}
+	}
+}
+
 void Taskbar::setNotification(uint buttonIndex, uint subCategory) {
 	if (buttonIndex >= TASK::kNumButtons || subCategory >= kNumNotificationSubCategories) {
 		return;
diff --git a/engines/nancy/ui/taskbar.h b/engines/nancy/ui/taskbar.h
index 2ea0310e5bc..8536d3e90a5 100644
--- a/engines/nancy/ui/taskbar.h
+++ b/engines/nancy/ui/taskbar.h
@@ -76,6 +76,10 @@ public:
 	// if none. Cleared on the next call to handleInput().
 	int getClickedButton() const { return _clickedButton; }
 
+	// Grey out and disable every taskbar button while a popup (inventory /
+	// notebook / cellphone / conversation) is open, matching the original.
+	void setPopupLockout(bool locked);
+
 private:
 	enum ButtonState {
 		kButtonIdle         = 0,
@@ -138,6 +142,8 @@ private:
 	ButtonState _buttonStates[6];
 	ButtonOverride _overrides[6];
 	bool _notifications[6][kNumNotificationSubCategories];
+	// True while a popup is open: every button renders disabled and ignores input.
+	bool _popupLockout = false;
 };
 
 } // End of namespace UI


Commit: 11efa8821fdddd15156dd6a77ef371647b5c3385
    https://github.com/scummvm/scummvm/commit/11efa8821fdddd15156dd6a77ef371647b5c3385
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T01:15:46+03:00

Commit Message:
NANCY: NANCY10: Cleanup comments

Changed paths:
    engines/nancy/enginedata.h
    engines/nancy/ui/cellphonepopup.cpp
    engines/nancy/ui/inventorypopup.h


diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 0f07136cee9..40d56017f4c 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -686,9 +686,9 @@ struct UICL : public EngineData {
 	uint16 fontId1 = 0;
 	uint16 fontId2 = 0;
 
-	Common::Path outgoingRingSound;           // Process case 2 (post-dial ring)
-	Common::Path pickupSound;                 // Process cases 0/4 (call connect)
-	Common::Path invalidNumberSound;          // Process case 7 (try again)
+	Common::Path outgoingRingSound;           // post-dial ring
+	Common::Path pickupSound;                 // call connect
+	Common::Path invalidNumberSound;          // try again
 
 	uint16 contactCount = 0;
 	Common::Array<Contact> contacts;
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 1a421580b4e..d03bdd5e232 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -1004,9 +1004,8 @@ void CellPhonePopup::drawWelcomeScreen() {
 }
 
 void CellPhonePopup::drawBackButton(uint subButtonIndex) {
-	// subButtons[0] (original CUIButton 0x10) is the Back button that returns a
-	// sub-screen to the main view; subButtons[7] (0x17) is the equivalent Back
-	// button at the bottom of the zoomed email / browser content view.
+	// subButtons[0] is the Back button in the lower ribbon (help / sub-screens);
+	// subButtons[7] is the Back button at the bottom of the zoomed content view.
 	const UICL::ThreeRectWidget &back = _uiclData->subButtons[subButtonIndex];
 	if (back.srcRectIdle.isEmpty() || back.destRect.isEmpty()) {
 		return;
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
index e18896e7a5e..7162e96e628 100644
--- a/engines/nancy/ui/inventorypopup.h
+++ b/engines/nancy/ui/inventorypopup.h
@@ -87,9 +87,8 @@ private:
 	void setActiveFilterIndex(uint index);
 
 	// Play a popup button's click sound (the filter tabs and the close X),
-	// mirroring the original's per-CUIButton click cue. Falls back to the
-	// shared button-click slot in the popup header when the button carries no
-	// sound of its own.
+	// falling back to the shared button-click slot in the popup header when the
+	// button carries no sound of its own.
 	void playButtonClickSound(const UIButtonRecord &button);
 
 	// Apply the current scrollbar position to the page index, clamping


Commit: a46f421625b28d4877a3f35e02b394ed1d321bad
    https://github.com/scummvm/scummvm/commit/a46f421625b28d4877a3f35e02b394ed1d321bad
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T01:15:47+03:00

Commit Message:
NANCY: NANCY10: Notebook fixes

- Add missing journal and tasklist header - fix #16907
- Correct text layout - fix #16909
- Implement clicking on tasklist checkboxes (the ones that are done and
  can be checked), and the ones that aren't done yet - fix #16911

Changed paths:
    engines/nancy/misc/hypertext.cpp
    engines/nancy/misc/hypertext.h
    engines/nancy/ui/notebookpopup.cpp
    engines/nancy/ui/notebookpopup.h


diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index 56f7073eea1..6e737641559 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -76,6 +76,10 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 
 	_numDrawnLines = 0;
 
+	if (_recordMarkHotspots) {
+		_markHotspots.clear();
+	}
+
 	if (!_imageName.empty()) {
 		g_nancy->_resource->loadImage(_imageName, image);
 	}
@@ -351,6 +355,10 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 						// For now we do not check if we need to go to new line; neither does the original
 						_fullSurface.blitFrom(g_nancy->_graphics->_object0, markSrc, markDest);
 
+						if (_recordMarkHotspots) {
+							_markHotspots.push_back(markDest);
+						}
+
 						horizontalOffset += markDest.width() + 2;
 						break;
 					}
diff --git a/engines/nancy/misc/hypertext.h b/engines/nancy/misc/hypertext.h
index 3e683ea8560..c7565dd6c45 100644
--- a/engines/nancy/misc/hypertext.h
+++ b/engines/nancy/misc/hypertext.h
@@ -64,6 +64,12 @@ protected:
 	Common::Array<Common::String> _textLines;
 	Common::Array<Common::Rect> _hotspots;
 
+	// When set, drawAllText records the dest rect of every drawn mark glyph
+	// (in _fullSurface coords, in draw order). The notebook uses this to hit-
+	// test the tasklist's clickable checkboxes.
+	bool _recordMarkHotspots = false;
+	Common::Array<Common::Rect> _markHotspots;
+
 	// Data for displaying images inside text; used in Hypertext
 	Common::Path _imageName;
 	Common::Array<uint16> _imageLineIDs;
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 7895b960474..167b54530fb 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -19,6 +19,9 @@
  *
  */
 
+#include "common/random.h"
+#include "common/system.h"
+
 #include "engines/nancy/cursor.h"
 #include "engines/nancy/font.h"
 #include "engines/nancy/graphics.h"
@@ -93,6 +96,7 @@ void NotebookPopup::init() {
 
 	drawBackground();
 	drawTabs();
+	drawCaption();
 	drawContent();
 	drawForeground();
 
@@ -106,6 +110,14 @@ void NotebookPopup::registerGraphics() {
 	RenderObject::registerGraphics();
 }
 
+void NotebookPopup::updateGraphics() {
+	// Fire the deferred "I'm finished with that" line once its delay elapses.
+	if (_completeVoiceTime != 0 && g_system->getMillis() >= _completeVoiceTime) {
+		_completeVoiceTime = 0;
+		playCheckboxSound(true);
+	}
+}
+
 void NotebookPopup::open() {
 	if (_isVisible)
 		return;
@@ -219,6 +231,21 @@ void NotebookPopup::drawTabs() {
 	_needsRedraw = true;
 }
 
+void NotebookPopup::drawCaption() {
+	if ((uint)_activeTab >= _uinbData->tabCaptionSrcRects.size()) {
+		return;
+	}
+	const Common::Rect &spr = _uinbData->tabCaptionSrcRects[_activeTab];
+	if (spr.isEmpty() || _uinbData->tabCaptionDestRect.isEmpty()) {
+		return;
+	}
+
+	// Use the same game-frame-aware conversion as the tabs / close button so
+	// the caption lines up with them when the popup overlays the game frame.
+	const Common::Rect dstLocal = toPopupLocal(_uinbData->tabCaptionDestRect, false);
+	_drawSurface.blitFrom(_overlayImage, spr, Common::Point(dstLocal.left, dstLocal.top));
+}
+
 void NotebookPopup::drawTab(uint index, bool drawHover) {
 	const UIButtonSlot &tab = _uinbData->tabs[index];
 	if (!tab.enabled)
@@ -310,6 +337,7 @@ void NotebookPopup::handleInput(NancyInput &input) {
 		if (overClose) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				playButtonClickSound(closeBtn);
 				input.eatMouseInput();
 				close();
 				return;
@@ -317,6 +345,21 @@ void NotebookPopup::handleInput(NancyInput &input) {
 		}
 	}
 
+	// Tasklist checkboxes: an unchecked box gets a hotspot cursor and, on
+	// click, either checks off (event flag satisfied) or plays the rejection
+	// line. Checked before the tabs since the boxes sit inside the text area.
+	for (uint k = 0; k < _checkboxRects.size(); ++k) {
+		if (!_checkboxRects[k].contains(localMouse)) {
+			continue;
+		}
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			toggleCheckbox(_checkboxEntryIndices[k]);
+			input.eatMouseInput();
+		}
+		return;
+	}
+
 	// Tab hover + click. Mirrors the inventory popup's filter-tab
 	// handling so the same enter/exit redraw semantics apply: track
 	// whether any tab is hovered, restore non-hovered tabs to their
@@ -361,12 +404,7 @@ void NotebookPopup::handleInput(NancyInput &input) {
 				_scrollPos = 0.0f;
 				_scrollbarDragging = false;
 
-				// Play the page-flip sound (first slot of either
-				// actionable or no-action set; both have 3 alternates).
-				const Common::Path &soundName = _uinbData->noActionClickSounds[0];
-				if (!soundName.empty()) {
-					g_nancy->_sound->playSound(soundName.toString());
-				}
+				playButtonClickSound(tab.button);
 
 				refreshContent();
 			}
@@ -389,6 +427,7 @@ void NotebookPopup::refreshContent() {
 	// always sit visually above the text layer.
 	drawBackground();
 	drawTabs();
+	drawCaption();
 	drawContent();
 	drawForeground();
 }
@@ -442,12 +481,14 @@ void NotebookPopup::buildTextLines() {
 			}
 			if (markValue >= 1 && markValue <= 5) {
 				body = Common::String::format("<%u>", markValue) + body;
+				// Record which entry this mark belongs to so the recorded
+				// mark hotspots (in the same draw order) map back to entries.
+				_markEntryIndices.push_back((uint)i);
 			}
 		}
 
-		if (i > 0) {
-			body += "<n>";
-		}
+		// Entries concatenate directly; each entry's text already ends with its
+		// own newline, so an extra <n> here would double the inter-entry gap.
 		combined += body;
 	}
 
@@ -461,13 +502,21 @@ void NotebookPopup::drawContent() {
 		return;
 	}
 
-	// textRect from UINB is in chunk coords (relative to normalDestRect);
-	// convert to popup-local for the on-surface blit destination.
-	Common::Rect localTextRect = _uinbData->textRect;
-	localTextRect.translate(-_uinbData->header.normalDestRect.left,
-							-_uinbData->header.normalDestRect.top);
+	// Convert the text rect to popup-local with the same game-frame-aware
+	// conversion the tabs / close button use, so text and caption stay aligned
+	// with them when the popup overlays the game frame.
+	const Common::Rect localTextRect = toPopupLocal(_uinbData->textRect, false);
 
 	HypertextParser::clear();
+	_checkboxRects.clear();
+	_checkboxEntryIndices.clear();
+	_markEntryIndices.clear();
+
+	// Only the Tasklist has clickable checkboxes; record their glyph rects.
+	const UIButtonSlot &activeTab = _uinbData->tabs[_activeTab];
+	const bool tasksTab = activeTab.enabled && activeTab.id != 1;
+	_recordMarkHotspots = tasksTab;
+
 	buildTextLines();
 
 	// Chunk's textRect already provides top padding from the chrome.
@@ -495,8 +544,109 @@ void NotebookPopup::drawContent() {
 	_drawSurface.blitFrom(_fullSurface, srcSlice,
 							Common::Point(localTextRect.left, localTextRect.top));
 
+	if (tasksTab) {
+		buildCheckboxRects(localTextRect, scrollY, visibleH);
+	}
+
 	_needsRedraw = true;
 }
 
+void NotebookPopup::buildCheckboxRects(const Common::Rect &localTextRect, int scrollY, int visibleH) {
+	JournalData *journalData = (JournalData *)NancySceneState.getPuzzleData(JournalData::getTag());
+	if (!journalData || !journalData->journalEntries.contains(kNotebookTabTasks)) {
+		return;
+	}
+	const Common::Array<JournalData::Entry> &entries = journalData->journalEntries[kNotebookTabTasks];
+
+	const Common::Rect visibleWindow(localTextRect.left, localTextRect.top,
+										localTextRect.left + localTextRect.width(),
+										localTextRect.top + visibleH);
+
+	const uint count = MIN(_markHotspots.size(), _markEntryIndices.size());
+	for (uint k = 0; k < count; ++k) {
+		const uint entryIndex = _markEntryIndices[k];
+		// Only unchecked boxes (mark 7) are clickable.
+		if (entryIndex >= entries.size() || entries[entryIndex].mark != 7) {
+			continue;
+		}
+
+		// Mark rects are in _fullSurface coords; map to popup-local (offset by
+		// the text rect, minus the scroll) and widen the hit area to the right.
+		Common::Rect box = _markHotspots[k];
+		box.translate(localTextRect.left, localTextRect.top - scrollY);
+		box.right += 20;
+
+		const Common::Rect clipped = box.findIntersectingRect(visibleWindow);
+		if (clipped.isEmpty()) {
+			continue;
+		}
+		_checkboxRects.push_back(clipped);
+		_checkboxEntryIndices.push_back(entryIndex);
+	}
+}
+
+void NotebookPopup::toggleCheckbox(uint entryIndex) {
+	JournalData *journalData = (JournalData *)NancySceneState.getPuzzleData(JournalData::getTag());
+	if (!journalData || !journalData->journalEntries.contains(kNotebookTabTasks)) {
+		return;
+	}
+	Common::Array<JournalData::Entry> &entries = journalData->journalEntries[kNotebookTabTasks];
+	if (entryIndex >= entries.size() || entries[entryIndex].mark != 7) {
+		return;
+	}
+
+	// For a clickable task, sceneID doubles as the completion event-flag index
+	// (-1 = no requirement). The box can be checked off only once that flag is
+	// set; otherwise Nancy says she isn't finished yet.
+	const int16 flag = (int16)entries[entryIndex].sceneID;
+	const bool canComplete = (flag == -1) || NancySceneState.getEventFlag(flag, g_nancy->_true);
+	if (canComplete) {
+		entries[entryIndex].mark = 8;
+		refreshContent();
+		// A check-off plays an immediate click, then Nancy's spoken line a beat
+		// later (deferred so the two cues don't step on each other).
+		playButtonClickSound(_uinbData->header.secondaryButton);
+		_completeVoiceTime = g_system->getMillis() + 400;
+	} else {
+		playCheckboxSound(false);
+	}
+}
+
+void NotebookPopup::playButtonClickSound(const UIButtonRecord &button) {
+	SoundDescription sound = button.clickSound;
+	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND")) {
+		// Fall back to the header's shared button-click slot (2; 0/1 = open/close).
+		sound = _uinbData->header.sounds[2];
+	}
+	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND")) {
+		return;
+	}
+	g_nancy->_sound->loadSound(sound);
+	g_nancy->_sound->playSound(sound);
+}
+
+void NotebookPopup::playCheckboxSound(bool actionable) {
+	const Common::Path *set = actionable ? _uinbData->actionableClickSounds
+										 : _uinbData->noActionClickSounds;
+
+	// Pick a random variant, falling back to any valid one.
+	const uint start = g_nancy->_randomSource->getRandomNumber(UINB::kNumPageSoundsPerSet - 1);
+	for (uint n = 0; n < UINB::kNumPageSoundsPerSet; ++n) {
+		const Common::String name = set[(start + n) % UINB::kNumPageSoundsPerSet].toString();
+		if (name.empty() || name.equalsIgnoreCase("NO SOUND")) {
+			continue;
+		}
+		// The names are raw filenames; play them on the checkbox sound's
+		// channel / volume, taken from the close button's click sound (the
+		// header sound slots are all "NO SOUND" for the notebook).
+		SoundDescription sound = _uinbData->header.secondaryButton.clickSound;
+		sound.name = name;
+		sound.numLoops = 1;
+		g_nancy->_sound->loadSound(sound);
+		g_nancy->_sound->playSound(sound);
+		return;
+	}
+}
+
 } // End of namespace UI
 } // End of namespace Nancy
diff --git a/engines/nancy/ui/notebookpopup.h b/engines/nancy/ui/notebookpopup.h
index e1eead411d2..78f2f1a57ae 100644
--- a/engines/nancy/ui/notebookpopup.h
+++ b/engines/nancy/ui/notebookpopup.h
@@ -42,6 +42,7 @@ public:
 
 	void init() override;
 	void registerGraphics() override;
+	void updateGraphics() override;
 	void handleInput(NancyInput &input);
 
 	void open();
@@ -66,6 +67,9 @@ private:
 	void drawBackground();
 	void drawTabs();
 	void drawTab(uint index, bool drawHover = false);
+	// Blit the active tab's title image ("CASE JOURNAL" / "TASKS") into the
+	// header strip above the text area.
+	void drawCaption();
 	void drawContent();
 	// Paint foreground widgets (close button, scrollbar) on top of the
 	// already-drawn background + content layers.
@@ -73,6 +77,10 @@ private:
 	void drawCloseButton(bool hovered);
 	void drawScrollbar(UIButtonState state);
 
+	// Play a popup button's click sound (close X and the tab buttons), like the
+	// inventory popup. Falls back to the shared button-click slot in the header.
+	void playButtonClickSound(const UIButtonRecord &button);
+
 	// Returns the on-popup-surface bounding rect of the slider thumb at
 	// the current scroll position (in popup-local coords).
 	Common::Rect computeThumbRect() const;
@@ -85,6 +93,16 @@ private:
 	// entries.
 	void buildTextLines();
 
+	// Tasklist checkboxes. Rebuild the popup-local hit rects for the clickable
+	// (unchecked) checkboxes from the mark hotspots recorded during drawContent.
+	void buildCheckboxRects(const Common::Rect &localTextRect, int scrollY, int visibleH);
+	// Check off a task if its completion event flag is set, else play the
+	// "not finished yet" line.
+	void toggleCheckbox(uint entryIndex);
+	// Play a random checkbox voice line: actionable = "finished with that",
+	// !actionable = "can't check that off yet".
+	void playCheckboxSound(bool actionable);
+
 	const UINB *_uinbData;
 
 	Graphics::ManagedSurface _overlayImage;     // popup background image
@@ -100,6 +118,18 @@ private:
 	bool _scrollbarHovered = false;
 	int _scrollbarGrabOffset = 0;
 
+	// Deferred "I'm finished with that" voice: checking a task off plays an
+	// immediate click, then this fires the spoken line a beat later (0 = none).
+	uint32 _completeVoiceTime = 0;
+
+	// Tasklist checkboxes: popup-local hit rects for the clickable (unchecked)
+	// boxes and the task-entry index each maps to. Rebuilt every drawContent().
+	Common::Array<Common::Rect> _checkboxRects;
+	Common::Array<uint> _checkboxEntryIndices;
+	// Task-entry index for each mark buildTextLines emits, in draw order, so
+	// the recorded mark hotspots can be mapped back to their entries.
+	Common::Array<uint> _markEntryIndices;
+
 	// journalEntries HashMap keys: _surfaceID = 3 holds task entries,
 	// _surfaceID = 4 holds journal entries.
 	enum NotebookTab {




More information about the Scummvm-git-logs mailing list