diff src/name/blackcap/clipman/Pasteboard.kt @ 0:be282c48010a

Incomplete; checking it in as a backup.
author David Barts <n5jrn@me.com>
date Tue, 14 Jan 2020 14:07:19 -0800
parents
children 9dd58db4d15a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/clipman/Pasteboard.kt	Tue Jan 14 14:07:19 2020 -0800
@@ -0,0 +1,139 @@
+/*
+ * We call the clipboard a "pasteboard" for our internal class name, not
+ * because I prefer that term (I don't) but so as to not clash with the
+ * AWT's Clipboard class.
+ */
+package name.blackcap.clipman
+
+import java.awt.Toolkit
+import java.awt.datatransfer.Clipboard
+import java.awt.datatransfer.ClipboardOwner
+import java.awt.datatransfer.DataFlavor
+import java.awt.datatransfer.Transferable
+import java.awt.datatransfer.UnsupportedFlavorException
+import java.io.IOException
+import java.io.InputStream
+import java.nio.charset.Charset
+import java.util.logging.Level
+import java.util.logging.Logger
+import kotlin.collections.HashMap
+
+/* Constants, etc. */
+val CHARSET_NAME = "UTF-8"
+
+/*
+ * Represents an error dealing with pasteboard items.
+ */
+class PasteboardError(): Exception()
+
+/**
+ * Represents an item of data in the clipboard and how to read and
+ * write it.
+ */
+sealed class PasteboardItem {
+    data class Plain(val plain: String): PasteboardItem()
+    data class HTML(val plain: String, val html: String): PasteboardItem()
+
+    private class PasteboardData(val item: PasteboardItem):
+    Transferable, ClipboardOwner {
+        private val CHARSET = Charset.forName(CHARSET_NAME)
+        private val HTML_FLAVOR = DataFlavor("text/html; document=all; class=\"[B\"; charset=" + CHARSET_NAME)
+        private val _data: HashMap<DataFlavor, Any>
+        private val flavors: Array<DataFlavor>
+
+        init {
+            _data = HashMap<DataFlavor, Any>().apply {
+                when (item) {
+                    is Plain ->  put(DataFlavor.stringFlavor, item.plain as Any)
+                    is HTML -> {
+                        put(DataFlavor.stringFlavor, item.plain as Any)
+                        put(HTML_FLAVOR, item.html as Any)
+                    }
+                }
+            }
+            _data.keys.asIterable().run {
+                flavors = Array<DataFlavor>(count()) { elementAt(it) }
+            }
+        }
+
+        override fun getTransferData(flavor: DataFlavor): Any {
+            return _data.get(flavor) ?: throw UnsupportedFlavorException(flavor)
+        }
+
+        override fun getTransferDataFlavors(): Array<DataFlavor> = flavors
+        override fun isDataFlavorSupported(flavor: DataFlavor) = _data.containsKey(flavor)
+        override fun lostOwnership(clipboard: Clipboard, contents: Transferable) {}
+    }
+
+    companion object {
+        private val CLIPBOARD = Toolkit.getDefaultToolkit().systemClipboard
+
+        /**
+         * Read the item in the pasteboard.
+         * @return a PasteboardItem? object, null if nothing could be read
+         */
+        fun read() : PasteboardItem? {
+            check()
+            var plain = getClipboardData(DataFlavor.stringFlavor)
+            if (plain == null) {
+                return null
+            }
+            var html = getClipboardData(DataFlavor.allHtmlFlavor)
+            if (html == null) {
+                html = htmlFromRTF()
+            }
+            return if (html == null) { Plain(plain) } else { HTML(plain, html) }
+        }
+
+        /**
+         * Write an item to the pasteboard.
+         * @param item a PasteboardItem to write
+         */
+        fun write(item: PasteboardItem) {
+            check()
+            val pbdata = PasteboardData(item)
+            CLIPBOARD.setContents(pbdata, pbdata)
+        }
+
+        private fun check() {
+            if (CLIPBOARD == null) {
+                throw RuntimeException("no clipboard available!")
+            }
+        }
+
+        private fun getClipboardData(flavor: DataFlavor): String? {
+            try {
+                return CLIPBOARD.getData(flavor) as String?
+            } catch (e: IOException) {
+                return null
+            } catch (e: UnsupportedFlavorException) {
+                return null
+            }
+        }
+
+        private fun htmlFromRTF(): String? {
+            /* see if there's an appropriate flavor */
+            var rtf: DataFlavor? = null
+            for (flavor in CLIPBOARD.availableDataFlavors) {
+                if (flavor.isRepresentationClassInputStream() &&
+                    "text".equals(flavor.primaryType ?: "", ignoreCase=true) &&
+                    "rtf".equals(flavor.subType ?: "", ignoreCase=true)) {
+                        rtf = flavor
+                        break
+                }
+            }
+            if (rtf == null) {
+                return null
+            }
+
+            (CLIPBOARD.getData(rtf) as InputStream).use {
+                val (html, errors) = rtfToHtml(it)
+                if (errors != null) {
+                    LOGGER.log(Level.WARNING, errors)
+                    return null
+                }
+                return html
+            }
+        }
+    }
+}