phase 2 kotlin WIP
This commit is contained in:
parent
a218bfb8db
commit
b286ee9cf1
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
222
src/dorkbox/notify/LAFUtil.kt
Executable 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package dorkbox.notify
|
||||
|
||||
enum class Pos {
|
||||
enum class Position {
|
||||
/**
|
||||
* top vertically, left horizontally
|
||||
*/
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user