# HG changeset patch # User David Barts # Date 1586727066 25200 # Node ID 19d9da731c43a55ec3b71f1d64e6a003f2d1327a # Parent 88066346f129c4911e46534e1aaee73d8d39f2d7 Recoded; cleaned up root namespace, removed race conditions. diff -r 88066346f129 -r 19d9da731c43 build.xml --- a/build.xml Mon Feb 10 16:40:09 2020 -0700 +++ b/build.xml Sun Apr 12 14:31:06 2020 -0700 @@ -85,7 +85,9 @@ + classpathref="compile.classpath"> + + @@ -108,7 +110,7 @@ + version="1.01"/> diff -r 88066346f129 -r 19d9da731c43 setup.sh --- a/setup.sh Mon Feb 10 16:40:09 2020 -0700 +++ b/setup.sh Sun Apr 12 14:31:06 2020 -0700 @@ -1,7 +1,7 @@ #!/bin/bash export JRE_HOME="$(/usr/libexec/java_home)" -export KOTLIN_HOME="/usr/local/Cellar/kotlin/1.3.61" +export KOTLIN_HOME="/usr/local/Cellar/kotlin/1.3.71" export ANT_HOME="$HOME/java/apache-ant-1.10.1" if [[ "$PATH" != *$ANT_HOME/bin* ]] diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/CoerceDialog.kt --- a/src/name/blackcap/clipman/CoerceDialog.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/CoerceDialog.kt Sun Apr 12 14:31:06 2020 -0700 @@ -17,7 +17,7 @@ import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener -class CoerceDialog: JDialog(frame.v), ActionListener { +class CoerceDialog: JDialog(Application.frame), ActionListener { private val FONTS = GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames.copyOf().apply { sort() @@ -161,9 +161,9 @@ } private fun coerce() { - val selected = queue.v.getSelected() + val selected = Application.queue.getSelected() if (selected == null) { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "No item selected.", "Error", JOptionPane.ERROR_MESSAGE) @@ -177,7 +177,7 @@ Pair(selected.contents.plain, selected.contents.html) } if (html == null) { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "Only styled texts may be coerced.", "Error", JOptionPane.ERROR_MESSAGE) @@ -197,7 +197,7 @@ private fun badSize(control: JComboBox, default: Int, fontType: String): Boolean { val size = control.selectedItem as? Float if (size == null || size < 1.0f) { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "Invalid ${fontType} font size.", "Error", JOptionPane.ERROR_MESSAGE) @@ -226,5 +226,3 @@ } } } - -val coerceDialog = CoerceDialog() diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/Files.kt --- a/src/name/blackcap/clipman/Files.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/Files.kt Sun Apr 12 14:31:06 2020 -0700 @@ -76,6 +76,8 @@ } val LOGGER = run { + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1\$tFT%1\$tT%1\$tz %2\$s%n%4\$s: %5\$s%6\$s%n") LF_DIR.makeIfNeeded() Logger.getLogger(LONGNAME).apply { addHandler(FileHandler(LOG_FILE.toString()).apply { diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/Main.kt --- a/src/name/blackcap/clipman/Main.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/Main.kt Sun Apr 12 14:31:06 2020 -0700 @@ -35,10 +35,6 @@ 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() : WindowListener { // events we don't care about @@ -91,7 +87,7 @@ }) } piv.searchable.addMouseListener(this) - queue.v.add(QueueItem(contents, piv)) + Application.queue.add(QueueItem(contents, piv)) oldContents = contents } } @@ -131,15 +127,15 @@ if (source == null) { return } - queue.v.deselectAll() + Application.queue.deselectAll() source.selected = true source.validate() - anyRequired.enable() + Application.anyRequired.enable() source.basedOn.let { if (it is PasteboardItem.HTML || it is PasteboardItem.RTF) { - styledRequired.enable() + Application.styledRequired.enable() } else { - styledRequired.disable() + Application.styledRequired.disable() } } } @@ -154,7 +150,7 @@ private fun maybeShowPopup(e: MouseEvent) { if (e.isPopupTrigger()) { - popupMenu.show(e.component, e.x, e.y) + Application.popupMenu.show(e.component, e.x, e.y) } } @@ -162,36 +158,70 @@ override fun mouseExited(e: MouseEvent) { } } +object Application { + /* name we call ourselves */ + val MYNAME = "ExifWasher" + + /* global UI objects, must be created on the Swing thread */ + var queue: PasteboardQueue by setOnce() + var frame: JFrame by setOnce() + var coerceDialog: CoerceDialog by setOnce() + var menuItemListener: MenuItemListener by setOnce() + var popupMenu: MyPopupMenu by setOnce() + var searchDialog: SearchDialog by setOnce() + var settingsDialog: SettingsDialog by setOnce() + + /* used by the menus, but not themselves Swing objects */ + val anyRequired = SelectionRequired() + val styledRequired = SelectionRequired() + + fun initialize() { + /* make ourselves look more native */ + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + /* initialize reusable GUI objects */ + frame = JFrame(MYNAME) /* must init this gui object first */ + coerceDialog = CoerceDialog() + menuItemListener = MenuItemListener() /* must init before menus */ + popupMenu = MyPopupMenu() + searchDialog = SearchDialog() + settingsDialog = SettingsDialog() + + /* set up the main frame */ + val con = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + border = BorderFactory.createEmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER) + background = frame.background + } + frame.apply { + jMenuBar = MyMenuBar() + contentPane.add( + JScrollPane(con).apply { + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + preferredSize = Dimension(CPWIDTH, CPHEIGHT) + background = frame.background + }, BorderLayout.CENTER) + pack() + setVisible(true) + addWindowListener(KillIt()) + } + setMacMenus() + + /* launch the updating thread */ + queue = PasteboardQueue(con, settingsDialog.qLength) + UpdateIt(1000).apply { start() } + } +} + + /* entry point */ fun main(args: Array) { LOGGER.log(Level.INFO, "beginning execution") if (OS.type == OS.MAC) { System.setProperty("apple.laf.useScreenMenuBar", "true") } - lateinit var con: JPanel - inSynSwingThread { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - frame.v = JFrame(MYNAME) - con = JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - border = BorderFactory.createEmptyBorder(PANEL_BORDER, PANEL_BORDER, PANEL_BORDER, PANEL_BORDER) - background = frame.v.background - } - frame.v.jMenuBar = menuBar - frame.v.apply { - contentPane.add( - JScrollPane(con).apply { - verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS - horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER - preferredSize = Dimension(CPWIDTH, CPHEIGHT) - background = frame.v.background - }, BorderLayout.CENTER) - pack() - setVisible(true) - addWindowListener(KillIt()) - } - setMacMenus() + inSwingThread { + Application.initialize() } - queue.v = PasteboardQueue(con, settingsDialog.qLength) - UpdateIt(1000).apply { start() } } diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/Menus.kt --- a/src/name/blackcap/clipman/Menus.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/Menus.kt Sun Apr 12 14:31:06 2020 -0700 @@ -18,20 +18,20 @@ override fun actionPerformed(e: ActionEvent) { when (e.actionCommand) { "File.Quit" -> System.exit(0) - "File.Preferences" -> settingsDialog.setVisible(true) + "File.Preferences" -> Application.settingsDialog.setVisible(true) "Edit.Clone" -> onlyIfSelected { PasteboardItem.write(it.contents) } - "Edit.Coerce" -> onlyIfSelected { coerceDialog.setVisible(true) } - "Edit.Find" -> searchDialog.setVisible(true) - "Edit.FindAgain" -> searchDialog.find() + "Edit.Coerce" -> onlyIfSelected { Application.coerceDialog.setVisible(true) } + "Edit.Find" -> Application.searchDialog.setVisible(true) + "Edit.FindAgain" -> Application.searchDialog.find() "Help.About" -> showAboutDialog() else -> throw RuntimeException("unexpected actionCommand!") } } private fun onlyIfSelected(block: (QueueItem) -> Unit) { - val selected = queue.v.getSelected() + val selected = Application.queue.getSelected() if (selected == null) { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "No item selected.", "Error", JOptionPane.ERROR_MESSAGE) @@ -41,8 +41,6 @@ } } -val menuItemListener = MenuItemListener() - /** * Track menu items that require something to be selected in order * to work, and allow them to be enabled and disabled en masse. @@ -66,9 +64,6 @@ fun disable() = setEnabled(false) } -val anyRequired = SelectionRequired() -val styledRequired = SelectionRequired() - /** * 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. @@ -79,37 +74,37 @@ add(JMenu("File").apply { add(JMenuItem("Quit").apply { actionCommand = "File.Quit" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_Q) }) add(JMenuItem("Preferences…").apply { actionCommand = "File.Preferences" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_COMMA) }) }) } add(JMenu("Edit").apply { - add(anyRequired.add(JMenuItem("Clone").apply { + add(Application.anyRequired.add(JMenuItem("Clone").apply { setEnabled(false) actionCommand = "Edit.Clone" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_C) })) - add(styledRequired.add(JMenuItem("Coerce…").apply { + add(Application.styledRequired.add(JMenuItem("Coerce…").apply { setEnabled(false) actionCommand = "Edit.Coerce" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_K) })) add(JMenuItem("Find…").apply { actionCommand = "Edit.Find" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_F) }) add(JMenuItem("Find Again").apply { actionCommand = "Edit.FindAgain" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) makeShortcut(KeyEvent.VK_G) }) }) @@ -117,7 +112,7 @@ add(JMenu("Help").apply { add(JMenuItem("About ClipMan…").apply { actionCommand = "Help.About" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) }) }) } @@ -134,32 +129,28 @@ } } -val menuBar = MyMenuBar() - /** * The popup menu we display when the user requests so atop a clipboard * item. */ class MyPopupMenu: JPopupMenu() { init { - add(anyRequired.add(JMenuItem("Clone").apply { + add(Application.anyRequired.add(JMenuItem("Clone").apply { actionCommand = "Edit.Clone" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) })) - add(styledRequired.add(JMenuItem("Coerce…").apply { + add(Application.styledRequired.add(JMenuItem("Coerce…").apply { actionCommand = "Edit.Coerce" - addActionListener(menuItemListener) + addActionListener(Application.menuItemListener) })) } } -val popupMenu = MyPopupMenu() - /** * Show an About dialog. */ fun showAboutDialog() { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "ClipMan, a clipboard manager.\n" + "© MMXX, David W. Barts", "About ClipMan", diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/Misc.kt --- a/src/name/blackcap/clipman/Misc.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/Misc.kt Sun Apr 12 14:31:06 2020 -0700 @@ -8,6 +8,9 @@ import java.nio.charset.Charset import javax.swing.* import javax.swing.text.JTextComponent +import kotlin.annotation.* +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.* /** * Name of the character set (encoding) we preferentially use for many @@ -17,35 +20,39 @@ val CHARSET = Charset.forName(CHARSET_NAME) /** - * 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. + * 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. + * * @param <T> type of the associated value */ -class LateInit { - private var _v: T? = null +class SetOnceImpl: ReadWriteProperty { + private var setOnceValue: 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") + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + if (setOnceValue == null) { + throw RuntimeException("${property.name} has not been initialized") } else { - return _v!! + return setOnceValue!! } } - @Synchronized set(value) { - if (_v != null) { - throw IllegalStateException("cannot set already-initialized value") + + @Synchronized + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T): Unit { + if (setOnceValue != null) { + throw RuntimeException("${property.name} has already been initialized") } - _v = value + setOnceValue = value } } /** + * Normal way to create a setOnce var: + * var something: SomeType by setOnce() + */ +fun setOnce(): SetOnceImpl = SetOnceImpl() + +/** * Run something in the Swing thread, asynchronously. * @param block lambda containing code to run */ @@ -99,4 +106,3 @@ } return null } - diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/Osdep.kt.mac.osdep --- a/src/name/blackcap/clipman/Osdep.kt.mac.osdep Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/Osdep.kt.mac.osdep Sun Apr 12 14:31:06 2020 -0700 @@ -4,13 +4,14 @@ package name.blackcap.clipman import com.apple.eawt.AboutHandler -import com.apple.eawt.Application import com.apple.eawt.PreferencesHandler +import com.apple.eawt.QuitStrategy fun setMacMenus() { - Application.getApplication().run { + com.apple.eawt.Application.getApplication().run { setAboutHandler(AboutHandler({ showAboutDialog() })) setPreferencesHandler( - PreferencesHandler({ settingsDialog.setVisible(true) })) + PreferencesHandler({ Application.settingsDialog.setVisible(true) })) + setQuitStrategy(QuitStrategy.CLOSE_ALL_WINDOWS) } } diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/PasteboardQueue.kt --- a/src/name/blackcap/clipman/PasteboardQueue.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/PasteboardQueue.kt Sun Apr 12 14:31:06 2020 -0700 @@ -151,8 +151,8 @@ var extra = queue.removeFirst().view inSwingThread { if (extra.searchable.selected) { - anyRequired.disable() - styledRequired.disable() + Application.anyRequired.disable() + Application.styledRequired.disable() } parent.remove(extra.contents) } diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/PasteboardView.kt --- a/src/name/blackcap/clipman/PasteboardView.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/PasteboardView.kt Sun Apr 12 14:31:06 2020 -0700 @@ -56,7 +56,7 @@ * Dynamically size or resize this view. */ fun resize() { - autoSize(queue.v.parent.size.width - + autoSize(Application.queue.parent.size.width - 2 * (PANEL_BORDER + OUTER_BORDER + INNER_BORDER + MARGIN_BORDER)) } } @@ -89,11 +89,11 @@ class PasteboardItemView(label: String, val searchable: ClipText) { private val outerBorder = BorderFactory.createMatteBorder(OUTER_BORDER_TOP, OUTER_BORDER, OUTER_BORDER, OUTER_BORDER, - queue.v.parent.background) + Application.queue.parent.background) val contents = JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) - background = queue.v.parent.background + background = Application.queue.parent.background border = outerBorder add(JLabel(label).apply { horizontalAlignment = JLabel.LEFT diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/SearchDialog.kt --- a/src/name/blackcap/clipman/SearchDialog.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/SearchDialog.kt Sun Apr 12 14:31:06 2020 -0700 @@ -11,7 +11,7 @@ import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener -class SearchDialog: JDialog(frame.v), ActionListener, DocumentListener { +class SearchDialog: JDialog(Application.frame), ActionListener, DocumentListener { /* the search term */ private val _searchFor = JTextField(25).also { it.border = BorderFactory.createLineBorder(Color.GRAY, 1) @@ -155,7 +155,7 @@ origin = null return } - fun doFind(o: PasteboardQueue.Offset?) = queue.v.find(searchFor, + fun doFind(o: PasteboardQueue.Offset?) = Application.queue.find(searchFor, direction = direction, foldCase = ignoreCase, origin = o) var result = doFind(origin) if (result == null && origin != null && autoWrap) { @@ -187,5 +187,3 @@ override fun insertUpdate(e: DocumentEvent) = changedUpdate(e) override fun removeUpdate(e: DocumentEvent) = changedUpdate(e) } - -val searchDialog = SearchDialog() \ No newline at end of file diff -r 88066346f129 -r 19d9da731c43 src/name/blackcap/clipman/SettingsDialog.kt --- a/src/name/blackcap/clipman/SettingsDialog.kt Mon Feb 10 16:40:09 2020 -0700 +++ b/src/name/blackcap/clipman/SettingsDialog.kt Sun Apr 12 14:31:06 2020 -0700 @@ -25,7 +25,7 @@ /* work around name shadowing */ private val _PROPS = PROPERTIES -class SettingsDialog: JDialog(frame.v), ActionListener, ChangeListener { +class SettingsDialog: JDialog(Application.frame), ActionListener, ChangeListener { /* max queue length */ private val _qLength = _PROPS.getInt("queue.length") private val _qlSlider = JSlider(10000, 30000, spinToSlide(_qLength)).also { @@ -104,7 +104,7 @@ when (e.actionCommand) { "OK" -> { writeProperties() - queue.v.maxSize = qLength + Application.queue.maxSize = qLength setVisible(false) } "Cancel" -> { @@ -150,7 +150,7 @@ if (message != null && !message.isEmpty()) { LOGGER.log(Level.WARNING, message) } - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.frame, "Unable to write settings.", "Error", JOptionPane.ERROR_MESSAGE) @@ -158,8 +158,6 @@ } } -val settingsDialog = SettingsDialog() - fun Properties.getString(key: String): String = getProperty(key) as String fun Properties.getInt(key: String): Int = getString(key).toInt()