Mercurial > cgi-bin > hgweb.cgi > ClipMan
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 } |