JAL-3141 JAL-3056 backups regime option suppressed in JalviewJS
[jalview.git] / src / jalview / jbgui / GPreferences.java
index 9023864..361ba71 100755 (executable)
  */
 package jalview.jbgui;
 
+import jalview.bin.Cache;
 import jalview.fts.core.FTSDataColumnPreferences;
 import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
 import jalview.fts.service.pdb.PDBFTSRestClient;
+import jalview.gui.Desktop;
+import jalview.gui.JalviewBooleanRadioButtons;
+import jalview.gui.JvOptionPane;
 import jalview.gui.JvSwingUtils;
 import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.BackupFilenameParts;
+import jalview.io.BackupFiles;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -42,8 +49,11 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.FocusEvent;
 import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
@@ -54,13 +64,18 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JFileChooser;
 import javax.swing.JLabel;
+import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
+import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
 import javax.swing.SwingConstants;
 import javax.swing.border.Border;
 import javax.swing.border.EmptyBorder;
@@ -84,6 +99,9 @@ public class GPreferences extends JPanel
   private static final Font LABEL_FONT_ITALIC = JvSwingUtils
           .getLabelFont(false, true);
 
+  private static final Font LABEL_FONT_BOLD = JvSwingUtils
+          .getLabelFont(true, false);
+
   /*
    * Visual tab components
    */
@@ -231,6 +249,10 @@ public class GPreferences extends JPanel
    */
   protected JComboBox<Object> epsRendering = new JComboBox<>();
 
+  protected JComboBox<Object> htmlRendering = new JComboBox<>();
+
+  protected JComboBox<Object> svgRendering = new JComboBox<>();
+
   protected JLabel userIdWidthlabel = new JLabel();
 
   protected JCheckBox autoIdWidth = new JCheckBox();
@@ -264,12 +286,51 @@ public class GPreferences extends JPanel
 
   protected JCheckBox sortByTree = new JCheckBox();
 
-
   /*
    * Web Services tab
    */
   protected JPanel wsTab = new JPanel();
 
+  /*
+   * Backups tab components
+   * a lot of these are member variables instead of local variables only so that they
+   * can be enabled/disabled easily in one go
+   */
+
+  protected JCheckBox enableBackupFiles = new JCheckBox();
+
+  protected JPanel presetsPanel = new JPanel();
+
+  protected JButton revertButton = new JButton();
+
+  protected JComboBox<IntKeyStringValueEntry> backupfilesPresetsCombo = new JComboBox<>();
+
+  protected JPanel suffixPanel = new JPanel();
+
+  protected JPanel keepfilesPanel = new JPanel();
+
+  protected JPanel exampleFilesPanel = new JPanel();
+
+  protected JTextField suffixTemplate = new JTextField(null, 8);
+
+  protected JLabel suffixTemplateLabel = new JLabel();
+
+  protected JLabel suffixDigitsLabel = new JLabel();
+
+  protected JSpinner suffixDigitsSpinner = new JSpinner();
+
+  protected JalviewBooleanRadioButtons suffixReverse = new JalviewBooleanRadioButtons();
+
+  protected JalviewBooleanRadioButtons backupfilesKeepAll = new JalviewBooleanRadioButtons();
+
+  public JSpinner backupfilesRollMaxSpinner = new JSpinner();
+
+  protected JLabel oldBackupFilesLabel = new JLabel();
+
+  protected JalviewBooleanRadioButtons backupfilesConfirmDelete = new JalviewBooleanRadioButtons();
+
+  protected JTextArea backupfilesExampleLabel = new JTextArea();
+
   /**
    * Creates a new GPreferences object.
    */
@@ -291,7 +352,7 @@ public class GPreferences extends JPanel
    */
   private void jbInit() throws Exception
   {
-    final JTabbedPane tabbedPane = new JTabbedPane();
+    final JTabbedPane tabbedPane = jalview.jbgui.GDesktop.createTabbedPane();
     this.setLayout(new BorderLayout());
     JPanel okCancelPanel = initOkCancelPanel();
     this.add(tabbedPane, BorderLayout.CENTER);
@@ -312,6 +373,12 @@ public class GPreferences extends JPanel
     tabbedPane.add(initConnectionsTab(),
             MessageManager.getString("label.connections"));
 
+       if (!Platform.isJS()) 
+       {
+         tabbedPane.add(initBackupsTab(), 
+                       MessageManager.getString("label.backups"));
+       }
+
     tabbedPane.add(initLinksTab(),
             MessageManager.getString("label.urllinks"));
 
@@ -324,8 +391,11 @@ public class GPreferences extends JPanel
     /*
      * See WsPreferences for the real work of configuring this tab.
      */
-    wsTab.setLayout(new BorderLayout());
-    tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
+    if (!Platform.isJS())
+    {
+      wsTab.setLayout(new BorderLayout());
+      tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
+    }
 
     /*
      * Handler to validate a tab before leaving it - currently only for
@@ -383,7 +453,7 @@ public class GPreferences extends JPanel
   }
 
   /**
-   * Initialises the Output tabbed panel.
+   * Initialises the Output tab
    * 
    * @return
    */
@@ -391,18 +461,35 @@ public class GPreferences extends JPanel
   {
     JPanel outputTab = new JPanel();
     outputTab.setLayout(null);
-    JLabel epsLabel = new JLabel();
+
+    JLabel epsLabel = new JLabel(
+            MessageManager.formatMessage("label.rendering_style", "EPS"));
     epsLabel.setFont(LABEL_FONT);
     epsLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-    epsLabel.setText(MessageManager.getString("label.eps_rendering_style"));
-    epsLabel.setBounds(new Rectangle(9, 31, 140, 24));
+    epsLabel.setBounds(new Rectangle(9, 31, 160, 24));
     epsRendering.setFont(LABEL_FONT);
-    epsRendering.setBounds(new Rectangle(154, 34, 187, 21));
+    epsRendering.setBounds(new Rectangle(174, 34, 187, 21));
+    JLabel htmlLabel = new JLabel(
+            MessageManager.formatMessage("label.rendering_style", "HTML"));
+    htmlLabel.setFont(LABEL_FONT);
+    htmlLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    htmlLabel.setBounds(new Rectangle(9, 55, 160, 24));
+    htmlRendering.setFont(LABEL_FONT);
+    htmlRendering.setBounds(new Rectangle(174, 58, 187, 21));
+    JLabel svgLabel = new JLabel(
+            MessageManager.formatMessage("label.rendering_style", "SVG"));
+    svgLabel.setFont(LABEL_FONT);
+    svgLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    svgLabel.setBounds(new Rectangle(9, 79, 160, 24));
+    svgRendering.setFont(LABEL_FONT);
+    svgRendering.setBounds(new Rectangle(174, 82, 187, 21));
+
     JLabel jLabel1 = new JLabel();
     jLabel1.setFont(LABEL_FONT);
     jLabel1.setHorizontalAlignment(SwingConstants.CENTER);
     jLabel1.setText(MessageManager.getString("label.append_start_end"));
     jLabel1.setFont(LABEL_FONT);
+
     fastajv.setFont(LABEL_FONT);
     fastajv.setHorizontalAlignment(SwingConstants.LEFT);
     clustaljv.setText(MessageManager.getString("label.clustal") + "     ");
@@ -419,7 +506,7 @@ public class GPreferences extends JPanel
     TitledBorder titledBorder2 = new TitledBorder(
             MessageManager.getString("label.file_output"));
     jPanel11.setBorder(titledBorder2);
-    jPanel11.setBounds(new Rectangle(30, 72, 196, 182));
+    jPanel11.setBounds(new Rectangle(30, 120, 196, 182));
     GridLayout gridLayout3 = new GridLayout();
     jPanel11.setLayout(gridLayout3);
     gridLayout3.setRows(8);
@@ -438,7 +525,7 @@ public class GPreferences extends JPanel
             MessageManager.getString("label.automatically_set_id_width"));
     autoIdWidth.setToolTipText(JvSwingUtils.wrapTooltip(true, MessageManager
             .getString("label.adjusts_width_generated_eps_png")));
-    autoIdWidth.setBounds(new Rectangle(228, 96, 188, 23));
+    autoIdWidth.setBounds(new Rectangle(228, 144, 320, 23));
     autoIdWidth.addActionListener(new ActionListener()
     {
 
@@ -456,10 +543,10 @@ public class GPreferences extends JPanel
     userIdWidthlabel.setToolTipText(
             JvSwingUtils.wrapTooltip(true, MessageManager.getString(
                     "label.manually_specify_width_left_column")));
-    userIdWidthlabel.setBounds(new Rectangle(236, 120, 168, 23));
+    userIdWidthlabel.setBounds(new Rectangle(236, 168, 320, 23));
     userIdWidth.setFont(JvSwingUtils.getTextAreaFont());
     userIdWidth.setText("");
-    userIdWidth.setBounds(new Rectangle(232, 144, 84, 23));
+    userIdWidth.setBounds(new Rectangle(232, 192, 84, 23));
     userIdWidth.addActionListener(new ActionListener()
     {
 
@@ -472,10 +559,10 @@ public class GPreferences extends JPanel
     modellerOutput.setFont(LABEL_FONT);
     modellerOutput
             .setText(MessageManager.getString("label.use_modeller_output"));
-    modellerOutput.setBounds(new Rectangle(228, 226, 168, 23));
+    modellerOutput.setBounds(new Rectangle(228, 274, 320, 23));
     embbedBioJSON.setFont(LABEL_FONT);
     embbedBioJSON.setText(MessageManager.getString("label.embbed_biojson"));
-    embbedBioJSON.setBounds(new Rectangle(228, 200, 250, 23));
+    embbedBioJSON.setBounds(new Rectangle(228, 248, 250, 23));
 
     jPanel11.add(jLabel1);
     jPanel11.add(blcjv);
@@ -489,9 +576,19 @@ public class GPreferences extends JPanel
     outputTab.add(userIdWidth);
     outputTab.add(userIdWidthlabel);
     outputTab.add(modellerOutput);
-    outputTab.add(embbedBioJSON);
-    outputTab.add(epsLabel);
-    outputTab.add(epsRendering);
+    if (!Platform.isJS())
+    {
+      /*
+       * JalviewJS doesn't support Lineart option or SVG output
+       */
+      outputTab.add(embbedBioJSON);
+      outputTab.add(epsLabel);
+      outputTab.add(epsRendering);
+      outputTab.add(htmlLabel);
+      outputTab.add(htmlRendering);
+      outputTab.add(svgLabel);
+      outputTab.add(svgRendering);
+    }
     outputTab.add(jPanel11);
     return outputTab;
   }
@@ -1009,14 +1106,13 @@ public class GPreferences extends JPanel
         hiddenColour_actionPerformed(hiddenColour);
       }
     });
-    
+
     useLegacyGap = new JCheckBox(
             MessageManager.getString("label.ov_legacy_gap"));
     useLegacyGap.setFont(LABEL_FONT);
     useLegacyGap.setHorizontalAlignment(SwingConstants.LEFT);
     useLegacyGap.setVerticalTextPosition(SwingConstants.TOP);
-    gapLabel = new JLabel(
-            MessageManager.getString("label.gap_colour"));
+    gapLabel = new JLabel(MessageManager.getString("label.gap_colour"));
     gapLabel.setFont(LABEL_FONT);
     gapLabel.setHorizontalAlignment(SwingConstants.LEFT);
     gapLabel.setVerticalTextPosition(SwingConstants.TOP);
@@ -1252,6 +1348,17 @@ public class GPreferences extends JPanel
     docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120));
     structureTab.add(docFieldPref);
 
+    /*
+     * hide Chimera options in JalviewJS
+     */
+    if (Platform.isJS()) 
+    {
+      pathLabel.setVisible(false);
+      chimeraPath.setVisible(false);
+      viewerLabel.setVisible(false);
+      structViewer.setVisible(false);
+    }
+    
     return structureTab;
   }
 
@@ -1628,9 +1735,856 @@ public class GPreferences extends JPanel
     visualTab.add(fontNameCB);
     visualTab.add(fontSizeCB);
     visualTab.add(fontStyleCB);
+    
+    if (Platform.isJS())
+    {
+      startupCheckbox.setVisible(false);
+      startupFileTextfield.setVisible(false);
+    }
+    
     return visualTab;
   }
 
+  /**
+   * Load the saved Backups options EXCEPT "Enabled" and "Scheme"
+   */
+
+  protected void loadLastSavedBackupsOptions()
+  {
+    enableBackupFiles
+            .setSelected(Cache.getDefault(BackupFiles.ENABLED, !Platform.isJS()));
+    setComboIntStringKey(backupfilesPresetsCombo,
+            Cache.getDefault(BackupFiles.NS + "_PRESET", 1));
+    suffixTemplate.setText(Cache.getDefault(BackupFiles.SUFFIX,
+            ".bak" + BackupFiles.NUM_PLACEHOLDER));
+    suffixDigitsSpinner
+            .setValue(Cache.getDefault(BackupFiles.SUFFIX_DIGITS, 3));
+    suffixReverse.setSelected(
+            Cache.getDefault(BackupFiles.REVERSE_ORDER, false));
+    backupfilesKeepAll
+            .setSelected(Cache.getDefault(BackupFiles.NO_MAX, false));
+    backupfilesRollMaxSpinner
+            .setValue(Cache.getDefault(BackupFiles.ROLL_MAX, 3));
+    backupfilesConfirmDelete.setSelected(
+            Cache.getDefault(BackupFiles.CONFIRM_DELETE_OLD, true));
+
+    backupsOptionsSetEnabled();
+    updateBackupFilesExampleLabel();
+  }
+
+  private boolean warnAboutSuffixReverseChange()
+  {
+    boolean savedSuffixReverse = Cache.getDefault(BackupFiles.REVERSE_ORDER,
+            false);
+    int savedSuffixDigits = Cache.getDefault(BackupFiles.SUFFIX_DIGITS, 3);
+    String savedSuffixTemplate = Cache.getDefault(BackupFiles.SUFFIX,
+            ".bak" + BackupFiles.NUM_PLACEHOLDER);
+
+    boolean nowSuffixReverse = suffixReverse.isSelected();
+    int nowSuffixDigits = getSpinnerInt(suffixDigitsSpinner, 3);
+    String nowSuffixTemplate = suffixTemplate.getText();
+    return nowSuffixReverse != savedSuffixReverse
+            && nowSuffixDigits == savedSuffixDigits
+            && nowSuffixTemplate != null
+            && nowSuffixTemplate.equals(savedSuffixTemplate);
+  }
+
+  /**
+   * Initialises the Backups tabbed panel.
+   * 
+   * @return
+   */
+  private JPanel initBackupsTab()
+  {
+    JPanel backupsTab = new JPanel();
+    backupsTab.setBorder(new TitledBorder(
+            MessageManager.getString("label.backup_files")));
+    backupsTab.setLayout(new GridBagLayout());
+
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.weightx = 0.0;
+    gbc.weighty = 0.0;
+    gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+    gbc.fill = GridBagConstraints.NONE;
+
+    initBackupsTabPresetsPanel();
+    initBackupsTabSuffixPanel();
+    initBackupsTabKeepFilesPanel();
+    initBackupsTabFilenameExamplesPanel();
+
+    enableBackupFiles.setFont(LABEL_FONT_BOLD);
+    enableBackupFiles
+            .setText(MessageManager.getString("label.enable_backupfiles"));
+    enableBackupFiles.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        // enable other options only when the first is checked
+        backupsOptionsSetEnabled();
+      }
+    });
+
+
+    // enable checkbox 1 col
+    gbc.gridwidth = 1;
+    gbc.gridheight = 1;
+    gbc.gridx = 0;
+    gbc.gridy = 0; // row 0
+    backupsTab.add(enableBackupFiles, gbc);
+
+    // summary of scheme box (over two rows)
+    gbc.gridx = 1;
+    gbc.weightx = 0.0;
+    gbc.gridheight = 2;
+    gbc.anchor = GridBagConstraints.FIRST_LINE_END;
+    gbc.fill = GridBagConstraints.BOTH;
+    backupsTab.add(exampleFilesPanel, gbc);
+    gbc.gridheight = 1;
+    gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+    gbc.fill = GridBagConstraints.NONE;
+
+    // fill empty space on right
+    gbc.gridx++;
+    gbc.weightx = 1.0;
+    backupsTab.add(new JPanel(), gbc);
+
+    // schemes box
+    gbc.weightx = 0.0;
+    gbc.gridx = 0;
+    gbc.gridy++; // row 1
+    backupsTab.add(presetsPanel, gbc);
+
+    // gbc.anchor = GridBagConstraints.NORTHWEST;
+    // now using whole row
+    gbc.gridwidth = 2;
+    gbc.gridheight = 1;
+    // keep files box
+    gbc.gridx = 0;
+    gbc.gridy++; // row 2
+    backupsTab.add(keepfilesPanel, gbc);
+
+    // filename strategy box
+    gbc.gridy++; // row 3
+    backupsTab.add(suffixPanel, gbc);
+
+    // fill empty space
+    gbc.gridy++; // row 4
+    gbc.weighty = 1.0;
+    backupsTab.add(new JPanel(), gbc);
+
+    backupsOptionsSetEnabled();
+    return backupsTab;
+  }
+
+  protected static final int BACKUPFILESSCHEMECUSTOMISE = 0;
+
+  private static final IntKeyStringValueEntry[] backupfilesPresetEntries = {
+      new IntKeyStringValueEntry(1,
+              MessageManager.getString("label.default")),
+      new IntKeyStringValueEntry(2,
+              MessageManager.getString("label.single_file")),
+      new IntKeyStringValueEntry(3,
+              MessageManager.getString("label.keep_all_versions")),
+      new IntKeyStringValueEntry(4,
+              MessageManager.getString("label.rolled_backups")),
+      // ...
+      // IMPORTANT, keep "Custom" entry with key 0 (even though it appears last)
+      new IntKeyStringValueEntry(BACKUPFILESSCHEMECUSTOMISE,
+              MessageManager.getString("label.customise")) };
+
+  private static final Map<Integer, BackupFilesPresetEntry> backupfilesPresetEntriesValues = new HashMap<Integer, BackupFilesPresetEntry>()
+  {
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 125L;
+
+    {
+      put(1, new BackupFilesPresetEntry(
+              ".bak" + BackupFiles.NUM_PLACEHOLDER, 3, false, false, 3,
+              false));
+      put(2, new BackupFilesPresetEntry("~", 1, false, false, 1, false));
+      put(3, new BackupFilesPresetEntry(".v" + BackupFiles.NUM_PLACEHOLDER,
+              3, false, true, 10, true));
+      put(4, new BackupFilesPresetEntry(
+              "_bak." + BackupFiles.NUM_PLACEHOLDER, 1, true, false, 9,
+              false));
+    }
+  };
+
+  private JPanel initBackupsTabPresetsPanel()
+  {
+
+    String title = MessageManager.getString("label.schemes");
+    // TitledBorder tb = new TitledBorder(new EmptyBorder(0, 0, 0, 0), title);
+    // TitledBorder tb = new TitledBorder(title);
+    // tb.setTitleFont(LABEL_FONT);
+    // presetsPanel.setBorder(tb);
+
+    presetsPanel.setLayout(new GridBagLayout());
+
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.weightx = 0.0;
+    gbc.weighty = 0.0;
+    gbc.anchor = GridBagConstraints.BASELINE_LEADING;
+    gbc.fill = GridBagConstraints.NONE;
+    gbc.gridwidth = 1;
+    gbc.gridheight = 1;
+
+    // "Scheme: "
+    gbc.gridx = 0;
+    gbc.gridy = 0;
+    presetsPanel.add(new JLabel(title + ":"), gbc);
+
+    for (int i = 0; i < backupfilesPresetEntries.length; i++)
+    {
+      backupfilesPresetsCombo.addItem(backupfilesPresetEntries[i]);
+    }
+
+    // put "Previously saved scheme" item in italics (it's not really
+    // selectable, as such -- it deselects itself when selected) and
+    // "Customise" in bold
+    backupfilesPresetsCombo
+            .setRenderer(new BackupFilesPresetsComboBoxRenderer());
+    backupfilesPresetsCombo.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        backupsTabUpdatePresets();
+      }
+    });
+
+    // dropdown list of preset schemes
+    gbc.gridx = 1;
+    presetsPanel.add(backupfilesPresetsCombo, gbc);
+
+    revertButton.setText(MessageManager.getString("label.cancel_changes"));
+    revertButton.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        loadLastSavedBackupsOptions();
+      }
+
+    });
+    revertButton.setFont(LABEL_FONT);
+
+    // "Cancel changes" button (aligned with combo box above)
+    gbc.gridx = 1;
+    gbc.gridy++;
+    presetsPanel.add(revertButton, gbc);
+
+    return presetsPanel;
+  }
+
+  private JPanel initBackupsTabFilenameExamplesPanel()
+  {
+    String title = MessageManager
+            .getString("label.summary_of_backups_scheme");
+    TitledBorder tb = new TitledBorder(title);
+    exampleFilesPanel.setBorder(tb);
+    exampleFilesPanel.setLayout(new GridBagLayout());
+
+
+    backupfilesExampleLabel.setEditable(false);
+    backupfilesExampleLabel
+            .setBackground(exampleFilesPanel.getBackground());
+
+    updateBackupFilesExampleLabel();
+
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.weightx = 1.0;
+    gbc.weighty = 1.0;
+    gbc.fill = GridBagConstraints.NONE;
+    gbc.anchor = GridBagConstraints.FIRST_LINE_START;
+
+    exampleFilesPanel.add(backupfilesExampleLabel, gbc);
+    return exampleFilesPanel;
+  }
+
+  private void backupsTabUpdatePresets()
+  {
+    IntKeyStringValueEntry entry = (IntKeyStringValueEntry) backupfilesPresetsCombo
+            .getSelectedItem();
+    int key = entry.getKey();
+    String value = entry.getValue();
+
+    // BACKUPFILESSCHEMECUSTOMISE (==0) reserved for "Custom"
+    if (key != BACKUPFILESSCHEMECUSTOMISE)
+    {
+      if (backupfilesPresetEntriesValues.containsKey(key))
+      {
+        backupsSetOptions(backupfilesPresetEntriesValues.get(key));
+      }
+      else
+      {
+        System.out.println("Preset '" + value + "' not implemented");
+      }
+    }
+
+    backupfilesCustomOptionsSetEnabled();
+    updateBackupFilesExampleLabel();
+  }
+
+  protected int getComboIntStringKey(JComboBox<IntKeyStringValueEntry> c)
+  {
+    IntKeyStringValueEntry e = (IntKeyStringValueEntry) c.getSelectedItem();
+    return e != null ? e.getKey() : 0;
+  }
+
+  protected void setComboIntStringKey(JComboBox<IntKeyStringValueEntry> c,
+          int key)
+  {
+    for (int i = 0; i < c.getItemCount(); i++)
+    {
+      IntKeyStringValueEntry e = c.getItemAt(i);
+      if (e.getKey() == key)
+      {
+        c.setSelectedIndex(i);
+        break;
+      }
+    }
+    backupsTabUpdatePresets();
+  }
+
+  private JPanel initBackupsTabSuffixPanel()
+  {
+    suffixPanel.setBorder(new TitledBorder(
+            MessageManager.getString("label.backup_filename_strategy")));
+    suffixPanel.setLayout(new GridBagLayout());
+
+    suffixTemplateLabel
+            .setText(MessageManager.getString("label.append_to_filename"));
+    suffixTemplateLabel.setHorizontalAlignment(SwingConstants.LEFT);
+    suffixTemplateLabel.setFont(LABEL_FONT);
+
+    final String tooltip = JvSwingUtils.wrapTooltip(true,
+            MessageManager.getString("label.append_to_filename_tooltip"));
+    suffixTemplate.setToolTipText(tooltip);
+    suffixTemplate.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        updateBackupFilesExampleLabel();
+        backupfilesCustomOptionsSetEnabled();
+      }
+
+    });
+    KeyListener kl = new KeyListener()
+    {
+      @Override
+      public void keyReleased(KeyEvent e)
+      {
+        updateBackupFilesExampleLabel();
+        backupfilesCustomOptionsSetEnabled();
+      }
+
+      @Override
+      public void keyPressed(KeyEvent e)
+      {
+      }
+
+      // disable use of ':' or '/' or '\'
+      @Override
+      public void keyTyped(KeyEvent e)
+      {
+        char c = e.getKeyChar();
+        if (c == ':' || c == '/' || c == '\\')
+        {
+          // don't process ':' or '/' or '\'
+          e.consume();
+        }
+      }
+
+    };
+    suffixTemplate.addKeyListener(kl);
+
+    // digits spinner
+    suffixDigitsLabel
+            .setText(MessageManager.getString("label.index_digits"));
+    suffixDigitsLabel.setHorizontalAlignment(SwingConstants.LEFT);
+    suffixDigitsLabel.setFont(LABEL_FONT);
+    int defaultmin = 1;
+    int defaultmax = 6;
+    ChangeListener c = new ChangeListener()
+    {
+      @Override
+      public void stateChanged(ChangeEvent e)
+      {
+        updateBackupFilesExampleLabel();
+      }
+
+    };
+    setIntegerSpinner(suffixDigitsSpinner, defaultmin, defaultmax, 3, c);
+
+    suffixReverse.setLabels(MessageManager.getString("label.reverse_roll"),
+            MessageManager.getString("label.increment_index"));
+    suffixReverse.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        boolean okay = true;
+        if (warnAboutSuffixReverseChange())
+        {
+          // Warning popup
+          okay = confirmSuffixReverseChange();
+        }
+        if (okay)
+        {
+          updateBackupFilesExampleLabel();
+        }
+        else
+        {
+          boolean savedSuffixReverse = Cache
+                  .getDefault(BackupFiles.REVERSE_ORDER, false);
+          suffixReverse.setSelected(savedSuffixReverse);
+        }
+      }
+    });
+
+    GridBagConstraints sgbc = new GridBagConstraints();
+
+    // first row (template text box)
+    sgbc.anchor = GridBagConstraints.WEST;
+    sgbc.gridx = 0;
+    sgbc.gridy = 0;
+    sgbc.gridwidth = 1;
+    sgbc.gridheight = 1;
+    sgbc.weightx = 1.0;
+    sgbc.weighty = 0.0;
+    sgbc.fill = GridBagConstraints.NONE;
+    suffixPanel.add(suffixTemplateLabel, sgbc);
+
+    sgbc.gridx = 1;
+    sgbc.fill = GridBagConstraints.HORIZONTAL;
+    suffixPanel.add(suffixTemplate, sgbc);
+
+    // second row (number of digits spinner)
+    sgbc.gridy = 1;
+
+    sgbc.gridx = 0;
+    sgbc.fill = GridBagConstraints.NONE;
+    suffixPanel.add(suffixDigitsLabel, sgbc);
+
+    sgbc.gridx = 1;
+    sgbc.fill = GridBagConstraints.HORIZONTAL;
+    suffixPanel.add(suffixDigitsSpinner, sgbc);
+
+    // third row (forward order radio selection)
+    sgbc.gridx = 0;
+    sgbc.gridy = 2;
+    sgbc.gridwidth = GridBagConstraints.REMAINDER;
+    sgbc.fill = GridBagConstraints.HORIZONTAL;
+    suffixPanel.add(suffixReverse.getFalseButton(), sgbc);
+
+    // fourth row (reverse order radio selection)
+    sgbc.gridy = 3;
+    suffixPanel.add(suffixReverse.getTrueButton(), sgbc);
+    return suffixPanel;
+  }
+
+  private boolean confirmSuffixReverseChange()
+  {
+    boolean ret = false;
+    String warningMessage = MessageManager
+            .getString("label.warning_confirm_change_reverse");
+    int confirm = JvOptionPane.showConfirmDialog(Desktop.desktop,
+            warningMessage,
+            MessageManager.getString("label.change_increment_decrement"),
+            JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE);
+
+    ret = (confirm == JvOptionPane.YES_OPTION);
+    return ret;
+  }
+
+  private JPanel initBackupsTabKeepFilesPanel()
+  {
+    keepfilesPanel.setBorder(
+            new TitledBorder(MessageManager.getString("label.keep_files")));
+    keepfilesPanel.setLayout(new GridBagLayout());
+
+    backupfilesKeepAll.setLabels(
+            MessageManager.getString("label.keep_all_backup_files"),
+            MessageManager.getString(
+                    "label.keep_only_this_number_of_backup_files"));
+    backupfilesKeepAll.addTrueActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        updateBackupFilesExampleLabel();
+      }
+    });
+    backupfilesKeepAll.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        keepRollMaxOptionsEnabled();
+        updateBackupFilesExampleLabel();
+      }
+    });
+
+    ChangeListener c = new ChangeListener()
+    {
+      @Override
+      public void stateChanged(ChangeEvent e)
+      {
+        updateBackupFilesExampleLabel();
+      }
+
+    };
+    setIntegerSpinner(backupfilesRollMaxSpinner, 1, 999, 4, true, c);
+
+    backupfilesConfirmDelete.setLabels(
+            MessageManager.getString("label.always_ask"),
+            MessageManager.getString("label.auto_delete"));
+    // update the enabled section
+    keepRollMaxOptionsEnabled();
+
+    GridBagConstraints kgbc = new GridBagConstraints();
+
+    // first row (template text box)
+    kgbc.anchor = GridBagConstraints.WEST;
+    kgbc.gridx = 0;
+    kgbc.gridy = 0;
+    kgbc.gridwidth = GridBagConstraints.REMAINDER;
+    kgbc.gridheight = 1;
+    kgbc.weightx = 1.0;
+    kgbc.weighty = 0.0;
+    kgbc.fill = GridBagConstraints.HORIZONTAL;
+    keepfilesPanel.add(backupfilesKeepAll.getTrueButton(), kgbc);
+
+    // second row
+    kgbc.gridy = 1;
+
+    kgbc.gridx = 0;
+    kgbc.gridwidth = GridBagConstraints.RELATIVE;
+    keepfilesPanel.add(backupfilesKeepAll.getFalseButton(), kgbc);
+
+    kgbc.gridx = 1;
+    kgbc.gridwidth = GridBagConstraints.REMAINDER;
+    keepfilesPanel.add(backupfilesRollMaxSpinner, kgbc);
+
+    // third row (indented)
+    kgbc.gridy = 2;
+    kgbc.insets = new Insets(0, 20, 0, 0);
+
+    kgbc.gridx = 0;
+    kgbc.gridwidth = GridBagConstraints.REMAINDER;
+    kgbc.fill = GridBagConstraints.HORIZONTAL;
+    kgbc.weightx = 1.0;
+    /*
+    keepfilesPanel.add(backupfilesConfirmDelete.getTrueButton(), kgbc);
+    
+    // fourth row (indented)
+    kgbc.gridy = 3;
+    keepfilesPanel.add(backupfilesConfirmDelete.getFalseButton(), kgbc);
+    */
+
+    JPanel jp = new JPanel();
+    jp.setLayout(new FlowLayout());
+    oldBackupFilesLabel
+            .setText(MessageManager
+                    .getString("label.autodelete_old_backup_files"));
+    oldBackupFilesLabel.setFont(LABEL_FONT);
+    oldBackupFilesLabel.setHorizontalAlignment(SwingConstants.LEFT);
+    jp.add(oldBackupFilesLabel);
+    jp.add(backupfilesConfirmDelete.getTrueButton());
+    jp.add(backupfilesConfirmDelete.getFalseButton());
+    keepfilesPanel.add(jp, kgbc);
+
+    return keepfilesPanel;
+  }
+
+  protected void updateBackupFilesExampleLabel()
+  {
+    int exampleindex = 12;
+    String base = MessageManager.getString("label.filename") + ".fa";
+    if (base == null || base.length() == 0)
+    {
+      base = "file_name.fa";
+    }
+
+    boolean reverse = suffixReverse.isSelected();
+    boolean keepAll = backupfilesKeepAll.isSelected();
+    int rollMax = 4;
+    String suffix = suffixTemplate.getText();
+    int digits = 3;
+
+    backupfilesExampleLabel.setFont(LABEL_FONT_ITALIC);
+    if (suffix == null || suffix.length() == 0)
+    {
+      backupfilesExampleLabel
+              .setText(MessageManager.getString("label.no_backup_files"));
+      backupfilesExampleLabel.setFont(LABEL_FONT_BOLD);
+      return;
+    }
+
+    rollMax = getSpinnerInt(backupfilesRollMaxSpinner, 4);
+    rollMax = rollMax < 1 ? 1 : rollMax;
+
+    if (suffix.indexOf(BackupFiles.NUM_PLACEHOLDER) == -1)
+    {
+      rollMax = 1;
+    }
+
+    digits = getSpinnerInt(suffixDigitsSpinner, 3);
+    digits = digits < 1 ? 1 : digits;
+
+    int lowersurround = 2;
+    int uppersurround = 0;
+    StringBuilder exampleSB = new StringBuilder();
+    boolean firstLine = true;
+    if (reverse)
+    {
+
+      int min = 1;
+      int max = keepAll ? exampleindex : rollMax;
+      for (int index = min; index <= max; index++)
+      {
+        if (index == min + lowersurround && index < max - uppersurround - 1)
+        {
+          exampleSB.append("\n...");
+        }
+        else if (index > min + lowersurround && index < max - uppersurround)
+        {
+          // nothing
+        }
+        else
+        {
+          if (firstLine)
+          {
+            firstLine = false;
+          }
+          else
+          {
+            exampleSB.append("\n");
+          }
+          exampleSB.append(BackupFilenameParts.getBackupFilename(index,
+                  base, suffix, digits));
+          if (min == max)
+          {
+            // no extra text needed
+          }
+          else if (index == min)
+          {
+            String newest = MessageManager.getString("label.braced_newest");
+            if (newest != null && newest.length() > 0)
+            {
+              exampleSB.append(" " + newest);
+            }
+          }
+          else if (index == max)
+          {
+            String oldest = MessageManager.getString("label.braced_oldest");
+            if (oldest != null && oldest.length() > 0)
+            {
+              exampleSB.append(" " + oldest);
+            }
+          }
+        }
+      }
+    }
+    else
+    {
+
+      int min = (keepAll || exampleindex - rollMax < 0) ? 1
+              : exampleindex - rollMax + 1;
+      int max = exampleindex;
+
+      for (int index = min; index <= max; index++)
+      {
+
+        if (index == min + lowersurround && index < max - uppersurround - 1)
+        {
+          exampleSB.append("\n...");
+        }
+        else if (index > min + lowersurround && index < max - uppersurround)
+        {
+          // nothing
+        }
+        else
+        {
+          if (firstLine)
+          {
+            firstLine = false;
+          }
+          else
+          {
+            exampleSB.append("\n");
+          }
+          exampleSB.append(BackupFilenameParts.getBackupFilename(index,
+                  base, suffix, digits));
+          if (min == max)
+          {
+            // no extra text needed
+          }
+          else if (index == min)
+          {
+            String oldest = MessageManager.getString("label.braced_oldest");
+            if (oldest != null && oldest.length() > 0)
+            {
+              exampleSB.append(" " + oldest);
+            }
+          }
+          else if (index == max)
+          {
+            String newest = MessageManager.getString("label.braced_newest");
+            if (newest != null && newest.length() > 0)
+            {
+              exampleSB.append(" " + newest);
+            }
+          }
+        }
+      }
+
+    }
+
+    backupfilesExampleLabel.setText(exampleSB.toString());
+  }
+
+  protected void setIntegerSpinner(JSpinner s, int min, int max, int def,
+          boolean useExistingVal, ChangeListener c)
+  {
+    int i = def;
+    if (useExistingVal)
+    {
+      try
+      {
+        i = Integer.parseInt((String) s.getValue());
+      } catch (Exception e)
+      {
+        System.out.println(
+                "Exception casting the initial value of s.getValue()");
+      }
+    }
+
+    setIntegerSpinner(s, min, max, i, c);
+  }
+
+  protected void setIntegerSpinner(JSpinner s, int min, int max, int def,
+          ChangeListener c)
+  {
+    // integer spinner for number of digits
+    if (def > max)
+    {
+      max = def;
+    }
+    SpinnerModel sModel = new SpinnerNumberModel(def, min, max, 1);
+    s.setModel(sModel);
+
+    s.addChangeListener(c);
+
+  }
+
+  protected static int getSpinnerInt(JSpinner s, int def)
+  {
+    int i = def;
+    try
+    {
+      s.commitEdit();
+      i = (Integer) s.getValue();
+    } catch (Exception e)
+    {
+      System.out.println("Failed casting (Integer) JSpinner s.getValue()");
+    }
+    return i;
+  }
+
+  private void keepRollMaxOptionsEnabled()
+  {
+    boolean enabled = backupfilesKeepAll.isEnabled()
+            && !backupfilesKeepAll.isSelected();
+    oldBackupFilesLabel.setEnabled(enabled);
+    backupfilesRollMaxSpinner.setEnabled(enabled);
+    backupfilesConfirmDelete.setEnabled(enabled);
+  }
+
+  private void backupfilesKeepAllSetEnabled(boolean tryEnabled)
+  {
+    boolean enabled = tryEnabled && enableBackupFiles.isSelected()
+            && getComboIntStringKey(backupfilesPresetsCombo) == 0
+            && suffixTemplate.getText()
+                    .indexOf(BackupFiles.NUM_PLACEHOLDER) > -1;
+    keepfilesPanel.setEnabled(enabled);
+    backupfilesKeepAll.setEnabled(enabled);
+    oldBackupFilesLabel.setEnabled(enabled);
+    keepRollMaxOptionsEnabled();
+  }
+
+  private void backupfilesSuffixTemplateDigitsSetEnabled()
+  {
+    boolean enabled = suffixTemplate.isEnabled() && suffixTemplate.getText()
+            .indexOf(BackupFiles.NUM_PLACEHOLDER) > -1;
+    suffixDigitsLabel.setEnabled(enabled);
+    suffixDigitsSpinner.setEnabled(enabled);
+    suffixReverse.setEnabled(enabled);
+  }
+
+  private void backupfilesSuffixTemplateSetEnabled(boolean tryEnabled)
+  {
+    boolean enabled = tryEnabled && enableBackupFiles.isSelected()
+            && getComboIntStringKey(backupfilesPresetsCombo) == 0;
+    suffixPanel.setEnabled(enabled);
+    suffixTemplateLabel.setEnabled(enabled);
+    suffixTemplate.setEnabled(enabled);
+    backupfilesSuffixTemplateDigitsSetEnabled();
+  }
+
+  protected void backupfilesCustomOptionsSetEnabled()
+  {
+    int scheme = getComboIntStringKey(backupfilesPresetsCombo);
+    boolean enabled = scheme == 0 && enableBackupFiles.isSelected();
+
+    backupfilesSuffixTemplateSetEnabled(enabled);
+    backupfilesKeepAllSetEnabled(enabled);
+  }
+
+  private void backupfilesSummarySetEnabled()
+  {
+    boolean enabled = enableBackupFiles.isSelected();
+    backupfilesExampleLabel.setEnabled(enabled);
+    exampleFilesPanel.setEnabled(enabled);
+  }
+
+  private void backupfilesPresetsSetEnabled()
+  {
+    boolean enabled = enableBackupFiles.isSelected();
+    presetsPanel.setEnabled(enabled);
+    backupfilesPresetsCombo.setEnabled(enabled);
+  }
+
+  protected void backupsOptionsSetEnabled()
+  {
+    backupfilesPresetsSetEnabled();
+    backupfilesSummarySetEnabled();
+    backupfilesCustomOptionsSetEnabled();
+  }
+
+  protected void backupsSetOptions(String suffix, int digits,
+          boolean reverse, boolean keepAll, int rollMax,
+          boolean confirmDelete)
+  {
+    suffixTemplate.setText(suffix);
+    suffixDigitsSpinner.setValue(digits);
+    suffixReverse.setSelected(reverse);
+    backupfilesKeepAll.setSelected(keepAll);
+    backupfilesRollMaxSpinner.setValue(rollMax);
+    backupfilesConfirmDelete.setSelected(confirmDelete);
+  }
+
+  protected void backupsSetOptions(BackupFilesPresetEntry p)
+  {
+    backupsSetOptions(p.suffix, p.digits, p.reverse, p.keepAll, p.rollMax,
+            p.confirmDelete);
+  }
+
   protected void autoIdWidth_actionPerformed()
   {
     // TODO Auto-generated method stub
@@ -1816,3 +2770,86 @@ public class GPreferences extends JPanel
 
   }
 }
+
+class IntKeyStringValueEntry
+{
+  int k;
+
+  String v;
+
+  public IntKeyStringValueEntry(int k, String v)
+  {
+    this.k = k;
+    this.v = v;
+  }
+
+  @Override
+  public String toString()
+  {
+    return this.getValue();
+  }
+
+  public int getKey()
+  {
+    return k;
+  }
+
+  public String getValue()
+  {
+    return v;
+  }
+}
+
+class BackupFilesPresetEntry
+{
+  String suffix;
+
+  int digits;
+
+  boolean reverse;
+
+  boolean keepAll;
+
+  int rollMax;
+
+  boolean confirmDelete;
+
+  public BackupFilesPresetEntry(String suffix, int digits, boolean reverse,
+          boolean keepAll, int rollMax, boolean confirmDelete)
+  {
+    this.suffix = suffix;
+    this.digits = digits;
+    this.reverse = reverse;
+    this.keepAll = keepAll;
+    this.rollMax = rollMax;
+    this.confirmDelete = confirmDelete;
+  }
+}
+
+class BackupFilesPresetsComboBoxRenderer extends DefaultListCellRenderer
+{
+  /**
+   * 
+   */
+  private static final long serialVersionUID = 88L;
+
+  @Override
+  public Component getListCellRendererComponent(JList list, Object value,
+          int index, boolean isSelected, boolean cellHasFocus)
+  {
+    super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+    
+    try {
+      IntKeyStringValueEntry e = (IntKeyStringValueEntry) value;
+      if (e.getKey() == GPreferences.BACKUPFILESSCHEMECUSTOMISE)
+      {
+        // "Customise" item
+        this.setFont(this.getFont().deriveFont(Font.BOLD));
+      }
+    } catch (Exception e) {
+      return this;
+    }
+
+    return this;
+  }
+}