Newer
Older
jrex / src / main / java / jrex / ui / MainFrame.java
package jrex.ui;

import jrex.ui.model.MutableTableModel;
import jrex.ui.model.PivotTableModel;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Table;
import com.google.gson.Gson;
import com.ibm.icu.text.CaseMap;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Stream;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import javax.swing.text.DefaultEditorKit;
import jrex.Expressions;
import jrex.ui.model.SimpleListModel;

/**
 * Primary user interface frame.
 *
 * @author Mark George <mark.george@otago.ac.nz>
 */
public class MainFrame extends javax.swing.JFrame {

   private final Color defaultTextColor;
   private PivotTableModel original;

   public MainFrame() {
      initComponents();
      splitPane.setResizeWeight(0.66);
      spnColumns.setModel(new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 1));

      JPopupMenu rightClickMenu = new JPopupMenu();

      Action cut = new DefaultEditorKit.CutAction();
      cut.putValue(Action.NAME, "Cut");
      cut.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control X"));
      rightClickMenu.add(cut);

      Action copy = new DefaultEditorKit.CopyAction();
      copy.putValue(Action.NAME, "Copy");
      copy.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control C"));
      rightClickMenu.add(copy);

      Action paste = new DefaultEditorKit.PasteAction();
      paste.putValue(Action.NAME, "Paste");
      paste.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control V"));
      rightClickMenu.add(paste);

      Action tab = new DefaultEditorKit.InsertTabAction();
      tab.putValue(Action.NAME, "Insert Tab");
      rightClickMenu.add(tab);

      txtInput.setComponentPopupMenu(rightClickMenu);
      txtOutput.setComponentPopupMenu(rightClickMenu);
      txtRegex.setComponentPopupMenu(rightClickMenu);
      txtFormat.setComponentPopupMenu(rightClickMenu);
      txtBlanks.setComponentPopupMenu(rightClickMenu);

      defaultTextColor = txtRegex.getBackground();
      pnlReplace.setVisible(false);
      pnlReplace.setComponents(txtInput, txtOutput);

      Icon folderIcon = desaturateIcon(UIManager.getIcon("FileView.directoryIcon"));
      Icon floppyIcon = desaturateIcon(UIManager.getIcon("FileView.floppyDriveIcon"));
      Icon fileIcon = desaturateIcon(UIManager.getIcon("FileView.fileIcon"));

      btnSave.setIcon(floppyIcon);
      btnSaveExpr.setIcon(floppyIcon);
      btnLoadExpr.setIcon(folderIcon);
      btnLoad.setIcon(folderIcon);
      btnClear.setIcon(fileIcon);
   }

   private Icon desaturateIcon(Icon icon) {
      int w = icon.getIconWidth();
      int h = icon.getIconHeight();
      GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
      GraphicsDevice gd = ge.getDefaultScreenDevice();
      GraphicsConfiguration gc = gd.getDefaultConfiguration();
      BufferedImage iconImage = gc.createCompatibleImage(w, h, Transparency.TRANSLUCENT);
      Graphics2D g2d = iconImage.createGraphics();
      icon.paintIcon(null, g2d, 0, 0);
      ColorConvertOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
      op.filter(iconImage, iconImage);
      return new ImageIcon(iconImage);
   }

   /**
    * This method is called from within the constructor to initialize the form.
    * WARNING: Do NOT modify this code. The content of this method is always
    * regenerated by the Form Editor.
    */
   @SuppressWarnings("unchecked")
   // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
   private void initComponents() {

      bgQuotedIn = new javax.swing.ButtonGroup();
      bgCase = new javax.swing.ButtonGroup();
      bgQuoted = new javax.swing.ButtonGroup();
      splitPane = new javax.swing.JSplitPane();
      pnlInput = new javax.swing.JPanel();
      btnClear = new javax.swing.JButton();
      btnLoad = new javax.swing.JButton();
      scrollInput = new javax.swing.JScrollPane();
      txtInput = new javax.swing.JTextArea();
      lblExpression = new javax.swing.JLabel();
      txtRegex = new javax.swing.JTextField();
      spnColumns = new javax.swing.JSpinner();
      btnTsvInput = new javax.swing.JButton();
      btnCsvInput = new javax.swing.JButton();
      btnMatch = new javax.swing.JButton();
      scrollTable = new javax.swing.JScrollPane();
      jTable = new javax.swing.JTable();
      cbReplace = new javax.swing.JCheckBox();
      btnPivotWideToLong = new javax.swing.JButton();
      btnPivotLongToWide = new javax.swing.JButton();
      btnTranspose = new javax.swing.JButton();
      lblColumns = new javax.swing.JLabel();
      btnSaveExpr = new javax.swing.JButton();
      btnLoadExpr = new javax.swing.JButton();
      cbGuess = new javax.swing.JCheckBox();
      cbHasHeadings = new javax.swing.JCheckBox();
      pnlQuoted = new javax.swing.JPanel();
      jLabel1 = new javax.swing.JLabel();
      rbDouble = new javax.swing.JRadioButton();
      rbSingle = new javax.swing.JRadioButton();
      jLabel3 = new javax.swing.JLabel();
      lblCase = new javax.swing.JLabel();
      pnlCase = new javax.swing.JPanel();
      cbTrim = new javax.swing.JCheckBox();
      cbBlanks = new javax.swing.JCheckBox();
      txtBlanks = new javax.swing.JTextField();
      cbStripQuotes = new javax.swing.JCheckBox();
      rbStripDouble = new javax.swing.JRadioButton();
      rbStringSingle = new javax.swing.JRadioButton();
      pnlOutput = new javax.swing.JPanel();
      lblFormat = new javax.swing.JLabel();
      txtFormat = new javax.swing.JTextField();
      btnFormat = new javax.swing.JButton();
      scrollOutput = new javax.swing.JScrollPane();
      txtOutput = new javax.swing.JTextArea();
      lblArgumentMarker = new javax.swing.JLabel();
      btnSwap = new javax.swing.JButton();
      btnCsvOutput = new javax.swing.JButton();
      btnTsvOutput = new javax.swing.JButton();
      txtMarker = new javax.swing.JTextField();
      btnSave = new javax.swing.JButton();
      rbUnique = new javax.swing.JCheckBox();
      cbIncludeHeadings = new javax.swing.JCheckBox();
      btnSql = new javax.swing.JButton();
      cbQuoteHeadings = new javax.swing.JCheckBox();
      pnlReplace = new jrex.ui.ReplacePanel();

      setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

      splitPane.setBorder(null);
      splitPane.setDividerLocation(400);
      splitPane.setDividerSize(15);
      splitPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT);
      splitPane.setResizeWeight(0.66);
      splitPane.setName("splitPane"); // NOI18N
      splitPane.setOneTouchExpandable(true);

      pnlInput.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Input", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, new java.awt.Font("Dialog", 0, 12))); // NOI18N
      pnlInput.setName("pnlInput"); // NOI18N

      btnClear.setText("Clear");
      btnClear.setName("btnClear"); // NOI18N
      btnClear.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnClearActionPerformed(evt);
         }
      });

      btnLoad.setText("Open");
      btnLoad.setName("btnLoad"); // NOI18N
      btnLoad.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnLoadActionPerformed(evt);
         }
      });

      scrollInput.setName("scrollInput"); // NOI18N

      txtInput.setColumns(20);
      txtInput.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
      txtInput.setRows(5);
      txtInput.setName("txtInput"); // NOI18N
      scrollInput.setViewportView(txtInput);

      lblExpression.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
      lblExpression.setText("Capture Expression: ");
      lblExpression.setName("lblExpression"); // NOI18N

      txtRegex.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
      txtRegex.setName("txtRegex"); // NOI18N
      txtRegex.setPreferredSize(new java.awt.Dimension(14, 25));
      txtRegex.addKeyListener(new java.awt.event.KeyAdapter() {
         public void keyPressed(java.awt.event.KeyEvent evt) {
            txtRegexKeyPressed(evt);
         }
      });

      spnColumns.setName("spnColumns"); // NOI18N
      spnColumns.addMouseWheelListener(new java.awt.event.MouseWheelListener() {
         public void mouseWheelMoved(java.awt.event.MouseWheelEvent evt) {
            spnColumnsMouseWheelMoved(evt);
         }
      });

      btnTsvInput.setText("TSV");
      btnTsvInput.setName("btnTsvInput"); // NOI18N
      btnTsvInput.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnTsvInputActionPerformed(evt);
         }
      });

      btnCsvInput.setText("CSV");
      btnCsvInput.setName("btnCsvInput"); // NOI18N
      btnCsvInput.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnCsvInputActionPerformed(evt);
         }
      });

      btnMatch.setMnemonic('c');
      btnMatch.setText("Capture");
      btnMatch.setName("btnMatch"); // NOI18N
      btnMatch.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnMatchActionPerformed(evt);
         }
      });

      scrollTable.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
      scrollTable.setName("scrollTable"); // NOI18N

      jTable.setName("jTable"); // NOI18N
      scrollTable.setViewportView(jTable);

      cbReplace.setText("Search/Replace ");
      cbReplace.setName("cbReplace"); // NOI18N
      cbReplace.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            cbReplaceActionPerformed(evt);
         }
      });

      btnPivotWideToLong.setText("Pivot Wide to Long");
      btnPivotWideToLong.setName("btnPivotWideToLong"); // NOI18N
      btnPivotWideToLong.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnPivotWideToLongActionPerformed(evt);
         }
      });

      btnPivotLongToWide.setText("Pivot Long to Wide");
      btnPivotLongToWide.setName("btnPivotLongToWide"); // NOI18N
      btnPivotLongToWide.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnPivotLongToWideActionPerformed(evt);
         }
      });

      btnTranspose.setText("Transpose");
      btnTranspose.setName("btnTranspose"); // NOI18N
      btnTranspose.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnTransposeActionPerformed(evt);
         }
      });

      lblColumns.setText("Columns: ");
      lblColumns.setName("lblColumns"); // NOI18N

      btnSaveExpr.setText("Save Expressions");
      btnSaveExpr.setName("btnSaveExpr"); // NOI18N
      btnSaveExpr.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnSaveExprActionPerformed(evt);
         }
      });

      btnLoadExpr.setText("Load Expressions");
      btnLoadExpr.setName("btnLoadExpr"); // NOI18N
      btnLoadExpr.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnLoadExprActionPerformed(evt);
         }
      });

      cbGuess.setSelected(true);
      cbGuess.setText("Guess?");
      cbGuess.setName("cbGuess"); // NOI18N

      cbHasHeadings.setSelected(true);
      cbHasHeadings.setText("Has Column Headings");
      cbHasHeadings.setName("cbHasHeadings"); // NOI18N
      cbHasHeadings.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            cbHasHeadingsActionPerformed(evt);
         }
      });

      pnlQuoted.setFocusable(false);
      pnlQuoted.setName("pnlQuoted"); // NOI18N
      pnlQuoted.setLayout(new java.awt.GridLayout(1, 1));

      jLabel1.setText("Quotes:");
      jLabel1.setName("jLabel1"); // NOI18N

      bgQuoted.add(rbDouble);
      rbDouble.setText("\"");
      rbDouble.setName("rbDouble"); // NOI18N

      bgQuoted.add(rbSingle);
      rbSingle.setSelected(true);
      rbSingle.setText("'");
      rbSingle.setName("rbSingle"); // NOI18N

      jLabel3.setText("Quoted:");
      jLabel3.setName("jLabel3"); // NOI18N

      lblCase.setText("Case:");
      lblCase.setName("lblCase"); // NOI18N

      pnlCase.setName("pnlCase"); // NOI18N
      pnlCase.setLayout(new java.awt.GridLayout(1, 1));

      cbTrim.setSelected(true);
      cbTrim.setText("Trim?");
      cbTrim.setName("cbTrim"); // NOI18N

      cbBlanks.setText("Replace blanks? ");
      cbBlanks.setName("cbBlanks"); // NOI18N

      txtBlanks.setText("null");
      txtBlanks.setName("txtBlanks"); // NOI18N

      cbStripQuotes.setText("Strip Quotes?");
      cbStripQuotes.setName("cbStripQuotes"); // NOI18N

      bgQuotedIn.add(rbStripDouble);
      rbStripDouble.setText("\"");
      rbStripDouble.setName("rbStripDouble"); // NOI18N

      bgQuotedIn.add(rbStringSingle);
      rbStringSingle.setSelected(true);
      rbStringSingle.setText("'");
      rbStringSingle.setName("rbStringSingle"); // NOI18N

      javax.swing.GroupLayout pnlInputLayout = new javax.swing.GroupLayout(pnlInput);
      pnlInput.setLayout(pnlInputLayout);
      pnlInputLayout.setHorizontalGroup(
         pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addComponent(scrollInput)
         .addGroup(pnlInputLayout.createSequentialGroup()
            .addComponent(btnLoad)
            .addGap(18, 18, 18)
            .addComponent(cbReplace)
            .addGap(18, 18, 18)
            .addComponent(btnSaveExpr)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(btnLoadExpr)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addComponent(btnClear))
         .addGroup(pnlInputLayout.createSequentialGroup()
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
               .addGroup(pnlInputLayout.createSequentialGroup()
                  .addComponent(btnPivotWideToLong)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(btnPivotLongToWide)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(btnTranspose))
               .addGroup(pnlInputLayout.createSequentialGroup()
                  .addComponent(jLabel1)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(rbSingle)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(rbDouble)
                  .addGap(18, 18, 18)
                  .addComponent(cbTrim)
                  .addGap(18, 18, 18)
                  .addComponent(cbBlanks)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(txtBlanks, javax.swing.GroupLayout.PREFERRED_SIZE, 62, javax.swing.GroupLayout.PREFERRED_SIZE))
               .addGroup(pnlInputLayout.createSequentialGroup()
                  .addComponent(lblColumns)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(spnColumns, javax.swing.GroupLayout.PREFERRED_SIZE, 46, javax.swing.GroupLayout.PREFERRED_SIZE)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(cbGuess)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(cbHasHeadings)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(cbStripQuotes)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(rbStringSingle)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(rbStripDouble)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(btnCsvInput)
                  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                  .addComponent(btnTsvInput)))
            .addContainerGap(92, Short.MAX_VALUE))
         .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnlInputLayout.createSequentialGroup()
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
               .addComponent(jLabel3)
               .addComponent(lblCase))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
               .addComponent(pnlQuoted, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
               .addComponent(pnlCase, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
               .addComponent(scrollTable, javax.swing.GroupLayout.Alignment.TRAILING)))
         .addGroup(pnlInputLayout.createSequentialGroup()
            .addComponent(lblExpression)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(txtRegex, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(btnMatch))
      );

      pnlInputLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {btnPivotLongToWide, btnPivotWideToLong, btnTranspose});

      pnlInputLayout.setVerticalGroup(
         pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addGroup(pnlInputLayout.createSequentialGroup()
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
               .addComponent(btnLoad, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
               .addComponent(btnClear, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
               .addComponent(cbReplace)
               .addComponent(btnSaveExpr)
               .addComponent(btnLoadExpr))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(scrollInput, javax.swing.GroupLayout.DEFAULT_SIZE, 34, Short.MAX_VALUE)
            .addGap(11, 11, 11)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
               .addComponent(lblColumns)
               .addComponent(spnColumns, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(btnCsvInput)
               .addComponent(btnTsvInput)
               .addComponent(cbGuess)
               .addComponent(cbHasHeadings)
               .addComponent(cbStripQuotes)
               .addComponent(rbStripDouble)
               .addComponent(rbStringSingle))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
               .addComponent(lblExpression)
               .addComponent(txtRegex, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(btnMatch))
            .addGap(6, 6, 6)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
               .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                  .addComponent(jLabel1)
                  .addComponent(rbDouble)
                  .addComponent(rbSingle)
                  .addComponent(cbTrim))
               .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
                  .addComponent(cbBlanks)
                  .addComponent(txtBlanks, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
               .addComponent(pnlQuoted, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
               .addComponent(pnlCase, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(lblCase, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(scrollTable, javax.swing.GroupLayout.DEFAULT_SIZE, 79, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlInputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
               .addComponent(btnPivotWideToLong)
               .addComponent(btnPivotLongToWide)
               .addComponent(btnTranspose)))
      );

      splitPane.setTopComponent(pnlInput);

      pnlOutput.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Output", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, new java.awt.Font("Dialog", 0, 12))); // NOI18N
      pnlOutput.setName("pnlOutput"); // NOI18N

      lblFormat.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
      lblFormat.setText("Format Expression: ");
      lblFormat.setName("lblFormat"); // NOI18N

      txtFormat.setName("txtFormat"); // NOI18N

      btnFormat.setMnemonic('f');
      btnFormat.setText("Format");
      btnFormat.setName("btnFormat"); // NOI18N
      btnFormat.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnFormatActionPerformed(evt);
         }
      });

      scrollOutput.setName("scrollOutput"); // NOI18N

      txtOutput.setColumns(20);
      txtOutput.setFont(new java.awt.Font("Monospaced", 0, 12)); // NOI18N
      txtOutput.setRows(5);
      txtOutput.setName("txtOutput"); // NOI18N
      scrollOutput.setViewportView(txtOutput);

      lblArgumentMarker.setText("Column Prefix: ");
      lblArgumentMarker.setName("lblArgumentMarker"); // NOI18N

      btnSwap.setMnemonic('o');
      btnSwap.setText("Output to Input");
      btnSwap.setName("btnSwap"); // NOI18N
      btnSwap.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnSwapActionPerformed(evt);
         }
      });

      btnCsvOutput.setText("CSV");
      btnCsvOutput.setName("btnCsvOutput"); // NOI18N
      btnCsvOutput.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnCsvOutputActionPerformed(evt);
         }
      });

      btnTsvOutput.setText("TSV");
      btnTsvOutput.setName("btnTsvOutput"); // NOI18N
      btnTsvOutput.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnTsvOutputActionPerformed(evt);
         }
      });

      txtMarker.setText("?");
      txtMarker.setName("txtMarker"); // NOI18N

      btnSave.setText("Save");
      btnSave.setName("btnSave"); // NOI18N
      btnSave.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnSaveActionPerformed(evt);
         }
      });

      rbUnique.setText("Make Unique?");
      rbUnique.setName("rbUnique"); // NOI18N

      cbIncludeHeadings.setSelected(true);
      cbIncludeHeadings.setText("Include Column Headings?");
      cbIncludeHeadings.setName("cbIncludeHeadings"); // NOI18N

      btnSql.setText("SQL");
      btnSql.setName("btnSql"); // NOI18N
      btnSql.addActionListener(new java.awt.event.ActionListener() {
         public void actionPerformed(java.awt.event.ActionEvent evt) {
            btnSqlActionPerformed(evt);
         }
      });

      cbQuoteHeadings.setText("Quote Headings?");
      cbQuoteHeadings.setName("cbQuoteHeadings"); // NOI18N

      javax.swing.GroupLayout pnlOutputLayout = new javax.swing.GroupLayout(pnlOutput);
      pnlOutput.setLayout(pnlOutputLayout);
      pnlOutputLayout.setHorizontalGroup(
         pnlOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addComponent(scrollOutput)
         .addGroup(pnlOutputLayout.createSequentialGroup()
            .addComponent(lblFormat)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(txtFormat)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(btnFormat))
         .addGroup(pnlOutputLayout.createSequentialGroup()
            .addComponent(lblArgumentMarker)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(txtMarker, javax.swing.GroupLayout.PREFERRED_SIZE, 32, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addGap(18, 18, 18)
            .addComponent(btnCsvOutput)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(btnTsvOutput, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(btnSql)
            .addGap(18, 18, 18)
            .addComponent(cbIncludeHeadings)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(cbQuoteHeadings)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 39, Short.MAX_VALUE)
            .addComponent(btnSave))
         .addGroup(pnlOutputLayout.createSequentialGroup()
            .addComponent(rbUnique)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addComponent(btnSwap))
      );

      pnlOutputLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {btnCsvOutput, btnTsvOutput});

      pnlOutputLayout.setVerticalGroup(
         pnlOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addGroup(pnlOutputLayout.createSequentialGroup()
            .addGroup(pnlOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
               .addComponent(lblArgumentMarker)
               .addComponent(txtMarker, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(btnCsvOutput)
               .addComponent(btnTsvOutput)
               .addComponent(btnSave)
               .addComponent(cbIncludeHeadings)
               .addComponent(btnSql)
               .addComponent(cbQuoteHeadings))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
               .addComponent(btnSwap)
               .addComponent(rbUnique))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addGroup(pnlOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
               .addComponent(lblFormat)
               .addComponent(txtFormat, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
               .addComponent(btnFormat))
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(scrollOutput, javax.swing.GroupLayout.DEFAULT_SIZE, 132, Short.MAX_VALUE)
            .addContainerGap())
      );

      splitPane.setRightComponent(pnlOutput);

      pnlReplace.setName("pnlReplace"); // NOI18N

      javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
      getContentPane().setLayout(layout);
      layout.setHorizontalGroup(
         layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addGroup(layout.createSequentialGroup()
            .addContainerGap()
            .addComponent(splitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 772, Short.MAX_VALUE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(pnlReplace, javax.swing.GroupLayout.DEFAULT_SIZE, 286, Short.MAX_VALUE))
      );
      layout.setVerticalGroup(
         layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
         .addGroup(layout.createSequentialGroup()
            .addContainerGap()
            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
               .addComponent(pnlReplace, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
               .addComponent(splitPane))
            .addContainerGap())
      );

      pack();
   }// </editor-fold>//GEN-END:initComponents

   private void match() {
      jTable.setRowSorter(null);

      txtOutput.setText("");
      String regex = txtRegex.getText();
      String text = txtInput.getText();

      String[] input = null;

      try {

         Pattern pattern = Pattern.compile(regex);

         int flagsMask = pattern.flags();

         StringBuilder flags = new StringBuilder();

         boolean dotall = false;

         // work out which flags are being used
         if ((flagsMask & Pattern.DOTALL) == Pattern.DOTALL) {
            flags.append("DOTALL ");
            dotall = true;
         }

         if ((flagsMask & Pattern.MULTILINE) == Pattern.MULTILINE) {
            flags.append("MULTILINE ");
         }

         if ((flagsMask & Pattern.CASE_INSENSITIVE) == Pattern.CASE_INSENSITIVE) {
            flags.append("CASE_INSENSITIVE ");
         }

         PivotTableModel model = new PivotTableModel();
         jTable.setModel(model);

         int numMatches = 0;

         // it DOTALL mode is on then grab the entire text rather then seperating by line
         if (dotall) {
            input = new String[1];
            input[0] = text;
         } else {
            input = text.split("\n");
         }

         for (String line : input) {
            Matcher matcher = pattern.matcher(line);
            while (matcher.find()) {

               // group() matches the entire line if there was a match.
               // This if-statment is to suppress extra empty groups from being
               // produced if the regex matches the entire line
               if (matcher.group().length() != line.length()) {
                  continue;
               }

               numMatches++;

               txtOutput.append(matcher.group(0) + "\n");

               String[] groups = new String[matcher.groupCount()];
               for (int j = 1; j <= matcher.groupCount(); j++) {
                  String s = matcher.group(j);

                  if (cbTrim.isSelected()) {
                     s = s.trim();
                  }

                  if (cbBlanks.isSelected() && s.isEmpty()) {
                     s = txtBlanks.getText();
                  }

                  groups[j - 1] = s;
               }

               if (cbHasHeadings.isSelected() && numMatches == 1) {
                  model.setColumnHeadings(groups);
               } else {
                  model.addRow(groups);
               }
            }
         }

         model.fireTableStructureChanged();
         TableRowSorter<PivotTableModel> sorter = new TableRowSorter<>();
         jTable.setRowSorter(sorter);
         sorter.setModel(model);

         this.original = new PivotTableModel();

         for (Table.Cell<Integer, Integer, String> cell : model.getTable().cellSet()) {
            this.original.getTable().put(cell.getRowKey(), cell.getColumnKey(), cell.getValue());
         }

         txtOutput.append("\nMatched " + numMatches + " line(s) out of "
                 + input.length + " line(s) of input.");

         if (!flags.toString().isEmpty()) {
            txtOutput.append("\n\nFlags: " + flags.toString().trim());
         }

      } catch (PatternSyntaxException ex) {
         txtOutput.setText("");
         txtOutput.append(ex.getMessage());
         txtRegex.setBackground(new Color(1f, 0.3f, 0.3f));
      }

      int colCount = jTable.getColumnCount();

      pnlQuoted.removeAll();
      pnlCase.removeAll();

      for (int i = 0; i < colCount; i++) {
         JCheckBox cbQuote = new JCheckBox();
         cbQuote.setHorizontalAlignment(SwingConstants.CENTER);
         cbQuote.setName(String.valueOf(i));

         cbQuote.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
               quoteCheckBoxClicked(evt);
            }
         });

         pnlQuoted.add(cbQuote);

         JComboBox cmbCase = new JComboBox();
         cmbCase.setName(String.valueOf(i));
         SimpleListModel mdlCombo = new SimpleListModel();
         mdlCombo.addElement("Unchanged");
         mdlCombo.addElement("Lower Case");
         mdlCombo.addElement("Upper Case");
         mdlCombo.addElement("Sentence Case");
         mdlCombo.addElement("Title Case");
         mdlCombo.addElement("Surname Case");

         cmbCase.setModel(mdlCombo);
         cmbCase.setSelectedIndex(0);

         cmbCase.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
               caseComboClicked(evt);
            }
         });

         pnlCase.add(cmbCase);

      }

      pack();
   }

   private void caseComboClicked(ActionEvent evt) {
      JComboBox cmbCase = (JComboBox) evt.getSource();
      String selectedCase = (String) cmbCase.getSelectedItem();

      int columnIdx = Integer.valueOf(cmbCase.getName());
      PivotTableModel model = (PivotTableModel) jTable.getModel();
      Map<Integer, String> column = model.getTable().column(columnIdx);

      column.keySet().forEach(key -> {
         String value = column.get(key);

         switch (selectedCase) {
            case "Upper Case":
               value = CaseMap.toUpper().apply(Locale.getDefault(), value);
               break;
            case "Lower Case":
               value = CaseMap.toLower().apply(Locale.getDefault(), value);
               break;
            case "Sentence Case":
               value = CaseMap.Lower.toTitle().sentences().apply(Locale.getDefault(), null, value);
               break;
            case "Title Case":
               value = CaseMap.toTitle().apply(Locale.getDefault(), null, value);
               break;
            case "Surname Case":
               value = surnameCase(value);
               break;
            case "Unchanged":
               value = original.getTable().get(key, columnIdx);
               break;
         }

         column.put(key, value);

      });

      model.fireTableStructureChanged();

   }

   private void quoteCheckBoxClicked(ActionEvent evt) {
      JCheckBox cb = (JCheckBox) evt.getSource();

      int columnIdx = Integer.valueOf(cb.getName());
      PivotTableModel model = (PivotTableModel) jTable.getModel();
      Map<Integer, String> column = model.getTable().column(columnIdx);
      column.keySet().forEach(key -> {
         String value = column.get(key);

         String quote = rbDouble.isSelected() ? "\"" : "'";
         if (cb.isSelected() && !value.equals(txtBlanks.getText())) {
            value = quote + value + quote;
            column.put(key, value);
         } else {
            value = value.replaceAll(quote + "(.*)" + quote, "$1");
            column.put(key, value);
         }
      });

      model.fireTableStructureChanged();
   }

   private void format() {
      if (jTable.getColumnCount() == 0) {
         return;
      }

      txtOutput.setText("");

      if (cbIncludeHeadings.isSelected()) {
         PivotTableModel model = (PivotTableModel) jTable.getModel();
         List<String> headings = model.getHeadings();
         if (cbQuoteHeadings.isSelected()) {
            String quote = rbSingle.isSelected() ? "'" : "\"";
            List<String> quoted = new ArrayList<>(headings.size());
            headings.forEach(heading -> quoted.add(quote + heading + quote));
            txtOutput.setText(formatRow(quoted) + "\n");
         } else {
            txtOutput.setText(formatRow(headings) + "\n");
         }
      }

      for (int row = 0; row < jTable.getRowCount(); row++) {

         List<String> rowData = new ArrayList<>(jTable.getColumnCount());

         for (int col = 0; col < jTable.getColumnCount(); col++) {
            rowData.add((String) jTable.getValueAt(row, col));
         }

         String formatted = formatRow(rowData);
         txtOutput.append(formatted + "\n");
      }

      if (rbUnique.isSelected()) {

         String output = txtOutput.getText();
         txtOutput.setText("");

         Stream.of(output.split("\n"))
                 .distinct()
                 .forEach(line -> txtOutput.append(line + "\n"));
      }

   }

   private String surnameCase(String string) {
      // See:  https://stackoverflow.com/a/15738441

      final String DELIMITERS = " -'";

      StringBuilder sb = new StringBuilder();
      boolean capNext = true;

      for (char c : string.toCharArray()) {
         c = (capNext)
                 ? Character.toTitleCase(c)
                 : Character.toLowerCase(c);
         sb.append(c);
         capNext = (DELIMITERS.indexOf((int) c) >= 0); // explicit cast not needed
      }
      return sb.toString();

   }

   private String formatRow(List<String> rowData) {
      String[] values = new String[rowData.size()];

      String marker = txtMarker.getText();

      String formatter = this.txtFormat.getText();

      // escape meta-charachters
      switch (marker) {
         case "?":
         case "$":
         case "*":
         case "|":
         case "+":
         case "{":
         case "(":
         case ")":
         case "^":
         case "[":
         case "]":
         case "\\":
            marker = "\\" + marker;
      }

      PivotTableModel model = (PivotTableModel) jTable.getModel();
      List<String> headings = model.getHeadings();
      for (String heading : headings) {
         formatter = formatter.replaceAll(marker + heading, marker + model.getHeadingIndex(heading));
      }

      formatter = formatter.replaceAll(marker + "(\\d*)", "%$1\\$s");

      for (int col = 0; col < rowData.size(); col++) {
         values[col] = rowData.get(col);
      }

      return String.format(formatter, (Object[]) values);
   }

   private void transpose() {
      PivotTableModel model = (PivotTableModel) jTable.getModel();
      model.transpose();
   }

   private void pivotWideToLong() {
      if (jTable.getColumnCount() == 0) {
         return;
      }

      PivotTableModel model = (PivotTableModel) jTable.getModel();
      ListMultimap<String, String> columns = new WideToLongPivotDialog(this).mapColumns(model);
      if (columns != null) {
         model.pivotWideToLong(columns.get("preserved"), columns.get("values"));
      }
   }

   private void pivotLongToWide() throws NonUniqueKeyException {
      PivotTableModel model = (PivotTableModel) jTable.getModel();
      ListMultimap<String, String> columns = new LongToWidePivotDialog(this).mapColumns(model);
      if (columns != null) {
         model.pivotLongToWide(columns.get("repeating"), columns.get("header").get(0), columns.get("value").get(0));
      }
   }

   private void csv() {
      Integer cols = (Integer) spnColumns.getValue();
      Boolean quoted = cbStripQuotes.isSelected();
      Boolean isSingleQuotes = rbStringSingle.isSelected();

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < cols - 1; i++) {
         if (quoted && isSingleQuotes) {
            sb.append("'(.*?)',");
         } else if (quoted && !isSingleQuotes) {
            sb.append("\"(.*?)\",");
         } else {
            sb.append("(.*?),");
         }
      }
      if (quoted && isSingleQuotes) {
         sb.append("'(.*)'");
      } else if (quoted && !isSingleQuotes) {
         sb.append("\"(.*)\"");
      } else {
         sb.append("(.*)");
      }
      txtRegex.setText(sb.toString());
   }

   private void tsv() {
      Integer cols = (Integer) spnColumns.getValue();
      Boolean quoted = cbStripQuotes.isSelected();
      Boolean isSingleQuotes = rbStringSingle.isSelected();

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < cols - 1; i++) {
         if (quoted && isSingleQuotes) {
            sb.append("'(.*?)'\\t");
         } else if (quoted && !isSingleQuotes) {
            sb.append("\"(.*?)\"\\t");
         } else {
            sb.append("(.*?)\\t");
         }
      }
      if (quoted && isSingleQuotes) {
         sb.append("'(.*)'");
      } else if (quoted && !isSingleQuotes) {
         sb.append("\"(.*)\"");
      } else {
         sb.append("(.*)");
      }
      txtRegex.setText(sb.toString());
   }

   private void load() {
      JFileChooser jfc = new JFileChooser();
      if (jfc.showOpenDialog(MainFrame.this) == JFileChooser.APPROVE_OPTION) {
         try {
            String data = readFile(jfc.getSelectedFile());

            txtInput.setText(data);

         } catch (Exception ex) {
            JOptionPane.showMessageDialog(this, "Error", ex.getMessage(), JOptionPane.ERROR_MESSAGE);
         }
      }
   }

   private void save() {
      JFileChooser jfc = new JFileChooser();
      int result = jfc.showSaveDialog(this);
      if (result == JFileChooser.APPROVE_OPTION) {
         File f = jfc.getSelectedFile();
         if (f.exists()) {
            int confirm = JOptionPane.showConfirmDialog(this, "Overwrite existing file?");
            if (confirm == JOptionPane.YES_OPTION) {
               try (BufferedWriter writer = new BufferedWriter(new FileWriter(f))) {
                  writer.write(txtOutput.getText());
               } catch (Exception e) {
                  JOptionPane.showMessageDialog(this, "Error", e.getMessage(), JOptionPane.ERROR_MESSAGE);
               }
            }
         } else {
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(f))) {
               writer.write(txtOutput.getText());
            } catch (Exception e) {
               JOptionPane.showMessageDialog(this, "Error", e.getMessage(), JOptionPane.ERROR_MESSAGE);
            }
         }
      }
   }

   public String readFile(File file) throws Exception {
      StringBuilder fileContents;
      try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
         @SuppressWarnings("UnusedAssignment")
         String line = null;
         fileContents = new StringBuilder();
         while ((line = reader.readLine()) != null) {
            fileContents.append(line);
            fileContents.append(System.getProperty("line.separator"));
         }
      }
      return fileContents.toString();
   }

   private void btnFormatActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnFormatActionPerformed
      format();
   }//GEN-LAST:event_btnFormatActionPerformed

   private void btnSwapActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSwapActionPerformed
      txtInput.setText(txtOutput.getText());
   }//GEN-LAST:event_btnSwapActionPerformed

   private void btnCsvOutputActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnCsvOutputActionPerformed
      if (jTable.getRowCount() > 0) {
         cbIncludeHeadings.setSelected(true);
         TableModel model = jTable.getModel();
         String marker = txtMarker.getText();

         Integer cols = (Integer) jTable.getColumnCount();

         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < cols; i++) {
            sb.append(marker).append(model.getColumnName(i)).append(",");
         }

         String format = sb.toString();

         txtFormat.setText(format.substring(0, format.length() - 1));

         format();
      }
   }//GEN-LAST:event_btnCsvOutputActionPerformed

   private void btnTsvOutputActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnTsvOutputActionPerformed
      if (jTable.getRowCount() > 0) {
         cbIncludeHeadings.setSelected(true);
         TableModel model = jTable.getModel();
         String marker = txtMarker.getText();

         Integer cols = (Integer) jTable.getColumnCount();

         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < cols; i++) {
            sb.append(marker).append(model.getColumnName(i)).append("\t");
         }

         String format = sb.toString();

         txtFormat.setText(format.substring(0, format.length() - 1));

         format();
      }
   }//GEN-LAST:event_btnTsvOutputActionPerformed

   private void btnSaveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSaveActionPerformed
      save();
   }//GEN-LAST:event_btnSaveActionPerformed

   private void btnSqlActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSqlActionPerformed
      if (jTable.getColumnCount() == 0) {
         return;
      }

      cbIncludeHeadings.setSelected(false);
      SqlDialog sqlDialog = new SqlDialog(this, true, (PivotTableModel) jTable.getModel());
      sqlDialog.setVisible(true);
      txtFormat.setText(sqlDialog.getSql());
      format();
   }//GEN-LAST:event_btnSqlActionPerformed

   private void cbHasHeadingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbHasHeadingsActionPerformed
      // TODO add your handling code here:
   }//GEN-LAST:event_cbHasHeadingsActionPerformed

   private void btnLoadExprActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnLoadExprActionPerformed
      String json = null;

      JFileChooser jfc = new JFileChooser();
      if (jfc.showOpenDialog(MainFrame.this) == JFileChooser.APPROVE_OPTION) {
         try {
            json = readFile(jfc.getSelectedFile());
         } catch (Exception ex) {
            JOptionPane.showMessageDialog(this, "Error", ex.getMessage(), JOptionPane.ERROR_MESSAGE);
         }

         Expressions exp = new Gson().fromJson(json, Expressions.class);
         txtBlanks.setText(exp.getBlanks());
         cbBlanks.setSelected(exp.getBlanksEnabled());
         txtRegex.setText(exp.getCapture());
         txtFormat.setText(exp.getFormat());
         txtMarker.setText(exp.getPrefix());

         List<List<String>> replacements = exp.getReplacements();
         MutableTableModel model = pnlReplace.getModel();
         model.getTable().clear();
         for (List<String> replacement : replacements) {
            model.addRow(replacement);
         }
      }

   }//GEN-LAST:event_btnLoadExprActionPerformed

   private void btnSaveExprActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnSaveExprActionPerformed

      Expressions exp = new Expressions();
      exp.setBlanks(txtBlanks.getText());
      exp.setBlanksEnabled(cbBlanks.isSelected());
      exp.setCapture(txtRegex.getText());
      exp.setFormat(txtFormat.getText());
      exp.setPrefix(txtMarker.getText());
      exp.setReplacements(pnlReplace.getModel().getRows());
      String json = new Gson().toJson(exp);

      JFileChooser jfc = new JFileChooser();
      int result = jfc.showSaveDialog(this);
      if (result == JFileChooser.APPROVE_OPTION) {
         File f = jfc.getSelectedFile();
         if (f.exists()) {
            int confirm = JOptionPane.showConfirmDialog(this, "Overwrite existing file?");
            if (confirm == JOptionPane.YES_OPTION) {
               try (BufferedWriter writer = new BufferedWriter(new FileWriter(f))) {
                  writer.write(json);
               } catch (Exception e) {
                  JOptionPane.showMessageDialog(this, "Error", e.getMessage(), JOptionPane.ERROR_MESSAGE);
               }
            }
         } else {
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(f))) {
               writer.write(json);
            } catch (Exception e) {
               JOptionPane.showMessageDialog(this, "Error", e.getMessage(), JOptionPane.ERROR_MESSAGE);
            }
         }
      }
   }//GEN-LAST:event_btnSaveExprActionPerformed

   private void btnTransposeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnTransposeActionPerformed
      if (jTable.getColumnCount() == 0) {
         return;
      }

      if (cbHasHeadings.isSelected()) {
         JOptionPane.showMessageDialog(this, "Turn off column headings and recapture before transposing.");
         return;
      }
      transpose();
   }//GEN-LAST:event_btnTransposeActionPerformed

   private void btnPivotLongToWideActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnPivotLongToWideActionPerformed
      if (jTable.getColumnCount() == 0) {
         return;
      }

      try {
         pivotLongToWide();
      } catch (NonUniqueKeyException e) {
         JOptionPane.showMessageDialog(this, e.getMessage(), "Duplicate Data Error", JOptionPane.ERROR_MESSAGE);
      }
   }//GEN-LAST:event_btnPivotLongToWideActionPerformed

   private void btnPivotWideToLongActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnPivotWideToLongActionPerformed
      pivotWideToLong();
   }//GEN-LAST:event_btnPivotWideToLongActionPerformed

   private void cbReplaceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbReplaceActionPerformed
      pnlReplace.setVisible(cbReplace.isSelected());
   }//GEN-LAST:event_cbReplaceActionPerformed

   private void btnMatchActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnMatchActionPerformed
      if (txtInput.getText().isEmpty()) {
         return;
      }

      match();
   }//GEN-LAST:event_btnMatchActionPerformed

   private void btnCsvInputActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnCsvInputActionPerformed
      if (txtInput.getText().isEmpty()) {
         return;
      }

      if (cbGuess.isSelected()) {
         String line = txtInput.getText().split("\n")[0];
         String commas = line.replaceAll("[^,]", "");
         spnColumns.setValue(commas.length() + 1);
      }
      csv();
      match();
   }//GEN-LAST:event_btnCsvInputActionPerformed

   private void btnTsvInputActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnTsvInputActionPerformed
      if (txtInput.getText().isEmpty()) {
         return;
      }

      if (cbGuess.isSelected()) {
         String line = txtInput.getText().split("\n")[0];
         String commas = line.replaceAll("[^\t]", "");
         spnColumns.setValue(commas.length() + 1);
      }
      tsv();
      match();
   }//GEN-LAST:event_btnTsvInputActionPerformed

   private void spnColumnsMouseWheelMoved(java.awt.event.MouseWheelEvent evt) {//GEN-FIRST:event_spnColumnsMouseWheelMoved
      Integer value = (Integer) spnColumns.getValue();
      value -= evt.getUnitsToScroll();
      spnColumns.setValue(value > 0 ? value : 0);
   }//GEN-LAST:event_spnColumnsMouseWheelMoved

   private void txtRegexKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_txtRegexKeyPressed
      // restore default color
      txtRegex.setBackground(defaultTextColor);
   }//GEN-LAST:event_txtRegexKeyPressed

   private void btnLoadActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnLoadActionPerformed
      load();
   }//GEN-LAST:event_btnLoadActionPerformed

   private void btnClearActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnClearActionPerformed
      txtInput.setText("");
   }//GEN-LAST:event_btnClearActionPerformed

   public String getPrefix() {
      return txtMarker.getText();
   }


   // Variables declaration - do not modify//GEN-BEGIN:variables
   private javax.swing.ButtonGroup bgCase;
   private javax.swing.ButtonGroup bgQuoted;
   private javax.swing.ButtonGroup bgQuotedIn;
   private javax.swing.JButton btnClear;
   private javax.swing.JButton btnCsvInput;
   private javax.swing.JButton btnCsvOutput;
   private javax.swing.JButton btnFormat;
   private javax.swing.JButton btnLoad;
   private javax.swing.JButton btnLoadExpr;
   private javax.swing.JButton btnMatch;
   private javax.swing.JButton btnPivotLongToWide;
   private javax.swing.JButton btnPivotWideToLong;
   private javax.swing.JButton btnSave;
   private javax.swing.JButton btnSaveExpr;
   private javax.swing.JButton btnSql;
   private javax.swing.JButton btnSwap;
   private javax.swing.JButton btnTranspose;
   private javax.swing.JButton btnTsvInput;
   private javax.swing.JButton btnTsvOutput;
   private javax.swing.JCheckBox cbBlanks;
   private javax.swing.JCheckBox cbGuess;
   private javax.swing.JCheckBox cbHasHeadings;
   private javax.swing.JCheckBox cbIncludeHeadings;
   private javax.swing.JCheckBox cbQuoteHeadings;
   private javax.swing.JCheckBox cbReplace;
   private javax.swing.JCheckBox cbStripQuotes;
   private javax.swing.JCheckBox cbTrim;
   private javax.swing.JLabel jLabel1;
   private javax.swing.JLabel jLabel3;
   private javax.swing.JTable jTable;
   private javax.swing.JLabel lblArgumentMarker;
   private javax.swing.JLabel lblCase;
   private javax.swing.JLabel lblColumns;
   private javax.swing.JLabel lblExpression;
   private javax.swing.JLabel lblFormat;
   private javax.swing.JPanel pnlCase;
   private javax.swing.JPanel pnlInput;
   private javax.swing.JPanel pnlOutput;
   private javax.swing.JPanel pnlQuoted;
   private jrex.ui.ReplacePanel pnlReplace;
   private javax.swing.JRadioButton rbDouble;
   private javax.swing.JRadioButton rbSingle;
   private javax.swing.JRadioButton rbStringSingle;
   private javax.swing.JRadioButton rbStripDouble;
   private javax.swing.JCheckBox rbUnique;
   private javax.swing.JScrollPane scrollInput;
   private javax.swing.JScrollPane scrollOutput;
   private javax.swing.JScrollPane scrollTable;
   private javax.swing.JSplitPane splitPane;
   private javax.swing.JSpinner spnColumns;
   private javax.swing.JTextField txtBlanks;
   private javax.swing.JTextField txtFormat;
   private javax.swing.JTextArea txtInput;
   private javax.swing.JTextField txtMarker;
   private javax.swing.JTextArea txtOutput;
   private javax.swing.JTextField txtRegex;
   // End of variables declaration//GEN-END:variables

}