view src/name/blackcap/clipman/PasteboardQueue.kt @ 45:ca8a23bae4fa

Adjust spacing in preferences.
author David Barts <n5jrn@me.com>
date Mon, 10 Feb 2020 06:55:07 -0700
parents 33fbe3a78d84
children 88066346f129
line wrap: on
line source

/*
 * The queue of pasteboard items we manage. New stuff gets added to the
 * tail, and old stuff truncated off the head.
 */
package name.blackcap.clipman

import java.awt.Container
import java.awt.Rectangle
import java.util.Collections
import java.util.LinkedList
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.*
import javax.swing.text.DefaultHighlighter

/**
 * A queue that tracks the data we display and the widgets used to
 * display them. We never explicitly remove stuff from the queue,
 * though items will get silently discarded to prevent the queue from
 * exceeding the specified maximum size.
 */
class PasteboardQueue(val parent: Container, maxSize: Int) {
    private val queue = LinkedList<QueueItem>()
    private var _maxSize = maxSize
    private var scrollPane: JScrollPane? = null
    init {
        var sp: Container? = parent
        while (sp != null) {
            if (sp is JScrollPane) {
                scrollPane = sp
                break
            }
            sp = sp.parent
        }
    }

    data class Offset(val inQueue: Int, val inItem: Int)
    enum class Direction { FORWARDS, BACKWARDS }

    /**
     * The maximum allowed size of this queue. Attempts to make the queue
     * larger than this size, or specifying a size smaller than the current
     * size, will result in the oldest item(s) being discarded. A size less
     * than or equal to zero means an unlimited size.
     */
    var maxSize: Int
    get() { return _maxSize }
    @Synchronized set(value) {
        _maxSize = value
        truncate()
    }

    /**
     * Add a QueueItem to the end of the queue.
     * @param item QueueItem to add
     */
    @Synchronized fun add(item: QueueItem) {
        inSwingThread {
            parent.add(item.view.contents)
            scrollPane?.run {
                validate()
                verticalScrollBar.run { value = maximum + 1 }
            }
        }
        queue.addLast(item)
        truncate()
    }

    /**
     * Find and highlight the next occurrence of the specified string
     * @param string to search
     * @param whether to search backwards (default forwards)
     * @param case-folding flag (default true)
     * @param starting point (0, 0) for forwards, (m, n) for backwards
     * @return position where start of string was found, or null
     */
    fun find(needle: String, direction: Direction = Direction.FORWARDS,
        foldCase: Boolean = true, origin: Offset? = null): Offset?
    {
        /* clean up any old highlights */
        queue.forEach {
            val hiliter = it.view.searchable.highlighter
            hiliter.highlights.forEach {
                hiliter.removeHighlight(it)
            }
        }

        /* get starting item index */
        val qMax = queue.size
        var norigin = origin ?: when (direction) {
            Direction.FORWARDS -> Offset(0, 0)
            Direction.BACKWARDS -> Offset(qMax - 1, -1)
        }

        /* loop initialization */
        val (start, incr, search) = if (direction == Direction.FORWARDS) {
            Triple( 0,  1, { n: String, h: String, o: Int -> h.indexOf(n, o, foldCase) })
        } else {
            Triple(-1, -1, { n: String, h: String, o: Int -> h.lastIndexOf(n, o, foldCase) })
        }
        val painter = DefaultHighlighter.DefaultHighlightPainter(null);
        var pos = -1

        /* try and find it */
        while (norigin.inQueue >= 0 && norigin.inQueue < qMax) {
            val si = queue.get(norigin.inQueue).view.searchable
            val doc = si.document
            val text = doc.getText(0, doc.length)
            pos = if (norigin.inItem >= 0) norigin.inItem else text.length - 1
            pos = search(needle, text, pos)
            if (pos >= 0) {
                si.highlighter.addHighlight(pos, pos+needle.length, painter)
                val r = si.modelToView(pos).apply {
                    add(si.modelToView(pos + needle.length - 1))
                }
                si.scrollRectToVisible(r)
                break
            }
            norigin = Offset(norigin.inQueue + incr, start)
        }
        return if (pos >= 0) Offset(norigin.inQueue, pos) else null
    }

    /**
     * Ensure none of the searchables in this queue are selected.
     */
    fun deselectAll() {
        queue.forEach {
            val s = it.view.searchable as? ClipText
            if (s != null && s.selected) {
                s.selected = false
                s.validate()
            }
        }
    }

    /**
     * Return the selected item, or null if nothing has been selected
     */
    fun getSelected(): QueueItem? {
        queue.forEach {
            if ((it.view.searchable as? ClipText)?.selected ?: false) {
                return it
            }
        }
        return null
    }

    private fun truncate() {
        if (_maxSize > 0) {
            var size = queue.size
            var dirty = false
            while (size > _maxSize) {
                var extra = queue.removeFirst().view
                inSwingThread {
                    if (extra.searchable.selected) {
                        anyRequired.disable()
                        styledRequired.disable()
                    }
                    parent.remove(extra.contents)
                }
                dirty = true
                size -= 1
            }
            if (dirty) {
                inSwingThread { parent.validate() }
            }
        }
    }
}

/**
 * An item in the above queue. Linking model to view here sorta violates
 * MVC principles, but rules are sometimes best broken. Doing it this way
 * makes it impossible for the view queue to fail to follow the data
 * queue.
 */
data class QueueItem(val contents: PasteboardItem, val view: PasteboardItemView)