# HG changeset patch # User David Barts # Date 1580323807 28800 # Node ID 8aa2dfac27eb213d0559af9f27b3222651d0eade # Parent ff35fabaea3a97249b0c1c87e1bf638818c508a3 Big reorg; compiled but untested. diff -r ff35fabaea3a -r 8aa2dfac27eb .hgignore --- a/.hgignore Wed Jan 29 10:47:46 2020 -0800 +++ b/.hgignore Wed Jan 29 10:50:07 2020 -0800 @@ -1,3 +1,4 @@ ~$ \.bak$ ^work/ +^bundles/ diff -r ff35fabaea3a -r 8aa2dfac27eb build.xml --- a/build.xml Wed Jan 29 10:47:46 2020 -0800 +++ b/build.xml Wed Jan 29 10:50:07 2020 -0800 @@ -109,7 +109,8 @@ + vendor="David Barts <n5jrn@me.com>" + copyright="© MMXX, David W. Barts"/> diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/Main.kt --- a/src/name/blackcap/clipman/Main.kt Wed Jan 29 10:47:46 2020 -0800 +++ b/src/name/blackcap/clipman/Main.kt Wed Jan 29 10:50:07 2020 -0800 @@ -4,25 +4,19 @@ 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.MouseEvent +import java.awt.event.MouseListener 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 @@ -35,19 +29,19 @@ val CPWIDTH = 640 val CPHEIGHT = 480 -/* border widths */ +/* width of main panel border */ 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 +/* the queue of data we deal with and the main application frame */ +val queue = LateInit() +val frame = LateInit() + /* kills the updating thread (and does a system exit) when needed */ -class KillIt(val thr: Thread) : WindowListener { +class KillIt() : WindowListener { // events we don't care about override fun windowActivated(e: WindowEvent) {} override fun windowClosed(e: WindowEvent) {} @@ -58,41 +52,14 @@ // 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() { +class UpdateIt(val interval: Int): Thread(), MouseListener { @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 @@ -100,52 +67,30 @@ 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) - } + val piv = if (html == null) { + PasteboardItemView("Plain text", ClipText().apply { + contentType = "text/plain" + text = plain + font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE) + }) } 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) + val (dhtml, style) = preproc(html) + val hek = MyEditorKit().apply { + style.addStyleSheet(defaultStyleSheet) + styleSheet = style } + PasteboardItemView("Styled text", ClipText().apply { + editorKit = hek + text = dhtml + }) } - queue.add(QueueItem(widget, searchable!!, contents)) + piv.searchable.addMouseListener(this) + queue.v.add(QueueItem(contents, piv)) oldContents = contents } } @@ -160,15 +105,10 @@ } } - private fun stdLabel(text: String) = JLabel(text).apply { - horizontalAlignment = JLabel.LEFT - alignmentX = JLabel.LEFT_ALIGNMENT - } - private fun preproc(html: String): Pair { 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)) + 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 { @@ -182,113 +122,67 @@ } return Pair(scrubbed, sty) } + + /* MouseListener methods */ + + override fun mouseClicked(e: MouseEvent) { + val source = e.getSource() as? ClipText + if (source == null) { + return + } + queue.v.deselectAll() + source.selected = true + source.validate() + SelectionRequired.enable() + } + + override fun mousePressed(e: MouseEvent) { + maybeShowPopup(e) + } + + override fun mouseReleased(e: MouseEvent) { + maybeShowPopup(e) + } + + private fun maybeShowPopup(e: MouseEvent) { + if (e.isPopupTrigger()) { + popupMenu.show(e.component, e.x, e.y) + } + } + + override fun mouseEntered(e: MouseEvent) { } + override fun mouseExited(e: MouseEvent) { } } +/* entry point */ fun main(args: Array) { 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 + lateinit var con: JPanel inSynSwingThread { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - frame = JFrame(MYNAME) + frame.v = JFrame(MYNAME) con = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) border = EmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER) - background = frame!!.background + background = frame.v.background } - frame!!.apply { - jMenuBar = makeMenuBar() + frame.v.jMenuBar = menuBar + frame.v.apply { contentPane.add( - JScrollPane(con!!).apply { + JScrollPane(con).apply { verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER preferredSize = Dimension(CPWIDTH, CPHEIGHT) - background = frame!!.background + background = frame.v.background }, BorderLayout.CENTER) pack() setVisible(true) + addWindowListener(KillIt()) } } - 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") - } + queue.v = PasteboardQueue(con, 10) + UpdateIt(1000).apply { start() } } - -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)) -} diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/Menus.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/name/blackcap/clipman/Menus.kt Wed Jan 29 10:50:07 2020 -0800 @@ -0,0 +1,166 @@ +/* + * Menu-related stuff, pertaining to both the menu bar and popup menus. + */ +package name.blackcap.clipman + +import java.awt.Toolkit +import java.awt.event.ActionEvent +import java.awt.event.ActionListener +import java.awt.event.KeyEvent +import java.util.LinkedList +import javax.swing.* + +/** + * Listen to actionEvents from both menu bar and popup menu selections. + */ +class MenuItemListener: ActionListener { + override fun actionPerformed(e: ActionEvent) { + when (e.actionCommand) { + "File.Quit" -> System.exit(0) + "Edit.Clone" -> onlyIfSelected { queue.v.add(it) } + "Edit.Coerce" -> onlyIfSelected { doCoerceFonts(it) } + "Edit.Delete" -> onlyIfSelected { + queue.v.delete(it) + SelectionRequired.disable() + } + "Edit.Find" -> searchDialog.setVisible(true) + "Edit.FindAgain" -> searchDialog.find() + else -> throw RuntimeException("unexpected actionCommand!") + } + } + + private fun onlyIfSelected(block: (QueueItem) -> Unit) { + val selected = queue.v.getSelected() + if (selected == null) { + JOptionPane.showMessageDialog(frame.v, + "No item selected.", + "Error", + JOptionPane.ERROR_MESSAGE); + } else { + block(selected) + } + } + + private fun doCoerceFonts(item: QueueItem) { + /* not finished */ + } +} + +val menuItemListener = MenuItemListener() + +/** + * Our menu bar. What we display depends somewhat on the system type, as + * the Mac gives us a gratuitous menu bar entry for handling some stuff. + */ +class MyMenuBar: JMenuBar() { + init { + if (OS.type != OS.MAC) { + add(JMenu("File").apply { + add(JMenuItem("Quit").apply { + actionCommand = "File.Quit" + addActionListener(menuItemListener) + makeShortcut(KeyEvent.VK_Q) + }) + }) + } + add(JMenu("Edit").apply { + add(SelectionRequired.add(JMenuItem("Clone").apply { + setEnabled(false) + actionCommand = "Edit.Clone" + addActionListener(menuItemListener) + makeShortcut(KeyEvent.VK_C) + })) + add(SelectionRequired.add(JMenuItem("Coerce…").apply { + setEnabled(false) + actionCommand = "Edit.Coerce" + addActionListener(menuItemListener) + makeShortcut(KeyEvent.VK_K) + })) + add(SelectionRequired.add(JMenuItem("Delete").apply { + setEnabled(false) + actionCommand = "Edit.Delete" + addActionListener(menuItemListener) + setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)) + })) + add(JMenuItem("Find…").apply { + actionCommand = "Edit.Find" + addActionListener(menuItemListener) + makeShortcut(KeyEvent.VK_F) + }) + add(JMenuItem("Find Again").apply { + setEnabled(false) + actionCommand = "Edit.FindAgain" + addActionListener(menuItemListener) + makeShortcut(KeyEvent.VK_G) + }) + }) + if (OS.type != OS.MAC) { + add(JMenu("Help").apply { + add(JMenuItem("About ClipMan…").apply { + actionCommand = "Help.About" + addActionListener(menuItemListener) + }) + }) + } + } + + fun getMenu(name: String): JMenu? { + subElements.forEach { + val jmenu = it.component as? JMenu + if (jmenu?.text == name) { + return jmenu + } + } + return null + } +} + +val menuBar = MyMenuBar() + +/** + * The popup menu we display when the user requests so atop a clipboard + * item. + */ +class MyPopupMenu: JPopupMenu() { + init { + add(SelectionRequired.add(JMenuItem("Clone").apply { + actionCommand = "Edit.Clone" + addActionListener(menuItemListener) + })) + add(SelectionRequired.add(JMenuItem("Coerce…").apply { + actionCommand = "Edit.Coerce" + addActionListener(menuItemListener) + })) + add(SelectionRequired.add(JMenuItem("Delete").apply { + actionCommand = "Edit.Delete" + addActionListener(menuItemListener) + })) + } +} + +val popupMenu = MyPopupMenu() + +/** + * Track menu items that require something to be selected in order + * to work, and allow them to be enabled and disabled en masse. + */ +object SelectionRequired { + private val cache = LinkedList() + + fun add(item: JMenuItem): JMenuItem { + cache.add(item) + return item + } + + fun enable() { + cache.forEach { + it.setEnabled(true) + } + } + + fun disable() { + cache.forEach { + it.setEnabled(false) + } + } +} \ No newline at end of file diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/Misc.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/name/blackcap/clipman/Misc.kt Wed Jan 29 10:50:07 2020 -0800 @@ -0,0 +1,94 @@ +/* + * Miscellaneous utility stuff. + */ +package name.blackcap.clipman + +import java.awt.Dimension +import java.awt.Toolkit +import javax.swing.* +import javax.swing.text.JTextComponent + +/** + * Allows a val to have lateinit functionality. It is an error to attempt + * to retrieve this object's value unless it has been set, and it is an + * error to attempt to set the value of an already-set object. + * @param <T> type of the associated value + */ +class LateInit { + private var _v: T? = null + + /** + * The value associated with this object. The name of this property is + * deliberately short. + */ + var v: T + get() { + if (_v == null) { + throw IllegalStateException("cannot retrieve un-initialized value") + } else { + return _v!! + } + } + @Synchronized set(value) { + if (_v != null) { + throw IllegalStateException("cannot set already-initialized value") + } + _v = value + } +} + +/** + * Run something in the Swing thread, asynchronously. + * @param block lambda containing code to run + */ +fun inSwingThread(block: () -> Unit) { + SwingUtilities.invokeLater(Runnable(block)) +} + +/** + * Run something in the Swing thread, synchronously. + * @param block lambda containing code to run + */ +fun inSynSwingThread(block: () -> Unit) { + SwingUtilities.invokeAndWait(Runnable(block)) +} + +/** + * Autosize a JTextComponent to a given width. + * @param width the width + */ +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) +} + +/** + * Make a shortcut for a menu item, using the standard combining key + * (control, command, etc.) for the system we're on. + * @param key KeyEvent constant describing the key + */ +fun JMenuItem.makeShortcut(key: Int): Unit { + val SC_KEY_MASK = Toolkit.getDefaultToolkit().menuShortcutKeyMask + setAccelerator(KeyStroke.getKeyStroke(key, SC_KEY_MASK)) +} + +/** + * Given a MenuElement object, get the item whose text matches the + * specified text. + * @param text to match + * @return first matched element, null if no match found + */ +fun MenuElement.getItem(name: String) : JMenuItem? { + subElements.forEach { + val jMenuItem = it.component as? JMenuItem + if (jMenuItem?.text == name) { + return jMenuItem + } + } + return null +} + diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/Pasteboard.kt --- a/src/name/blackcap/clipman/Pasteboard.kt Wed Jan 29 10:47:46 2020 -0800 +++ b/src/name/blackcap/clipman/Pasteboard.kt Wed Jan 29 10:50:07 2020 -0800 @@ -21,11 +21,6 @@ /* 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. diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/PasteboardQueue.kt --- a/src/name/blackcap/clipman/PasteboardQueue.kt Wed Jan 29 10:47:46 2020 -0800 +++ b/src/name/blackcap/clipman/PasteboardQueue.kt Wed Jan 29 10:50:07 2020 -0800 @@ -11,7 +11,7 @@ import java.util.logging.Level import java.util.logging.Logger import javax.swing.* -import javax.swing.text.JTextComponent +import javax.swing.text.DefaultHighlighter /** * A queue that tracks the data we display and the widgets used to @@ -56,7 +56,7 @@ */ @Synchronized fun add(item: QueueItem) { inSwingThread { - parent.add(item.component) + parent.add(item.view.contents) scrollPane?.run { validate() verticalScrollBar.run { value = maximum + 1 } @@ -67,6 +67,16 @@ } /** + * Delete something from the queue. + * @param object to delete + */ + @Synchronized fun delete(item: QueueItem) { + if (queue.remove(item)) { + parent.validate() + } + } + + /** * Find and highlight the next occurrence of the specified string * @param string to search * @param whether to search backwards (default forwards) @@ -77,18 +87,68 @@ fun find(needle: String, direction: Direction = Direction.FORWARDS, foldCase: Boolean = true, origin: Offset? = null): Offset? { - /* canonicalize the origin */ - val norigin = if (origin == null) { - if (direction == Direction.FORWARDS) { - Offset(0, 0) - } else { - Offset(queue.size - 1, queue.last.searchable.document.length) + /* clean up any old highlights */ + queue.forEach { + val hiliter = it.view.searchable.highlighter + hiliter.highlights.forEach { + hiliter.removeHighlight(it) } - } else { - origin + } + + /* get starting item index */ + val qMax = queue.size + var norigin = origin ?: when (direction) { + Direction.FORWARDS -> Offset(0, 0) + Direction.BACKWARDS -> Offset(qMax - 1, -1) } - /* XXX - not finished */ + /* 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) + 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 } @@ -97,8 +157,13 @@ var size = queue.size var dirty = false while (size > _maxSize) { - var extra = queue.removeFirst().component - inSwingThread { parent.remove(extra) } + var extra = queue.removeFirst().view + inSwingThread { + if (extra.searchable.selected) { + SelectionRequired.disable() + } + parent.remove(extra.contents) + } dirty = true size -= 1 } @@ -110,9 +175,9 @@ } /** - * An item in the above queue. + * 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 component: JComponent, - val searchable: JTextComponent, - val contents: PasteboardItem) +data class QueueItem(val contents: PasteboardItem, val view: PasteboardItemView) diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/PasteboardView.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/name/blackcap/clipman/PasteboardView.kt Wed Jan 29 10:50:07 2020 -0800 @@ -0,0 +1,104 @@ +/* + * View-related pasteboard stuff. + */ +package name.blackcap.clipman + +import java.awt.Color +import java.awt.Dimension +import javax.swing.* +import javax.swing.border.* +import javax.swing.text.html.StyleSheet +import javax.swing.text.html.HTMLEditorKit + +/* border widths */ +val OUTER_BORDER_TOP = 3 +val OUTER_BORDER = 9 +val INNER_BORDER = 1 +val MARGIN_BORDER = 3 + +/** + * What we use to display the text that is or was in the clipboard. + */ +class ClipText: JTextPane() { + private val normalBorder = CompoundBorder(LineBorder(Color.GRAY, INNER_BORDER), + EmptyBorder(MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER)) + private val selectedBorder = CompoundBorder(LineBorder(Color.BLACK, INNER_BORDER+1), + EmptyBorder(MARGIN_BORDER-1, MARGIN_BORDER-1, MARGIN_BORDER-1, MARGIN_BORDER-1)) + init { + border = normalBorder + setEditable(false) + alignmentX = JTextPane.LEFT_ALIGNMENT + resize() + } + + /** + * We allow the text to be considered "selected;" such text is used + * as the target for any subsequent editing operation. + */ + var selected: Boolean + get() { + return border === selectedBorder + } + set(value) { + border = if (value) { selectedBorder } else { normalBorder } + } + + /** + * Refuse to fill the window in the vertical dimension, because doing + * so leads to a misleading display of the clipboard contents, falsely + * implying the existence of a bunch of trailing whitespace. + */ + override fun getMaximumSize(): Dimension { + return Dimension(Int.MAX_VALUE, preferredSize.height) + } + + /** + * Dynamically size or resize this view. + */ + fun resize() { + autoSize(queue.v.parent.size.width - + 2 * (PANEL_BORDER + OUTER_BORDER + INNER_BORDER + MARGIN_BORDER)) + } +} + + +/** + * The stock HTMLEditorKit shares all style sheet data between all its + * instances. How unbelievably braindamaged. Correct that. + */ +class MyEditorKit: HTMLEditorKit() { + private var _styleSheet = defaultStyleSheet + override fun getStyleSheet() = _styleSheet + override fun setStyleSheet(value: StyleSheet) { + _styleSheet = value + } + + /** + * Return the default style sheet that all HTMLEditorKit's come with. + */ + val defaultStyleSheet: StyleSheet + get() { + return super.getStyleSheet() + } +} + +/** + * Views are based on a JLabel and a ClipText (which we use for searching), + * wrapped in a JPanel that we use to display both. + */ +class PasteboardItemView(label: String, val searchable: ClipText) { + private val outerBorder = + MatteBorder(OUTER_BORDER_TOP, OUTER_BORDER, OUTER_BORDER, OUTER_BORDER, + queue.v.parent.background) + + val contents = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + background = queue.v.parent.background + border = outerBorder + add(JLabel(label).apply { + horizontalAlignment = JLabel.LEFT + alignmentX = JLabel.LEFT_ALIGNMENT + }) + add(searchable) + } +} diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/RtfToHtml.kt --- a/src/name/blackcap/clipman/RtfToHtml.kt Wed Jan 29 10:47:46 2020 -0800 +++ b/src/name/blackcap/clipman/RtfToHtml.kt Wed Jan 29 10:50:07 2020 -0800 @@ -76,8 +76,8 @@ val exitStatus = job.waitFor() /* after it exits, wait for our data consumers to exit */ - outputConsumer.join(); - errorConsumer.join(); + outputConsumer.join() + errorConsumer.join() /* if it barfed, return an error, else return the HTML */ if (exitStatus != 0) { diff -r ff35fabaea3a -r 8aa2dfac27eb src/name/blackcap/clipman/SearchDialog.kt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/name/blackcap/clipman/SearchDialog.kt Wed Jan 29 10:50:07 2020 -0800 @@ -0,0 +1,140 @@ +/* + * The dialog that controls a search. + */ +package name.blackcap.clipman + +import java.awt.Color +import java.awt.Toolkit +import java.awt.event.ActionEvent +import java.awt.event.ActionListener +import javax.swing.* +import javax.swing.border.* +import javax.swing.event.DocumentEvent +import javax.swing.event.DocumentListener + +class SearchDialog: JDialog(frame.v), ActionListener, DocumentListener { + /* the search term */ + private val _searchFor = JTextField(50).also { + it.border = LineBorder(Color.GRAY, 1) + it.horizontalAlignment = JTextField.LEFT + it.alignmentX = JTextField.LEFT_ALIGNMENT + it.text = "" + it.document.addDocumentListener(this) + } + val searchFor: String + get() { + return _searchFor.text + } + + /* whether or not we should ignore case */ + private val _ignoreCase = JCheckBox("Ignore case", true) + val ignoreCase: Boolean + get() { + return _ignoreCase.isSelected() + } + + /* whether or not searches should wrap around */ + private val _autoWrap = JCheckBox("Auto wrap", false) + val autoWrap: Boolean + get() { + return _autoWrap.isSelected() + } + + /* which direction to search */ + private val _forwards = JRadioButton("Forward", true) + private val _backwards = JRadioButton("Backward", false) + private val _direction = ButtonGroup().apply { + add(_forwards) + add(_backwards) + } + val direction: PasteboardQueue.Direction + get() { + if (_forwards.isSelected()) { + return PasteboardQueue.Direction.FORWARDS + } + if (_backwards.isSelected()) { + return PasteboardQueue.Direction.BACKWARDS + } + throw RuntimeException("impossible button state!") + } + + /* where to begin searching from. unlike the other properties, this + one is read/write. null means to start from the beginning on + forward searches, and from the end on backward searches (i.e. + search everything) */ + var origin: PasteboardQueue.Offset? = null + + private val _find = JButton("Find").also { + it.actionCommand = "Find" + it.addActionListener(this) + } + + /* initializer */ + init { + title = "Find" + contentPane.apply { + add(Box(BoxLayout.Y_AXIS).apply { + add(Box(BoxLayout.Y_AXIS).apply { + add(JLabel("Search for").apply { + horizontalAlignment = JLabel.LEFT + alignmentX = JLabel.LEFT_ALIGNMENT + }) + add(_searchFor) + }) + add(Box(BoxLayout.X_AXIS).apply { + add(Box(BoxLayout.Y_AXIS).apply { + add(_ignoreCase) + add(_autoWrap) + }) + add(Box(BoxLayout.Y_AXIS).apply { + add(_forwards) + add(_backwards) + }) + }) + add(_find) + }) + } + rootPane.setDefaultButton(_find) + } + + override fun actionPerformed(e: ActionEvent) { + if (e.actionCommand == "Find") { + setVisible(false) + find() + } + } + + override fun setVisible(visible: Boolean) { + if (visible) { + _searchFor.run { + requestFocusInWindow() + selectAll() + } + } + super.setVisible(visible) + } + + fun find(): Unit { + fun doFind(o: PasteboardQueue.Offset?) = queue.v.find(searchFor, + direction = direction, foldCase = ignoreCase, origin = o) + var result = doFind(origin) + if (result == null && origin != null && autoWrap) { + result = doFind(null) + } + if (result == null) { + Toolkit.getDefaultToolkit().beep() + } + origin = result + } + + /* changing the search string resets the search origin */ + override fun changedUpdate(e: DocumentEvent) { + if (e.document === _searchFor.document) { + origin = null + } + } + override fun insertUpdate(e: DocumentEvent) = changedUpdate(e) + override fun removeUpdate(e: DocumentEvent) = changedUpdate(e) +} + +val searchDialog = SearchDialog() \ No newline at end of file