Mercurial > cgi-bin > hgweb.cgi > ImagePrep
annotate src/name/blackcap/imageprep/Misc.kt @ 15:fad32eda667f
Fix ImageWriter leak.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 19 Jul 2020 13:49:23 -0700 |
parents | 884f1415a330 |
children | 2bb46da74667 |
rev | line source |
---|---|
0 | 1 /* |
2 * Miscellaneous utility stuff. | |
3 */ | |
4 package name.blackcap.imageprep | |
5 | |
6 import java.awt.Component | |
7 import java.awt.Cursor | |
8 import java.awt.Dimension | |
9 import java.awt.Font | |
10 import java.awt.FontMetrics | |
11 import java.awt.Graphics | |
12 import java.awt.Point | |
13 import java.awt.Toolkit | |
14 import java.awt.event.MouseEvent | |
15 import java.io.File | |
16 import java.io.IOException | |
17 import javax.swing.* | |
18 import javax.swing.border.Border | |
19 import javax.swing.table.TableColumnModel | |
20 import kotlin.annotation.* | |
21 import kotlin.properties.ReadWriteProperty | |
22 import kotlin.reflect.* | |
23 | |
24 /** | |
25 * Delegate that makes a var that can only be set once. This is commonly | |
26 * needed in Swing, because some vars inevitably need to be declared at | |
27 * outer levels but initialized in the Swing event dispatch thread. | |
28 */ | |
29 class SetOnceImpl<T: Any>: ReadWriteProperty<Any?,T> { | |
30 private var setOnceValue: T? = null | |
31 | |
32 override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { | |
33 if (setOnceValue == null) { | |
34 throw RuntimeException("${property.name} has not been initialized") | |
35 } else { | |
36 return setOnceValue!! | |
37 } | |
38 } | |
39 | |
40 @Synchronized | |
41 override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T): Unit { | |
42 if (setOnceValue != null) { | |
43 throw RuntimeException("${property.name} has already been initialized") | |
44 } | |
45 setOnceValue = value | |
46 } | |
47 } | |
48 | |
49 fun <T: Any> setOnce(): SetOnceImpl<T> = SetOnceImpl<T>() | |
50 | |
51 /** | |
52 * Run something in the Swing thread, asynchronously. | |
53 * @param block lambda containing code to run | |
54 */ | |
55 fun inSwingThread(block: () -> Unit) { | |
56 SwingUtilities.invokeLater(Runnable(block)) | |
57 } | |
58 | |
59 /** | |
60 * Run something in the Swing thread, synchronously. | |
61 * @param block lambda containing code to run | |
62 */ | |
63 fun inSynSwingThread(block: () -> Unit) { | |
64 SwingUtilities.invokeAndWait(Runnable(block)) | |
65 } | |
66 | |
67 /** | |
68 * Make a shortcut for a menu item, using the standard combining key | |
69 * (control, command, etc.) for the system we're on. | |
70 * @param key KeyEvent constant describing the key | |
71 */ | |
72 fun JMenuItem.makeShortcut(key: Int): Unit { | |
73 val SC_KEY_MASK = Toolkit.getDefaultToolkit().menuShortcutKeyMask | |
74 setAccelerator(KeyStroke.getKeyStroke(key, SC_KEY_MASK)) | |
75 } | |
76 | |
77 /** | |
78 * Given a MenuElement object, get the item whose text matches the | |
79 * specified text. | |
80 * @param text to match | |
81 * @return first matched element, null if no match found | |
82 */ | |
83 fun MenuElement.getItem(name: String) : JMenuItem? { | |
84 subElements.forEach { | |
85 val jMenuItem = it.component as? JMenuItem | |
86 if (jMenuItem?.text == name) { | |
87 return jMenuItem | |
88 } | |
89 } | |
90 return null | |
91 } | |
92 | |
93 /** | |
94 * Change to the standard wait cursor. | |
95 */ | |
96 fun Component.useWaitCursor() { | |
97 this.cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) | |
98 } | |
99 | |
100 /** | |
101 * Return back to the normal cursor(). | |
102 */ | |
103 fun Component.useNormalCursor() { | |
104 this.cursor = Cursor.getDefaultCursor() | |
105 } | |
106 | |
107 /** | |
108 * Thrown if the programmer botches something in our DSL. | |
109 */ | |
110 class SwingWorkerException(message: String): Exception(message) { } | |
111 | |
112 /** | |
113 * A simplified SwingWorker DSL. It does not support intermediate | |
114 * results. Just lets one define a background task and something | |
115 * to execute when complete. | |
116 * | |
117 * @param T Type returned by inBackground (Java doInBackground) task. | |
118 */ | |
119 class SwingWorkerBuilder<T>: SwingWorker<T,Unit>() { | |
120 private var inBackgroundLambda: (SwingWorkerBuilder<T>.() -> T)? = null | |
121 private var whenDoneLambda: (SwingWorkerBuilder<T>.() -> Unit)? = null | |
122 | |
123 private fun <U> setOnce(prop: KMutableProperty0<(SwingWorkerBuilder<T>.() -> U)?>, value: SwingWorkerBuilder<T>.() -> U) { | |
124 if (prop.get() != null) { | |
125 throw SwingWorkerException(prop.name.removeSuffix("Lambda") + " already defined!") | |
126 } | |
127 prop.set(value) | |
128 } | |
129 | |
130 /** | |
131 * Define the inBackground task. | |
132 */ | |
133 fun inBackground(lambda: SwingWorkerBuilder<T>.() -> T): Unit { | |
134 setOnce<T>(::inBackgroundLambda, lambda) | |
135 } | |
136 | |
137 /** | |
138 * Define the whenDone task. | |
139 */ | |
140 fun whenDone(lambda: SwingWorkerBuilder<T>.() -> Unit): Unit { | |
141 setOnce<Unit>(::whenDoneLambda, lambda) | |
142 } | |
143 | |
144 /** | |
145 * Validates we've been properly initialized. | |
146 */ | |
147 fun validate(): Unit { | |
148 if (inBackgroundLambda == null) { | |
149 throw SwingWorkerException("inBackground not defined!") | |
150 } | |
151 } | |
152 | |
153 /* standard overrides for SwingWorker follow */ | |
154 | |
155 override fun doInBackground(): T = inBackgroundLambda!!.invoke(this) | |
156 | |
157 override fun done(): Unit = whenDoneLambda?.invoke(this) ?: Unit | |
158 } | |
159 | |
160 /** | |
161 * Provides for an outer swingWorker block to contain the DSL. | |
162 */ | |
163 fun <T> swingWorker(initializer: SwingWorkerBuilder<T>.() -> Unit): Unit { | |
164 SwingWorkerBuilder<T>().run { | |
165 initializer() | |
166 validate() | |
167 execute() | |
168 } | |
169 } | |
170 | |
171 /** | |
172 * Close a dialog (don't just hide it). | |
173 */ | |
174 fun JDialog.close() { | |
175 setVisible(false) | |
176 dispose() | |
177 } | |
178 | |
179 /** | |
180 * Set column width of a table. | |
181 */ | |
182 fun JTable.setColWidth(col: Int, width: Int, string: String?) { | |
183 val FUZZ = 16 | |
184 columnModel.getColumn(col).preferredWidth = if (string.isNullOrEmpty()) { | |
185 width | |
186 } else { | |
187 maxOf(width, graphics.fontMetrics.stringWidth(string) + FUZZ) | |
188 } | |
189 } | |
190 | |
191 /** | |
192 * Set overall width of a table. | |
193 */ | |
194 fun JTable.setOverallWidth() { | |
195 val tcm = columnModel | |
196 val limit = tcm.columnCount - 1 | |
197 var total = 0 | |
198 for (i in 0 .. limit) { | |
199 total += tcm.getColumn(i).preferredWidth | |
200 } | |
201 preferredSize = Dimension(total, preferredSize.height) | |
202 } | |
203 | |
204 /** | |
205 * A JTable for displaying metadata. Columns that might get harmfully | |
206 * truncated have tooltips when truncation happens. | |
207 */ | |
208 class JExifTable(rowData: Array<Array<out Any?>>, colNames: Array<out Any?>): JTable(rowData, colNames) { | |
209 override fun getToolTipText(e: MouseEvent): String? { | |
210 val pos = e.point | |
211 val col = columnAtPoint(pos) | |
212 if (!setOf("Key", "Type").contains(getColumnName(col))) { | |
213 return null | |
214 } | |
215 val contents = getValueAt(rowAtPoint(pos), col) as String? | |
216 if (contents == null) { | |
217 return null | |
218 } | |
219 val needed = graphics.fontMetrics.stringWidth(contents) | |
220 val actual = columnModel.getColumn(col).width | |
221 return if (needed > actual) contents else null | |
222 } | |
223 } | |
224 | |
225 /** | |
226 * Add a border to a JComponent. The new border is in addition to (and outside | |
227 * of) whatever existing standard border the component had. | |
228 */ | |
229 fun JComponent.addBorder(b: Border) { | |
230 if (border == null) | |
231 border = b | |
232 else | |
233 border = BorderFactory.createCompoundBorder(b, border) | |
234 } | |
235 | |
236 fun ioExceptionDialog(parent: Component, file: File, op: String, e: IOException) { | |
1 | 237 val msg = e.message |
0 | 238 val fileName = file.getName() |
239 val dmsg = if (msg.isNullOrEmpty()) { | |
240 "Unable to ${op} ${fileName}." | |
241 } else if (fileName in msg) { | |
242 msg | |
243 } else { | |
244 "Unable to ${op} ${fileName}:\n ${msg}" | |
245 } | |
246 JOptionPane.showMessageDialog(parent, | |
247 dmsg, "Error", JOptionPane.ERROR_MESSAGE) | |
248 } | |
5
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
249 |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
250 private val homeDir = System.getProperty("user.home") |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
251 fun tilde(s: String?): String { |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
252 if (s.isNullOrEmpty()) |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
253 return homeDir |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
254 if (s.startsWith("~/") || s.startsWith("~\\")) |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
255 return File(homeDir, s.substring(2).trimStart(s[1])).toString() |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
256 else |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
257 return s |
884f1415a330
Rationalized directory management.
David Barts <n5jrn@me.com>
parents:
1
diff
changeset
|
258 } |