view src/name/blackcap/clipman/Main.kt @ 24:dac8dfb4b549

Preliminary menu bar support.
author David Barts <n5jrn@me.com>
date Thu, 23 Jan 2020 19:25:17 -0800
parents c10a447b9e1b
children 8aa2dfac27eb
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.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.KeyEvent
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.HTMLEditorKit
import javax.swing.text.html.StyleSheet
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)
    }
}

/* HTMLEditorKit shares all stylesheets. How unbelievably braindamaged. */
class MyEditorKit: HTMLEditorKit() {
    private var _styleSheet = defaultStyleSheet
    override fun getStyleSheet() = _styleSheet
    override fun setStyleSheet(value: StyleSheet) {
        _styleSheet = value
    }

    val defaultStyleSheet: StyleSheet
    get() {
        return super.getStyleSheet()
    }
}

/* 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: PasteboardItem? = null
        while (true) {
            if (enabled) {
                var contents = PasteboardItem.read()
                if ((contents != null) && (contents != oldContents)) {
                    val stdWidth = queue.parent.size.width - 2 * (PANEL_BORDER+OUTER_BORDER+INNER_BORDER+MARGIN_BORDER)
                    val widget = JPanel().apply {
                        layout = BoxLayout(this, BoxLayout.Y_AXIS)
                        background = queue.parent.background
                        border = outerBorder
                    }
                    val (plain, html) = when(contents) {
                        is PasteboardItem.Plain -> Pair(contents.plain, null)
                        is PasteboardItem.HTML -> Pair(null, contents.html)
                        is PasteboardItem.RTF -> Pair(contents.plain, contents.html)
                    }
                    var searchable: JTextComponent? = null
                    if (html == null) {
                        widget.run {
                            add(stdLabel("Plain text"))
                            searchable = ClipText().apply {
                                contentType = "text/plain"
                                text = plain
                                font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE)
                                border = stdBorder
                                autoSize(stdWidth)
                                setEditable(false)
                                alignmentX = JTextPane.LEFT_ALIGNMENT
                            }
                            add(searchable)
                        }
                    } else {
                        widget.run {
                            add(stdLabel("Styled text"))
                            val (dhtml, style) = preproc(html)
                            val hek = MyEditorKit().apply {
                                style.addStyleSheet(defaultStyleSheet)
                                styleSheet = style
                            }
                            searchable = ClipText().apply {
                                editorKit = hek
                                text = dhtml
                                border = stdBorder
                                autoSize(stdWidth)
                                setEditable(false)
                                alignmentX = JTextPane.LEFT_ALIGNMENT
                            }
                            add(searchable)
                        }
                    }
                    queue.add(QueueItem(widget, searchable!!, contents))
                    oldContents = contents
                }
            }
            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("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")
    if (OS.type == OS.MAC) {
        System.setProperty("apple.laf.useScreenMenuBar", "true")
    }
    var frame: JFrame? = null
    var con: JPanel? = null
    inSynSwingThread {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        frame = JFrame(MYNAME)
        con = JPanel().apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            border = EmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER)
            background = frame!!.background
        }
        frame!!.apply {
            jMenuBar = makeMenuBar()
            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)) }
}

class MenuItemListener: ActionListener {
    override fun actionPerformed(e: ActionEvent) {
        println(e.actionCommand + " selected")
    }
}

fun makeMenuBar() = JMenuBar().apply {
    val al: ActionListener = MenuItemListener()
    if (OS.type != OS.MAC) {
        add(JMenu("File").apply {
            add(JMenuItem("Quit").apply {
                actionCommand = "File.Quit"
                addActionListener(al)
                makeShortcut(KeyEvent.VK_Q)
            })
        })
    }
    add(JMenu("Edit").apply {
        add(JMenuItem("Clone").apply {
            actionCommand = "Edit.Clone"
            addActionListener(al)
            makeShortcut(KeyEvent.VK_C)
        })
        add(JMenuItem("Coerce…").apply {
            actionCommand = "Edit.Coerce"
            addActionListener(al)
            makeShortcut(KeyEvent.VK_K)
        })
        add(JMenuItem("Delete").apply {
            actionCommand = "Edit.Delete"
            addActionListener(al)
            setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0))
        })
        add(JMenuItem("Find…").apply {
            actionCommand = "Edit.Find"
            addActionListener(al)
            makeShortcut(KeyEvent.VK_F)
        })
    })
    if (OS.type != OS.MAC) {
        add(JMenu("Help").apply {
            add(JMenuItem("About ClipMan…").apply {
                actionCommand = "Help.About"
                addActionListener(al)
            })
        })
    }
}

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)
}

val SC_KEY_MASK = Toolkit.getDefaultToolkit().menuShortcutKeyMask
fun JMenuItem.makeShortcut(key: Int): Unit {
    setAccelerator(KeyStroke.getKeyStroke(key, SC_KEY_MASK))
}