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

org.fernice.reflare.ui.FlareScrollBarUI.kt Maven / Gradle / Ivy

There is a newer version: 1.3.1
Show newest version
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package org.fernice.reflare.ui

import fernice.reflare.SCROLL_BAR_MINIMALISTIC_PROPERTY
import fernice.reflare.classes
import fernice.reflare.isMinimalistic
import fernice.reflare.light.FButton
import org.fernice.reflare.Defaults
import org.fernice.reflare.element.ComponentElement
import org.fernice.reflare.element.ScrollBarElement
import org.fernice.reflare.element.StyleTreeElementLookup
import org.fernice.reflare.platform.Platform
import org.fernice.reflare.render.use
import java.awt.Color
import java.awt.Component
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Rectangle
import java.beans.PropertyChangeListener
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JScrollBar
import javax.swing.SwingConstants
import javax.swing.UIManager
import javax.swing.plaf.ComponentUI
import javax.swing.plaf.basic.BasicScrollBarUI

@Suppress("ACCIDENTAL_OVERRIDE")
class FlareScrollBarUI(scrollbar: JScrollBar) : BasicScrollBarUI(), FlareUI {

    override val element: ComponentElement = ScrollBarElement(scrollbar)

    override fun installDefaults() {
        super.installDefaults()

        minimumThumbSize = Dimension(30, 30)
        maximumThumbSize = Dimension(1000, 1000)

        incrGap = 0
        decrGap = 0

        scrollbar.isMinimalistic = Platform.isMac
        scrollbar.isOpaque = false
        scrollbar.border = FlareBorder.create(this)
        scrollbar.font = Defaults.FONT_SERIF

        updateScrollBar()

        StyleTreeElementLookup.registerElement(scrollbar, this)
    }

    override fun uninstallDefaults() {
        super.uninstallDefaults()

        StyleTreeElementLookup.deregisterElement(scrollbar)
    }

    override fun createPropertyChangeListener(): PropertyChangeListener {
        val propertyChangeListener = super.createPropertyChangeListener()
        return PropertyChangeListener { event ->
            when (event.propertyName) {
                SCROLL_BAR_MINIMALISTIC_PROPERTY -> updateScrollBar()
            }

            propertyChangeListener.propertyChange(event)
        }
    }

    private fun updateScrollBar(){
        val minimalistic = scrollbar.isMinimalistic
        scrollBarWidth = if (minimalistic) 12 else 16
        if (minimalistic) element.classes.add("flr-scrollbar-minimalistic") else element.classes.remove("flr-scrollbar-minimalistic")
    }

    override fun getMinimumSize(c: JComponent): Dimension? {
        element.pulseForComputation()
        return super.getMinimumSize(c)
    }

    override fun getPreferredSize(c: JComponent): Dimension? {
        element.pulseForComputation()
        return super.getPreferredSize(c)
    }

    override fun getMaximumSize(c: JComponent): Dimension? {
        element.pulseForComputation()
        return super.getMaximumSize(c)
    }

    override fun paint(graphics: Graphics, component: JComponent) {
        paintBackground(component, graphics)

        super.paint(graphics, component)
    }

    override fun paintTrack(g: Graphics, c: JComponent, trackBounds: Rectangle) {}

    override fun paintThumb(g: Graphics, c: JComponent, thumbBounds: Rectangle) {
        if (thumbBounds.isEmpty || !scrollbar.isEnabled) {
            return
        }

        val vertical = scrollbar.orientation == JScrollBar.VERTICAL
        val showButtons = !scrollbar.isMinimalistic

        val w = thumbBounds.width
        val h = thumbBounds.height
        val padding = if (showButtons) 0 else 2

        val wp = if (vertical) padding else 0
        val hp = if (vertical) 0 else padding

        val radii = when {
            showButtons -> 0
            scrollbar.orientation == JScrollBar.VERTICAL -> w - 2
            scrollbar.orientation == JScrollBar.HORIZONTAL -> h - 2
            else -> 0
        }

        g.use { g2 ->
            g2.color = scrollbar.foreground
            g2.fillRoundRect(thumbBounds.x + wp, thumbBounds.y + hp, w - (wp * 2), h - (hp * 2), radii, radii)
        }
    }

    private fun paintBackground(component: JComponent, g: Graphics) {
        element.paintBackground(g)
    }

    override fun paintBorder(c: Component, g: Graphics) {
        element.paintBorder(g)
    }

    override fun layoutVScrollbar(sb: JScrollBar) {
        val sbSize = sb.size
        val sbInsets = sb.insets

        val showButtons = !scrollbar.isMinimalistic

        /*
         * Width and left edge of the buttons and thumb.
         */
        val itemW = sbSize.width - (sbInsets.left + sbInsets.right)
        val itemX = sbInsets.left

        /* Nominal locations of the buttons, assuming their preferred
         * size will fit.
         */
        var decrButtonH = if (showButtons) itemW else 0
        val decrButtonY = sbInsets.top

        var incrButtonH = if (showButtons) itemW else 0
        var incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH)

        /* The thumb must fit within the height left over after we
         * subtract the preferredSize of the buttons and the insets
         * and the gaps
         */
        val sbInsetsH = sbInsets.top + sbInsets.bottom
        val sbButtonsH = decrButtonH + incrButtonH
        val gaps = decrGap + incrGap
        val trackH = (sbSize.height - (sbInsetsH + sbButtonsH) - gaps).toFloat()

        /* Compute the height and origin of the thumb.   The case
         * where the thumb is at the bottom edge is handled specially
         * to avoid numerical problems in computing thumbY.  Enforce
         * the thumbs min/max dimensions.  If the thumb doesn't
         * fit in the track (trackH) we'll hide it later.
         */
        val min = sb.minimum.toFloat()
        val extent = sb.visibleAmount.toFloat()
        val range = sb.maximum - min
        val value = sb.value.toFloat()

        var thumbH = if (range <= 0)
            getMaximumThumbSize().height
        else
            (trackH * (extent / range)).toInt()
        thumbH = Math.max(thumbH, getMinimumThumbSize().height)
        thumbH = Math.min(thumbH, getMaximumThumbSize().height)

        var thumbY = incrButtonY - incrGap - thumbH
        if (value < sb.maximum - sb.visibleAmount) {
            val thumbRange = trackH - thumbH
            thumbY = (0.5f + thumbRange * ((value - min) / (range - extent))).toInt()
            thumbY += decrButtonY + decrButtonH + decrGap
        }

        /* If the buttons don't fit, allocate half of the available
         * space to each and move the lower one (incrButton) down.
         */
        val sbAvailButtonH = sbSize.height - sbInsetsH
        if (sbAvailButtonH < sbButtonsH) {
            decrButtonH = sbAvailButtonH / 2
            incrButtonH = decrButtonH
            incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH)
        }
        decrButton.setBounds(itemX, decrButtonY, itemW, decrButtonH)
        incrButton.setBounds(itemX, incrButtonY, itemW, incrButtonH)

        /* Update the trackRect field.
         */
        val itrackY = decrButtonY + decrButtonH + decrGap
        val itrackH = incrButtonY - incrGap - itrackY
        trackRect.setBounds(itemX, itrackY, itemW, itrackH)

        /* If the thumb isn't going to fit, zero it's bounds.  Otherwise
         * make sure it fits between the buttons.  Note that setting the
         * thumbs bounds will cause a repaint.
         */
        if (thumbH >= trackH.toInt()) {
            if (UIManager.getBoolean("ScrollBar.alwaysShowThumb")) {
                // This is used primarily for GTK L&F, which expands the
                // thumb to fit the track when it would otherwise be hidden.
                setThumbBounds(itemX, itrackY, itemW, itrackH)
            } else {
                // Other L&F's simply hide the thumb in this case.
                setThumbBounds(0, 0, 0, 0)
            }
        } else {
            if (thumbY + thumbH > incrButtonY - incrGap) {
                thumbY = incrButtonY - incrGap - thumbH
            }
            if (thumbY < decrButtonY + decrButtonH + decrGap) {
                thumbY = decrButtonY + decrButtonH + decrGap + 1
            }
            setThumbBounds(itemX, thumbY, itemW, thumbH)
        }
    }

    override fun layoutHScrollbar(sb: JScrollBar) {
        val sbSize = sb.size
        val sbInsets = sb.insets

        val showButtons = !scrollbar.isMinimalistic

        /* Height and top edge of the buttons and thumb.
         */
        val itemH = sbSize.height - (sbInsets.top + sbInsets.bottom)
        val itemY = sbInsets.top

        val ltr = sb.componentOrientation.isLeftToRight

        /* Nominal locations of the buttons, assuming their preferred
         * size will fit.
         */
        var leftButtonW = if (showButtons) itemH else 0
        var rightButtonW = if (showButtons) itemH else 0
        if (!ltr) {
            val temp = leftButtonW
            leftButtonW = rightButtonW
            rightButtonW = temp
        }
        val leftButtonX = sbInsets.left
        var rightButtonX = sbSize.width - (sbInsets.right + rightButtonW)
        val leftGap = if (ltr) decrGap else incrGap
        val rightGap = if (ltr) incrGap else decrGap

        /* The thumb must fit within the width left over after we
         * subtract the preferredSize of the buttons and the insets
         * and the gaps
         */
        val sbInsetsW = sbInsets.left + sbInsets.right
        val sbButtonsW = leftButtonW + rightButtonW
        val trackW = (sbSize.width - (sbInsetsW + sbButtonsW) - (leftGap + rightGap)).toFloat()

        /* Compute the width and origin of the thumb.  Enforce
         * the thumbs min/max dimensions.  The case where the thumb
         * is at the right edge is handled specially to avoid numerical
         * problems in computing thumbX.  If the thumb doesn't
         * fit in the track (trackH) we'll hide it later.
         */
        val min = sb.minimum.toFloat()
        val max = sb.maximum.toFloat()
        val extent = sb.visibleAmount.toFloat()
        val range = max - min
        val value = sb.value.toFloat()

        var thumbW = if (range <= 0)
            getMaximumThumbSize().width
        else
            (trackW * (extent / range)).toInt()
        thumbW = Math.max(thumbW, getMinimumThumbSize().width)
        thumbW = Math.min(thumbW, getMaximumThumbSize().width)

        var thumbX = if (ltr) rightButtonX - rightGap - thumbW else leftButtonX + leftButtonW + leftGap
        if (value < max - sb.visibleAmount) {
            val thumbRange = trackW - thumbW
            thumbX = if (ltr) {
                (0.5f + thumbRange * ((value - min) / (range - extent))).toInt()
            } else {
                (0.5f + thumbRange * ((max - extent - value) / (range - extent))).toInt()
            }
            thumbX += leftButtonX + leftButtonW + leftGap
        }

        /* If the buttons don't fit, allocate half of the available
         * space to each and move the right one over.
         */
        val sbAvailButtonW = sbSize.width - sbInsetsW
        if (sbAvailButtonW < sbButtonsW) {
            leftButtonW = sbAvailButtonW / 2
            rightButtonW = leftButtonW
            rightButtonX = sbSize.width - (sbInsets.right + rightButtonW + rightGap)
        }

        (if (ltr) decrButton else incrButton).setBounds(leftButtonX, itemY, leftButtonW, itemH)
        (if (ltr) incrButton else decrButton).setBounds(rightButtonX, itemY, rightButtonW, itemH)

        /* Update the trackRect field.
         */
        val itrackX = leftButtonX + leftButtonW + leftGap
        val itrackW = rightButtonX - rightGap - itrackX
        trackRect.setBounds(itrackX, itemY, itrackW, itemH)

        /* Make sure the thumb fits between the buttons.  Note
         * that setting the thumbs bounds causes a repaint.
         */
        if (thumbW >= trackW.toInt()) {
            if (UIManager.getBoolean("ScrollBar.alwaysShowThumb")) {
                // This is used primarily for GTK L&F, which expands the
                // thumb to fit the track when it would otherwise be hidden.
                setThumbBounds(itrackX, itemY, itrackW, itemH)
            } else {
                // Other L&F's simply hide the thumb in this case.
                setThumbBounds(0, 0, 0, 0)
            }
        } else {
            if (thumbX + thumbW > rightButtonX - rightGap) {
                thumbX = rightButtonX - rightGap - thumbW
            }
            if (thumbX < leftButtonX + leftButtonW + leftGap) {
                thumbX = leftButtonX + leftButtonW + leftGap + 1
            }
            setThumbBounds(thumbX, itemY, thumbW, itemH)
        }
    }

    override fun createIncreaseButton(orientation: Int): JButton = FlareArrowButton(orientation)
    override fun createDecreaseButton(orientation: Int): JButton = FlareArrowButton(orientation)

    companion object {

        @JvmStatic
        fun createUI(component: JComponent): ComponentUI {
            return FlareScrollBarUI(component as JScrollBar)
        }
    }
}

private class FlareArrowButton(var direction: Int) : FButton(), SwingConstants {

    init {
        isRequestFocusEnabled = false
        classes.add("flr-scrollbar-button")
    }

    override fun paint(g: Graphics?) {
        super.paint(g)

        val origColor: Color = g!!.color
        val isEnabled: Boolean = isEnabled
        val w: Int = size.width
        val h: Int = size.height
        var size: Int

        // If there's no room to draw arrow, bail
        if (h < 5 || w < 5) {
            g.color = origColor
            return
        }

        // Draw the arrow
        size = Math.min((h - 4) / 3, (w - 4) / 3)
        size = Math.max(size, 2)
        paintTriangle(
            g, (w - size) / 2, (h - size) / 2,
            size, direction, isEnabled
        )
    }

    /**
     * Returns the preferred size of the `BasicArrowButton`.
     *
     * @return the preferred size
     */
    override fun getPreferredSize(): Dimension {
        return Dimension(16, 16)
    }

    /**
     * Returns the minimum size of the `BasicArrowButton`.
     *
     * @return the minimum size
     */
    override fun getMinimumSize(): Dimension {
        return Dimension(5, 5)
    }

    /**
     * Returns the maximum size of the `BasicArrowButton`.
     *
     * @return the maximum size
     */
    override fun getMaximumSize(): Dimension {
        return Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)
    }

    /**
     * Returns whether the arrow button should get the focus.
     * `BasicArrowButton`s are used as a child component of
     * composite components such as `JScrollBar` and
     * `JComboBox`. Since the composite component typically gets the
     * focus, this method is overriden to return `false`.
     *
     * @return `false`
     */
    @Deprecated(message = "Deprecated in Java")
    @Suppress("OverridingDeprecatedMember")
    override fun isFocusTraversable(): Boolean {
        return false
    }

    /**
     * Paints a triangle.
     *
     * @param g the `Graphics` to draw to
     * @param x the x coordinate
     * @param y the y coordinate
     * @param size the size of the triangle to draw
     * @param direction the direction in which to draw the arrow;
     * one of `SwingConstants.NORTH`,
     * `SwingConstants.SOUTH`, `SwingConstants.EAST` or
     * `SwingConstants.WEST`
     * @param isEnabled whether or not the arrow is drawn enabled
     */
    private fun paintTriangle(
        g: Graphics, x: Int, y: Int, size: Int,
        direction: Int, isEnabled: Boolean,
    ) {
        var size0 = size
        val oldColor = g.color
        val mid: Int
        var i: Int
        var j: Int

        j = 0
        size0 = Math.max(size0, 2)
        mid = size0 / 2 - 1

        g.translate(x, y)
        if (isEnabled)
            g.color = foreground
        else
            g.color = foreground

        when (direction) {
            SwingConstants.NORTH -> {
                i = 0
                while (i < size0) {
                    g.drawLine(mid - i, i, mid + i, i)
                    i++
                }
                if (!isEnabled) {
                    g.color = foreground
                    g.drawLine(mid - i + 2, i, mid + i, i)
                }
            }
            SwingConstants.SOUTH -> {
                if (!isEnabled) {
                    g.translate(1, 1)
                    g.color = foreground
                    i = size0 - 1
                    while (i >= 0) {
                        g.drawLine(mid - i, j, mid + i, j)
                        j++
                        i--
                    }
                    g.translate(-1, -1)
                    g.color = foreground
                }

                j = 0
                i = size0 - 1
                while (i >= 0) {
                    g.drawLine(mid - i, j, mid + i, j)
                    j++
                    i--
                }
            }
            SwingConstants.WEST -> {
                i = 0
                while (i < size0) {
                    g.drawLine(i, mid - i, i, mid + i)
                    i++
                }
                if (!isEnabled) {
                    g.color = foreground
                    g.drawLine(i, mid - i + 2, i, mid + i)
                }
            }
            SwingConstants.EAST -> {
                if (!isEnabled) {
                    g.translate(1, 1)
                    g.color = foreground
                    i = size0 - 1
                    while (i >= 0) {
                        g.drawLine(j, mid - i, j, mid + i)
                        j++
                        i--
                    }
                    g.translate(-1, -1)
                    g.color = foreground
                }

                j = 0
                i = size0 - 1
                while (i >= 0) {
                    g.drawLine(j, mid - i, j, mid + i)
                    j++
                    i--
                }
            }
        }
        g.translate(-x, -y)
        g.color = oldColor
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy