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
This commit is contained in:
SomeTroglodyte 2023-04-16 20:20:56 +02:00 committed by GitHub
parent ab1d823477
commit 4f30d27d0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 30 deletions

View File

@ -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) =

View File

@ -108,11 +108,13 @@ data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : Not
constructor(locations: Sequence<Vector2>) : 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
}
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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)

View File

@ -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<Actor?>? = 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<ListItem>? = 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<Notification>()
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<CategoryHeader>()
val itemWidths = mutableListOf<Float>()
@ -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()
}
}
}

View File

@ -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 {