changeset 41:33fbe3a78d84

Got the settings stuff compiling (untested).
author David Barts <n5jrn@me.com>
date Sat, 08 Feb 2020 22:10:01 -0700
parents c803a2c89ea0
children 99220aa136d9
files 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.default.osdep src/name/blackcap/clipman/Osdep.kt.mac.osdep src/name/blackcap/clipman/Pasteboard.kt src/name/blackcap/clipman/PasteboardQueue.kt src/name/blackcap/clipman/SettingsDialog.kt
diffstat 10 files changed, 365 insertions(+), 44 deletions(-) [+]
line wrap: on
line diff
--- a/src/name/blackcap/clipman/CoerceDialog.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/CoerceDialog.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -17,19 +17,18 @@
 import javax.swing.event.DocumentEvent
 import javax.swing.event.DocumentListener
 
+val FONTS =
+    GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames.copyOf().apply {
+        sort()
+    }
+val SIZES =
+    arrayOf(9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 16.0f, 18.0f,
+        24.0f, 36.0f, 48.0f, 64.0f, 72.0f, 96.0f, 144.0f, 288.0f)
+
 class CoerceDialog: JDialog(frame.v), ActionListener {
-    private val FONTS =
-        GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames.copyOf().apply {
-            sort()
-        }
-    private val SIZES =
-        arrayOf(9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 16.0f, 18.0f,
-            24.0f, 36.0f, 48.0f, 64.0f, 72.0f, 96.0f, 144.0f, 288.0f)
-    private val DSIZEI = 6  /* SIZES[6] = 16 */
-
     /* the proportional font family */
     private val _pFamily = JComboBox<String>(FONTS).apply {
-        selectedIndex = getFontIndex(Font.SERIF)
+        selectedItem = settingsDialog.pFamily
         alignmentX = JComboBox.LEFT_ALIGNMENT
     }
     val pFamily: String
@@ -39,7 +38,7 @@
 
     /* the proportional font size */
     private val _pSize = JComboBox<Float>(SIZES).also {
-        it.selectedIndex = DSIZEI
+        it.selectedItem = settingsDialog.pSize
         it.alignmentX = JComboBox.LEFT_ALIGNMENT
         it.setEditable(true)
     }
@@ -50,7 +49,7 @@
 
     /* the monospaced font family */
     private val _mFamily = JComboBox<String>(FONTS).apply {
-        selectedIndex = getFontIndex(Font.MONOSPACED)
+        selectedItem = settingsDialog.mFamily
         alignmentX = JComboBox.LEFT_ALIGNMENT
     }
     val mFamily: String
@@ -60,7 +59,7 @@
 
     /* the monospaced font size */
     private val _mSize = JComboBox<Float>(SIZES).also {
-        it.selectedIndex = DSIZEI
+        it.selectedItem = settingsDialog.mSize
         it.alignmentX = JComboBox.LEFT_ALIGNMENT
         it.setEditable(true)
     }
@@ -183,7 +182,7 @@
                     "Error",
                     JOptionPane.ERROR_MESSAGE)
             } else {
-                if (badSize(_pSize, "proportionally-spaced") || badSize(_mSize, "monospaced")) {
+                if (badSize(_pSize, settingsDialog.pSize, "proportionally-spaced") || badSize(_mSize, settingsDialog.mSize, "monospaced")) {
                     return
                 }
                 PasteboardItem.write(
@@ -195,28 +194,19 @@
         }
     }
 
-    private fun badSize(control: JComboBox<Float>, fontType: String): Boolean {
+    private fun badSize(control: JComboBox<Float>, default: Float, fontType: String): Boolean {
         val size = control.selectedItem as? Float
         if (size == null || size < 1.0f) {
             JOptionPane.showMessageDialog(frame.v,
                 "Invalid ${fontType} font size.",
                 "Error",
                 JOptionPane.ERROR_MESSAGE)
-            control.selectedIndex = DSIZEI
+            control.selectedItem = default
             return true
         }
         return false
     }
 
-    private fun getFontIndex(font: String): Int {
-        val found = FONTS.indexOf(font)
-        if (found < 0) {
-            LOGGER.log(Level.WARNING, "font '${font}' not found")
-            return 0
-        }
-        return found
-    }
-
     private fun normalizeFont(font: String): String {
         val lcFont = font.toLowerCase()
         return when (lcFont) {
--- a/src/name/blackcap/clipman/Files.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/Files.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -6,7 +6,8 @@
 
 import java.io.BufferedReader
 import java.io.File
-import java.io.FileReader
+import java.io.FileInputStream
+import java.io.InputStreamReader
 import java.util.Properties
 import java.util.logging.FileHandler
 import java.util.logging.Level
@@ -62,11 +63,15 @@
 
 /* make some usable objects */
 
-val PROPERTIES = run {
+val DPROPERTIES = Properties().apply {
+    OS::class.java.getClassLoader().getResourceAsStream("default.properties").use { load(it) }
+}
+
+val PROPERTIES = Properties(DPROPERTIES).apply {
     PF_DIR.makeIfNeeded()
     PROP_FILE.createNewFile()
-    Properties().apply {
-        BufferedReader(FileReader(PROP_FILE)).use { load(it) }
+    BufferedReader(InputStreamReader(FileInputStream(PROP_FILE), CHARSET)).use  {
+        load(it)
     }
 }
 
--- a/src/name/blackcap/clipman/Main.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/Main.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -18,6 +18,7 @@
 import javax.swing.*
 import javax.swing.text.html.StyleSheet
 import kotlin.concurrent.thread
+import kotlin.math.roundToInt
 import org.jsoup.Jsoup
 import org.jsoup.nodes.*
 
@@ -75,7 +76,7 @@
                         PasteboardItemView("Plain text", ClipText(contents).apply {
                             contentType = "text/plain"
                             text = plain
-                            font = Font(Font.MONOSPACED, Font.PLAIN, MONO_SIZE)
+                            font = Font(settingsDialog.mFamily, Font.PLAIN, settingsDialog.mSize.roundToInt())
                             resize()
                         })
                     } else {
@@ -108,8 +109,10 @@
 
     private fun preproc(html: String): Pair<String, StyleSheet> {
         val sty = StyleSheet().apply {
-            addRule("body { font-family: serif; font-size: ${PROP_SIZE}; }")
-            addRule("code, kbd, pre, samp, tt { font-family: monospace; font-size: ${MONO_SIZE}; }")
+            addRule("body { font-family: \"%s\"; font-size: %.2f; }".format(
+                settingsDialog.pFamily, settingsDialog.pSize))
+            addRule("code, kbd, pre, samp, tt { font-family: \"%s\"; font-size: %.2f}; }".format(
+                settingsDialog.mFamily, settingsDialog.mSize))
         }
         val scrubbed = Jsoup.parse(html).run {
             select("style").forEach {
@@ -190,7 +193,8 @@
             setVisible(true)
             addWindowListener(KillIt())
         }
+        setMacMenus()
     }
-    queue.v = PasteboardQueue(con, 10)
+    queue.v = PasteboardQueue(con, settingsDialog.qLength)
     UpdateIt(1000).apply { start() }
 }
--- a/src/name/blackcap/clipman/Menus.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/Menus.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -18,15 +18,12 @@
     override fun actionPerformed(e: ActionEvent) {
         when (e.actionCommand) {
             "File.Quit" -> System.exit(0)
+            "File.Preferences" -> 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()
-            "Help.About" -> JOptionPane.showMessageDialog(frame.v,
-                "ClipMan, a clipboard manager.\n"
-                + "© MMXX, David W. Barts",
-                "About ClipMan",
-                JOptionPane.PLAIN_MESSAGE)
+            "Help.About" -> showAboutDialog()
             else -> throw RuntimeException("unexpected actionCommand!")
         }
     }
@@ -85,6 +82,11 @@
                     addActionListener(menuItemListener)
                     makeShortcut(KeyEvent.VK_Q)
                 })
+                add(JMenuItem("Preferences…").apply {
+                    actionCommand = "File.Preferences"
+                    addActionListener(menuItemListener)
+                    makeShortcut(KeyEvent.VK_COMMA)
+                })
             })
         }
         add(JMenu("Edit").apply {
@@ -152,3 +154,14 @@
 }
 
 val popupMenu = MyPopupMenu()
+
+/**
+ * Show an About dialog.
+ */
+fun showAboutDialog() {
+    JOptionPane.showMessageDialog(frame.v,
+        "ClipMan, a clipboard manager.\n"
+        + "© MMXX, David W. Barts",
+        "About ClipMan",
+        JOptionPane.PLAIN_MESSAGE)
+}
--- a/src/name/blackcap/clipman/Misc.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/Misc.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -5,6 +5,7 @@
 
 import java.awt.Dimension
 import java.awt.Toolkit
+import java.nio.charset.Charset
 import javax.swing.*
 import javax.swing.text.JTextComponent
 
@@ -13,6 +14,7 @@
  * things.
  */
 val CHARSET_NAME = "UTF-8"
+val CHARSET = Charset.forName(CHARSET_NAME)
 
 /**
  * Allows a val to have lateinit functionality. It is an error to attempt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/clipman/Osdep.kt.default.osdep	Sat Feb 08 22:10:01 2020 -0700
@@ -0,0 +1,8 @@
+/*
+ * OS-dependent code, version for all non-Mac systems.
+ */
+package name.blackcap.clipman
+
+fun setMacMenus() {
+    /* no-op */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/clipman/Osdep.kt.mac.osdep	Sat Feb 08 22:10:01 2020 -0700
@@ -0,0 +1,16 @@
+/*
+ * OS-dependent code, Mac version.
+ */
+package name.blackcap.clipman
+
+import com.apple.eawt.AboutHandler
+import com.apple.eawt.Application
+import com.apple.eawt.PreferencesHandler
+
+fun setMacMenus() {
+    Application.getApplication().run {
+        setAboutHandler(AboutHandler({ showAboutDialog() }))
+        setPreferencesHandler(
+            PreferencesHandler({ settingsDialog.setVisible(true) }))
+    }
+}
--- a/src/name/blackcap/clipman/Pasteboard.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/Pasteboard.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -19,7 +19,6 @@
 import kotlin.collections.HashMap
 
 /* Kotlin bug: compaion class fails to see these unless they are out here. */
-private val CHARSET = Charset.forName(CHARSET_NAME)
 private val HTML_FLAVOR = DataFlavor("text/html; document=all; class=\"[B\"; charset=" + CHARSET_NAME)
 
 /**
--- a/src/name/blackcap/clipman/PasteboardQueue.kt	Wed Feb 05 16:47:25 2020 -0800
+++ b/src/name/blackcap/clipman/PasteboardQueue.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -44,11 +44,11 @@
      * than or equal to zero means an unlimited size.
      */
     var maxSize: Int
-        get() { return _maxSize }
-        @Synchronized set(value) {
-            _maxSize = value
-            truncate()
-        }
+    get() { return _maxSize }
+    @Synchronized set(value) {
+        _maxSize = value
+        truncate()
+    }
 
     /**
      * Add a QueueItem to the end of the queue.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/clipman/SettingsDialog.kt	Sat Feb 08 22:10:01 2020 -0700
@@ -0,0 +1,284 @@
+/*
+ * The dialog that controls font corecion.
+ */
+package name.blackcap.clipman
+
+import java.awt.Color
+import java.awt.Container
+import java.awt.Dimension
+import java.awt.Font
+import java.awt.GraphicsEnvironment
+import java.awt.Toolkit
+import java.awt.event.ActionEvent
+import java.awt.event.ActionListener
+import java.io.BufferedWriter
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.util.Hashtable
+import java.util.Properties
+import java.util.logging.Level
+import java.util.logging.Logger
+import javax.swing.*
+import javax.swing.event.ChangeEvent
+import javax.swing.event.ChangeListener
+import kotlin.math.log10
+import kotlin.math.pow
+import kotlin.math.roundToInt
+import kotlin.text.toFloat
+import kotlin.text.toInt
+
+/* work around name shadowing */
+private val _PROPS = PROPERTIES
+
+class SettingsDialog: JDialog(frame.v), ActionListener, ChangeListener {
+    /* the proportional font family */
+    private val _pFamily = JComboBox<String>(FONTS).apply {
+        selectedItem = _PROPS.getString("prop.family")
+        alignmentX = JComboBox.LEFT_ALIGNMENT
+    }
+    val pFamily: String
+    get() {
+        return _pFamily.selectedItem as String
+    }
+
+    /* the proportional font size */
+    private val _pSize = JComboBox<Float>(SIZES).also {
+        it.selectedItem = _PROPS.getFloat("prop.size")
+        it.alignmentX = JComboBox.LEFT_ALIGNMENT
+        it.setEditable(true)
+    }
+    val pSize: Float
+    get() {
+        return _pSize.selectedItem as Float
+    }
+
+    /* the monospaced font family */
+    private val _mFamily = JComboBox<String>(FONTS).apply {
+        selectedItem = _PROPS.getString("mono.family")
+        alignmentX = JComboBox.LEFT_ALIGNMENT
+    }
+    val mFamily: String
+    get() {
+        return _mFamily.selectedItem as String
+    }
+
+    /* the monospaced font size */
+    private val _mSize = JComboBox<Float>(SIZES).also {
+        it.selectedItem = _PROPS.getFloat("mono.size")
+        it.alignmentX = JComboBox.LEFT_ALIGNMENT
+        it.setEditable(true)
+    }
+    val mSize: Float
+    get() {
+        return _mSize.selectedItem as Float
+    }
+
+    /* max queue length */
+    private val _qLength = _PROPS.getInt("queue.length")
+    private val _qlSlider = JSlider(10000, 30000, spinToSlide(_qLength)).also {
+        it.majorTickSpacing = 10000
+        it.paintTicks = true
+        it.labelTable = Hashtable<Int, String>().apply {
+            put(10000, "10")
+            put(20000, "100")
+            put(30000, "1000")
+        }
+        it.addChangeListener(this)
+    }
+    private val _qlSpinner = JSpinner(SpinnerNumberModel(_qLength, 10, 1000, 1)).also {
+        it.addChangeListener(this)
+    }
+    val qLength: Int
+    get() {
+        return _qlSpinner.value as Int
+    }
+
+    /* standard spacing between elements (10 pixels ≅ 1/7") and half that */
+    private val BW = 5
+    private val BW2 = 10
+
+    /* buttons */
+    private val _ok = JButton("OK").also {
+        it.actionCommand = "OK"
+        it.addActionListener(this)
+    }
+
+    private val _cancel = JButton("Cancel").also {
+        it.actionCommand = "Cancel"
+        it.addActionListener(this)
+    }
+
+    private val _rad = JButton("Restore All Defaults").also {
+        it.actionCommand = "Restore"
+        it.addActionListener(this)
+    }
+
+    /* initializer */
+    init {
+        title = "Preferences"
+        contentPane.apply {
+            add(Box(BoxLayout.Y_AXIS).apply {
+                add(Box(BoxLayout.Y_AXIS).apply {
+                    border = BorderFactory.createEmptyBorder(BW2, BW2, BW, BW2)
+                    alignmentX = Box.CENTER_ALIGNMENT
+                    add(leftLabel("Default proportionally-spaced font:"))
+                    add(Box.createVerticalStrut(BW))
+                    add(Box(BoxLayout.X_AXIS).apply {
+                        alignmentX = Box.LEFT_ALIGNMENT
+                        add(Box.createGlue())
+                        add(Box(BoxLayout.Y_AXIS).apply {
+                            add(leftLabel("Family:"))
+                            add(_pFamily)
+                        })
+                        add(Box.createGlue())
+                        add(Box(BoxLayout.Y_AXIS).apply {
+                            add(leftLabel("Size:"))
+                            add(_pSize)
+                        })
+                        add(Box.createGlue())
+                    })
+                })
+                add(JSeparator())
+                add(Box(BoxLayout.Y_AXIS).apply {
+                    alignmentX = Box.CENTER_ALIGNMENT
+                    border = BorderFactory.createEmptyBorder(BW, BW2, BW, BW2)
+                    add(leftLabel("Default monospaced font:"))
+                    add(Box.createVerticalStrut(BW))
+                    add(Box(BoxLayout.X_AXIS).apply {
+                        alignmentX = Box.LEFT_ALIGNMENT
+                        add(Box.createGlue())
+                        add(Box(BoxLayout.Y_AXIS).apply {
+                            add(leftLabel("Family:"))
+                            add(_mFamily)
+                        })
+                        add(Box.createGlue())
+                        add(Box(BoxLayout.Y_AXIS).apply {
+                            add(leftLabel("Size:"))
+                            add(_mSize)
+                        })
+                        add(Box.createGlue())
+                    })
+                })
+                add(JSeparator())
+                add(Box(BoxLayout.Y_AXIS).apply {
+                    alignmentX = Box.CENTER_ALIGNMENT
+                    border = BorderFactory.createEmptyBorder(BW, BW2, BW, BW2)
+                    add(leftLabel("Maximum queue size:"))
+                    add(Box.createVerticalStrut(BW))
+                    add(Box(BoxLayout.X_AXIS).apply {
+                        alignmentX = Box.LEFT_ALIGNMENT
+                        add(Box.createGlue())
+                        add(_qlSlider)
+                        add(Box.createGlue())
+                        add(_qlSpinner)
+                        add(Box.createGlue())
+                    })
+                })
+                add(JSeparator())
+                add(Box(BoxLayout.X_AXIS).apply {
+                    alignmentX = Box.CENTER_ALIGNMENT
+                    border = BorderFactory.createEmptyBorder(BW, BW2, BW, BW2)
+                    add(Box.createGlue())
+                    add(_rad)
+                    add(Box.createGlue())
+                    add(_cancel)
+                    add(Box.createGlue())
+                    add(_ok)
+                    add(Box.createGlue())
+                })
+            })
+        }
+        rootPane.setDefaultButton(_ok)
+        pack()
+        setResizable(false)
+    }
+
+    override fun actionPerformed(e: ActionEvent) {
+        when (e.actionCommand) {
+            "OK" -> {
+                writeProperties()
+                queue.v.maxSize = qLength
+                setVisible(false)
+            }
+            "Cancel" -> setVisible(false)
+            "Restore" -> revertProperties()
+        }
+    }
+
+    override fun stateChanged(e: ChangeEvent) {
+        when (val source = e.source) {
+            source === _qlSlider ->
+                _qlSpinner.value = (10.0).pow(_qlSlider.value.toDouble() / 10000.0).roundToInt()
+            source === _qlSpinner ->
+                _qlSlider.value = spinToSlide(_qlSpinner.value as Int)
+        }
+    }
+
+    private fun spinToSlide(value: Int): Int =
+        (log10(value.toDouble()) * 10000.0).roundToInt()
+
+    private fun leftLabel(text: String) = JLabel(text).apply {
+        alignmentX = JLabel.LEFT_ALIGNMENT
+    }
+
+    private fun badSize(control: JComboBox<Float>, default: Float, fontType: String): Boolean {
+        val size = control.selectedItem as? Float
+        if (size == null || size < 1.0f) {
+            JOptionPane.showMessageDialog(frame.v,
+                "Invalid ${fontType} font size.",
+                "Error",
+                JOptionPane.ERROR_MESSAGE)
+            control.selectedItem = default
+            return true
+        }
+        return false
+    }
+
+    private fun revertProperties()
+    {
+        val params = arrayOf("mono.family", "mono.size", "prop.family", "prop.size", "queue.length")
+        for (param in params) {
+            _PROPS.put(param, DPROPERTIES.get(param))
+        }
+        _mFamily.selectedItem = _PROPS.getString("mono.family")
+        _mSize.selectedItem = _PROPS.getFloat("mono.size")
+        _pFamily.selectedItem = _PROPS.getString("prop.family")
+        _pSize.selectedItem = _PROPS.getFloat("prop.size")
+        val ql = _PROPS.getInt("queue.length")
+        _qlSpinner.value = ql
+        _qlSlider.value = spinToSlide(ql)
+    }
+
+    private fun writeProperties()
+    {
+        try {
+            BufferedWriter(OutputStreamWriter(FileOutputStream(PROP_FILE), CHARSET)).use {
+                _PROPS.put("mono.family", mFamily)
+                _PROPS.put("mono.size", mSize.toString())
+                _PROPS.put("prop.family", pFamily)
+                _PROPS.put("prop.size", pSize.toString())
+                _PROPS.put("queue.length", qLength.toString())
+                _PROPS.store(it, null)
+            }
+        } catch (e: IOException) {
+            LOGGER.log(Level.WARNING, "IOException writing properties file")
+            val message = e.message
+            if (message != null && !message.isEmpty()) {
+                LOGGER.log(Level.WARNING, message)
+            }
+            JOptionPane.showMessageDialog(frame.v,
+                "Unable to write settings.",
+                "Error",
+                JOptionPane.ERROR_MESSAGE)
+        }
+    }
+}
+
+val settingsDialog = SettingsDialog()
+
+fun Properties.getString(key: String): String = get(key) as String
+
+fun Properties.getInt(key: String): Int = getString(key).toInt()
+
+fun Properties.getFloat(key: String): Float = getString(key).toFloat()