phase 2 kotlin WIP

This commit is contained in:
Robinson 2023-01-27 21:32:25 +01:00
parent a218bfb8db
commit b286ee9cf1
No known key found for this signature in database
GPG Key ID: 8E7DB78588BD6F5C
14 changed files with 511 additions and 450 deletions

View File

@ -1,20 +0,0 @@
/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.notify
interface ActionHandler<T> {
fun handle(value: T)
}

View File

@ -15,42 +15,39 @@
*/
package dorkbox.notify
import dorkbox.util.SwingUtil
import java.awt.Frame
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import java.awt.event.WindowStateListener
import javax.swing.ImageIcon
import javax.swing.JFrame
import javax.swing.JPanel
// this is a child to a Jframe/window (instead of globally to the screen).
class AsApplication internal constructor(private val notification: Notify, image: ImageIcon?, private val appWindow: JFrame, theme: Theme) : INotify {
internal class AsApplication internal constructor(
private val notification: Notify,
private val notifyCanvas: NotifyCanvas,) : NotifyType {
companion object {
private const val glassPanePrefix = "dorkbox.notify"
}
private val look: LookAndFeel
private val notifyCanvas: NotifyCanvas
private val window = notification.attachedFrame!!
private val parentListener: ComponentListener
private val windowStateListener: WindowStateListener
private var glassPane: JPanel? = null
private var glassPane: JPanel
// NOTE: this is on the swing EDT
init {
notifyCanvas = NotifyCanvas(this, notification, image, theme)
look = LookAndFeel(this, appWindow, notifyCanvas, notification, appWindow.bounds, false)
// this makes sure that our notify canvas stay anchored to the parent window (if it's hidden/shown/moved/etc)
parentListener = object : ComponentListener {
override fun componentShown(e: ComponentEvent) {
look.reLayout(appWindow.bounds)
notification.notifyLook?.reLayout(window.bounds)
}
override fun componentHidden(e: ComponentEvent) {}
override fun componentResized(e: ComponentEvent) {
look.reLayout(appWindow.bounds)
notification.notifyLook?.reLayout(window.bounds)
}
override fun componentMoved(e: ComponentEvent) {}
@ -59,81 +56,61 @@ class AsApplication internal constructor(private val notification: Notify, image
windowStateListener = WindowStateListener { e ->
val state = e.newState
if (state and Frame.ICONIFIED == 0) {
look.reLayout(appWindow.bounds)
notification.notifyLook?.reLayout(window.bounds)
}
}
appWindow.addWindowStateListener(windowStateListener)
appWindow.addComponentListener(parentListener)
window.addWindowStateListener(windowStateListener)
window.addComponentListener(parentListener)
val glassPane_ = appWindow.glassPane
if (glassPane_ is JPanel) {
glassPane = glassPane_
val name = glassPane_.name
val pane = window.glassPane
if (pane is JPanel) {
glassPane = pane
val name = glassPane.name
if (name != glassPanePrefix) {
// We just tweak the already existing glassPane, instead of replacing it with our own
// glassPane = new JPanel();
glassPane_.layout = null
glassPane_.name = glassPanePrefix
glassPane.layout = null
glassPane.name = glassPanePrefix
// glassPane.setSize(appWindow.getSize());
// glassPane.setOpaque(false);
// appWindow.setGlassPane(glassPane);
}
glassPane_.add(notifyCanvas)
glassPane.add(notifyCanvas)
if (!glassPane_.isVisible) {
glassPane_.isVisible = true
if (!glassPane.isVisible) {
glassPane.isVisible = true
}
} else {
System.err.println("Not able to add notification to custom glassPane")
throw RuntimeException("Not able to add the notification to the window glassPane")
}
}
override fun onClick(x: Int, y: Int) {
look.onClick(x, y)
}
/**
* Shakes the popup
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
*/
override fun shake(durationInMillis: Int, amplitude: Int) {
look.shake(durationInMillis, amplitude)
}
override fun setVisible(visible: Boolean) {
override fun setVisible(visible: Boolean, look: LookAndFeel) {
// this is because the order of operations are different based upon visibility.
look.updatePositionsPre(visible)
look.updatePositionsPost(visible)
}
// called on the Swing EDT.
override fun close() {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater {
look.close()
glassPane!!.remove(notifyCanvas)
appWindow.removeWindowStateListener(windowStateListener)
appWindow.removeComponentListener(parentListener)
glassPane.remove(notifyCanvas)
window.removeWindowStateListener(windowStateListener)
window.removeComponentListener(parentListener)
var found = false
val components = glassPane!!.components
for (component in components) {
if (component is NotifyCanvas) {
found = true
break
}
var found = false
val components = glassPane.components
for (component in components) {
if (component is NotifyCanvas) {
found = true
break
}
}
if (!found) {
// hide the glass pane if there are no more notifications on it.
glassPane!!.isVisible = false
}
notification.onClose()
if (!found) {
// hide the glass pane if there are no more notifications on it.
glassPane.isVisible = false
}
}
}

View File

@ -15,23 +15,16 @@
*/
package dorkbox.notify
import dorkbox.util.ScreenUtil
import dorkbox.util.SwingUtil
import java.awt.Dimension
import java.awt.GraphicsEnvironment
import java.awt.MouseInfo
import javax.swing.ImageIcon
import javax.swing.JWindow
// we can't use regular popup, because if we have no owner, it won't work!
// instead, we just create a JWindow and use it to hold our content
class AsDesktop internal constructor(private val notification: Notify, image: ImageIcon?, theme: Theme) : JWindow(), INotify {
internal class AsDesktop internal constructor(val notification: Notify, notifyCanvas: NotifyCanvas) : JWindow(), NotifyType {
companion object {
private const val serialVersionUID = 1L
}
private val look: LookAndFeel
// this is on the swing EDT
init {
isAlwaysOnTop = true
@ -43,49 +36,10 @@ class AsDesktop internal constructor(private val notification: Notify, image: Im
setSize(NotifyCanvas.WIDTH, NotifyCanvas.HEIGHT)
setLocation(Short.MIN_VALUE.toInt(), Short.MIN_VALUE.toInt())
val device = if (notification.screenNumber == Short.MIN_VALUE.toInt()) {
// set screen position based on mouse
val mouseLocation = MouseInfo.getPointerInfo().location
ScreenUtil.getMonitorAtLocation(mouseLocation)
} else {
// set screen position based on specified screen
var screenNumber = notification.screenNumber
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
val screenDevices = ge.screenDevices
if (screenNumber < 0) {
screenNumber = 0
} else if (screenNumber > screenDevices.size - 1) {
screenNumber = screenDevices.size - 1
}
screenDevices[screenNumber]
}
val bounds = device.defaultConfiguration.bounds
val notifyCanvas = NotifyCanvas(this, notification, image, theme)
contentPane.add(notifyCanvas)
look = LookAndFeel(this, this, notifyCanvas, notification, bounds, true)
}
override fun onClick(x: Int, y: Int) {
look.onClick(x, y)
}
/**
* Shakes the popup
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
*/
override fun shake(durationInMillis: Int, amplitude: Int) {
look.shake(durationInMillis, amplitude)
}
override fun setVisible(visible: Boolean) {
override fun setVisible(visible: Boolean, look: LookAndFeel) {
// was it already visible?
if (visible == isVisible) {
// prevent "double setting" visible state
@ -103,19 +57,10 @@ class AsDesktop internal constructor(private val notification: Notify, image: Im
}
}
// setVisible(false) with any extra logic
fun doHide() {
super.setVisible(false)
}
// called on the Swing EDT
override fun close() {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater {
doHide()
look.close()
removeAll()
dispose()
notification.onClose()
}
super.setVisible(false)
removeAll()
dispose()
}
}

View File

@ -20,7 +20,7 @@ import java.awt.event.MouseEvent
internal class ClickAdapter : MouseAdapter() {
override fun mouseReleased(e: MouseEvent) {
val parent = (e.source as NotifyCanvas).parent
parent.onClick(e.x, e.y)
val notifyCanvas = e.source as NotifyCanvas
notifyCanvas.onClick(e.x, e.y)
}
}

222
src/dorkbox/notify/LAFUtil.kt Executable file
View File

@ -0,0 +1,222 @@
/*
* Copyright 2015 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.notify
import dorkbox.swingActiveRender.ActionHandlerLong
import dorkbox.tweenEngine.TweenCallback.Events.COMPLETE
import dorkbox.tweenEngine.TweenEngine.Companion.create
import dorkbox.tweenEngine.TweenEquations
import dorkbox.util.ScreenUtil
import java.awt.GraphicsEnvironment
import java.awt.MouseInfo
import java.awt.Rectangle
import java.awt.event.MouseAdapter
import java.util.*
internal object LAFUtil{
val popups: MutableMap<String, PopupList> = HashMap()
// access is only from a single thread ever, so unsafe is preferred.
val animation = create().unsafe().build()
val accessor = NotifyAccessor()
// this is for updating the tween engine during active-rendering
val frameStartHandler = ActionHandlerLong { deltaInNanos -> animation.update(deltaInNanos) }
const val SPACER = 10
const val MARGIN = 20
val windowListener: java.awt.event.WindowAdapter = WindowAdapter()
val mouseListener: MouseAdapter = ClickAdapter()
val RANDOM = Random()
private val MOVE_DURATION = Notify.MOVE_DURATION
fun getGraphics(screen: Int): Rectangle {
val device = if (screen == Short.MIN_VALUE.toInt()) {
// set screen position based on mouse
val mouseLocation = MouseInfo.getPointerInfo().location
ScreenUtil.getMonitorAtLocation(mouseLocation)
} else {
// set screen position based on specified screen
var screenNumber = screen
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
val screenDevices = ge.screenDevices
if (screenNumber < 0) {
screenNumber = 0
} else if (screenNumber > screenDevices.size - 1) {
screenNumber = screenDevices.size - 1
}
screenDevices[screenNumber]
}
return device.defaultConfiguration.bounds
}
fun getAnchorX(position: Position, bounds: Rectangle, isDesktop: Boolean): Int {
// we use the screen that the mouse is currently on.
val startX = if (isDesktop) {
bounds.getX().toInt()
} else {
0
}
val screenWidth = bounds.getWidth().toInt()
return when (position) {
Position.TOP_LEFT, Position.BOTTOM_LEFT -> MARGIN + startX
Position.CENTER -> startX + screenWidth / 2 - NotifyCanvas.WIDTH / 2 - MARGIN / 2
Position.TOP_RIGHT, Position.BOTTOM_RIGHT -> startX + screenWidth - NotifyCanvas.WIDTH - MARGIN
}
}
fun getAnchorY(position: Position, bounds: Rectangle, isDesktop: Boolean): Int {
val startY = if (isDesktop) {
bounds.getY().toInt()
} else {
0
}
val screenHeight = bounds.getHeight().toInt()
return when (position) {
Position.TOP_LEFT, Position.TOP_RIGHT -> startY + MARGIN
Position.CENTER -> startY + screenHeight / 2 - NotifyCanvas.HEIGHT / 2 - MARGIN / 2 - SPACER
Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT -> if (isDesktop) {
startY + screenHeight - NotifyCanvas.HEIGHT - MARGIN
} else {
screenHeight - NotifyCanvas.HEIGHT - MARGIN - SPACER * 2
}
}
}
// only called on the swing EDT thread
fun addPopupToMap(sourceLook: LookAndFeel) {
synchronized(popups) {
val id = sourceLook.idAndPosition
var looks = popups[id]
if (looks == null) {
looks = PopupList()
popups[id] = looks
}
val index = looks.size()
sourceLook.popupIndex = index
// the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up
val anchorX = sourceLook.anchorX
val anchorY = sourceLook.anchorY
val targetY = if (index == 0) {
anchorY
} else {
val growDown = growDown(sourceLook)
if (sourceLook.isDesktopNotification && index == 1) {
// have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
// this is only done when the 2nd popup is added to the list
looks.calculateOffset(growDown, anchorX, anchorY)
}
if (growDown) {
anchorY + index * (NotifyCanvas.HEIGHT + SPACER) + looks.offsetY
} else {
anchorY - index * (NotifyCanvas.HEIGHT + SPACER) + looks.offsetY
}
}
looks.add(sourceLook)
sourceLook.setLocation(anchorX, targetY)
if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) {
// begin a timeline to get rid of the popup (default is 5 seconds)
animation.to(sourceLook, NotifyAccessor.PROGRESS, accessor, sourceLook.hideAfterDurationInSeconds)
.target(NotifyCanvas.WIDTH.toFloat())
.ease(TweenEquations.Linear)
.addCallback(COMPLETE) { sourceLook.notification.onClose() }
.start()
}
}
}
// only called on the swing app or SwingActiveRender thread
fun removePopupFromMap(sourceLook: LookAndFeel): Boolean {
val growDown = growDown(sourceLook)
var popupsAreEmpty: Boolean
synchronized(popups) {
popupsAreEmpty = popups.isEmpty()
val allLooks = popups[sourceLook.idAndPosition]
// there are two loops because it is necessary to cancel + remove all tweens BEFORE adding new ones.
var adjustPopupPosition = false
val iterator = allLooks!!.iterator()
while (iterator.hasNext()) {
val look = iterator.next()
if (look.tween != null) {
look.tween!!.cancel() // cancel does its thing on the next tick of animation cycle
look.tween = null
}
if (look === sourceLook) {
if (look.hideTween != null) {
look.hideTween!!.cancel()
look.hideTween = null
}
adjustPopupPosition = true
iterator.remove()
}
if (adjustPopupPosition) {
look.popupIndex--
}
}
// have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
val offsetY = allLooks.offsetY
for (index in 0 until allLooks.size()) {
val look = allLooks[index]
// the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up
val changedY = if (growDown) {
look.anchorY + (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY)
} else {
look.anchorY - (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY)
}
// now animate that popup to its new location
look.tween = animation
.to(look, NotifyAccessor.Y_POS, accessor, MOVE_DURATION)
.target(changedY.toFloat())
.ease(TweenEquations.Linear)
.addCallback(COMPLETE) {
// make sure to remove the tween once it's done, otherwise .kill can do weird things.
look.tween = null
}
.start()
}
}
return popupsAreEmpty
}
fun growDown(look: LookAndFeel): Boolean {
return when (look.position) {
Position.TOP_LEFT, Position.TOP_RIGHT, Position.CENTER -> true
else -> false
}
}
}

View File

@ -15,218 +15,52 @@
*/
package dorkbox.notify
import dorkbox.swingActiveRender.ActionHandlerLong
import dorkbox.notify.LAFUtil.RANDOM
import dorkbox.notify.LAFUtil.SPACER
import dorkbox.notify.LAFUtil.accessor
import dorkbox.notify.LAFUtil.addPopupToMap
import dorkbox.notify.LAFUtil.animation
import dorkbox.notify.LAFUtil.frameStartHandler
import dorkbox.notify.LAFUtil.getAnchorX
import dorkbox.notify.LAFUtil.getAnchorY
import dorkbox.notify.LAFUtil.growDown
import dorkbox.notify.LAFUtil.mouseListener
import dorkbox.notify.LAFUtil.popups
import dorkbox.notify.LAFUtil.removePopupFromMap
import dorkbox.notify.LAFUtil.windowListener
import dorkbox.swingActiveRender.SwingActiveRender
import dorkbox.tweenEngine.Tween
import dorkbox.tweenEngine.TweenCallback.Events.COMPLETE
import dorkbox.tweenEngine.TweenEngine.Companion.create
import dorkbox.tweenEngine.TweenEquations
import dorkbox.util.ScreenUtil
import java.awt.Point
import java.awt.Rectangle
import java.awt.Window
import java.awt.event.MouseAdapter
import java.util.*
internal class LookAndFeel(
private val notify: INotify,
private val parent: Window,
private val notifyCanvas: NotifyCanvas,
private val notification: Notify,
val notification: Notify,
parentBounds: Rectangle,
private val isDesktopNotification: Boolean
val isDesktopNotification: Boolean
) {
companion object {
private val popups: MutableMap<String?, PopupList> = HashMap()
// access is only from a single thread ever, so unsafe is preferred.
val animation = create().unsafe().build()
val accessor = NotifyAccessor()
// this is for updating the tween engine during active-rendering
private val frameStartHandler = ActionHandlerLong { deltaInNanos -> animation.update(deltaInNanos) }
const val SPACER = 10
const val MARGIN = 20
private val windowListener: java.awt.event.WindowAdapter = WindowAdapter()
private val mouseListener: MouseAdapter = ClickAdapter()
private val RANDOM = Random()
private val MOVE_DURATION = Notify.MOVE_DURATION
private fun getAnchorX(position: Pos, bounds: Rectangle, isDesktop: Boolean): Int {
// we use the screen that the mouse is currently on.
val startX = if (isDesktop) {
bounds.getX().toInt()
} else {
0
}
val screenWidth = bounds.getWidth().toInt()
return when (position) {
Pos.TOP_LEFT, Pos.BOTTOM_LEFT -> MARGIN + startX
Pos.CENTER -> startX + screenWidth / 2 - NotifyCanvas.WIDTH / 2 - MARGIN / 2
Pos.TOP_RIGHT, Pos.BOTTOM_RIGHT -> startX + screenWidth - NotifyCanvas.WIDTH - MARGIN
}
}
private fun getAnchorY(position: Pos, bounds: Rectangle, isDesktop: Boolean): Int {
val startY = if (isDesktop) {
bounds.getY().toInt()
} else {
0
}
val screenHeight = bounds.getHeight().toInt()
return when (position) {
Pos.TOP_LEFT, Pos.TOP_RIGHT -> startY + MARGIN
Pos.CENTER -> startY + screenHeight / 2 - NotifyCanvas.HEIGHT / 2 - MARGIN / 2 - SPACER
Pos.BOTTOM_LEFT, Pos.BOTTOM_RIGHT -> if (isDesktop) {
startY + screenHeight - NotifyCanvas.HEIGHT - MARGIN
} else {
screenHeight - NotifyCanvas.HEIGHT - MARGIN - SPACER * 2
}
}
}
// only called on the swing EDT thread
private fun addPopupToMap(sourceLook: LookAndFeel) {
synchronized(popups) {
val id = sourceLook.idAndPosition
var looks = popups[id]
if (looks == null) {
looks = PopupList()
popups[id] = looks
}
val index = looks.size()
sourceLook.popupIndex = index
// the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up
val anchorX = sourceLook.anchorX
val anchorY = sourceLook.anchorY
val targetY = if (index == 0) {
anchorY
} else {
val growDown = growDown(sourceLook)
if (sourceLook.isDesktopNotification && index == 1) {
// have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
// this is only done when the 2nd popup is added to the list
looks.calculateOffset(growDown, anchorX, anchorY)
}
if (growDown) {
anchorY + index * (NotifyCanvas.HEIGHT + SPACER) + looks.offsetY
} else {
anchorY - index * (NotifyCanvas.HEIGHT + SPACER) + looks.offsetY
}
}
looks.add(sourceLook)
sourceLook.setLocation(anchorX, targetY)
if (sourceLook.hideAfterDurationInSeconds > 0 && sourceLook.hideTween == null) {
// begin a timeline to get rid of the popup (default is 5 seconds)
animation.to(sourceLook, NotifyAccessor.PROGRESS, accessor, sourceLook.hideAfterDurationInSeconds)
.target(NotifyCanvas.WIDTH.toFloat())
.ease(TweenEquations.Linear)
.addCallback(COMPLETE) { sourceLook.notify.close() }
.start()
}
}
}
// only called on the swing app or SwingActiveRender thread
private fun removePopupFromMap(sourceLook: LookAndFeel): Boolean {
val growDown = growDown(sourceLook)
var popupsAreEmpty: Boolean
synchronized(popups) {
popupsAreEmpty = popups.isEmpty()
val allLooks = popups[sourceLook.idAndPosition]
// there are two loops because it is necessary to cancel + remove all tweens BEFORE adding new ones.
var adjustPopupPosition = false
val iterator = allLooks!!.iterator()
while (iterator.hasNext()) {
val look = iterator.next()
if (look.tween != null) {
look.tween!!.cancel() // cancel does its thing on the next tick of animation cycle
look.tween = null
}
if (look === sourceLook) {
if (look.hideTween != null) {
look.hideTween!!.cancel()
look.hideTween = null
}
adjustPopupPosition = true
iterator.remove()
}
if (adjustPopupPosition) {
look.popupIndex--
}
}
// have to adjust for offsets when the window-manager has a toolbar that consumes space and prevents overlap.
val offsetY = allLooks.offsetY
for (index in 0 until allLooks.size()) {
val look = allLooks[index]
// the popups are ALL the same size!
// popups at TOP grow down, popups at BOTTOM grow up
val changedY = if (growDown) {
look.anchorY + (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY)
} else {
look.anchorY - (look.popupIndex * (NotifyCanvas.HEIGHT + SPACER) + offsetY)
}
// now animate that popup to its new location
look.tween = animation
.to(look, NotifyAccessor.Y_POS, accessor, MOVE_DURATION)
.target(changedY.toFloat())
.ease(TweenEquations.Linear)
.addCallback(COMPLETE) {
// make sure to remove the tween once it's done, otherwise .kill can do weird things.
look.tween = null
}
.start()
}
}
return popupsAreEmpty
}
private fun growDown(look: LookAndFeel): Boolean {
return when (look.position) {
Pos.TOP_LEFT, Pos.TOP_RIGHT, Pos.CENTER -> true
else -> false
}
}
}
@Volatile
var anchorX: Int
@Volatile
private var anchorX: Int
@Volatile
private var anchorY: Int
private val hideAfterDurationInSeconds: Float
private val position: Pos
var anchorY: Int
val hideAfterDurationInSeconds: Float
val position: Position
// this is used in combination with position, so that we can track which screen and what position a popup is in
private var idAndPosition: String? = null
private var popupIndex = 0
var idAndPosition: String
var popupIndex = 0
@Volatile
private var tween: Tween<*>? = null
var tween: Tween<*>? = null
@Volatile
private var hideTween: Tween<*>? = null
private val onGeneralAreaClickAction = notification.onGeneralAreaClickAction // explicitly make a copy
var hideTween: Tween<*>? = null
init {
if (isDesktopNotification) {
@ -248,19 +82,6 @@ internal class LookAndFeel(
anchorY = getAnchorY(position, parentBounds, isDesktopNotification)
}
fun onClick(x: Int, y: Int) {
// Check - we were over the 'X' (and thus no notify), or was it in the general area?
// reasonable position for detecting mouse over
if (!notifyCanvas.isCloseButton(x, y)) {
// only call the general click handler IF we click in the general area!
onGeneralAreaClickAction.invoke(notification)
}
// we always close the notification popup
notify.close()
}
// only called from an application
fun reLayout(bounds: Rectangle) {
// when the parent window moves, we stop all animation and snap the popup into place. This simplifies logic greatly

View File

@ -36,16 +36,15 @@ import javax.swing.JFrame
*
* These notifications are for a single screen only, and cannot be anchored to an application.
*
* <pre>
* `Notify.create()
* ```
* `Notify()
* .title("Title Text")
* .text("Hello World!")
* .useDarkStyle()
* .showWarning();
` *
</pre> *
* .darkStyle()
* .showWarning()
* ```
*/
@Suppress("unused")
@Suppress("unused", "MemberVisibilityCanBePrivate")
class Notify private constructor() {
companion object {
const val DIALOG_CONFIRM = "dialog-confirm.png"
@ -130,7 +129,7 @@ class Notify private constructor() {
imageCache[imageName] = SoftReference(ImageIcon(bufferedImage))
}
private fun getImage(imageName: String): ImageIcon? {
private fun getImage(imageName: String): ImageIcon {
var resourceAsStream: InputStream? = null
var image = imageCache[imageName]?.get()
@ -148,28 +147,65 @@ class Notify private constructor() {
resourceAsStream?.close()
}
return image
return image!!
}
}
@Volatile
internal var notifyPopup: NotifyType? = null
@Volatile
internal var notifyLook: LookAndFeel? = null
internal var title = "Notification"
internal var text = "Lorem ipsum"
private var theme: Theme? = null
internal var position = Pos.BOTTOM_RIGHT
internal var hideAfterDurationInMillis = 0
internal var hideCloseButton = false
private var isDark = false
internal var screenNumber = Short.MIN_VALUE.toInt()
@Volatile
var title = "Notification"
private var icon: ImageIcon? = null
internal var onGeneralAreaClickAction: Notify.()->Unit = {}
@Volatile
var text = "Lorem ipsum"
private var notifyPopup: INotify? = null
private var name: String? = null
private var shakeDurationInMillis = 0
private var shakeAmplitude = 0
private var appWindow: JFrame? = null
@Volatile
var theme = Theme.defaultLight
@Volatile
var position = Position.BOTTOM_RIGHT
@Volatile
var hideAfterDurationInMillis = 0
/**
* Is the close button in the top-right corner of the notification visible
*/
@Volatile
var hideCloseButton = false
@Volatile
var screen = Short.MIN_VALUE.toInt()
@Volatile
var image: ImageIcon? = null
/**
* Called when the notification is closed, either via close button or via close()
*/
@Volatile
var onCloseAction: Notify.()->Unit = {}
/**
* Called when the "general area" (but specifically not the "close button") is clicked.
*/
@Volatile
var onClickAction: Notify.()->Unit = {}
@Volatile
var name = DIALOG_ERROR
@Volatile
var shakeDurationInMillis = 0
@Volatile
var shakeAmplitude = 0
@Volatile
var attachedFrame: JFrame? = null
/**
* Specifies the main text
@ -208,14 +244,16 @@ class Notify private constructor() {
// now we want to center the image
bufferedImage = ImageUtil.getSquareBufferedImage(bufferedImage)
icon = ImageIcon(bufferedImage)
this.image = ImageIcon(bufferedImage)
return this
}
/**
* Specifies the position of the notification on screen, by default it is [bottom-right][Pos.BOTTOM_RIGHT].
* Specifies the position of the notification on screen, by default it is [bottom-right][Position.BOTTOM_RIGHT].
*/
fun position(position: Pos): Notify {
fun position(position: Position): Notify {
this.position = position
return this
}
@ -235,26 +273,25 @@ class Notify private constructor() {
}
/**
* Specifies what to do when the user clicks on the notification (in addition o the notification hiding, which happens whenever the
* notification is clicked on). This does not apply when clicking on the "close" button
* Called when the notification is closed, either via close button or via close()
*/
fun onAction(onAction: Notify.()->Unit): Notify {
onGeneralAreaClickAction = onAction
fun onCloseAction(onAction: Notify.()->Unit): Notify {
onCloseAction = onAction
return this
}
/**
* Specifies that the notification should use the built-in dark styling, rather than the default, light-gray notification style.
* Called when the "general area" (but specifically not the "close button") is clicked.
*/
fun darkStyle(): Notify {
isDark = true
fun onClickAction(onAction: Notify.()->Unit): Notify {
onClickAction = onAction
return this
}
/**
* Specifies what the theme should be, if other than the default. This will always take precedence over the defaults.
*/
fun text(theme: Theme?): Notify {
fun theme(theme: Theme): Notify {
this.theme = theme
return this
}
@ -272,7 +309,7 @@ class Notify private constructor() {
*/
fun showWarning() {
name = DIALOG_WARNING
icon = getImage(DIALOG_WARNING)
image = getImage(DIALOG_WARNING)
show()
}
@ -281,7 +318,7 @@ class Notify private constructor() {
*/
fun showInformation() {
name = DIALOG_INFORMATION
icon = getImage(DIALOG_INFORMATION)
image = getImage(DIALOG_INFORMATION)
show()
}
@ -290,7 +327,7 @@ class Notify private constructor() {
*/
fun showError() {
name = DIALOG_ERROR
icon = getImage(DIALOG_ERROR)
image = getImage(DIALOG_ERROR)
show()
}
@ -299,7 +336,7 @@ class Notify private constructor() {
*/
fun showConfirm() {
name = DIALOG_CONFIRM
icon = getImage(DIALOG_CONFIRM)
image = getImage(DIALOG_CONFIRM)
show()
}
@ -308,39 +345,40 @@ class Notify private constructor() {
* ignored.
*/
fun show() {
val notify = this@Notify
// must be done in the swing EDT
SwingUtil.invokeAndWaitQuietly {
val notify = this@Notify
val image = notify.icon
val theme = if (notify.theme != null) {
// use custom provided theme
notify.theme!!
} else {
Theme(TITLE_TEXT_FONT, MAIN_TEXT_FONT, notify.isDark)
}
val window = appWindow
val window = notify.attachedFrame
val shakeDuration = notify.shakeDurationInMillis
val shakeAmp = notify.shakeAmplitude
val notifyPopup = if (window == null) {
AsDesktop(notify, image, theme)
val notifyCanvas = NotifyCanvas(notify, notify.image, theme)
val notifyPopup: NotifyType
val look: LookAndFeel
if (window == null) {
notifyPopup = AsDesktop(notify, notifyCanvas)
look = LookAndFeel(notifyPopup, notifyCanvas, notify, LAFUtil.getGraphics(notify.screen), true)
} else {
AsApplication(notify, image, window, theme)
notifyPopup = AsApplication(notify, notifyCanvas)
look = LookAndFeel(window, notifyCanvas, notify, window.bounds, false)
}
notifyPopup.setVisible(true)
notifyPopup.setVisible(true, look)
if (shakeDurationInMillis > 0) {
notifyPopup.shake(notify.shakeDurationInMillis, notify.shakeAmplitude)
if (shakeDuration > 0) {
look.shake(shakeDuration, shakeAmp)
}
notify.notifyPopup = notifyPopup
notify.notifyLook = look
}
// don't need to hang onto these.
icon = null
}
/**
* "shakes" the notification, to bring user attention to it.
* "Shakes" the notification, to bring user attention to it.
*
* @param durationInMillis now long it will shake
* @param amplitude a measure of how much it needs to shake. 4 is a small amount of shaking, 10 is a lot.
@ -348,9 +386,11 @@ class Notify private constructor() {
fun shake(durationInMillis: Int, amplitude: Int): Notify {
shakeDurationInMillis = durationInMillis
shakeAmplitude = amplitude
if (notifyPopup != null) {
val popupLook = notifyLook
if (popupLook != null) {
// must be done in the swing EDT
SwingUtil.invokeLater { notifyPopup!!.shake(durationInMillis, amplitude) }
SwingUtil.invokeLater { popupLook.shake(durationInMillis, amplitude) }
}
return this
}
@ -359,19 +399,22 @@ class Notify private constructor() {
* Closes the notification. Particularly useful if it's an "infinite" duration notification.
*/
fun close() {
if (notifyPopup == null) {
throw NullPointerException("NotifyPopup")
val popup = notifyPopup
val look = notifyLook
if (popup !== null && look != null) {
// must be done in the swing EDT
SwingUtil.invokeLater {
look.close()
popup.close()
}
}
// must be done in the swing EDT
SwingUtil.invokeLater { notifyPopup!!.close() }
}
/**
* Specifies which screen to display on. If <0, it will show on screen 0. If > max-screens, it will show on the last screen.
*/
fun setScreen(screenNumber: Int): Notify {
this.screenNumber = screenNumber
this.screen = screenNumber
return this
}
@ -379,14 +422,21 @@ class Notify private constructor() {
* Attaches this notification to a specific JFrame, instead of having a global notification
*/
fun attach(frame: JFrame?): Notify {
appWindow = frame
attachedFrame = frame
return this
}
// called when this notification is closed.
fun onClose() {
// called when this notification is closed. called in the swing EDT!
internal fun onClose() {
this.notifyPopup!!.close()
this.onCloseAction.invoke(this)
notifyPopup = null
notifyLook = null
}
internal fun onClickAction() {
this.onClickAction.invoke(this)
}
}

View File

@ -15,6 +15,7 @@
*/
package dorkbox.notify
import dorkbox.util.SwingUtil
import java.awt.BasicStroke
import java.awt.Canvas
import java.awt.Color
@ -28,13 +29,9 @@ import javax.swing.ImageIcon
import javax.swing.JLabel
internal class NotifyCanvas(
val parent: INotify,
private val notification: Notify,
private val imageIcon: ImageIcon?,
private val theme: Theme
private val notification: Notify, private val imageIcon: ImageIcon?, private val theme: Theme
) : Canvas() {
private val showCloseButton: Boolean
private var cachedImage: BufferedImage
// for the progress bar. we directly draw this onscreen
@ -50,7 +47,7 @@ internal class NotifyCanvas(
isFocusable = false
background = theme.panel_BG
showCloseButton = !notification.hideCloseButton
// now we setup the rendering of the image
cachedImage = renderBackgroundInfo(notification.title, notification.text, theme, imageIcon)
@ -89,8 +86,9 @@ internal class NotifyCanvas(
// the progress bar and close button are the only things that can change, so we always draw them every time
val g2 = g.create() as Graphics2D
try {
if (showCloseButton) {
if (!notification.hideCloseButton) {
// manually draw the close button
val g3 = g.create() as Graphics2D
g3.color = theme.panel_BG
@ -118,11 +116,22 @@ internal class NotifyCanvas(
}
}
/**
* @return TRUE if we were over the 'X' or FALSE if the click was in the general area (and not over the 'X').
*/
fun isCloseButton(x: Int, y: Int): Boolean {
return showCloseButton && x >= 280 && y <= 20
fun onClick(x: Int, y: Int) {
// this must happen in the Swing EDT. This is usually called by the active renderer
SwingUtil.invokeLater {
// 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
// reasonable position for detecting mouse over
if (!isClickOnCloseButton) {
// only call the general click handler IF we click in the general area!
notification.onClickAction()
} else {
// we always close the notification popup
notification.onClose()
}
}
}
companion object {

View File

@ -15,9 +15,7 @@
*/
package dorkbox.notify
interface INotify {
internal interface NotifyType {
fun close()
fun shake(durationInMillis: Int, amplitude: Int)
fun setVisible(visible: Boolean)
fun onClick(x: Int, y: Int)
fun setVisible(visible: Boolean, look: LookAndFeel)
}

View File

@ -40,11 +40,11 @@ internal class PopupList {
val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
if (showFromTop) {
if (screenInsets.top > 0) {
offsetY = screenInsets.top - LookAndFeel.MARGIN
offsetY = screenInsets.top - LAFUtil.MARGIN
}
} else {
if (screenInsets.bottom > 0) {
offsetY = screenInsets.bottom + LookAndFeel.MARGIN
offsetY = screenInsets.bottom + LAFUtil.MARGIN
}
}
}

View File

@ -15,7 +15,7 @@
*/
package dorkbox.notify
enum class Pos {
enum class Position {
/**
* top vertically, left horizontally
*/

View File

@ -23,6 +23,16 @@ import java.awt.Font
* Settings available to change the theme
*/
class Theme {
companion object {
val defaultLight: Theme by lazy {
Theme(Notify.TITLE_TEXT_FONT, Notify.MAIN_TEXT_FONT, false)
}
val defaultDark: Theme by lazy {
Theme(Notify.TITLE_TEXT_FONT, Notify.MAIN_TEXT_FONT, true)
}
}
val panel_BG: Color
val titleText_FG: Color
val mainText_FG: Color
@ -67,4 +77,25 @@ class Theme {
this.closeX_FG = closeX_FG
this.progress_FG = progress_FG
}
/**
* True if we are the default "light" theme
*/
fun isLight(): Boolean {
return this === defaultLight
}
/**
* True if we are the default "dark" theme
*/
fun isDark(): Boolean {
return this === defaultDark
}
/**
* True if we are a custom theme
*/
fun isCustom(): Boolean {
return !isLight() && !isDark()
}
}

View File

@ -22,7 +22,7 @@ internal class WindowAdapter : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
if (e.newState != WindowEvent.WINDOW_CLOSED) {
val source = e.source as AsDesktop
source.close()
source.notification.close()
}
}

View File

@ -1,3 +1,5 @@
@file:Suppress("UNUSED_VALUE")
package dorkbox.notify
import dorkbox.util.ImageUtil
@ -49,11 +51,36 @@ object NotifyTest {
// bottomRightInFrame(3, frame)
// topLeftInFrame(3, frame)
topRightMonitor(3)
react()
// topRightMonitor(3)
// bottomLeftScaled(3, frame, image)
// bottomLeftStacking(3, frame, image)
}
fun react() {
val notify = Notify.create()
notify.title("Notify title modify")
.text("This is a notification popup message This is a notification popup message This is a " +
"notification popup message")
.hideAfter(13000)
.position(Position.TOP_RIGHT)
// .setScreen(0)
.theme(Theme.defaultDark)
// .shake(1300, 4)
.shake(4300, 10)
// .hideCloseButton() // if the hideButton is visible, then it's possible to change things when clicked
.onClickAction {
notify.text = "HOWDY"
System.err.println("Notification clicked on!")
}
notify.show()
try {
Thread.sleep(3000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
fun topRightMonitor(count: Int) {
var notify: Notify
@ -63,13 +90,13 @@ object NotifyTest {
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message")
.hideAfter(13000)
.position(Pos.TOP_RIGHT)
.position(Position.TOP_RIGHT)
// .setScreen(0)
.darkStyle()
.theme(Theme.defaultDark)
// .shake(1300, 4)
.shake(4300, 10)
.hideCloseButton()
.onAction { System.err.println("Notification $i clicked on!") }
.onClickAction { System.err.println("Notification $i clicked on!") }
notify.show()
try {
Thread.sleep(3000)
@ -84,12 +111,12 @@ object NotifyTest {
.title("Notify scaled")
.text("This is a notification popup message scaled This is a notification popup message This is a " +
"notification popup message scaled ") // .hideAfter(13000)
.position(Pos.BOTTOM_LEFT) // .setScreen(0)
.position(Position.BOTTOM_LEFT) // .setScreen(0)
// .darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
// .hideCloseButton()
.onAction { System.err.println("Notification scaled clicked on!") }
.onClickAction { System.err.println("Notification scaled clicked on!") }
notify.image(image)
notify.show()
}
@ -103,13 +130,13 @@ object NotifyTest {
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message")
// .hideAfter(13000)
.position(Pos.BOTTOM_LEFT)
.position(Position.BOTTOM_LEFT)
// .setScreen(0)
// .darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
// .hideCloseButton()
.onAction { System.err.println("Notification $i clicked on!") }
.onClickAction { System.err.println("Notification $i clicked on!") }
if (i == 0) {
notify.image(image)
notify.show()
@ -132,13 +159,13 @@ object NotifyTest {
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message")
.hideAfter(13000)
.position(Pos.TOP_LEFT) // .position(Pos.CENTER)
.position(Position.TOP_LEFT) // .position(Pos.CENTER)
// .setScreen(0)
// .darkStyle()
// .shake(1300, 4)
// .shake(1300, 10)
.attach(frame) // .hideCloseButton()
.onAction { System.err.println("Notification $i clicked on!") }
.onClickAction { System.err.println("Notification $i clicked on!") }
notify.showWarning()
try {
@ -157,13 +184,14 @@ object NotifyTest {
.text("This is a notification " + i + " popup message This is a notification popup message This is a " +
"notification popup message")
.hideAfter(13000)
.position(Pos.BOTTOM_RIGHT) // .position(Pos.CENTER)
.position(Position.BOTTOM_RIGHT) // .position(Pos.CENTER)
// .setScreen(0)
.darkStyle() // .shake(1300, 4)
.theme(Theme.defaultDark)
// .shake(1300, 4)
.shake(1300, 10)
.attach(frame)
.hideCloseButton()
.onAction { System.err.println("Notification $i clicked on!") }
.onClickAction { System.err.println("Notification $i clicked on!") }
notify.showWarning()
try {