Mercurial > cgi-bin > hgweb.cgi > JpegWasher
view src/name/blackcap/exifwasher/WashDialog.kt @ 18:841f711c40bd
Add new dialogs, and make NO the default for dangerous YES answers.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sat, 11 Apr 2020 15:01:23 -0700 |
parents | e52fd1a575de |
children | 39b977021ea1 |
line wrap: on
line source
/* * The dialog that controls washing a single file. */ package name.blackcap.exifwasher import java.awt.Color import java.awt.Component import java.awt.Dimension import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.util.logging.Level import java.util.logging.Logger import javax.swing.* import javax.swing.table.DefaultTableModel import javax.swing.table.TableColumn import javax.swing.table.TableColumnModel import name.blackcap.exifwasher.exiv2.* class WashDialog : JDialog(Application.mainFrame) { private val BW = 9 private val BW2 = BW * 2 private val WIDTH = 640 private val HEIGHT = 480 private val COLUMN_NAMES = arrayOf<String>("Delete?", "Key", "Type", "Value") private val myTable = JTable(arrayOf<Array<Any>>(), COLUMN_NAMES).apply { autoCreateRowSorter = false border = BorderFactory.createLineBorder(Color.GRAY) gridColor = Color.GRAY rowSorter = null setShowGrid(true) } val impliedDeletions = mutableSetOf<String>() private val selectAll = JCheckBox("Select all for deletion", false).also { it.addActionListener(ActionListener { _ -> val limit = myTable.rowCount - 1 if (it.isSelected()) { impliedDeletions.clear() for (i in 0 .. limit) { if(!(myTable.getValueAt(i, 0) as Boolean)) { impliedDeletions.add(myTable.getValueAt(i, 1) as String) myTable.setValueAt(true, i, 0) } } } else { for (i in 0 .. limit) { if (impliedDeletions.contains(myTable.getValueAt(i, 1) as String)) { myTable.setValueAt(false, i, 0) } } } }) } private val resetButton = JButton("Reset").also { it.addActionListener(ActionListener { doReset() }) } private val cancelButton = JButton("Cancel").also { it.addActionListener(ActionListener { close() }) } /* deliberately not the default action, because it changes a file */ private val washButton = JButton("Wash").also { it.addActionListener(ActionListener { doWash() }) } private lateinit var washing: File /* a yes/no dialog in which NO is the default */ private fun noYesDialog(parent: Component, message: String, title: String): Int { val options = arrayOf<String>("No", "Yes") val status = JOptionPane.showOptionDialog(parent, message, title, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) return if (status == 1) JOptionPane.YES_OPTION else JOptionPane.NO_OPTION } /* initiates the washing of the Exif data */ fun wash(dirty: File) { title = "Washing: ${dirty.name}" /* warn (and allow user to back out) if not a JPEG */ val lcext = splitext(dirty.name).second.toLowerCase() if (lcext != ".jpg" && lcext != ".jpeg") { val answer = noYesDialog(Application.mainFrame, "File ${dirty.name} does not appear to be a JPEG image.\n"+ "Washing non-JPEG images may damage them. Proceed anyway?", "Non-JPEG file detected") if (answer != JOptionPane.YES_OPTION) { close() return } } selectAll.setSelected(false) washing = dirty useWaitCursor() swingWorker<Array<Array<Any>>?> { inBackground { try { val image = Image(dirty.canonicalPath) val meta = image.metadata val keys = meta.keys keys.sort() Array<Array<Any>>(keys.size) { val key = keys[it] val value = meta[key] arrayOf( !Application.settingsDialog.whitelist.contains(key), key, value.type, value.value) } } catch (e: Exiv2Exception) { LOGGER.log(Level.SEVERE, "unable to read metadata", e) null } } whenDone { useNormalCursor() val tableData = get() if (tableData == null) { JOptionPane.showMessageDialog(Application.mainFrame, "Unable to read metadata.", "Error", JOptionPane.ERROR_MESSAGE) } else { myTable.run { model = MyTableModel(tableData, COLUMN_NAMES) autoResizeMode = JTable.AUTO_RESIZE_OFF setColWidth(0, 0, COLUMN_NAMES[0]) setColWidth(1, 180, null) setColWidth(2, 72, "Undefined") setColWidth(3, 720, null) setOverallWidth() } setVisible(true) } } } } private class MyTableModel(tData: Array<Array<Any>>, cNames: Array<String>) : DefaultTableModel(tData, cNames) { override fun isCellEditable(row: Int, col: Int) = col == 0 override fun getColumnClass(col: Int) = if (col == 0) { java.lang.Boolean::class.java } else { java.lang.String::class.java } } private fun doReset() { myTable.model.run { for (i in 0 .. rowCount - 1) { val key = getValueAt(i, 1) as String setValueAt(!Application.settingsDialog.whitelist.contains(key), i, 0) } } myTable.validate() } private fun doWash() { setVisible(false) /* get path to the directory we create */ val outDir = if (Application.settingsDialog.outputToInputDir) { washing.canonicalFile.parent } else { Application.settingsDialog.outputTo } /* get new file name */ val (name, ext) = splitext(washing.name) var newFile = File(outDir, "${name}_washed${ext}") /* warn (and allow user to back out) if overwriting */ if (newFile.exists()) { val answer = noYesDialog(Application.mainFrame, "File ${newFile.name} already exists. Overwrite?", "Confirm overwriting file") if (answer != JOptionPane.YES_OPTION) { close() return } } /* copy the file, then edit the Exif in the copy */ useWaitCursor() swingWorker<Boolean> { inBackground { try { FileInputStream(washing).use { source -> FileOutputStream(newFile).use { target -> source.copyTo(target) } } val image = Image(newFile.canonicalPath) val meta = image.metadata meta.keys.forEach { if (!Application.settingsDialog.whitelist.contains(it)) { meta.erase(it) } } image.store() true } catch (e: IOException) { LOGGER.log(Level.SEVERE, "unable to copy input", e) false } catch (e: Exiv2Exception) { LOGGER.log(Level.SEVERE, "unable to edit metadata", e) false } } whenDone { useNormalCursor() close() /* if all went well, show the Exif in the new file */ if (get()) { ShowDialog().show(newFile) } else { try { if (newFile.exists()) { newFile.delete() } } catch (e: IOException) { LOGGER.log(Level.SEVERE, "unable to delete", e) } JOptionPane.showMessageDialog(Application.mainFrame, "Error\nUnable to wash: ${washing.canonicalPath}\nTo: ${newFile.canonicalPath}", "Error", JOptionPane.ERROR_MESSAGE) } } } } private fun splitext(s: String): Pair<String, String> { val pos = s.lastIndexOf('.') if (pos == -1) { return Pair(s, "") } return Pair(s.substring(0, pos), s.substring(pos)) } init { defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE /* delete if reusing */ title = "Untitled" contentPane.apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) add(Box(BoxLayout.Y_AXIS).apply { alignmentX = Box.CENTER_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, BW, BW) add(JScrollPane(myTable).apply { alignmentX = JScrollPane.LEFT_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, BW, BW) verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS preferredSize = Dimension(WIDTH, HEIGHT) background = Application.mainFrame.background }) add(selectAll.apply { alignmentX = JCheckBox.LEFT_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, 0, BW) }) }) add(Box(BoxLayout.X_AXIS).apply { alignmentX = Box.CENTER_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, BW2, BW) add(resetButton) add(Box.createHorizontalGlue()) add(cancelButton) add(Box.createHorizontalStrut(BW2)) add(washButton) }) } pack() } }