Mercurial > cgi-bin > hgweb.cgi > ImagePrep
diff src/name/blackcap/imageprep/Misc.kt @ 0:e0efe7848130
Initial commit. Untested!
author | David Barts <davidb@stashtea.com> |
---|---|
date | Thu, 16 Jul 2020 19:57:23 -0700 |
parents | |
children | 0bded24f746e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/name/blackcap/imageprep/Misc.kt Thu Jul 16 19:57:23 2020 -0700 @@ -0,0 +1,248 @@ +/* + * Miscellaneous utility stuff. + */ +package name.blackcap.imageprep + +import java.awt.Component +import java.awt.Cursor +import java.awt.Dimension +import java.awt.Font +import java.awt.FontMetrics +import java.awt.Graphics +import java.awt.Point +import java.awt.Toolkit +import java.awt.event.MouseEvent +import java.io.File +import java.io.IOException +import javax.swing.* +import javax.swing.border.Border +import javax.swing.table.TableColumnModel +import kotlin.annotation.* +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.* + +/** + * Delegate that makes a var that can only be set once. This is commonly + * needed in Swing, because some vars inevitably need to be declared at + * outer levels but initialized in the Swing event dispatch thread. + */ +class SetOnceImpl<T: Any>: ReadWriteProperty<Any?,T> { + private var setOnceValue: T? = null + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + if (setOnceValue == null) { + throw RuntimeException("${property.name} has not been initialized") + } else { + return setOnceValue!! + } + } + + @Synchronized + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T): Unit { + if (setOnceValue != null) { + throw RuntimeException("${property.name} has already been initialized") + } + setOnceValue = value + } +} + +fun <T: Any> setOnce(): SetOnceImpl<T> = SetOnceImpl<T>() + +/** + * 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)) +} + +/** + * 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 +} + +/** + * Change to the standard wait cursor. + */ +fun Component.useWaitCursor() { + this.cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) +} + +/** + * Return back to the normal cursor(). + */ +fun Component.useNormalCursor() { + this.cursor = Cursor.getDefaultCursor() +} + +/** + * Thrown if the programmer botches something in our DSL. + */ +class SwingWorkerException(message: String): Exception(message) { } + +/** + * A simplified SwingWorker DSL. It does not support intermediate + * results. Just lets one define a background task and something + * to execute when complete. + * + * @param T Type returned by inBackground (Java doInBackground) task. + */ +class SwingWorkerBuilder<T>: SwingWorker<T,Unit>() { + private var inBackgroundLambda: (SwingWorkerBuilder<T>.() -> T)? = null + private var whenDoneLambda: (SwingWorkerBuilder<T>.() -> Unit)? = null + + private fun <U> setOnce(prop: KMutableProperty0<(SwingWorkerBuilder<T>.() -> U)?>, value: SwingWorkerBuilder<T>.() -> U) { + if (prop.get() != null) { + throw SwingWorkerException(prop.name.removeSuffix("Lambda") + " already defined!") + } + prop.set(value) + } + + /** + * Define the inBackground task. + */ + fun inBackground(lambda: SwingWorkerBuilder<T>.() -> T): Unit { + setOnce<T>(::inBackgroundLambda, lambda) + } + + /** + * Define the whenDone task. + */ + fun whenDone(lambda: SwingWorkerBuilder<T>.() -> Unit): Unit { + setOnce<Unit>(::whenDoneLambda, lambda) + } + + /** + * Validates we've been properly initialized. + */ + fun validate(): Unit { + if (inBackgroundLambda == null) { + throw SwingWorkerException("inBackground not defined!") + } + } + + /* standard overrides for SwingWorker follow */ + + override fun doInBackground(): T = inBackgroundLambda!!.invoke(this) + + override fun done(): Unit = whenDoneLambda?.invoke(this) ?: Unit +} + +/** + * Provides for an outer swingWorker block to contain the DSL. + */ +fun <T> swingWorker(initializer: SwingWorkerBuilder<T>.() -> Unit): Unit { + SwingWorkerBuilder<T>().run { + initializer() + validate() + execute() + } +} + +/** + * Close a dialog (don't just hide it). + */ +fun JDialog.close() { + setVisible(false) + dispose() +} + +/** + * Set column width of a table. + */ +fun JTable.setColWidth(col: Int, width: Int, string: String?) { + val FUZZ = 16 + columnModel.getColumn(col).preferredWidth = if (string.isNullOrEmpty()) { + width + } else { + maxOf(width, graphics.fontMetrics.stringWidth(string) + FUZZ) + } +} + +/** + * Set overall width of a table. + */ +fun JTable.setOverallWidth() { + val tcm = columnModel + val limit = tcm.columnCount - 1 + var total = 0 + for (i in 0 .. limit) { + total += tcm.getColumn(i).preferredWidth + } + preferredSize = Dimension(total, preferredSize.height) +} + +/** + * A JTable for displaying metadata. Columns that might get harmfully + * truncated have tooltips when truncation happens. + */ +class JExifTable(rowData: Array<Array<out Any?>>, colNames: Array<out Any?>): JTable(rowData, colNames) { + override fun getToolTipText(e: MouseEvent): String? { + val pos = e.point + val col = columnAtPoint(pos) + if (!setOf("Key", "Type").contains(getColumnName(col))) { + return null + } + val contents = getValueAt(rowAtPoint(pos), col) as String? + if (contents == null) { + return null + } + val needed = graphics.fontMetrics.stringWidth(contents) + val actual = columnModel.getColumn(col).width + return if (needed > actual) contents else null + } +} + +/** + * Add a border to a JComponent. The new border is in addition to (and outside + * of) whatever existing standard border the component had. + */ +fun JComponent.addBorder(b: Border) { + if (border == null) + border = b + else + border = BorderFactory.createCompoundBorder(b, border) +} + +fun ioExceptionDialog(parent: Component, file: File, op: String, e: IOException) { + val msg = e.getMessage() + val fileName = file.getName() + val dmsg = if (msg.isNullOrEmpty()) { + "Unable to ${op} ${fileName}." + } else if (fileName in msg) { + msg + } else { + "Unable to ${op} ${fileName}:\n ${msg}" + } + JOptionPane.showMessageDialog(parent, + dmsg, "Error", JOptionPane.ERROR_MESSAGE) +}