From 4f30d27d0b92adcbd38e247bb95abf0a5d9763b5 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 16 Apr 2023 20:20:56 +0200 Subject: [PATCH] Notifications can be "selected" (#9182) * Allow "selecting" notifications * NotificationsScroll remembers position relative to topRight * NotificationsScroll tries to scroll selection into view * Fix notification selection and scroll-into-view * User option to control enlarging selected notifications * Post-merge missed changes * Move, flip and reword "Enlarge" option --- .../jsons/translations/template.properties | 2 + .../unciv/logic/civilization/Notification.kt | 6 +- .../com/unciv/models/metadata/GameSettings.kt | 3 + .../unciv/ui/popups/options/AdvancedTab.kt | 8 ++ .../NotificationsOverviewTable.kt | 11 +- .../worldscreen/NotificationsScroll.kt | 122 ++++++++++++++---- .../ui/screens/worldscreen/WorldScreen.kt | 2 +- 7 files changed, 124 insertions(+), 30 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 6425276d03..2bc69260df 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -785,6 +785,8 @@ Font size multiplier = Default Font = Enable Easter Eggs = +Enlarge selected notifications = + Order trade offers by amount = Enable display cutout (requires restart) = diff --git a/core/src/com/unciv/logic/civilization/Notification.kt b/core/src/com/unciv/logic/civilization/Notification.kt index 830cbfb7df..50958f4abf 100644 --- a/core/src/com/unciv/logic/civilization/Notification.kt +++ b/core/src/com/unciv/logic/civilization/Notification.kt @@ -108,11 +108,13 @@ data class LocationAction(var locations: ArrayList = ArrayList()) : Not constructor(locations: Sequence) : this(locations.toCollection(ArrayList())) constructor(vararg locations: Vector2?) : this(locations.asSequence().filterNotNull()) + @Transient + private var index = 0 + override fun execute(worldScreen: WorldScreen) { if (locations.isNotEmpty()) { - var index = locations.indexOf(worldScreen.mapHolder.selectedTile?.position) - index = ++index % locations.size // cycle through tiles worldScreen.mapHolder.setCenterPosition(locations[index], selectUnit = false) + index = ++index % locations.size // cycle through tiles } } } diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index e3e3ba261d..2446610ceb 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -119,6 +119,9 @@ class GameSettings { /** NotificationScroll on Word Screen visibility control - mapped to NotificationsScroll.UserSetting enum */ var notificationScroll: String = "" + /** If on, selected notifications are drawn enlarged with wider padding */ + var enlargeSelectedNotification = true + /** used to migrate from older versions of the settings */ var version: Int? = null diff --git a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt index 276642a2e5..15d22b47da 100644 --- a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt +++ b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt @@ -76,6 +76,8 @@ fun advancedTab( addSetUserId(this, settings) addEasterEggsCheckBox(this, settings) + + addEnlargeNotificationsCheckBox(this, settings) } private fun addCutoutCheckbox(table: Table, optionsPopup: OptionsPopup) { @@ -346,3 +348,9 @@ private fun addEasterEggsCheckBox(table: Table, settings: GameSettings) { val checkbox = "Enable Easter Eggs".toCheckBox(settings.enableEasterEggs) { settings.enableEasterEggs = it } table.add(checkbox).colspan(2).row() } + +private fun addEnlargeNotificationsCheckBox(table: Table, settings: GameSettings) { + val checkbox = "Enlarge selected notifications" + .toCheckBox(settings.enlargeSelectedNotification) { settings.enlargeSelectedNotification = it } + table.add(checkbox).colspan(2).row() +} diff --git a/core/src/com/unciv/ui/screens/overviewscreen/NotificationsOverviewTable.kt b/core/src/com/unciv/ui/screens/overviewscreen/NotificationsOverviewTable.kt index f2d7f2f53e..9baac6adda 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/NotificationsOverviewTable.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/NotificationsOverviewTable.kt @@ -6,6 +6,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.GUI import com.unciv.UncivGame import com.unciv.logic.civilization.Civilization +import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.Notification import com.unciv.logic.civilization.NotificationCategory import com.unciv.ui.components.ColorMarkupLabel @@ -90,10 +91,12 @@ class NotificationsOverviewTable( notificationTable.add(label).width(worldScreen.stage.width/2 - iconSize * notification.icons.size) notificationTable.background = BaseScreen.skinStrings.getUiBackground("OverviewScreen/NotificationOverviewTable/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape) notificationTable.touchable = Touchable.enabled - notificationTable.onClick { - UncivGame.Current.resetToWorldScreen() - notification.action?.execute(worldScreen) - } + if (notification.action != null) + notificationTable.onClick { + worldScreen.notificationsScroll.oneTimeNotification = notification + UncivGame.Current.resetToWorldScreen() + notification.action?.execute(worldScreen) + } notification.addNotificationIconsTo(notificationTable, worldScreen.gameInfo.ruleset, iconSize) diff --git a/core/src/com/unciv/ui/screens/worldscreen/NotificationsScroll.kt b/core/src/com/unciv/ui/screens/worldscreen/NotificationsScroll.kt index ebe5d0402e..2d04660de6 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/NotificationsScroll.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/NotificationsScroll.kt @@ -13,6 +13,7 @@ import com.badlogic.gdx.utils.Align import com.unciv.GUI import com.unciv.logic.civilization.Notification import com.unciv.logic.civilization.NotificationCategory +import com.unciv.ui.components.AutoScrollPane as ScrollPane import com.unciv.ui.components.ColorMarkupLabel import com.unciv.ui.components.WrappableLabel import com.unciv.ui.components.extensions.onClick @@ -21,7 +22,6 @@ import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.basescreen.BaseScreen -import com.unciv.ui.components.AutoScrollPane as ScrollPane /*TODO * Un-hiding the notifications when new ones arrive is a little pointless due to Categories: @@ -51,6 +51,12 @@ class NotificationsScroll( const val categoryTopPad = 15f /** Spacing between rightmost Label edge and right Screen limit */ const val rightPadToScreenEdge = 10f + /** Extra right padding when there's a selection */ + const val selectionExtraRightPad = 12f + /** Padding within ListItem, included in clickable area, except to the right */ + const val listItemPad = 3f + /** Top/Bottom padding within ListItem replaces [listItemPad] for the [highlightNotification] */ + const val selectedListItemPad = 15f /** Extra spacing between the outer edges of the category header decoration lines and * the left and right edges of the widest notification label - this is the background's * edge radius and looks (subjectively) nice. */ @@ -59,8 +65,11 @@ class NotificationsScroll( const val restoreButtonSize = 42f /** Distance of restore button to TileInfoTable and screen edge */ const val restoreButtonPad = 12f + /** Background tint for [oneTimeNotification] */ + private val oneTimeNotificationColor = Color.valueOf("fceea8") } + //region private fields private var notificationsHash: Int = 0 private var notificationsTable = Table() @@ -68,6 +77,7 @@ class NotificationsScroll( private var bottomSpacerCell: Cell? = null private val maxEntryWidth = worldScreen.stage.width * maxWidthOfStage * inverseScaleFactor + /** For category header decoration lines */ private val minCategoryLineWidth = worldScreen.stage.width * 0.075f /** Show restoreButton when less than this much of the pane is left */ @@ -77,6 +87,20 @@ class NotificationsScroll( private var userSetting = UserSetting.Visible private var userSettingChanged = false + private var enlargeHighlight = false + + /** onClick sets this to request highlighting on the next update (which it then triggers) */ + private var clickedNotification: Notification? = null + /** Set _only_ during updateContent to draw the actual highlighting */ + private var highlightNotification: Notification? = null + /** Set _only_ during updateContent to draw the highlighted entry with a colored background */ + private var coloredHighlight = false + /** Used once after updateContent to scroll the highlighted notification into view */ + private var selectedCell: Cell? = null + //endregion + + /** Display one additional notification once, to re-show an entry from the history in overview */ + var oneTimeNotification: Notification? = null init { actor = notificationsTable.right() @@ -111,12 +135,14 @@ class NotificationsScroll( coveredNotificationsBottom: Float ) { if (getUserSettingCheckDisabled()) return + enlargeHighlight = GUI.getSettings().enlargeSelectedNotification - val previousScrollX = when { - isScrollingDisabledX -> width // switching from Permanent - scrollX and maxX are 0 - userSetting.static -> maxX // Permanent: fully visible - notificationsTable.hasChildren() -> scrollX // save current scroll - else -> 0f // Swiching Hidden to Dynamic - animate "in" only + // Remember scroll position _relative to topRight_ + val previousScrollXinv = when { + isScrollingDisabledX -> 0f // switching from Permanent - scrollX and maxX are 0 + userSetting.static -> 0f // Permanent: fully visible + notificationsTable.hasChildren() -> maxX - scrollX // save current scroll + else -> maxX // Swiching Hidden to Dynamic - animate "in" only } val previousScrollY = scrollY @@ -128,8 +154,17 @@ class NotificationsScroll( updateSpacers(coveredNotificationsTop, coveredNotificationsBottom) } - scrollX = previousScrollX - scrollY = previousScrollY + scrollX = maxX - previousScrollXinv + scrollY = if (selectedCell == null) { + previousScrollY + } else selectedCell!!.let { + val actualBottom = (it.actorY + notificationsTable.y) * scaleFactor + val actualTop = (it.actorY + it.actorHeight + notificationsTable.y) * scaleFactor + val fullyVisible = actualBottom >= coveredNotificationsBottom && actualTop <= stage.height - coveredNotificationsTop + val centeredBottom = (stage.height - coveredNotificationsTop + coveredNotificationsBottom - it.actorHeight * scaleFactor) / 2 + val centeredScrollY = centeredBottom * inverseScaleFactor - it.actorY + maxY + if (fullyVisible) previousScrollY else centeredScrollY + } updateVisualScroll() applyUserSettingChange() @@ -152,14 +187,36 @@ class NotificationsScroll( coveredNotificationsTop: Float, coveredNotificationsBottom: Float ): Boolean { - // no news? - keep our list as it is - val newHash = notifications.hashCode() - if (notificationsHash == newHash) return false + selectedCell = null + + // Detect what to draw and if there's any changes part 1 + if (oneTimeNotification == null && clickedNotification != null) + oneTimeNotification = clickedNotification // reselecting can keep a "one-time" in the list + val newHash = notifications.hashCode() + oneTimeNotification.hashCode() * 31 + + // Determine highlight + coloredHighlight = false + var additionalNotification = emptySequence() + highlightNotification = if (oneTimeNotification == null) clickedNotification + else oneTimeNotification!!.apply { + if (this !in notifications) { + additionalNotification = sequenceOf(this) + coloredHighlight = true + } + oneTimeNotification = null + } + clickedNotification = null + + // Detect change part 2 - early exit if no re-render needed + // Note no change detection for highlightNotification - if there's a selection we always + // need the redraw to determine the selectedCell, to enable scroll-into-view + if (notificationsHash == newHash && highlightNotification == null) return false notificationsHash = newHash + // Rebuild the notifications list notificationsTable.clear() notificationsTable.pack() // forget last width! - if (notifications.isEmpty()) return true + if (notifications.isEmpty() && additionalNotification.none()) return true val categoryHeaders = mutableListOf() val itemWidths = mutableListOf() @@ -170,7 +227,7 @@ class NotificationsScroll( val backgroundDrawable = BaseScreen.skinStrings.getUiBackground("WorldScreen/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape) - val orderedNotifications = notifications.asReversed() + val orderedNotifications = (additionalNotification + notifications.asReversed()) .groupBy { NotificationCategory.safeValueOf(it.category) ?: NotificationCategory.General } .toSortedMap() // This sorts by Category ordinal, so far intentional - the order of the grouped lists are unaffected for ((category, categoryNotifications) in orderedNotifications) { @@ -184,7 +241,9 @@ class NotificationsScroll( for (notification in categoryNotifications) { val item = ListItem(notification, backgroundDrawable) itemWidths.add(item.itemWidth) - notificationsTable.add(item).right().row() + val itemCell = notificationsTable.add(item) + if (notification == highlightNotification) selectedCell = itemCell + itemCell.right().row() } } @@ -198,6 +257,7 @@ class NotificationsScroll( bottomSpacerCell = notificationsTable.add() .height(coveredNotificationsBottom * inverseScaleFactor).expandY() notificationsTable.row() + highlightNotification = null // no longer needed return true } @@ -220,10 +280,14 @@ class NotificationsScroll( add(label) captionWidth = prefWidth // of this wrapper including background rims captionWidth - }).pad(3f) + }).pad(listItemPad) + val rightPad = categoryHorizontalPad + rightPadToScreenEdge + ( + if (!enlargeHighlight || highlightNotification == null) 0f + else selectionExtraRightPad + ) rightLineCell = add(ImageGetter.getWhiteDot()) .minHeight(2f).width(minCategoryLineWidth) - .padRight(categoryHorizontalPad + rightPadToScreenEdge) + .padRight(rightPad) } /** Equalizes width by adjusting length of the decoration lines. @@ -247,11 +311,19 @@ class NotificationsScroll( val itemWidth: Float init { - val listItem = Table() - listItem.background = backgroundDrawable + val isSelected = notification === highlightNotification // Notification does not implement equality contract + val isEnlarged = isSelected && enlargeHighlight + val labelFontSize = if (isEnlarged) fontSize + fontSize / 2 else fontSize + val itemIconSize = if (isEnlarged) iconSize * 1.5f else iconSize + val topBottomPad = if (isEnlarged) selectedListItemPad else listItemPad - val maxLabelWidth = maxEntryWidth - (iconSize + 5f) * notification.icons.size - 10f - val label = WrappableLabel(notification.text, maxLabelWidth, Color.BLACK, fontSize, hideIcons = true) + val listItem = Table() + listItem.background = if (!isSelected || !coloredHighlight) backgroundDrawable else { + BaseScreen.skinStrings.getUiBackground("WorldScreen/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape, oneTimeNotificationColor) + } + + val maxLabelWidth = maxEntryWidth - (itemIconSize + 5f) * notification.icons.size - 10f + val label = WrappableLabel(notification.text, maxLabelWidth, Color.BLACK, labelFontSize, hideIcons = true) label.setAlignment(Align.center) if (label.prefWidth > maxLabelWidth * scaleFactor) { // can't explain why the comparison needs scaleFactor label.wrap = true @@ -260,15 +332,19 @@ class NotificationsScroll( listItem.add(label).padRight(10f) } - notification.addNotificationIconsTo(listItem, worldScreen.gameInfo.ruleset, iconSize) + notification.addNotificationIconsTo(listItem, worldScreen.gameInfo.ruleset, itemIconSize) itemWidth = listItem.prefWidth // includes the background NinePatch's leftWidth+rightWidth // using a large click area with no gap in between each message item. // this avoids accidentally clicking in between the messages, resulting in a map click - add(listItem).pad(3f, 3f, 3f, rightPadToScreenEdge) + add(listItem).pad(topBottomPad, listItemPad, topBottomPad, rightPadToScreenEdge) touchable = Touchable.enabled - onClick { notification.action?.execute(worldScreen) } + onClick { + notification.action?.execute(worldScreen) + clickedNotification = notification + GUI.setUpdateWorldOnNextRender() + } } } diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index eea4972206..96d7e19597 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -113,7 +113,7 @@ class WorldScreen( private val zoomController = ZoomButtonPair(mapHolder) internal val minimapWrapper = MinimapHolder(mapHolder) private val bottomTileInfoTable = TileInfoTable(viewingCiv) - private val notificationsScroll = NotificationsScroll(this) + internal val notificationsScroll = NotificationsScroll(this) internal val nextTurnButton = NextTurnButton() private val statusButtons = StatusButtons(nextTurnButton) private val tutorialTaskTable = Table().apply {