# HG changeset patch # User David Barts # Date 1581225001 25200 # Node ID 33fbe3a78d84c7076f0820cdf2dd40802fcac75d # Parent c803a2c89ea0ccf38086f2b3c84f42ea667be864 Got the settings stuff compiling (untested). diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/CoerceDialog.kt --- 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(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(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(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(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, fontType: String): Boolean { + private fun badSize(control: JComboBox, 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) { diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Files.kt --- 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) } } diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Main.kt --- 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 { 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() } } diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Menus.kt --- 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) +} diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Misc.kt --- 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 diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Osdep.kt.default.osdep --- /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 */ +} diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Osdep.kt.mac.osdep --- /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) })) + } +} diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/Pasteboard.kt --- 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) /** diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/PasteboardQueue.kt --- 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. diff -r c803a2c89ea0 -r 33fbe3a78d84 src/name/blackcap/clipman/SettingsDialog.kt --- /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(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(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(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(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().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, 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()