Fixed issues with shake

This commit is contained in:
Robinson 2023-02-14 13:05:06 +01:00
parent 097411136b
commit b67eea2b7a
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
5 changed files with 162 additions and 152 deletions

View File

@ -45,8 +45,8 @@ internal class AppAccessor : TweenAccessor<AppNotify> {
} }
SHAKE -> { SHAKE -> {
returnValues[0] = target.x.toFloat() returnValues[0] = target.shakeX.toFloat()
returnValues[1] = target.y.toFloat() returnValues[1] = target.shakeY.toFloat()
return 2 return 2
} }
@ -76,7 +76,7 @@ internal class AppAccessor : TweenAccessor<AppNotify> {
} }
SHAKE -> { SHAKE -> {
target.setLocationInternal(newValues[0].toInt(), newValues[1].toInt()) target.setLocationShake(newValues[0].toInt(), newValues[1].toInt())
return return
} }

View File

@ -72,10 +72,12 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
// for the progress bar. we directly draw this onscreen // for the progress bar. we directly draw this onscreen
// non-volatile because it's always accessed in the active render thread // non-volatile because it's always accessed in the active render thread
private var prevProgress = 0
override var progress = 0 override var progress = 0
set(value) {
@Volatile prevProgress = field
var mouseOver = false field = value
}
override var shakeTween: Tween<AppNotify>? = null override var shakeTween: Tween<AppNotify>? = null
override var moveTween: Tween<AppNotify>? = null override var moveTween: Tween<AppNotify>? = null
@ -88,6 +90,11 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
override var anchorX = 0 override var anchorX = 0
override var anchorY = 0 override var anchorY = 0
// the ONLY reason "shake" works, is because we configure the target X/Y location for moving based on where we WANT the popup to go
// and completely ignoring its current position
var shakeX = 0
var shakeY = 0
@Volatile @Volatile
var mouseY = 0 var mouseY = 0
@ -99,6 +106,9 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
// this is on the swing EDT // this is on the swing EDT
init { init {
addMouseListener(mouseListener)
addMouseMotionListener(mouseListener)
val actualSize = Dimension(Notify.WIDTH, Notify.HEIGHT) val actualSize = Dimension(Notify.WIDTH, Notify.HEIGHT)
preferredSize = actualSize preferredSize = actualSize
@ -110,10 +120,6 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
background = notification.theme.panel_BG background = notification.theme.panel_BG
addMouseListener(mouseListener)
addMouseMotionListener(mouseListener)
idAndPosition = parent.name + ":" + notification.position idAndPosition = parent.name + ":" + notification.position
anchorX = getAnchorX(notification.position, parent.bounds) anchorX = getAnchorX(notification.position, parent.bounds)
@ -152,14 +158,6 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
refresh() refresh()
} }
override fun getX(): Int {
return super.getX()
}
override fun getY(): Int {
return super.getY()
}
override fun refresh() { override fun refresh() {
cachedImage = renderBackgroundInfo(notification.title, notification.text, notification.theme, notification.image) cachedImage = renderBackgroundInfo(notification.title, notification.text, notification.theme, notification.image)
cachedClose = renderCloseButton(notification.theme, false) cachedClose = renderCloseButton(notification.theme, false)
@ -173,16 +171,8 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
// we cache the text + image (to an image), the two stats of the close "button" and then always render the close + progressbar // we cache the text + image (to an image), the two stats of the close "button" and then always render the close + progressbar
// use our cached image, so we don't have to re-render text/background/etc
try { try {
g.drawImage(cachedImage, 0, 0, null) draw(g)
if (mouseOver && mouseX >= 280 && mouseY <= 20) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
} catch (ignored: Exception) { } catch (ignored: Exception) {
// have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04): // have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04):
// java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148) // java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148)
@ -203,25 +193,44 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
// try to draw again // try to draw again
try { try {
g.drawImage(cachedImage, 0, 0, null) draw(g)
if (mouseOver && mouseX >= NotifyType.closeX && mouseY <= 20) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
} catch (ignored2: Exception) { } catch (ignored2: Exception) {
} }
} }
// the progress bar can change, so we always draw it every time // the progress bar can change, so we always draw it every time
if (progress > 0) { if (progress > 0 && prevProgress != progress) {
// draw the progress bar along the bottom // draw the progress bar along the bottom
g.color = notification.theme.progress_FG g.color = notification.theme.progress_FG
g.fillRect(0, Notify.HEIGHT - 2, progress, 2) g.fillRect(0, Notify.HEIGHT - 2, progress, 2)
} }
} }
private fun draw(g: Graphics) {
g.drawImage(cachedImage, 0, 0, null)
if (!notification.hideCloseButton) {
if (mouseX >= 280 && mouseY <= 20) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
}
}
fun onClick(x: Int, y: Int) {
// Check - we were over the 'X' (and thus no notify), or was it in the general area?
val isClickOnCloseButton = !notification.hideCloseButton && x >= 280 && y <= 20
if (isClickOnCloseButton) {
// we always close the notification popup
notification.onClose()
} else {
// only call the general click handler IF we click in the general area!
notification.onClickAction()
}
}
override fun setupHide() { override fun setupHide() {
if (hideTween == null && notification.hideAfterDurationInMillis > 0) { if (hideTween == null && notification.hideAfterDurationInMillis > 0) {
// begin a timeline to get rid of the popup (default is 5 seconds) // begin a timeline to get rid of the popup (default is 5 seconds)
@ -259,10 +268,10 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
override fun doShake(count: Int, targetX: Float, targetY: Float) { override fun doShake(count: Int, targetX: Float, targetY: Float) {
if (shakeTween != null) { if (shakeTween != null) {
shakeTween!!.valueRelative(targetX, targetY) shakeTween!!.valueRelative(targetX, targetY)
.repeatAutoReverse(count, 0f) .repeatAutoReverse(count, 0f)
} else { } else {
val tween = tweenEngine val tween = tweenEngine
.to(this, AppAccessor.X_Y_POS, tweenAccessor, 0.05f) .to(this, AppAccessor.SHAKE, tweenAccessor, 0.05f)
.valueRelative(targetX, targetY) .valueRelative(targetX, targetY)
.repeatAutoReverse(count, 0f) .repeatAutoReverse(count, 0f)
.ease(TweenEquations.Linear) .ease(TweenEquations.Linear)
@ -272,19 +281,12 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
} }
} }
fun onClick(x: Int, y: Int) { fun setLocationShake(x: Int, y: Int) {
// Check - we were over the 'X' (and thus no notify), or was it in the general area? val x1 = getX() - shakeX
val y1 = getY() - shakeY
val isClickOnCloseButton = !notification.hideCloseButton && x >= 280 && y <= 20 shakeX = x
shakeY = y
// reasonable position for detecting mouse over setLocationInternal(x1 + x, y1 + y)
if (isClickOnCloseButton) {
// we always close the notification popup
notification.onClose()
} else {
// only call the general click handler IF we click in the general area!
notification.onClickAction()
}
} }
override fun setLocationInternal(x: Int, y: Int) { override fun setLocationInternal(x: Int, y: Int) {
@ -292,6 +294,7 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
} }
// this is called during parent initialization (before we are initialized), so we cannot access objects here properly! // this is called during parent initialization (before we are initialized), so we cannot access objects here properly!
// NOTE: This is present here because DesktopNotify requires it (and consistency is important)
// override fun setLocation(x: Int, y: Int) { // override fun setLocation(x: Int, y: Int) {
// super.setLocation(x, y) // super.setLocation(x, y)
// } // }
@ -302,7 +305,6 @@ internal class AppNotify(override val notification: Notify): Canvas(), NotifyTyp
updatePositionsPost(this, this, visible) updatePositionsPost(this, this, visible)
} }
// called on the Swing EDT. // called on the Swing EDT.
override fun close() { override fun close() {
cancelMove() cancelMove()

View File

@ -45,8 +45,8 @@ internal class DesktopAccessor : TweenAccessor<DesktopNotify> {
} }
SHAKE -> { SHAKE -> {
returnValues[0] = target.x.toFloat() returnValues[0] = target.shakeX.toFloat()
returnValues[1] = target.y.toFloat() returnValues[1] = target.shakeY.toFloat()
return 2 return 2
} }
@ -76,7 +76,7 @@ internal class DesktopAccessor : TweenAccessor<DesktopNotify> {
} }
SHAKE -> { SHAKE -> {
target.setLocationInternal(newValues[0].toInt(), newValues[1].toInt()) target.setLocationShake(newValues[0].toInt(), newValues[1].toInt())
return return
} }

View File

@ -71,14 +71,16 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
// for the progress bar. we directly draw this onscreen // for the progress bar. we directly draw this onscreen
// non-volatile because it's always accessed in the active render thread // non-volatile because it's always accessed in the active render thread
private var prevProgress = 0
override var progress = 0 override var progress = 0
set(value) {
prevProgress = field
field = value
}
// The button is "hittable" from the entire corner // The button is "hittable" from the entire corner
private val closeButton: Rectangle private val closeButton: Rectangle
@Volatile
var mouseOver = false
override var shakeTween: Tween<DesktopNotify>? = null override var shakeTween: Tween<DesktopNotify>? = null
override var moveTween: Tween<DesktopNotify>? = null override var moveTween: Tween<DesktopNotify>? = null
override var hideTween: Tween<DesktopNotify>? = null override var hideTween: Tween<DesktopNotify>? = null
@ -90,6 +92,11 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
override var anchorX = 0 override var anchorX = 0
override var anchorY = 0 override var anchorY = 0
// the ONLY reason "shake" works, is because we configure the target X/Y location for moving based on where we WANT the popup to go
// and completely ignoring its current position
var shakeX = 0
var shakeY = 0
// this is on the swing EDT // this is on the swing EDT
init { init {
@ -115,14 +122,6 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
refresh() refresh()
} }
override fun getX(): Int {
return super.getX()
}
override fun getY(): Int {
return super.getY()
}
override fun refresh() { override fun refresh() {
cachedImage = renderBackgroundInfo(notification.title, notification.text, notification.theme, notification.image) cachedImage = renderBackgroundInfo(notification.title, notification.text, notification.theme, notification.image)
cachedClose = renderCloseButton(notification.theme, false) cachedClose = renderCloseButton(notification.theme, false)
@ -138,8 +137,92 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
anchorY = getAnchorY(notification.position, bounds) + calculateOffset(growDown, point) anchorY = getAnchorY(notification.position, bounds) + calculateOffset(growDown, point)
} }
/**
* have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
*/
private fun calculateOffset(showFromTop: Boolean, point: Point): Int {
val gc = ScreenUtil.getMonitorAtLocation(point).defaultConfiguration
val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
if (showFromTop) {
if (screenInsets.top > 0) {
return screenInsets.top - Notify.MARGIN
}
} else {
if (screenInsets.bottom > 0) {
return screenInsets.bottom + Notify.MARGIN
}
}
return 0
}
override fun paint(g: Graphics) {
// we cache the text + image (to an image), the two states of the close "button" and then always render the progressbar
try {
draw(g)
} catch (ignored: Exception) {
// have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04):
// java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148)
// at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:356)
// at sun.java2d.SurfaceDataProxy.updateSurfaceData(SurfaceDataProxy.java:498)
// at sun.java2d.SurfaceDataProxy.replaceData(SurfaceDataProxy.java:455)
// at sun.java2d.SurfaceData.getSourceSurfaceData(SurfaceData.java:233)
// at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:566)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
// at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
// at dorkbox.notify.NotifyCanvas.paint(NotifyCanvas.java:92)
// redo the cache
refresh()
// try to draw again
try {
draw(g)
} catch (ignored2: Exception) {
}
}
// the progress bar can change (only getting bigger!), so we always draw it when it grows
if (progress > 0 && prevProgress != progress) {
// draw the progress bar along the bottom
g.color = notification.theme.progress_FG
g.fillRect(0, Notify.HEIGHT - 2, progress, 2)
}
}
private fun draw(g: Graphics) {
g.drawImage(cachedImage, 0, 0, null)
if (!notification.hideCloseButton) {
if (closeButton.contains(MouseInfo.getPointerInfo().location)) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
}
}
fun onClick() {
// Check - we were over the 'X' (and thus no notify), or was it in the general area
val isClickOnCloseButton = !notification.hideCloseButton && closeButton.contains(MouseInfo.getPointerInfo().location)
if (isClickOnCloseButton) {
// we always close the notification popup
notification.onClose()
} else {
// only call the general click handler IF we click in the general area!
notification.onClickAction()
}
}
override fun setupHide() { override fun setupHide() {
if (hideTween == null && notification.hideAfterDurationInMillis > 0) { if (hideTween != null) {
hideTween!!.value(Notify.WIDTH.toFloat())
} else if (notification.hideAfterDurationInMillis > 0) {
// begin a timeline to get rid of the popup (default is 5 seconds) // begin a timeline to get rid of the popup (default is 5 seconds)
val tween = tweenEngine val tween = tweenEngine
.to(this, DesktopAccessor.PROGRESS, tweenAccessor, notification.hideAfterDurationInMillis / 1000.0f) .to(this, DesktopAccessor.PROGRESS, tweenAccessor, notification.hideAfterDurationInMillis / 1000.0f)
@ -174,12 +257,12 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
override fun doShake(count: Int, targetX: Float, targetY: Float) { override fun doShake(count: Int, targetX: Float, targetY: Float) {
if (shakeTween != null) { if (shakeTween != null) {
shakeTween!!.valueRelative(targetX, targetY) shakeTween!!.value(targetX, targetY)
.repeatAutoReverse(count, 0f) .repeatAutoReverse(count, 0f)
} else { } else {
val tween = tweenEngine val tween = tweenEngine
.to(this, DesktopAccessor.X_Y_POS, tweenAccessor, 0.05f) .to(this, DesktopAccessor.SHAKE, tweenAccessor, 0.05f)
.valueRelative(targetX, targetY) .value(targetX, targetY)
.repeatAutoReverse(count, 0f) .repeatAutoReverse(count, 0f)
.ease(TweenEquations.Linear) .ease(TweenEquations.Linear)
@ -188,85 +271,12 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
} }
} }
override fun paint(g: Graphics) { fun setLocationShake(x: Int, y: Int) {
// we cache the text + image (to an image), the two stats of the close "button" and then always render the close + progressbar val x1 = getX() - shakeX
val y1 = getY() - shakeY
// use our cached image, so we don't have to re-render text/background/etc shakeX = x
try { shakeY = y
g.drawImage(cachedImage, 0, 0, null) setLocationInternal(x1 + x, y1 + y)
if (mouseOver && closeButton.contains(MouseInfo.getPointerInfo().location)) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
} catch (ignored: Exception) {
// have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04):
// java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148)
// at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:356)
// at sun.java2d.SurfaceDataProxy.updateSurfaceData(SurfaceDataProxy.java:498)
// at sun.java2d.SurfaceDataProxy.replaceData(SurfaceDataProxy.java:455)
// at sun.java2d.SurfaceData.getSourceSurfaceData(SurfaceData.java:233)
// at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:566)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
// at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
// at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
// at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
// at dorkbox.notify.NotifyCanvas.paint(NotifyCanvas.java:92)
// redo the cache
refresh()
// try to draw again
try {
g.drawImage(cachedImage, 0, 0, null)
if (mouseOver && closeButton.contains(MouseInfo.getPointerInfo().location)) {
g.drawImage(cachedCloseEnabled, 0, 0, null)
} else {
g.drawImage(cachedClose, 0, 0, null)
}
} catch (ignored2: Exception) {
}
}
// the progress bar can change, so we always draw it every time
if (progress > 0) {
// draw the progress bar along the bottom
g.color = notification.theme.progress_FG
g.fillRect(0, Notify.HEIGHT - 2, progress, 2)
}
}
fun onClick() {
// Check - we were over the 'X', or was it in the general area?
if (mouseOver && closeButton.contains(MouseInfo.getPointerInfo().location)) {
// we always close the notification popup
notification.onClose()
} else {
// only call the general click handler IF we click in the general area!
notification.onClickAction()
}
}
/**
* have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
*/
private fun calculateOffset(showFromTop: Boolean, point: Point): Int {
val gc = ScreenUtil.getMonitorAtLocation(point).defaultConfiguration
val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
if (showFromTop) {
if (screenInsets.top > 0) {
return screenInsets.top - Notify.MARGIN
}
} else {
if (screenInsets.bottom > 0) {
return screenInsets.bottom + Notify.MARGIN
}
}
return 0
} }
override fun setLocationInternal(x: Int, y: Int) { override fun setLocationInternal(x: Int, y: Int) {
@ -301,14 +311,14 @@ internal class DesktopNotify(override val notification: Notify) : JWindow(), Not
cancelHide() cancelHide()
cancelShake() cancelShake()
super.setVisible(false)
removeWindowListener(windowCloseListener) removeWindowListener(windowCloseListener)
removeMouseMotionListener(mouseListener) removeMouseMotionListener(mouseListener)
removeMouseListener(mouseListener) removeMouseListener(mouseListener)
updatePositionsPre(component = this, notify = this, visible = false) updatePositionsPre(component = this, notify = this, visible = false)
super.setVisible(false)
removeAll() removeAll()
dispose() dispose()
} }

View File

@ -53,8 +53,6 @@ internal interface NotifyType<T> {
var anchorX: Int var anchorX: Int
var anchorY: Int var anchorY: Int
fun getX(): Int
fun getY(): Int
// for the progress bar. we directly draw this onscreen // for the progress bar. we directly draw this onscreen
// non-volatile because it's always accessed in the active render thread // non-volatile because it's always accessed in the active render thread