view src/name/blackcap/clipman/Main.kt @ 14:0cd912d29184

Better cope with Swing's idiosyncrasies.
author David Barts <n5jrn@me.com>
date Mon, 20 Jan 2020 22:59:04 -0800
parents fe0fcfc8b2aa
children 732f92dc3bc6
line wrap: on
line source

/*
 * The entry point and most of the view logic is here.
 */
package name.blackcap.clipman

import java.awt.BorderLayout
import java.awt.Color
import java.awt.Container
import java.awt.Dimension
import java.awt.Font
import java.awt.Toolkit;
import java.awt.datatransfer.*
import java.awt.event.WindowEvent
import java.awt.event.WindowListener
import java.util.Date
import java.util.concurrent.Semaphore
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.*
import javax.swing.border.*
import javax.swing.text.JTextComponent
import javax.swing.text.html.StyleSheet
import javax.swing.text.html.HTMLEditorKit
import kotlin.concurrent.thread
import org.jsoup.Jsoup
import org.jsoup.nodes.*

/* name we call ourselves */
val MYNAME = "ClipMan"

/* default sizes */
val CPWIDTH = 640
val CPHEIGHT = 480

/* border widths */
val PANEL_BORDER = 9
val OUTER_BORDER_TOP = 3
val OUTER_BORDER = 9
val INNER_BORDER = 1
val MARGIN_BORDER = 3

/* default font sizes in the text-display panes */
val MONO_SIZE = 14
val PROP_SIZE = 16

/* kills the updating thread (and does a system exit) when needed */
class KillIt(val thr: Thread) : WindowListener {
    // events we don't care about
    override fun windowActivated(e: WindowEvent) {}
    override fun windowClosed(e: WindowEvent) {}
    override fun windowDeactivated(e: WindowEvent) {}
    override fun windowDeiconified(e: WindowEvent) {}
    override fun windowIconified(e: WindowEvent) {}
    override fun windowOpened(e: WindowEvent) {}

    // and the one we do
    override fun windowClosing(e: WindowEvent) {
        thr.run { interrupt(); join() }
        LOGGER.log(Level.INFO, "execution complete")
        System.exit(0)
    }
}

class ClipText: JTextPane() {
    override fun getMaximumSize(): Dimension {
        return Dimension(Int.MAX_VALUE, preferredSize.height)
    }
}

/* the updating thread */
class UpdateIt(val queue: PasteboardQueue, val interval: Int): Thread() {
    @Volatile var enabled = true
    private val outerBorder =
        MatteBorder(OUTER_BORDER_TOP, OUTER_BORDER, OUTER_BORDER, OUTER_BORDER,
            queue.parent.background)
    private val stdBorder =
        CompoundBorder(LineBorder(Color.GRAY, INNER_BORDER),
            EmptyBorder(MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER))

    override fun run() {
        var oldContents = ""
        var newContents = ""
        while (true) {
            if (enabled) {
                var contents = PasteboardItem.read()
                if (contents != null) {
                    newContents = when (contents) {
                        is PasteboardItem.Plain -> contents.plain
                        is PasteboardItem.HTML -> contents.html
                    }
                    if (oldContents != newContents) {
                        var stdWidth: Int? = null
                        inSynSwingThread {
                            stdWidth = queue.parent.size.width - 2 * (PANEL_BORDER+OUTER_BORDER+INNER_BORDER+MARGIN_BORDER)
                        }
                        var widget = JPanel().apply {
                            layout = BoxLayout(this, BoxLayout.Y_AXIS)
                            background = queue.parent.background
                            border = outerBorder
                        }
                        when (contents) {
                            is PasteboardItem.Plain -> widget.run {
                                add(stdLabel("Plain text"))
                                add(ClipText().apply {
                                    contentType = "text/plain"
                                    text = contents.plain
                                    font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE)
                                    border = stdBorder
                                    autoSize(stdWidth!!)
                                    setEditable(false)
                                    alignmentX = JTextPane.LEFT_ALIGNMENT
                                })
                            }
                            is PasteboardItem.HTML -> widget.run {
                                add(stdLabel("Styled text"))
                                val (html, style) = preproc(contents.html)
                                val hek = HTMLEditorKit().apply {
                                    style.addStyleSheet(styleSheet)
                                    styleSheet = style
                                }
                                add(ClipText().apply {
                                    editorKit = hek
                                    text = html
                                    border = stdBorder
                                    autoSize(stdWidth!!)
                                    setEditable(false)
                                    alignmentX = JTextPane.LEFT_ALIGNMENT
                                })
                            }
                        }
                        queue.add(QueueItem(widget, contents))
                        oldContents = newContents
                    }
                }
            }
            if (Thread.interrupted()) {
                return
            }
            try {
                Thread.sleep(interval - System.currentTimeMillis() % interval)
            } catch (e: InterruptedException) {
                return
            }
        }
    }

    private fun stdLabel(text: String) = JLabel(text).apply {
        horizontalAlignment = JLabel.LEFT
        alignmentX = JLabel.LEFT_ALIGNMENT
    }

    private fun preproc(html: String): Pair<String, StyleSheet> {
        val sty = StyleSheet().apply {
            addRule("head, body { font-family: serif; font-size: %d; }".format(PROP_SIZE))
            addRule("code, kbd, pre, samp, tt { font-family: monospace; font-size: %d; }".format(MONO_SIZE))
        }
        val scrubbed = Jsoup.parse(html).run {
            select("style").forEach {
                it.dataNodes().forEach { sty.addRule(it.wholeData) }
            }
            select(":root>head>meta").remove()
            outputSettings()
                .charset(CHARSET_NAME)
                .syntax(Document.OutputSettings.Syntax.xml)
            outerHtml()
        }
        return Pair(scrubbed, sty)
    }
}

fun main(args: Array<String>) {
    LOGGER.log(Level.INFO, "beginning execution")
    setLookFeel()
    val frame = JFrame(MYNAME)
    val con = JPanel().apply {
        layout = BoxLayout(this, BoxLayout.Y_AXIS)
        border = EmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER)
        background = frame.background
    }
    inSynSwingThread {
        frame.apply {
            contentPane.add(
                JScrollPane(con).apply {
                    verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
                    horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
                    preferredSize = Dimension(CPWIDTH, CPHEIGHT)
                    background = frame.background
                }, BorderLayout.CENTER)
            pack()
            setVisible(true)
        }
    }
    val queue = PasteboardQueue(con, 10)
    val updater = UpdateIt(queue, 1000).apply { start() }
    inSwingThread { frame.addWindowListener(KillIt(updater)) }
}

fun setLookFeel() {
    inSwingThread {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
}

fun inSwingThread(block: () -> Unit) {
    SwingUtilities.invokeLater(Runnable(block))
}

fun inSynSwingThread(block: () -> Unit) {
    val ready = Semaphore(0)
    inSwingThread {
        block()
        ready.release()
    }
    ready.acquire()
}

fun JTextComponent.autoSize(width: Int): Unit {
    val SLOP = 10
    val dim = Dimension(width, width)
    preferredSize = dim
    size = dim
    val r = modelToView(document.length)
    preferredSize = Dimension(width, r.y + r.height + SLOP)
}