diff --git a/src/dorkbox/notify/ActionHandler.kt b/src/dorkbox/notify/ActionHandler.kt deleted file mode 100755 index a2776c7..0000000 --- a/src/dorkbox/notify/ActionHandler.kt +++ /dev/null @@ -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 { - fun handle(value: T) -} diff --git a/src/dorkbox/notify/AsApplication.kt b/src/dorkbox/notify/AsApplication.kt index dfbb4f7..0ca4805 100755 --- a/src/dorkbox/notify/AsApplication.kt +++ b/src/dorkbox/notify/AsApplication.kt @@ -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 } } } diff --git a/src/dorkbox/notify/AsDesktop.kt b/src/dorkbox/notify/AsDesktop.kt index 321ddb2..5e20a07 100755 --- a/src/dorkbox/notify/AsDesktop.kt +++ b/src/dorkbox/notify/AsDesktop.kt @@ -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() } } diff --git a/src/dorkbox/notify/ClickAdapter.kt b/src/dorkbox/notify/ClickAdapter.kt index 2981c0b..87d89fc 100755 --- a/src/dorkbox/notify/ClickAdapter.kt +++ b/src/dorkbox/notify/ClickAdapter.kt @@ -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) } } diff --git a/src/dorkbox/notify/LAFUtil.kt b/src/dorkbox/notify/LAFUtil.kt new file mode 100755 index 0000000..dcabc73 --- /dev/null +++ b/src/dorkbox/notify/LAFUtil.kt @@ -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 = 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 + } + } +} diff --git a/src/dorkbox/notify/LookAndFeel.kt b/src/dorkbox/notify/LookAndFeel.kt index 09d84e8..8e4e33c 100755 --- a/src/dorkbox/notify/LookAndFeel.kt +++ b/src/dorkbox/notify/LookAndFeel.kt @@ -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 = 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 diff --git a/src/dorkbox/notify/Notify.kt b/src/dorkbox/notify/Notify.kt index 7cabaa9..19b65c5 100755 --- a/src/dorkbox/notify/Notify.kt +++ b/src/dorkbox/notify/Notify.kt @@ -36,16 +36,15 @@ import javax.swing.JFrame * * These notifications are for a single screen only, and cannot be anchored to an application. * - *
- * `Notify.create()
+ * ```
+ * `Notify()
  * .title("Title Text")
  * .text("Hello World!")
- * .useDarkStyle()
- * .showWarning();
-` *
-
* + * .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) + } } diff --git a/src/dorkbox/notify/NotifyCanvas.kt b/src/dorkbox/notify/NotifyCanvas.kt index 21e33e9..1b99c11 100755 --- a/src/dorkbox/notify/NotifyCanvas.kt +++ b/src/dorkbox/notify/NotifyCanvas.kt @@ -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 { diff --git a/src/dorkbox/notify/INotify.kt b/src/dorkbox/notify/NotifyType.kt similarity index 81% rename from src/dorkbox/notify/INotify.kt rename to src/dorkbox/notify/NotifyType.kt index 7df799a..b3d21d8 100755 --- a/src/dorkbox/notify/INotify.kt +++ b/src/dorkbox/notify/NotifyType.kt @@ -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) } diff --git a/src/dorkbox/notify/PopupList.kt b/src/dorkbox/notify/PopupList.kt index 5fd5e5c..26bdb64 100755 --- a/src/dorkbox/notify/PopupList.kt +++ b/src/dorkbox/notify/PopupList.kt @@ -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 } } } diff --git a/src/dorkbox/notify/Pos.kt b/src/dorkbox/notify/Position.kt similarity index 97% rename from src/dorkbox/notify/Pos.kt rename to src/dorkbox/notify/Position.kt index 36df713..d4e16a1 100755 --- a/src/dorkbox/notify/Pos.kt +++ b/src/dorkbox/notify/Position.kt @@ -15,7 +15,7 @@ */ package dorkbox.notify -enum class Pos { +enum class Position { /** * top vertically, left horizontally */ diff --git a/src/dorkbox/notify/Theme.kt b/src/dorkbox/notify/Theme.kt index 38c9679..d7ad6e4 100755 --- a/src/dorkbox/notify/Theme.kt +++ b/src/dorkbox/notify/Theme.kt @@ -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() + } } diff --git a/src/dorkbox/notify/WindowAdapter.kt b/src/dorkbox/notify/WindowAdapter.kt index 9ff06aa..d3c5d82 100755 --- a/src/dorkbox/notify/WindowAdapter.kt +++ b/src/dorkbox/notify/WindowAdapter.kt @@ -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() } } diff --git a/test/dorkbox/notify/NotifyTest.kt b/test/dorkbox/notify/NotifyTest.kt index bf9178c..10b4595 100755 --- a/test/dorkbox/notify/NotifyTest.kt +++ b/test/dorkbox/notify/NotifyTest.kt @@ -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 {