changeset 47:19d9da731c43

Recoded; cleaned up root namespace, removed race conditions.
author David Barts <n5jrn@me.com>
date Sun, 12 Apr 2020 14:31:06 -0700
parents 88066346f129
children 7a75c743f973
files build.xml setup.sh src/name/blackcap/clipman/CoerceDialog.kt src/name/blackcap/clipman/Files.kt src/name/blackcap/clipman/Main.kt src/name/blackcap/clipman/Menus.kt src/name/blackcap/clipman/Misc.kt src/name/blackcap/clipman/Osdep.kt.mac.osdep src/name/blackcap/clipman/PasteboardQueue.kt src/name/blackcap/clipman/PasteboardView.kt src/name/blackcap/clipman/SearchDialog.kt src/name/blackcap/clipman/SettingsDialog.kt
diffstat 12 files changed, 136 insertions(+), 110 deletions(-) [+]
line wrap: on
line diff
--- 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 @@
   <target name="compile" depends="classpath"
           description="Compile Java sources to ${work.home}">
     <kotlinc src="${src.home}" output="${work.jar}"
-             classpathref="compile.classpath"/>
+             classpathref="compile.classpath">
+      <compilerarg line="-jvm-target 1.8"/>
+    </kotlinc>
   </target>
 
   <!-- make .jar file -->
@@ -108,7 +110,7 @@
       <fx:deploy nativeBundles="@{type}" outdir="${basedir}" outfile="${app.name}"
         signBundle="false">
         <fx:application mainClass="${app.entry}" name="${app.name}" toolkit="swing"
-          version="1.0"/>
+          version="1.01"/>
         <fx:info description="ClipMan, a clipboard manager." title="${app.name}"
           vendor="David Barts &lt;n5jrn@me.com&gt;"
           copyright="© MMXX, David W. Barts"/>
--- 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* ]]
--- 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<Float>, 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()
--- 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 {
--- 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<PasteboardQueue>()
-val frame = LateInit<JFrame>()
-
 /* 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<String>) {
     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() }
 }
--- 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",
--- 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 &lt;T&gt; type of the associated value
  */
-class LateInit<T> {
-    private var _v: T? = null
+class SetOnceImpl<T>: ReadWriteProperty<Any?,T> {
+    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 <T> setOnce(): SetOnceImpl<T> = SetOnceImpl<T>()
+
+/**
  * Run something in the Swing thread, asynchronously.
  * @param block lambda containing code to run
  */
@@ -99,4 +106,3 @@
     }
     return null
 }
-
--- 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)
     }
 }
--- 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)
                 }
--- 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
--- 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
--- 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()