All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.notify.AppNotify.kt Maven / Gradle / Ivy

Go to download

Linux, MacOS, or Windows (notification/growl/toast/) popups for the desktop for Java 8+

There is a newer version: 4.5
Show newest version
/*
 * Copyright 2017 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.notify.LAFUtil.tweenEngine
import dorkbox.tweenEngine.Tween
import dorkbox.tweenEngine.TweenEquations
import dorkbox.tweenEngine.TweenEvents
import java.awt.Canvas
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Rectangle
import java.awt.event.MouseAdapter
import java.awt.image.BufferedImage
import javax.swing.JFrame
import javax.swing.JPanel

// this is a child to a Jframe/window (instead of globally to the screen).
internal class AppNotify(override val notification: Notify): Canvas(), NotifyType {

    companion object {
        private const val glassPanePrefix = "dorkbox.notify"

        // the state of the glass pane BEFORE we modified it.
        private val previousStates = mutableMapOf>()

        private val tweenAccessor = AppAccessor()
        private val mouseListener: MouseAdapter = AppMouseAdapter()

        @Suppress("DuplicatedCode")
        private fun getAnchorX(position: Position, bounds: Rectangle): Int {
            // we use the screen that the mouse is currently on.
            val startX = 0
            val screenWidth = bounds.getWidth().toInt()

            return when (position) {
                Position.TOP_LEFT, Position.BOTTOM_LEFT -> Notify.MARGIN + startX
                Position.CENTER -> startX + screenWidth / 2 - Notify.WIDTH / 2 - Notify.MARGIN / 2
                Position.TOP_RIGHT, Position.BOTTOM_RIGHT -> startX + screenWidth - Notify.WIDTH - Notify.MARGIN
            }
        }

        private fun getAnchorY(position: Position, bounds: Rectangle): Int {
            val startY = 0
            val screenHeight = bounds.getHeight().toInt()

            return when (position) {
                Position.TOP_LEFT, Position.TOP_RIGHT -> startY + Notify.MARGIN
                Position.CENTER -> startY + screenHeight / 2 - Notify.HEIGHT / 2 - Notify.MARGIN / 2 - Notify.SPACER
                Position.BOTTOM_LEFT, Position.BOTTOM_RIGHT -> screenHeight - Notify.HEIGHT - Notify.MARGIN - Notify.SPACER * 2
            }
        }
    }


    private lateinit var cachedImage: BufferedImage
    private lateinit var cachedClose: BufferedImage
    private lateinit var cachedCloseEnabled: BufferedImage

    // for the progress bar. we directly draw this onscreen
    // non-volatile because it's always accessed in the active render thread
    private var prevProgress = 0
    override var progress = 0
        set(value) {
            prevProgress = field
            field = value
        }

    override var shakeTween: Tween? = null
    override var moveTween: Tween? = null
    override var hideTween: Tween? = null

    // this is used in combination with position, so that we can track which screen and what position a popup is in
    override var idAndPosition = ""
    override var popupIndex = 0

    override var anchorX = 0
    override var anchorY = 0

    // the ONLY reason "shake" works, is because we configure the target X/Y location for moving based on where we WANT the popup to go
    // and completely ignoring its current position
    var shakeX = 0
    var shakeY = 0


    @Volatile
    var mouseY = 0
    @Volatile
    var mouseX = 0

    private val parent = notification.attachedFrame!!
    private var glassPane: JPanel

    // this is on the swing EDT
    init {
        addMouseListener(mouseListener)
        addMouseMotionListener(mouseListener)

        val actualSize = Dimension(Notify.WIDTH, Notify.HEIGHT)

        preferredSize = actualSize
        maximumSize = actualSize
        minimumSize = actualSize
        size = actualSize

        isFocusable = true
        background = notification.theme.panel_BG


        idAndPosition = parent.name + ":" + notification.position

        anchorX = getAnchorX(notification.position, parent.bounds)
        anchorY = getAnchorY(notification.position, parent.bounds)

        val pane = parent.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
                // NOTE: This could cause problems!

                // glassPane = new JPanel();
                glassPane.layout = null
                glassPane.name = glassPanePrefix

                previousStates[parent] = Pair(name, glassPane.isVisible)

                if (!glassPane.isVisible) {
                    glassPane.isVisible = true
                }

                // glassPane.setSize(appWindow.getSize());
                // glassPane.setOpaque(false);
                // appWindow.setGlassPane(glassPane);
            }

            // add our "notify canvas" drawing element to the parent JFrame (or other component)
            glassPane.add(this)
        } else {
            throw RuntimeException("Not able to add the notification to the window glassPane")
        }

        // now we setup the rendering of the image
        refresh()
    }

    override fun refresh() {
        cachedImage = renderBackgroundInfo(notification.title, notification.text, notification.theme, notification.image)
        cachedClose = renderCloseButton(notification.theme, false)
        cachedCloseEnabled = renderCloseButton(notification.theme, true)

        idAndPosition = parent.name + ":" + notification.position

        anchorX = getAnchorX(notification.position, parent.bounds)
        anchorY = getAnchorY(notification.position, parent.bounds)
    }

    override fun paint(g: Graphics) {
        // we cache the text + image (to an image), the two stats of the close "button" and then always render the close + progressbar
        try {
            draw(g)
        } catch (ignored: Exception) {
            // have also seen (happened after screen/PC was "woken up", in Xubuntu 16.04):
            // java.lang.ClassCastException:sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:148)
            // at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:356)
            // at sun.java2d.SurfaceDataProxy.updateSurfaceData(SurfaceDataProxy.java:498)
            // at sun.java2d.SurfaceDataProxy.replaceData(SurfaceDataProxy.java:455)
            // at sun.java2d.SurfaceData.getSourceSurfaceData(SurfaceData.java:233)
            // at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:566)
            // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
            // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
            // at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
            // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
            // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
            // at dorkbox.notify.NotifyCanvas.paint(NotifyCanvas.java:92)

            // redo the cache
            refresh()

            // try to draw again
            try {
                draw(g)
            } catch (ignored2: Exception) {
            }
        }

        // the progress bar can change, so we always draw it every time
        if (progress > 0 && prevProgress != progress) {
            // draw the progress bar along the bottom
            g.color = notification.theme.progress_FG
            g.fillRect(0, Notify.HEIGHT - 2, progress, 2)
        }
    }

    private fun draw(g: Graphics) {
        g.drawImage(cachedImage, 0, 0, null)

        if (!notification.hideCloseButton) {
            if (mouseX >= 280 && mouseY <= 20) {
                g.drawImage(cachedCloseEnabled, 0, 0, null)
            } else {
                g.drawImage(cachedClose, 0, 0, null)
            }
        }
    }

    fun onClick(x: Int, y: Int) {
        // Check - we were over the 'X' (and thus no notify), or was it in the general area?
        val isClickOnCloseButton = !notification.hideCloseButton && x >= 280 && y <= 20

        if (isClickOnCloseButton) {
            // we always close the notification popup
            notification.onClose()
        } else {
            // only call the general click handler IF we click in the general area!
            notification.onClickAction()
        }
    }

    override fun setupHide() {
        if (hideTween == null && notification.hideAfterDurationInMillis > 0) {
            // begin a timeline to get rid of the popup (default is 5 seconds)
            val tween = tweenEngine
                .to(this, AppAccessor.PROGRESS, tweenAccessor, notification.hideAfterDurationInMillis / 1000.0f)
                .value(Notify.WIDTH.toFloat())
                .ease(TweenEquations.Linear)
                .addCallback(TweenEvents.COMPLETE) {
                    notification.onClose()
                }

            hideTween = tween
            tween.start()
        }
    }

    override fun setupMove(y: Float) {
        if (moveTween != null) {
            moveTween!!.value(y)
        } else {
            val tween = tweenEngine
                .to(this, AppAccessor.Y_POS, tweenAccessor, Notify.MOVE_DURATION)
                .value(y)
                .ease(TweenEquations.Linear)
                .addCallback(TweenEvents.COMPLETE) {
                    // make sure to remove the tween once it's done, otherwise .cancel can do weird things.
                    moveTween = null
                }

            moveTween = tween
            tween.start()
        }
    }

    override fun doShake(count: Int, targetX: Float, targetY: Float) {
        if (shakeTween != null) {
            shakeTween!!.valueRelative(targetX, targetY)
                .repeatAutoReverse(count, 0f)
        } else {
            val tween = tweenEngine
                .to(this, AppAccessor.SHAKE, tweenAccessor, 0.05f)
                .valueRelative(targetX, targetY)
                .repeatAutoReverse(count, 0f)
                .ease(TweenEquations.Linear)

            shakeTween = tween
            tween.start()
        }
    }

    fun setLocationShake(x: Int, y: Int) {
        val x1 = getX() - shakeX
        val y1 = getY() - shakeY
        shakeX = x
        shakeY = y
        setLocationInternal(x1 + x, y1 + y)
    }

    override fun setLocationInternal(x: Int, y: Int) {
        setLocation(x, y)
    }

    // this is called during parent initialization (before we are initialized), so we cannot access objects here properly!
    // NOTE: This is present here because DesktopNotify requires it (and consistency is important)
//    override fun setLocation(x: Int, y: Int) {
//        super.setLocation(x, y)
//    }

    override fun setVisible(visible: Boolean) {
        // this is because the order of operations are different based upon visibility.
        updatePositionsPre(this, this, visible)
        updatePositionsPost(this, this, visible)
    }

    // called on the Swing EDT.
    override fun close() {
        cancelMove()
        cancelHide()
        cancelShake()

        glassPane.remove(this)

        removeMouseMotionListener(mouseListener)
        removeMouseListener(mouseListener)

        updatePositionsPre(component = this, notify = this, visible = false)

        // revert the glass pane if there are no more notifications on it.
        var found = false
        val components = glassPane.components
        for (component in components) {
            if (component is AppNotify) {
                found = true
                break
            }
        }

        if (!found) {
            val (name, visibility) = previousStates.remove(parent)!!
            glassPane.name = name
            glassPane.isVisible = visibility
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy