view src/name/blackcap/clipman/Main.kt @ 47:19d9da731c43

Recoded; cleaned up root namespace, removed race conditions.
author David Barts <n5jrn@me.com>
date Sun, 12 Apr 2020 14:31:06 -0700
parents 339e2da5bf83
children 7a75c743f973
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.Container
import java.awt.Dimension
import java.awt.Font
import java.awt.datatransfer.*
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import java.awt.event.WindowEvent
import java.awt.event.WindowListener
import java.util.Date
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.*
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

/* width of main panel border */
val PANEL_BORDER = 9

/* 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() : 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) {
        LOGGER.log(Level.INFO, "execution complete")
        System.exit(0)
    }
}

/* the updating thread */
class UpdateIt(val interval: Int): Thread(), MouseListener {
    @Volatile var enabled = true

    override fun run() {
        var oldContents: PasteboardItem? = null
        while (true) {
            if (enabled) {
                var contents = PasteboardItem.read()
                if ((contents != null) && (contents != oldContents)) {
                    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)
                    }
                    val piv = if (html == null) {
                        PasteboardItemView("Plain text", ClipText(contents).apply {
                            contentType = "text/plain"
                            text = plain
                            font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE)
                            resize()
                        })
                    } else {
                        val (dhtml, style) = preproc(html)
                        val hek = MyEditorKit().apply {
                            style.addStyleSheet(defaultStyleSheet)
                            styleSheet = style
                        }
                        PasteboardItemView("Styled text", ClipText(contents).apply {
                            editorKit = hek
                            text = dhtml
                            resize()
                        })
                    }
                    piv.searchable.addMouseListener(this)
                    Application.queue.add(QueueItem(contents, piv))
                    oldContents = contents
                }
            }
            if (Thread.interrupted()) {
                return
            }
            try {
                Thread.sleep(interval - System.currentTimeMillis() % interval)
            } catch (e: InterruptedException) {
                return
            }
        }
    }

    private fun preproc(html: String): Pair<String, StyleSheet> {
        val sty = StyleSheet().apply {
            addRule("body { font-family: serif; font-size: ${PROP_SIZE}; }")
            addRule("code, kbd, pre, samp, tt { font-family: monospace; font-size: ${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)
    }

    /* MouseListener methods */

    override fun mouseClicked(e: MouseEvent) {
        val source = e.getSource() as? ClipText
        if (source == null) {
            return
        }
        Application.queue.deselectAll()
        source.selected = true
        source.validate()
        Application.anyRequired.enable()
        source.basedOn.let {
            if (it is PasteboardItem.HTML || it is PasteboardItem.RTF) {
                Application.styledRequired.enable()
            } else {
                Application.styledRequired.disable()
            }
        }
    }

 	override fun mousePressed(e: MouseEvent) {
 	    maybeShowPopup(e)
 	}

 	override fun mouseReleased(e: MouseEvent) {
 	    maybeShowPopup(e)
 	}

 	private fun maybeShowPopup(e: MouseEvent) {
 	    if (e.isPopupTrigger()) {
 	        Application.popupMenu.show(e.component, e.x, e.y)
 	    }
 	}

 	override fun mouseEntered(e: MouseEvent) { }
 	override fun mouseExited(e: MouseEvent) { }
}

object Application {
    /* name we call ourselves */
    val MYNAME = "ExifWasher"

    /* global UI objects, must be created on the Swing thread */
    var queue: PasteboardQueue by setOnce()
    var frame: JFrame by setOnce()
    var coerceDialog: CoerceDialog by setOnce()
    var menuItemListener: MenuItemListener by setOnce()
    var popupMenu: MyPopupMenu by setOnce()
    var searchDialog: SearchDialog by setOnce()
    var settingsDialog: SettingsDialog by setOnce()

    /* used by the menus, but not themselves Swing objects */
    val anyRequired = SelectionRequired()
    val styledRequired = SelectionRequired()

    fun initialize() {
        /* make ourselves look more native */
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        /* initialize reusable GUI objects */
        frame = JFrame(MYNAME)  /* must init this gui object first */
        coerceDialog = CoerceDialog()
        menuItemListener = MenuItemListener() /* must init before menus */
        popupMenu = MyPopupMenu()
        searchDialog = SearchDialog()
        settingsDialog = SettingsDialog()

        /* set up the main frame */
        val con = JPanel().apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            border = BorderFactory.createEmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER)
            background = frame.background
        }
        frame.apply {
            jMenuBar = MyMenuBar()
            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)
            addWindowListener(KillIt())
        }
        setMacMenus()

        /* launch the updating thread */
        queue = PasteboardQueue(con, settingsDialog.qLength)
        UpdateIt(1000).apply { start() }
    }
}


/* entry point */
fun main(args: Array<String>) {
    LOGGER.log(Level.INFO, "beginning execution")
    if (OS.type == OS.MAC) {
        System.setProperty("apple.laf.useScreenMenuBar", "true")
    }
    inSwingThread {
        Application.initialize()
    }
}