view src/name/blackcap/exifwasher/SettingsDialog.kt @ 43:8aada13933c6

exif -> jpeg
author davidb
date Sat, 02 May 2020 10:48:17 -0700
parents aafc9c127c7b
children 40911898ed23
line wrap: on
line source

/*
 * The dialog that controls our settings.
 */
package name.blackcap.exifwasher

import java.awt.Dimension
import java.awt.Toolkit
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStreamWriter
import java.util.Properties
import java.util.logging.Level
import java.util.logging.Logger
import javax.swing.*
import javax.swing.event.ListDataEvent
import javax.swing.event.ListDataListener
import javax.swing.event.ListSelectionListener
import kotlin.text.toBoolean

import name.blackcap.exifwasher.exiv2.*

/* work around name shadowing */
private val _PROPS = PROPERTIES

class SettingsDialog : JDialog(Application.mainFrame) {
    protected val BW = 9
    protected val BW2 = BW * 2

    /* where to send output, if not outputToInputDir */
    private var homeDir = System.getProperty("user.home")
    protected var _outputTo = _PROPS.getNotEmpty("outputTo", homeDir)
    protected var _dOutputTo = DPROPERTIES.getNotEmpty("outputTo", homeDir)
    val outputTo: String
    get() = _outputTo

    /* make output where input was found */
    protected var _outputToInputDir = _PROPS.getProperty("outputToInputDir", "false").toBoolean()
    protected var _dOutputToInputDir = DPROPERTIES.getProperty("outputToInputDir", "false").toBoolean()
    val outputToInputDir: Boolean
    get() = _outputToInputDir

    /* the whitelist of allowed Exif tags */
    protected var _whitelist = Whitelist.parse(_PROPS.getProperty("whitelist", ""))
    val whitelist: Whitelist
    get() = _whitelist

    /* the default whitelist, for factory resets */
    protected val _oWhitelist = _whitelist.clone()
    protected val _dWhitelist = Whitelist.parse(DPROPERTIES.getProperty("whitelist", ""))

    /* radio buttons to choose output directory policy */
    protected val outputToButton = JRadioButton("Output to:", !outputToInputDir).also {
        it.addActionListener(ActionListener { _ -> setOutputOpts(it.isSelected()) })
        it.noTaller()
    }
    protected val outputToInputButton = JRadioButton(
      "Output to directory containing input file.", outputToInputDir).also {
        it.addActionListener(ActionListener { _ -> setOutputOpts(!it.isSelected()) })
        it.noTaller()
    }
    protected val buttonGroup = ButtonGroup().apply {
        add(outputToButton)
        add(outputToInputButton)
    }
    protected fun setOutputOpts(toSpecific: Boolean) {
        _outputToInputDir = !toSpecific
        changeOutputTo.setEnabled(toSpecific)
    }

    /* displays the OutputTo directory */
    protected val outputToText = JTextField(outputTo, 40).apply {
        setEditable(false)
        noTaller()
    }

    /* pops up to change the above directory */
    protected val outputToChooser = JFileChooser(outputToText.text).apply {
        fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
    }

    /* requests the OutputTo directory be changed */
    protected val changeOutputTo = JButton("Change").also {
        it.addActionListener(ActionListener {
            val status = outputToChooser.showOpenDialog(this)
            if (status == JFileChooser.APPROVE_OPTION) {
                _outputTo = outputToChooser.selectedFile.canonicalPath
                outputToText.text = _outputTo
            }
        })
        it.setEnabled(!outputToInputDir)
    }

    /* bottom buttons to restore defaults, cancel, save */
    protected val restoreButton = JButton("Restore All Defaults").also {
        it.addActionListener(ActionListener {
            restore(_dOutputToInputDir, _dOutputTo, _dWhitelist)
        })
    }
    protected val cancelButton = JButton("Cancel").also {
        it.addActionListener(ActionListener {
            setVisible(false)
            restore(outputToInputDir, outputTo, whitelist)
        })
    }
    protected val saveButton = JButton("Save").also {
        it.addActionListener(ActionListener {
            setVisible(false)
            writeProperties()
        })
    }

    protected fun writeProperties() {
        _whitelist = Whitelist().apply {
            wlSelectorModel.toList().forEach { add(it) }
        }
        _PROPS.run {
            setProperty("outputTo", outputTo)
            setProperty("outputToInputDir", outputToInputDir.toString())
            setProperty("whitelist", whitelist.toString())
        }
        try {
            BufferedWriter(OutputStreamWriter(FileOutputStream(PROP_FILE), CHARSET)).use {
                _PROPS.store(it, null)
            }
        } catch (e: IOException) {
            LOGGER.log(Level.SEVERE, "unable to write settings", e)
            JOptionPane.showMessageDialog(Application.mainFrame,
                "Unable to write settings.",
                "Error",
                JOptionPane.ERROR_MESSAGE)
        }
    }

    protected fun restore(outputToInput: Boolean, output: String, wl: Whitelist) {
        outputToButton.setSelected(!outputToInput)
        changeOutputTo.setEnabled(!outputToInput)
        outputToText.text = output
        outputToInputButton.setSelected(outputToInput)
        wlSelectorModel.reset(wl.toList())
        validate()
    }

    /* so we can present a list of strings that is always sorted */
    protected class WhiteListModel(basedOn: Collection<String>): ListModel<String> {
        private val storage = ArrayList<String>(basedOn).apply { sort() }
        private val listeners = mutableListOf<ListDataListener>()

        /* so we can mutate the list */

        fun add(newItem: String): Unit {
            var index = storage.binarySearch(newItem)
            if (index < 0) {
                index = -(index + 1)
            }
            storage.add(index, newItem)
            notifyAll(ListDataEvent.INTERVAL_ADDED, index, index)
        }

        fun removeAt(index: Int): Unit {
            storage.removeAt(index)
            notifyAll(ListDataEvent.INTERVAL_REMOVED, index, index)
        }

        fun remove(oldItem: String): Unit {
            val index: Int = storage.binarySearch(oldItem)
            if (index < 0) {
                return
            }
            storage.removeAt(index)
            notifyAll(ListDataEvent.INTERVAL_REMOVED, index, index)
        }

        fun reset(basedOn: Collection<String>): Unit {
            val oldSize = storage.size
            storage.clear()
            notifyAll(ListDataEvent.INTERVAL_REMOVED, 0, oldSize)
            storage.addAll(basedOn)
            storage.sort()
            notifyAll(ListDataEvent.INTERVAL_ADDED, 0, storage.size)
        }

        /* misc. */

        fun toList(): List<String> = storage

        private fun notifyAll(eType: Int, index0: Int, index1: Int): Unit {
            val event = ListDataEvent(this, eType, index0, index1)
            when (eType) {
                ListDataEvent.CONTENTS_CHANGED -> listeners.forEach { it.contentsChanged(event) }
                ListDataEvent.INTERVAL_ADDED -> listeners.forEach { it.intervalAdded(event) }
                ListDataEvent.INTERVAL_REMOVED -> listeners.forEach { it.intervalRemoved(event) }
                else -> throw RuntimeException("unexpected event type!")
            }
        }

        /* so we are a proper ListModel */

        override fun addListDataListener(l: ListDataListener): Unit {
            listeners.add(l)
        }

        override fun removeListDataListener(l: ListDataListener): Unit {
            listeners.remove(l)
        }

        override fun getElementAt(index: Int): String = storage[index]

        override fun getSize(): Int = storage.size
    }

    protected class WLAddDialog(parent: SettingsDialog): JDialog(parent) {
        private val BW = parent.BW
        private val BW2 = parent.BW2

        private val toAdd = JTextField(40).apply {
            alignmentX = CENTER_ALIGNMENT
            border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
        }

        private val cancelButton = JButton("Cancel").also {
            it.addActionListener(ActionListener {
                toAdd.text = ""
                setVisible(false)
            })
        }

        private val addButton = JButton("Add").also {
            it.addActionListener(ActionListener {
                val newItem = toAdd.text?.trim()
                if (newItem.isNullOrEmpty()) {
                    Toolkit.getDefaultToolkit().beep()
                } else {
                    parent.wlSelectorModel.add(newItem)
                }
                toAdd.text = ""
                setVisible(false)
            })
        }

        init {
            title = "Add Item to Whitelist"
            contentPane.apply {
                layout = BoxLayout(this, BoxLayout.Y_AXIS)
                add(toAdd)
                add(Box(BoxLayout.X_AXIS).apply {
                    alignmentX = CENTER_ALIGNMENT
                    border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                    add(Box.createHorizontalGlue())
                    add(cancelButton)
                    add(Box.createHorizontalGlue())
                    add(addButton)
                    add(Box.createHorizontalGlue())
                })
            }
            pack()
            setResizable(false)
        }
    }

    /* the JList that holds our whitelist */
    protected val wlSelectorModel = WhiteListModel(whitelist.toList())
    protected val wlSelector: JList<String> = JList<String>().apply {
        visibleRowCount = 6
        model = wlSelectorModel
        clearSelection()
        addListSelectionListener(ListSelectionListener {
            wlDeleteButton.setEnabled(!isSelectionEmpty())
        })
    }

    /* buttons for managing the whitelist */
    protected val wlAddButton = JButton("Add").also {
        it.addActionListener(ActionListener { wlAddDialog.setVisible(true) })
    }
    protected val wlDeleteButton = JButton("Delete").also {
        it.addActionListener(ActionListener {
            wlSelector.selectedIndices.forEach { wlSelectorModel.removeAt(it) }
            setEnabled(false)
        })
        it.setEnabled(false)
    }

    /* the dialog that the Add button pops up */
    protected val wlAddDialog = WLAddDialog(this)

    init {
        title = "Settings"
        contentPane.apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            add(JTabbedPane().apply {
                addTab("Folders", JPanel().apply {
                    border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                    layout = BoxLayout(this, BoxLayout.Y_AXIS)
                    // yes, a one-item box. POS mis-aligns things if only
                    // some stuff is boxed, border widths be damned. sigh.
                    add(Box(BoxLayout.X_AXIS).apply {
                        alignmentX = LEFT_ALIGNMENT
                        border = BorderFactory.createEmptyBorder(BW, BW, 0, BW)
                        add(outputToInputButton)
                        add(Box.createHorizontalGlue())
                        pack()
                        noTaller()
                    })
                    add(Box(BoxLayout.X_AXIS).apply {
                        alignmentX = LEFT_ALIGNMENT
                        border = BorderFactory.createEmptyBorder(BW, BW, 0, BW)
                        add(outputToButton)
                        add(Box.createHorizontalStrut(BW2))
                        add(outputToText)
                        add(Box.createHorizontalGlue())
                        pack()
                        noTaller()
                    })
                    add(Box(BoxLayout.X_AXIS).apply {
                        alignmentX = LEFT_ALIGNMENT
                        border = BorderFactory.createEmptyBorder(0, BW, BW, BW)
                        add(Box.createHorizontalGlue())
                        add(changeOutputTo)
                        pack()
                        noTaller()
                    })
                    add(Box.createVerticalGlue())
                })
                addTab("Whitelist", JPanel().apply {
                    border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                    layout = BoxLayout(this, BoxLayout.Y_AXIS)
                    add(JScrollPane(wlSelector).apply {
                        alignmentX = CENTER_ALIGNMENT
                        border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                        verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
                        horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
                    })
                    add(Box.createVerticalGlue())
                    add(Box(BoxLayout.X_AXIS).apply {
                        alignmentX = CENTER_ALIGNMENT
                        border = BorderFactory.createEmptyBorder(BW, BW, BW, BW)
                        add(Box.createHorizontalGlue())
                        add(wlAddButton)
                        add(Box.createHorizontalGlue())
                        add(wlDeleteButton)
                        add(Box.createHorizontalGlue())
                    })
                })
            })
            add(Box(BoxLayout.X_AXIS).apply {
                border = BorderFactory.createEmptyBorder(BW, BW2, BW2, BW2)
                add(restoreButton)
                add(Box.createHorizontalGlue())
                add(cancelButton)
                add(Box.createHorizontalStrut(BW2))
                add(saveButton)
            })
        }
        pack()
        minimumSize = size
    }
}

fun Properties.getNotEmpty(key: String, default: String): String {
    val ret = getProperty(key)
    return if (ret.isNullOrEmpty()) default else ret
}

fun JComponent.noTaller() {
    maximumSize = Dimension(maximumSize.width, preferredSize.height)
}