comparison src/name/blackcap/clipman/Main.kt @ 27:8aa2dfac27eb

Big reorg; compiled but untested.
author David Barts <n5jrn@me.com>
date Wed, 29 Jan 2020 10:50:07 -0800
parents dac8dfb4b549
children f1fcc1281dad
comparison
equal deleted inserted replaced
26:ff35fabaea3a 27:8aa2dfac27eb
2 * The entry point and most of the view logic is here. 2 * The entry point and most of the view logic is here.
3 */ 3 */
4 package name.blackcap.clipman 4 package name.blackcap.clipman
5 5
6 import java.awt.BorderLayout 6 import java.awt.BorderLayout
7 import java.awt.Color
8 import java.awt.Container 7 import java.awt.Container
9 import java.awt.Dimension 8 import java.awt.Dimension
10 import java.awt.Font 9 import java.awt.Font
11 import java.awt.Toolkit;
12 import java.awt.datatransfer.* 10 import java.awt.datatransfer.*
13 import java.awt.event.ActionEvent 11 import java.awt.event.MouseEvent
14 import java.awt.event.ActionListener 12 import java.awt.event.MouseListener
15 import java.awt.event.KeyEvent
16 import java.awt.event.WindowEvent 13 import java.awt.event.WindowEvent
17 import java.awt.event.WindowListener 14 import java.awt.event.WindowListener
18 import java.util.Date 15 import java.util.Date
19 import java.util.concurrent.Semaphore
20 import java.util.logging.Level 16 import java.util.logging.Level
21 import java.util.logging.Logger 17 import java.util.logging.Logger
22 import javax.swing.* 18 import javax.swing.*
23 import javax.swing.border.* 19 import javax.swing.border.*
24 import javax.swing.text.JTextComponent
25 import javax.swing.text.html.HTMLEditorKit
26 import javax.swing.text.html.StyleSheet 20 import javax.swing.text.html.StyleSheet
27 import kotlin.concurrent.thread 21 import kotlin.concurrent.thread
28 import org.jsoup.Jsoup 22 import org.jsoup.Jsoup
29 import org.jsoup.nodes.* 23 import org.jsoup.nodes.*
30 24
33 27
34 /* default sizes */ 28 /* default sizes */
35 val CPWIDTH = 640 29 val CPWIDTH = 640
36 val CPHEIGHT = 480 30 val CPHEIGHT = 480
37 31
38 /* border widths */ 32 /* width of main panel border */
39 val PANEL_BORDER = 9 33 val PANEL_BORDER = 9
40 val OUTER_BORDER_TOP = 3
41 val OUTER_BORDER = 9
42 val INNER_BORDER = 1
43 val MARGIN_BORDER = 3
44 34
45 /* default font sizes in the text-display panes */ 35 /* default font sizes in the text-display panes */
46 val MONO_SIZE = 14 36 val MONO_SIZE = 14
47 val PROP_SIZE = 16 37 val PROP_SIZE = 16
48 38
39 /* the queue of data we deal with and the main application frame */
40 val queue = LateInit<PasteboardQueue>()
41 val frame = LateInit<JFrame>()
42
49 /* kills the updating thread (and does a system exit) when needed */ 43 /* kills the updating thread (and does a system exit) when needed */
50 class KillIt(val thr: Thread) : WindowListener { 44 class KillIt() : WindowListener {
51 // events we don't care about 45 // events we don't care about
52 override fun windowActivated(e: WindowEvent) {} 46 override fun windowActivated(e: WindowEvent) {}
53 override fun windowClosed(e: WindowEvent) {} 47 override fun windowClosed(e: WindowEvent) {}
54 override fun windowDeactivated(e: WindowEvent) {} 48 override fun windowDeactivated(e: WindowEvent) {}
55 override fun windowDeiconified(e: WindowEvent) {} 49 override fun windowDeiconified(e: WindowEvent) {}
56 override fun windowIconified(e: WindowEvent) {} 50 override fun windowIconified(e: WindowEvent) {}
57 override fun windowOpened(e: WindowEvent) {} 51 override fun windowOpened(e: WindowEvent) {}
58 52
59 // and the one we do 53 // and the one we do
60 override fun windowClosing(e: WindowEvent) { 54 override fun windowClosing(e: WindowEvent) {
61 thr.run { interrupt(); join() }
62 LOGGER.log(Level.INFO, "execution complete") 55 LOGGER.log(Level.INFO, "execution complete")
63 System.exit(0) 56 System.exit(0)
64 } 57 }
65 } 58 }
66 59
67 class ClipText: JTextPane() {
68 override fun getMaximumSize(): Dimension {
69 return Dimension(Int.MAX_VALUE, preferredSize.height)
70 }
71 }
72
73 /* HTMLEditorKit shares all stylesheets. How unbelievably braindamaged. */
74 class MyEditorKit: HTMLEditorKit() {
75 private var _styleSheet = defaultStyleSheet
76 override fun getStyleSheet() = _styleSheet
77 override fun setStyleSheet(value: StyleSheet) {
78 _styleSheet = value
79 }
80
81 val defaultStyleSheet: StyleSheet
82 get() {
83 return super.getStyleSheet()
84 }
85 }
86
87 /* the updating thread */ 60 /* the updating thread */
88 class UpdateIt(val queue: PasteboardQueue, val interval: Int): Thread() { 61 class UpdateIt(val interval: Int): Thread(), MouseListener {
89 @Volatile var enabled = true 62 @Volatile var enabled = true
90 private val outerBorder =
91 MatteBorder(OUTER_BORDER_TOP, OUTER_BORDER, OUTER_BORDER, OUTER_BORDER,
92 queue.parent.background)
93 private val stdBorder =
94 CompoundBorder(LineBorder(Color.GRAY, INNER_BORDER),
95 EmptyBorder(MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER, MARGIN_BORDER))
96 63
97 override fun run() { 64 override fun run() {
98 var oldContents: PasteboardItem? = null 65 var oldContents: PasteboardItem? = null
99 while (true) { 66 while (true) {
100 if (enabled) { 67 if (enabled) {
101 var contents = PasteboardItem.read() 68 var contents = PasteboardItem.read()
102 if ((contents != null) && (contents != oldContents)) { 69 if ((contents != null) && (contents != oldContents)) {
103 val stdWidth = queue.parent.size.width - 2 * (PANEL_BORDER+OUTER_BORDER+INNER_BORDER+MARGIN_BORDER)
104 val widget = JPanel().apply {
105 layout = BoxLayout(this, BoxLayout.Y_AXIS)
106 background = queue.parent.background
107 border = outerBorder
108 }
109 val (plain, html) = when(contents) { 70 val (plain, html) = when(contents) {
110 is PasteboardItem.Plain -> Pair(contents.plain, null) 71 is PasteboardItem.Plain -> Pair(contents.plain, null)
111 is PasteboardItem.HTML -> Pair(null, contents.html) 72 is PasteboardItem.HTML -> Pair(null, contents.html)
112 is PasteboardItem.RTF -> Pair(contents.plain, contents.html) 73 is PasteboardItem.RTF -> Pair(contents.plain, contents.html)
113 } 74 }
114 var searchable: JTextComponent? = null 75 val piv = if (html == null) {
115 if (html == null) { 76 PasteboardItemView("Plain text", ClipText().apply {
116 widget.run { 77 contentType = "text/plain"
117 add(stdLabel("Plain text")) 78 text = plain
118 searchable = ClipText().apply { 79 font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE)
119 contentType = "text/plain" 80 })
120 text = plain 81 } else {
121 font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE) 82 val (dhtml, style) = preproc(html)
122 border = stdBorder 83 val hek = MyEditorKit().apply {
123 autoSize(stdWidth) 84 style.addStyleSheet(defaultStyleSheet)
124 setEditable(false) 85 styleSheet = style
125 alignmentX = JTextPane.LEFT_ALIGNMENT
126 }
127 add(searchable)
128 } 86 }
129 } else { 87 PasteboardItemView("Styled text", ClipText().apply {
130 widget.run { 88 editorKit = hek
131 add(stdLabel("Styled text")) 89 text = dhtml
132 val (dhtml, style) = preproc(html) 90 })
133 val hek = MyEditorKit().apply {
134 style.addStyleSheet(defaultStyleSheet)
135 styleSheet = style
136 }
137 searchable = ClipText().apply {
138 editorKit = hek
139 text = dhtml
140 border = stdBorder
141 autoSize(stdWidth)
142 setEditable(false)
143 alignmentX = JTextPane.LEFT_ALIGNMENT
144 }
145 add(searchable)
146 }
147 } 91 }
148 queue.add(QueueItem(widget, searchable!!, contents)) 92 piv.searchable.addMouseListener(this)
93 queue.v.add(QueueItem(contents, piv))
149 oldContents = contents 94 oldContents = contents
150 } 95 }
151 } 96 }
152 if (Thread.interrupted()) { 97 if (Thread.interrupted()) {
153 return 98 return
158 return 103 return
159 } 104 }
160 } 105 }
161 } 106 }
162 107
163 private fun stdLabel(text: String) = JLabel(text).apply {
164 horizontalAlignment = JLabel.LEFT
165 alignmentX = JLabel.LEFT_ALIGNMENT
166 }
167
168 private fun preproc(html: String): Pair<String, StyleSheet> { 108 private fun preproc(html: String): Pair<String, StyleSheet> {
169 val sty = StyleSheet().apply { 109 val sty = StyleSheet().apply {
170 addRule("body { font-family: serif; font-size: %d; }".format(PROP_SIZE)) 110 addRule("body { font-family: serif; font-size: ${PROP_SIZE}; }")
171 addRule("code, kbd, pre, samp, tt { font-family: monospace; font-size: %d; }".format(MONO_SIZE)) 111 addRule("code, kbd, pre, samp, tt { font-family: monospace; font-size: ${MONO_SIZE}; }")
172 } 112 }
173 val scrubbed = Jsoup.parse(html).run { 113 val scrubbed = Jsoup.parse(html).run {
174 select("style").forEach { 114 select("style").forEach {
175 it.dataNodes().forEach { sty.addRule(it.wholeData) } 115 it.dataNodes().forEach { sty.addRule(it.wholeData) }
176 } 116 }
180 .syntax(Document.OutputSettings.Syntax.xml) 120 .syntax(Document.OutputSettings.Syntax.xml)
181 outerHtml() 121 outerHtml()
182 } 122 }
183 return Pair(scrubbed, sty) 123 return Pair(scrubbed, sty)
184 } 124 }
125
126 /* MouseListener methods */
127
128 override fun mouseClicked(e: MouseEvent) {
129 val source = e.getSource() as? ClipText
130 if (source == null) {
131 return
132 }
133 queue.v.deselectAll()
134 source.selected = true
135 source.validate()
136 SelectionRequired.enable()
137 }
138
139 override fun mousePressed(e: MouseEvent) {
140 maybeShowPopup(e)
141 }
142
143 override fun mouseReleased(e: MouseEvent) {
144 maybeShowPopup(e)
145 }
146
147 private fun maybeShowPopup(e: MouseEvent) {
148 if (e.isPopupTrigger()) {
149 popupMenu.show(e.component, e.x, e.y)
150 }
151 }
152
153 override fun mouseEntered(e: MouseEvent) { }
154 override fun mouseExited(e: MouseEvent) { }
185 } 155 }
186 156
157 /* entry point */
187 fun main(args: Array<String>) { 158 fun main(args: Array<String>) {
188 LOGGER.log(Level.INFO, "beginning execution") 159 LOGGER.log(Level.INFO, "beginning execution")
189 if (OS.type == OS.MAC) { 160 if (OS.type == OS.MAC) {
190 System.setProperty("apple.laf.useScreenMenuBar", "true") 161 System.setProperty("apple.laf.useScreenMenuBar", "true")
191 } 162 }
192 var frame: JFrame? = null 163 lateinit var con: JPanel
193 var con: JPanel? = null
194 inSynSwingThread { 164 inSynSwingThread {
195 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 165 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
196 frame = JFrame(MYNAME) 166 frame.v = JFrame(MYNAME)
197 con = JPanel().apply { 167 con = JPanel().apply {
198 layout = BoxLayout(this, BoxLayout.Y_AXIS) 168 layout = BoxLayout(this, BoxLayout.Y_AXIS)
199 border = EmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER) 169 border = EmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER)
200 background = frame!!.background 170 background = frame.v.background
201 } 171 }
202 frame!!.apply { 172 frame.v.jMenuBar = menuBar
203 jMenuBar = makeMenuBar() 173 frame.v.apply {
204 contentPane.add( 174 contentPane.add(
205 JScrollPane(con!!).apply { 175 JScrollPane(con).apply {
206 verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS 176 verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
207 horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER 177 horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
208 preferredSize = Dimension(CPWIDTH, CPHEIGHT) 178 preferredSize = Dimension(CPWIDTH, CPHEIGHT)
209 background = frame!!.background 179 background = frame.v.background
210 }, BorderLayout.CENTER) 180 }, BorderLayout.CENTER)
211 pack() 181 pack()
212 setVisible(true) 182 setVisible(true)
183 addWindowListener(KillIt())
213 } 184 }
214 } 185 }
215 val queue = PasteboardQueue(con!!, 10) 186 queue.v = PasteboardQueue(con, 10)
216 val updater = UpdateIt(queue, 1000).apply { start() } 187 UpdateIt(1000).apply { start() }
217 inSwingThread { frame!!.addWindowListener(KillIt(updater)) }
218 } 188 }
219
220 class MenuItemListener: ActionListener {
221 override fun actionPerformed(e: ActionEvent) {
222 println(e.actionCommand + " selected")
223 }
224 }
225
226 fun makeMenuBar() = JMenuBar().apply {
227 val al: ActionListener = MenuItemListener()
228 if (OS.type != OS.MAC) {
229 add(JMenu("File").apply {
230 add(JMenuItem("Quit").apply {
231 actionCommand = "File.Quit"
232 addActionListener(al)
233 makeShortcut(KeyEvent.VK_Q)
234 })
235 })
236 }
237 add(JMenu("Edit").apply {
238 add(JMenuItem("Clone").apply {
239 actionCommand = "Edit.Clone"
240 addActionListener(al)
241 makeShortcut(KeyEvent.VK_C)
242 })
243 add(JMenuItem("Coerce…").apply {
244 actionCommand = "Edit.Coerce"
245 addActionListener(al)
246 makeShortcut(KeyEvent.VK_K)
247 })
248 add(JMenuItem("Delete").apply {
249 actionCommand = "Edit.Delete"
250 addActionListener(al)
251 setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0))
252 })
253 add(JMenuItem("Find…").apply {
254 actionCommand = "Edit.Find"
255 addActionListener(al)
256 makeShortcut(KeyEvent.VK_F)
257 })
258 })
259 if (OS.type != OS.MAC) {
260 add(JMenu("Help").apply {
261 add(JMenuItem("About ClipMan…").apply {
262 actionCommand = "Help.About"
263 addActionListener(al)
264 })
265 })
266 }
267 }
268
269 fun inSwingThread(block: () -> Unit) {
270 SwingUtilities.invokeLater(Runnable(block))
271 }
272
273 fun inSynSwingThread(block: () -> Unit) {
274 val ready = Semaphore(0)
275 inSwingThread {
276 block()
277 ready.release()
278 }
279 ready.acquire()
280 }
281
282 fun JTextComponent.autoSize(width: Int): Unit {
283 val SLOP = 10
284 val dim = Dimension(width, width)
285 preferredSize = dim
286 size = dim
287 val r = modelToView(document.length)
288 preferredSize = Dimension(width, r.y + r.height + SLOP)
289 }
290
291 val SC_KEY_MASK = Toolkit.getDefaultToolkit().menuShortcutKeyMask
292 fun JMenuItem.makeShortcut(key: Int): Unit {
293 setAccelerator(KeyStroke.getKeyStroke(key, SC_KEY_MASK))
294 }