org.omegat.gui.align.AlignPanelController.java Source code

Java tutorial

Introduction

Here is the source code for org.omegat.gui.align.AlignPanelController.java

Source

/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool 
      with fuzzy matching, translation memory, keyword search, 
      glossaries, and translation leveraging into updated projects.
    
 Copyright (C) 2016 Aaron Madlon-Kay
           Home page: http://www.omegat.org/
           Support center: http://groups.yahoo.com/group/OmegaT/
    
 This file is part of OmegaT.
    
 OmegaT is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.
    
 OmegaT is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
    
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 **************************************************************************/

package org.omegat.gui.align;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.MissingResourceException;
import java.util.concurrent.CancellationException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.swing.AbstractButton;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.JTable.DropLocation;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import org.apache.commons.io.FilenameUtils;
import org.omegat.core.Core;
import org.omegat.core.segmentation.SRX;
import org.omegat.core.segmentation.Segmenter;
import org.omegat.filters2.master.FilterMaster;
import org.omegat.gui.align.Aligner.AlgorithmClass;
import org.omegat.gui.align.Aligner.CalculatorType;
import org.omegat.gui.align.Aligner.ComparisonMode;
import org.omegat.gui.align.Aligner.CounterType;
import org.omegat.gui.align.MutableBead.Status;
import org.omegat.gui.filters2.FiltersCustomizer;
import org.omegat.gui.main.ProjectUICommands;
import org.omegat.gui.segmentation.SegmentationCustomizer;
import org.omegat.util.Language;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.Preferences;
import org.omegat.util.StringUtil;
import org.omegat.util.gui.DelegatingComboBoxRenderer;
import org.omegat.util.gui.RoundedCornerBorder;
import org.omegat.util.gui.Styles;

import gen.core.filters.Filters;

/**
 * Controller for the alignment UI
 * 
 * @author Aaron Madlon-Kay
 */
public class AlignPanelController {

    private final Aligner aligner;
    private final String defaultSaveDir;
    private boolean modified = false;
    private SRX customizedSRX;
    private Filters customizedFilters;

    private SwingWorker<?, ?> loader;

    private boolean doHighlight = true;
    private Pattern highlightPattern = Pattern.compile(Preferences.getPreferenceDefault(
            Preferences.ALIGNER_HIGHLIGHT_PATTERN, Preferences.ALIGNER_HIGHLIGHT_PATTERN_DEFAULT));

    private int ppRow = -1;
    private int ppCol = -1;

    private AlignPanel panel;
    private AlignMenuFrame frame;

    /**
     * The alignment workflow is separated into two phases:
     * <ol>
     * <li>Align: Verify and tweak the results of automatic algorithmic alignment
     * <li>Edit: Manually edit the results
     * </ol>
     */
    private enum Phase {
        ALIGN, EDIT, PINPOINT
    }

    private Phase phase = Phase.ALIGN;

    public AlignPanelController(Aligner aligner, String defaultSaveDir) {
        this.aligner = aligner;
        this.defaultSaveDir = defaultSaveDir;
    }

    /**
     * Display the align tool. The tool is not modal, so this call will return immediately.
     * 
     * @param parent
     *            Parent window of the align tool
     */
    public void show(Component parent) {
        frame = new AlignMenuFrame();
        frame.setTitle(OStrings.getString("ALIGNER_PANEL"));
        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                closeFrame(frame);
            }
        });

        panel = new AlignPanel();

        ActionListener comparisonListener = e -> {
            ComparisonMode newValue = (ComparisonMode) ((JComboBox<?>) e.getSource()).getSelectedItem();
            if (newValue != aligner.comparisonMode && confirmReset(frame)) {
                aligner.comparisonMode = newValue;
                reloadBeads();
            } else {
                panel.comparisonComboBox.setSelectedItem(aligner.comparisonMode);
            }
        };
        panel.comparisonComboBox.addActionListener(comparisonListener);
        panel.comparisonComboBox.setRenderer(new EnumRenderer<ComparisonMode>("ALIGNER_ENUM_COMPARISON_MODE_"));

        ActionListener algorithmListener = e -> {
            AlgorithmClass newValue = (AlgorithmClass) ((JComboBox<?>) e.getSource()).getSelectedItem();
            if (newValue != aligner.algorithmClass && confirmReset(frame)) {
                aligner.algorithmClass = newValue;
                reloadBeads();
            } else {
                panel.algorithmComboBox.setSelectedItem(aligner.algorithmClass);
            }
        };
        panel.algorithmComboBox.addActionListener(algorithmListener);
        panel.algorithmComboBox.setRenderer(new EnumRenderer<AlgorithmClass>("ALIGNER_ENUM_ALGORITHM_CLASS_"));

        ActionListener calculatorListener = e -> {
            CalculatorType newValue = (CalculatorType) ((JComboBox<?>) e.getSource()).getSelectedItem();
            if (newValue != aligner.calculatorType && confirmReset(frame)) {
                aligner.calculatorType = newValue;
                reloadBeads();
            } else {
                panel.calculatorComboBox.setSelectedItem(aligner.calculatorType);
            }
        };
        panel.calculatorComboBox.addActionListener(calculatorListener);
        panel.calculatorComboBox.setRenderer(new EnumRenderer<CalculatorType>("ALIGNER_ENUM_CALCULATOR_TYPE_"));

        ActionListener counterListener = e -> {
            CounterType newValue = (CounterType) ((JComboBox<?>) e.getSource()).getSelectedItem();
            if (newValue != aligner.counterType && confirmReset(frame)) {
                aligner.counterType = newValue;
                reloadBeads();
            } else {
                panel.counterComboBox.setSelectedItem(aligner.counterType);
            }
        };
        panel.counterComboBox.addActionListener(counterListener);
        panel.counterComboBox.setRenderer(new EnumRenderer<CounterType>("ALIGNER_ENUM_COUNTER_TYPE_"));

        ActionListener segmentingListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean newValue = ((AbstractButton) e.getSource()).isSelected();
                if (newValue != aligner.segment && confirmReset(frame)) {
                    aligner.segment = newValue;
                    reloadBeads();
                } else {
                    panel.segmentingCheckBox.setSelected(aligner.segment);
                    frame.segmentingItem.setSelected(aligner.segment);
                }
            }
        };
        panel.segmentingCheckBox.addActionListener(segmentingListener);
        frame.segmentingItem.addActionListener(segmentingListener);

        ActionListener segmentingRulesListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (confirmReset(frame)) {
                    SegmentationCustomizer customizer = new SegmentationCustomizer(false, SRX.getDefault(),
                            Core.getSegmenter().getSRX(), null);
                    if (customizer.show(frame)) {
                        customizedSRX = customizer.getResult();
                        Core.setSegmenter(new Segmenter(customizedSRX));
                        reloadBeads();
                    }
                }
            }
        };
        panel.segmentingRulesButton.addActionListener(segmentingRulesListener);
        frame.segmentingRulesItem.addActionListener(segmentingRulesListener);

        ActionListener filterSettingsListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (confirmReset(frame)) {
                    FiltersCustomizer customizer = new FiltersCustomizer(false,
                            FilterMaster.createDefaultFiltersConfig(), Core.getFilterMaster().getConfig(), null);
                    if (customizer.show(frame)) {
                        customizedFilters = customizer.getResult();
                        Core.setFilterMaster(new FilterMaster(customizedFilters));
                        aligner.clearLoaded();
                        reloadBeads();
                    }
                }
            }
        };
        panel.fileFilterSettingsButton.addActionListener(filterSettingsListener);
        frame.fileFilterSettingsItem.addActionListener(filterSettingsListener);

        TableCellRenderer renderer = new MultilineCellRenderer();
        panel.table.setDefaultRenderer(Object.class, renderer);
        panel.table.setDefaultRenderer(Boolean.class, renderer);
        panel.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                resizeRows(panel.table);
            }
        });

        ActionListener oneAdjustListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] rows = panel.table.getSelectedRows();
                int col = panel.table.getSelectedColumn();
                boolean up = e.getSource().equals(panel.moveUpButton) || e.getSource().equals(frame.moveUpItem);
                BeadTableModel model = (BeadTableModel) panel.table.getModel();
                if ((e.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) != 0) {
                    int trgRow = up ? model.prevBeadFromRow(rows[0]) : model.nextBeadFromRow(rows[rows.length - 1]);
                    moveRows(rows, col, trgRow);
                } else {
                    int offset = up ? -1 : 1;
                    slideRows(rows, col, offset);
                }
            }
        };
        panel.moveUpButton.addActionListener(oneAdjustListener);
        frame.moveUpItem.addActionListener(oneAdjustListener);
        panel.moveDownButton.addActionListener(oneAdjustListener);
        frame.moveDownItem.addActionListener(oneAdjustListener);

        ActionListener mergeListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] rows = panel.table.getSelectedRows();
                int col = panel.table.getSelectedColumn();
                BeadTableModel model = (BeadTableModel) panel.table.getModel();
                if (rows.length == 1) {
                    rows = new int[] { rows[0], model.nextNonEmptyCell(rows[0], col) };
                }
                int beads = model.beadsInRowSpan(rows);
                if (beads < 1) {
                    // Do nothing
                } else if (beads == 1) {
                    mergeRows(rows, col);
                } else {
                    moveRows(rows, col, rows[0]);
                }
            }
        };
        panel.mergeButton.addActionListener(mergeListener);
        frame.mergeItem.addActionListener(mergeListener);

        ActionListener splitListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int[] rows = panel.table.getSelectedRows();
                int col = panel.table.getSelectedColumn();
                BeadTableModel model = (BeadTableModel) panel.table.getModel();
                int beads = model.beadsInRowSpan(rows);
                if (beads != 1) {
                    // Do nothing
                } else if (rows.length == 1) {
                    splitRow(rows[0], col);
                } else {
                    splitBead(rows, col);
                }
            }
        };
        panel.splitButton.addActionListener(splitListener);
        frame.splitItem.addActionListener(splitListener);

        ActionListener editListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                int row = panel.table.getSelectedRow();
                int col = panel.table.getSelectedColumn();
                editRow(row, col);
            }
        };
        panel.editButton.addActionListener(editListener);
        frame.editItem.addActionListener(editListener);

        ListSelectionListener selectionListener = new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                updateCommandAvailability(panel, frame);
            }
        };
        panel.table.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener);
        panel.table.getSelectionModel().addListSelectionListener(selectionListener);

        ActionListener saveListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!confirmSaveTMX(panel)) {
                    return;
                }
                while (true) {
                    JFileChooser chooser = new JFileChooser();
                    chooser.setSelectedFile(new File(defaultSaveDir, getOutFileName()));
                    chooser.setDialogTitle(OStrings.getString("ALIGNER_PANEL_DIALOG_SAVE"));
                    if (JFileChooser.APPROVE_OPTION == chooser.showSaveDialog(frame)) {
                        File file = chooser.getSelectedFile();
                        if (file.isFile()) {
                            if (JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog(frame,
                                    StringUtil.format(OStrings.getString("ALIGNER_PANEL_DIALOG_OVERWRITE"),
                                            file.getName()),
                                    OStrings.getString("ALIGNER_DIALOG_WARNING_TITLE"),
                                    JOptionPane.WARNING_MESSAGE)) {
                                continue;
                            }
                        }
                        List<MutableBead> beads = ((BeadTableModel) panel.table.getModel()).getData();
                        try {
                            aligner.writePairsToTMX(file,
                                    MutableBead.beadsToEntries(aligner.srcLang, aligner.trgLang, beads));
                            modified = false;
                        } catch (Exception ex) {
                            Log.log(ex);
                            JOptionPane.showMessageDialog(frame, OStrings.getString("ALIGNER_PANEL_SAVE_ERROR"),
                                    OStrings.getString("ERROR_TITLE"), JOptionPane.ERROR_MESSAGE);
                        }
                    }
                    break;
                }
            }
        };
        panel.saveButton.addActionListener(saveListener);
        frame.saveItem.addActionListener(saveListener);

        ActionListener resetListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (confirmReset(frame)) {
                    if (phase == Phase.ALIGN) {
                        aligner.restoreDefaults();
                    }
                    reloadBeads();
                }
            }
        };
        panel.resetButton.addActionListener(resetListener);
        frame.resetItem.addActionListener(resetListener);

        ActionListener reloadListener = e -> {
            if (confirmReset(frame)) {
                aligner.clearLoaded();
                reloadBeads();
            }
        };
        frame.reloadItem.addActionListener(reloadListener);

        ActionListener removeTagsListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean newValue = ((AbstractButton) e.getSource()).isSelected();
                if (newValue != aligner.removeTags && confirmReset(frame)) {
                    aligner.removeTags = newValue;
                    aligner.clearLoaded();
                    reloadBeads();
                } else {
                    panel.removeTagsCheckBox.setSelected(aligner.removeTags);
                    frame.removeTagsItem.setSelected(aligner.removeTags);
                }
            }
        };
        panel.removeTagsCheckBox.addActionListener(removeTagsListener);
        frame.removeTagsItem.addActionListener(removeTagsListener);

        panel.continueButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                phase = Phase.EDIT;
                updatePanel();
            }
        });

        ActionListener highlightListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                doHighlight = ((AbstractButton) e.getSource()).isSelected();
                updateHighlight();
            }
        };
        panel.highlightCheckBox.addActionListener(highlightListener);
        frame.highlightItem.addActionListener(highlightListener);

        ActionListener highlightPatternListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                PatternPanelController patternEditor = new PatternPanelController(highlightPattern);
                highlightPattern = patternEditor.show(frame);
                Preferences.setPreference(Preferences.ALIGNER_HIGHLIGHT_PATTERN, highlightPattern.pattern());
                updateHighlight();
            }
        };
        panel.highlightPatternButton.addActionListener(highlightPatternListener);
        frame.highlightPatternItem.addActionListener(highlightPatternListener);

        frame.markAcceptedItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setStatus(MutableBead.Status.ACCEPTED, panel.table.getSelectedRows());
            }
        });

        frame.markNeedsReviewItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setStatus(MutableBead.Status.NEEDS_REVIEW, panel.table.getSelectedRows());
            }
        });

        frame.clearMarkItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setStatus(MutableBead.Status.DEFAULT, panel.table.getSelectedRows());
            }
        });

        frame.toggleSelectedItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleEnabled(panel.table.getSelectedRows());
            }
        });

        frame.closeItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                closeFrame(frame);
            }
        });

        frame.keepAllItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleAllEnabled(true);
            }
        });

        frame.keepNoneItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleAllEnabled(false);
            }
        });

        frame.realignPendingItem.addActionListener(e -> {
            realignPending();
        });

        frame.pinpointAlignStartItem.addActionListener(e -> {
            phase = Phase.PINPOINT;
            ppRow = panel.table.getSelectedRow();
            ppCol = panel.table.getSelectedColumn();
            panel.table.clearSelection();
            updatePanel();
        });

        frame.pinpointAlignEndItem.addActionListener(e -> {
            pinpointAlign(panel.table.getSelectedRow(), panel.table.getSelectedColumn());
        });

        frame.pinpointAlignCancelItem.addActionListener(e -> {
            phase = Phase.EDIT;
            ppRow = -1;
            ppCol = -1;
            panel.table.repaint();
            updatePanel();
        });

        panel.table.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (phase == Phase.PINPOINT) {
                    JTable table = (JTable) e.getSource();
                    int row = table.rowAtPoint(e.getPoint());
                    int col = table.columnAtPoint(e.getPoint());
                    pinpointAlign(row, col);
                }
            }
        });

        frame.resetItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R,
                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | KeyEvent.SHIFT_DOWN_MASK));
        frame.realignPendingItem.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        frame.saveItem.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        frame.closeItem.setAccelerator(
                KeyStroke.getKeyStroke(KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));

        // emacs-like keys for table navigation
        // See javax.swing.plaf.BasicTableUI.Actions for supported action names.
        setKeyboardShortcut(panel.table, "selectNextRow", 'n');
        setKeyboardShortcut(panel.table, "selectNextRowExtendSelection", 'N');
        setKeyboardShortcut(panel.table, "selectPreviousRow", 'p');
        setKeyboardShortcut(panel.table, "selectPreviousRowExtendSelection", 'P');
        setKeyboardShortcut(panel.table, "selectNextColumn", 'f');
        setKeyboardShortcut(panel.table, "selectNextColumnExtendSelection", 'F');
        setKeyboardShortcut(panel.table, "selectPreviousColumn", 'b');
        setKeyboardShortcut(panel.table, "selectPreviousColumnExtendSelection", 'B');

        panel.table.setTransferHandler(new AlignTransferHandler());
        panel.table.addPropertyChangeListener("dropLocation", new DropLocationListener());
        if (Preferences.isPreference(Preferences.PROJECT_FILES_USE_FONT)) {
            try {
                String fontName = Preferences.getPreference(Preferences.TF_SRC_FONT_NAME);
                int fontSize = Integer.parseInt(Preferences.getPreference(Preferences.TF_SRC_FONT_SIZE));
                panel.table.setFont(new Font(fontName, Font.PLAIN, fontSize));
            } catch (Exception e) {
                Log.log(e);
            }
        }

        // Set initial state
        updateHighlight();
        updatePanel();
        reloadBeads();

        frame.add(panel);
        frame.pack();
        frame.setMinimumSize(frame.getSize());
        frame.setLocationRelativeTo(parent);
        frame.setVisible(true);
    }

    private static void setKeyboardShortcut(JComponent comp, Object actionName, char stroke) {
        comp.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(stroke), actionName);
    }

    private static void resizeRows(JTable table) {
        for (int row = 0; row < table.getRowCount(); row++) {
            int max = 0;
            for (int col = BeadTableModel.COL_SRC; col < table.getColumnCount(); col++) {
                int colWidth = table.getColumnModel().getColumn(col).getWidth();
                TableCellRenderer cellRenderer = table.getCellRenderer(row, col);
                Component c = table.prepareRenderer(cellRenderer, row, col);
                c.setBounds(0, 0, colWidth, Integer.MAX_VALUE);
                int height = c.getPreferredSize().height;
                max = Math.max(max, height);
            }
            table.setRowHeight(row, max);
        }
    }

    private void slideRows(int[] rows, int col, int offset) {
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        panel.table.clearSelection();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        List<Integer> realRows = model.realCellsInRowSpan(col, rows);
        int[] resultRows = model.slide(realRows, col, offset);
        int selStart = resultRows[0];
        int selEnd = resultRows[1];
        // If we have a multi-cell selection, trim the selection so that the result remains slideable
        if (selStart != selEnd) {
            while (offset < 0 && !model.canMove(selStart, col, true)) {
                selStart++;
            }
            while (offset > 0 && !model.canMove(selEnd, col, false)) {
                selEnd--;
            }
        }
        panel.table.changeSelection(selStart, col, false, false);
        panel.table.changeSelection(selEnd, col, false, true);
        ensureSelectionVisible(initialRect);
    }

    private void moveRows(int[] rows, int col, int trgRow) {
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        panel.table.clearSelection();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        List<Integer> realRows = model.realCellsInRowSpan(col, rows);
        int[] resultRows = model.move(realRows, col, trgRow);
        panel.table.changeSelection(resultRows[0], col, false, false);
        panel.table.changeSelection(resultRows[1], col, false, true);
        ensureSelectionVisible(initialRect);
    }

    private void mergeRows(int[] rows, int col) {
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        panel.table.clearSelection();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        List<Integer> realRows = model.realCellsInRowSpan(col, rows);
        int resultRow = model.mergeRows(realRows, col);
        panel.table.changeSelection(resultRow, col, false, false);
        ensureSelectionVisible(initialRect);
    }

    private void splitRow(int row, int col) {
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        if (!model.isEditableColumn(col)) {
            throw new IllegalArgumentException();
        }
        String text = panel.table.getValueAt(row, col).toString();
        String reference = (String) panel.table.getValueAt(row,
                col == BeadTableModel.COL_SRC ? BeadTableModel.COL_TRG : BeadTableModel.COL_SRC);
        SplittingPanelController splitter = new SplittingPanelController(text, reference);
        String[] split = splitter.show(SwingUtilities.getWindowAncestor(panel.table));
        if (split.length == 1) {
            return;
        }
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        panel.table.clearSelection();
        int resultRows[] = model.splitRow(row, col, split);
        panel.table.changeSelection(resultRows[0], col, false, false);
        panel.table.changeSelection(resultRows[resultRows.length - 1], col, false, true);
        ensureSelectionVisible(initialRect);
    }

    private void splitBead(int[] rows, int col) {
        modified = true;
        panel.table.clearSelection();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        Rectangle initialRect = panel.table.getVisibleRect();
        model.splitBead(rows);
        panel.table.changeSelection(rows[0], col, false, false);
        panel.table.changeSelection(rows[rows.length - 1], col, false, true);
        ensureSelectionVisible(initialRect);
    }

    private void editRow(int row, int col) {
        String text = panel.table.getValueAt(row, col).toString();
        EditingPanelController splitter = new EditingPanelController(text);
        String newText = splitter.show(SwingUtilities.getWindowAncestor(panel.table));
        if (newText == null || text.equals(newText)) {
            return;
        }
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        panel.table.clearSelection();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        model.editRow(row, col, newText);
        panel.table.changeSelection(row, col, false, false);
        ensureSelectionVisible(initialRect);
    }

    private void realignPending() {
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        List<MutableBead> data = model.getData();
        List<MutableBead> toAlign = new ArrayList<>();
        List<MutableBead> result = new ArrayList<>(data.size());
        for (MutableBead bead : data) {
            if (bead.status == Status.ACCEPTED) {
                if (!toAlign.isEmpty()) {
                    result.addAll(aligner.doAlign(toAlign));
                    toAlign.clear();
                }
                result.add(bead);
            } else {
                toAlign.add(bead);
            }
        }
        if (!toAlign.isEmpty()) {
            result.addAll(aligner.doAlign(toAlign));
        }
        modified = true;
        model.replaceData(result);
        panel.table.repaint();
        resizeRows(panel.table);
    }

    private void pinpointAlign(int row, int col) {
        if (row == ppRow || col == ppCol) {
            return;
        }
        modified = true;
        Rectangle initialRect = panel.table.getVisibleRect();
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        IntStream.of(ppRow, row).forEach(i -> {
            List<Integer> rowspan = model.getRowExtentsForBeadAtRow(i);
            if (rowspan.size() > 1) {
                model.splitBead(rowspan.stream().mapToInt(Integer::intValue).toArray());
            }
        });
        int relocateCol = ppRow < row ? ppCol : col;
        List<String> toRelocate = new ArrayList<>();
        for (int i = Math.min(ppRow, row); i <= Math.max(ppRow, row); i++) {
            String line = model.removeLine(i, relocateCol);
            if (line != null) {
                toRelocate.add(line);
            }
        }
        int resultRow = model.insertLines(toRelocate, Math.max(ppRow, row), relocateCol);
        model.setStatusAtRow(resultRow, Status.ACCEPTED);
        panel.table.changeSelection(resultRow, ppCol, false, false);
        panel.table.changeSelection(resultRow, col, false, true);
        ppRow = -1;
        ppCol = -1;
        phase = Phase.EDIT;
        ensureSelectionVisible(initialRect);
        updatePanel();
    }

    private void toggleEnabled(int... rows) {
        if (rows.length == 0) {
            return;
        }
        modified = true;
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        model.toggleBeadsAtRows(rows);
        panel.table.repaint();
    }

    private void toggleAllEnabled(boolean value) {
        modified = true;
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        model.toggleAllBeads(value);
        panel.table.repaint();
    }

    private void setStatus(MutableBead.Status status, int... rows) {
        if (rows.length == 0) {
            return;
        }
        modified = true;
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        for (int row : rows) {
            model.setStatusAtRow(row, status);
        }
        int nextBeadRow = model.nextBeadFromRow(rows[rows.length - 1]);
        if (nextBeadRow != -1) {
            int[] cols = panel.table.getSelectedColumns();
            panel.table.changeSelection(nextBeadRow, cols[0], false, false);
            panel.table.changeSelection(nextBeadRow, cols[cols.length - 1], false, true);
            ensureSelectionVisible(panel.table.getVisibleRect());
        }
    }

    private void ensureSelectionVisible(Rectangle initialView) {
        panel.table.repaint();
        resizeRows(panel.table);
        int[] rows = panel.table.getSelectedRows();
        int[] cols = panel.table.getSelectedColumns();
        Rectangle selectionRect = panel.table.getCellRect(rows[0], cols[0], true)
                .union(panel.table.getCellRect(rows[rows.length - 1], cols[cols.length - 1], true));
        panel.table.scrollRectToVisible(initialView);
        panel.table.scrollRectToVisible(selectionRect);
    }

    private boolean confirmReset(Component comp) {
        if (!modified) {
            return true;
        }
        return JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(comp,
                OStrings.getString("ALIGNER_PANEL_RESET_WARNING_MESSAGE"),
                OStrings.getString("ALIGNER_DIALOG_WARNING_TITLE"), JOptionPane.OK_CANCEL_OPTION);
    }

    /**
     * Reloads the beads with the current settings. The loading itself takes place on a background thread.
     * Calls {@link #updatePanel(AlignPanel, AlignMenuFrame)} afterwards.
     * 
     * @param panel
     * @param frame
     */
    private void reloadBeads() {
        if (loader != null) {
            loader.cancel(true);
        }
        phase = Phase.ALIGN;
        panel.progressBar.setVisible(true);
        panel.continueButton.setEnabled(false);
        panel.controlsPanel.setVisible(false);
        loader = new SwingWorker<List<MutableBead>, Object>() {
            @Override
            protected List<MutableBead> doInBackground() throws Exception {
                return aligner.alignImpl().filter(o -> !isCancelled()).map(MutableBead::new)
                        .collect(Collectors.toList());
            }

            @Override
            protected void done() {
                List<MutableBead> beads = null;
                try {
                    beads = get();
                } catch (CancellationException ex) {
                    // Ignore
                } catch (Exception e) {
                    Log.log(e);
                    JOptionPane.showMessageDialog(panel, OStrings.getString("ALIGNER_ERROR_LOADING"),
                            OStrings.getString("ERROR_TITLE"), JOptionPane.ERROR_MESSAGE);
                }
                panel.continueButton.setEnabled(true);
                panel.progressBar.setVisible(false);
                panel.comparisonComboBox
                        .setModel(new DefaultComboBoxModel<>(aligner.allowedModes.toArray(new ComparisonMode[0])));

                String distanceValue = null;
                if (beads != null) {
                    double avgDist = MutableBead.calculateAvgDist(beads);
                    distanceValue = StringUtil.format(OStrings.getString("ALIGNER_PANEL_LABEL_AVGSCORE"),
                            avgDist == Long.MAX_VALUE ? "-" : String.format("%.3f", avgDist));
                    panel.table.setModel(new BeadTableModel(beads));
                    for (int i = 0; i < BeadTableModel.COL_SRC; i++) {
                        TableColumn col = panel.table.getColumnModel().getColumn(i);
                        col.setMaxWidth(col.getWidth());
                    }
                    modified = false;
                }
                panel.averageDistanceLabel.setText(distanceValue);

                updatePanel();
            }
        };
        loader.execute();
    }

    /**
     * Ensure that the panel controls and available menu items are synced with the settings of the underlying
     * aligner.
     * 
     * @param panel
     * @param frame
     */
    private void updatePanel() {
        panel.comparisonComboBox.setSelectedItem(aligner.comparisonMode);
        panel.algorithmComboBox.setSelectedItem(aligner.algorithmClass);
        panel.calculatorComboBox.setSelectedItem(aligner.calculatorType);
        panel.counterComboBox.setSelectedItem(aligner.counterType);
        panel.segmentingCheckBox.setSelected(aligner.segment);
        frame.segmentingItem.setSelected(aligner.segment);
        panel.segmentingRulesButton.setEnabled(aligner.segment);
        frame.segmentingRulesItem.setEnabled(aligner.segment);
        panel.removeTagsCheckBox.setSelected(aligner.removeTags);
        frame.removeTagsItem.setSelected(aligner.removeTags);

        panel.advancedPanel.setVisible(phase == Phase.ALIGN);
        panel.segmentationControlsPanel.setVisible(phase == Phase.ALIGN);
        panel.filteringControlsPanel.setVisible(phase == Phase.ALIGN);
        panel.continueButton.setVisible(phase == Phase.ALIGN);
        panel.controlsPanel.setVisible(phase != Phase.ALIGN);
        panel.controlsPanel.setEnabled(phase == Phase.EDIT);
        panel.saveButton.setVisible(phase != Phase.ALIGN);
        panel.saveButton.setEnabled(phase == Phase.EDIT);
        String instructions = null;
        switch (phase) {
        case ALIGN:
            instructions = OStrings.getString("ALIGNER_PANEL_ALIGN_PHASE_HELP");
            break;
        case EDIT:
            instructions = OStrings.getString("ALIGNER_PANEL_EDIT_PHASE_HELP");
            break;
        case PINPOINT:
            instructions = OStrings.getString("ALIGNER_PANEL_PINPOINT_PHASE_HELP");
        }
        panel.instructionsLabel.setText(instructions);
        frame.editMenu.setEnabled(phase != Phase.ALIGN);
        for (Component c : frame.editMenu.getComponents()) {
            // Batch-enable/disable Edit menu items here, then override later if necessary
            c.setEnabled(phase == Phase.EDIT);
        }
        frame.optionsMenu.setEnabled(phase == Phase.ALIGN);
        frame.saveItem.setEnabled(phase == Phase.EDIT);

        panel.table.setCursor(Cursor
                .getPredefinedCursor(phase == Phase.PINPOINT ? Cursor.CROSSHAIR_CURSOR : Cursor.DEFAULT_CURSOR));
        frame.pinpointAlignStartItem.setVisible(phase != Phase.PINPOINT);
        frame.pinpointAlignEndItem.setVisible(phase == Phase.PINPOINT);
        // frame.pinpointAlign[Start|End]Item enabledness depends on table selection
        frame.pinpointAlignCancelItem.setVisible(phase == Phase.PINPOINT);
        frame.pinpointAlignCancelItem.setEnabled(phase == Phase.PINPOINT);

        JButton defaultButton = phase == Phase.ALIGN ? panel.continueButton
                : phase == Phase.EDIT ? panel.saveButton : null;
        frame.getRootPane().setDefaultButton(defaultButton);

        updateCommandAvailability(panel, frame);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                resizeRows(panel.table);
            }
        });
    }

    private void updateHighlight() {
        panel.highlightCheckBox.setSelected(doHighlight);
        frame.highlightItem.setSelected(doHighlight);
        panel.highlightPatternButton.setEnabled(doHighlight);
        frame.highlightPatternItem.setEnabled(doHighlight);
        panel.table.repaint();
    }

    private void updateCommandAvailability(AlignPanel panel, AlignMenuFrame frame) {
        if (!(panel.table.getModel() instanceof BeadTableModel)) {
            return;
        }
        int[] rows = panel.table.getSelectedRows();
        int[] cols = panel.table.getSelectedColumns();
        int col = cols.length > 0 ? cols[0] : -1;
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        List<Integer> realRows = model.realCellsInRowSpan(col, rows);
        boolean enabled = phase == Phase.EDIT && !realRows.isEmpty() && cols.length == 1
                && model.isEditableColumn(col);
        boolean canUp = enabled ? model.canMove(realRows.get(0), col, true) : false;
        boolean canDown = enabled ? model.canMove(realRows.get(realRows.size() - 1), col, false) : false;
        int beads = model.beadsInRowSpan(rows);
        boolean canSplit = (realRows.size() == 1 && rows.length == 1) || (!realRows.isEmpty() && beads == 1);
        boolean canMerge = realRows.size() > 1
                || (realRows.size() == 1 && rows.length == 1 && realRows.get(0) < panel.table.getRowCount() - 1);
        boolean canEdit = realRows.size() == 1;
        panel.moveDownButton.setEnabled(enabled && canDown);
        frame.moveDownItem.setEnabled(enabled && canDown);
        panel.moveUpButton.setEnabled(enabled && canUp);
        frame.moveUpItem.setEnabled(enabled && canUp);
        panel.splitButton.setEnabled(enabled && canSplit);
        frame.splitItem.setEnabled(enabled && canSplit);
        panel.mergeButton.setEnabled(enabled && canMerge);
        frame.mergeItem.setEnabled(enabled && canMerge);
        panel.editButton.setEnabled(enabled && canEdit);
        frame.editItem.setEnabled(enabled && canEdit);
        frame.pinpointAlignStartItem.setEnabled(enabled && rows.length == 1);
        frame.pinpointAlignEndItem.setEnabled(phase == Phase.PINPOINT && rows.length == 1 && cols.length == 1
                && realRows.size() == 1 && realRows.get(0) != ppRow && col != ppCol && model.isEditableColumn(col));
    }

    private String getOutFileName() {
        String src = FilenameUtils.getBaseName(aligner.srcFile);
        String trg = FilenameUtils.getBaseName(aligner.trgFile);
        if (src.equals(trg)) {
            return src + "_" + aligner.srcLang.getLanguage() + "_" + aligner.trgLang.getLanguage() + ".tmx";
        } else {
            return src + "_" + trg + ".tmx";
        }
    }

    private void closeFrame(JFrame frame) {
        if (confirmReset(frame)) {
            frame.setVisible(false);
            confirmSaveSRX(frame);
            confirmSaveFilters(frame);
            frame.dispose();
        }
    }

    /**
     * If the user has modified the SRX rules, offer to save them permanently. Otherwise they are simply
     * discarded. Does nothing when OmegaT's main window is not available (changes are always discarded under
     * standalone use).
     * 
     * @param comp
     *            Parent component for dialog boxes
     */
    private void confirmSaveSRX(Component comp) {
        if (Core.getMainWindow() == null || customizedSRX == null) {
            return;
        }
        if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(comp,
                OStrings.getString("ALIGNER_DIALOG_SEGMENTATION_CONFIRM_MESSAGE"),
                OStrings.getString("ALIGNER_DIALOG_CONFIRM_TITLE"), JOptionPane.OK_CANCEL_OPTION)) {
            if (Core.getProject().isProjectLoaded()
                    && Core.getProject().getProjectProperties().getProjectSRX() != null) {
                Core.getProject().getProjectProperties().setProjectSRX(customizedSRX);
                try {
                    Core.getProject().saveProjectProperties();
                } catch (Exception ex) {
                    Log.log(ex);
                    JOptionPane.showMessageDialog(comp, OStrings.getString("CT_ERROR_SAVING_PROJ"),
                            OStrings.getString("ERROR_TITLE"), JOptionPane.ERROR_MESSAGE);
                }
                ProjectUICommands.promptReload();
            } else {
                Preferences.setSRX(customizedSRX);
            }
        }
    }

    /**
     * If the user has modified the file filter settings, offer to save them permanently. Otherwise they are
     * simply discarded. Does nothing when OmegaT's main window is not available (changes are always discarded
     * under standalone use).
     * 
     * @param comp
     *            Parent component for dialog boxes
     */
    private void confirmSaveFilters(Component comp) {
        if (Core.getMainWindow() == null || customizedFilters == null) {
            return;
        }
        if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(comp,
                OStrings.getString("ALIGNER_DIALOG_FILTERS_CONFIRM_MESSAGE"),
                OStrings.getString("ALIGNER_DIALOG_CONFIRM_TITLE"), JOptionPane.OK_CANCEL_OPTION)) {
            if (Core.getProject().isProjectLoaded()
                    && Core.getProject().getProjectProperties().getProjectFilters() != null) {
                Core.getProject().getProjectProperties().setProjectFilters(customizedFilters);
                try {
                    Core.getProject().saveProjectProperties();
                } catch (Exception ex) {
                    Log.log(ex);
                    JOptionPane.showMessageDialog(comp, OStrings.getString("CT_ERROR_SAVING_PROJ"),
                            OStrings.getString("ERROR_TITLE"), JOptionPane.ERROR_MESSAGE);
                }
                ProjectUICommands.promptReload();
            } else {
                Preferences.setFilters(customizedFilters);
            }
        }
    }

    private boolean confirmSaveTMX(AlignPanel panel) {
        BeadTableModel model = (BeadTableModel) panel.table.getModel();
        boolean needsReview = false;
        for (MutableBead bead : model.getData()) {
            if (bead.status == MutableBead.Status.NEEDS_REVIEW) {
                needsReview = true;
                break;
            }
        }
        if (needsReview) {
            return JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(panel,
                    OStrings.getString("ALIGNER_DIALOG_NEEDSREVIEW_CONFIRM_MESSAGE"),
                    OStrings.getString("ALIGNER_DIALOG_CONFIRM_TITLE"), JOptionPane.OK_CANCEL_OPTION);
        } else {
            return true;
        }
    }

    static final Border FOCUS_BORDER = new MatteBorder(1, 1, 1, 1, new Color(0x76AFE8));

    // See: http://esus.com/creating-a-jtable-with-multiline-cells/
    class MultilineCellRenderer implements TableCellRenderer {
        private final JTextPane textArea = new JTextPane();
        private final Border noFocusBorder = new EmptyBorder(FOCUS_BORDER.getBorderInsets(textArea));
        private final JCheckBox checkBox = new JCheckBox();
        private final AttributeSet highlight;

        public MultilineCellRenderer() {
            // textArea.setLineWrap(true);
            // textArea.setWrapStyleWord(true);
            textArea.setOpaque(true);
            checkBox.setHorizontalAlignment(JLabel.CENTER);
            checkBox.setBorderPainted(true);
            SimpleAttributeSet sas = new SimpleAttributeSet();
            StyleConstants.setBackground(sas, Styles.EditorColor.COLOR_ALIGNER_HIGHLIGHT.getColor());
            highlight = sas;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                boolean hasFocus, int row, int column) {
            if (value instanceof Boolean) {
                doStyling(checkBox, table, isSelected, hasFocus, row, column);
                checkBox.setSelected((Boolean) value);
                return checkBox;
            } else {
                doStyling(textArea, table, isSelected, hasFocus, row, column);
                textArea.setText(null);
                if (value != null) {
                    String text = value.toString();
                    textArea.setText(text);
                    doHighlighting(text);
                }
                return textArea;
            }
        }

        private void doStyling(JComponent comp, JTable table, boolean isSelected, boolean hasFocus, int row,
                int column) {
            if (isSelected) {
                comp.setBackground(table.getSelectionBackground());
                comp.setForeground(table.getSelectionForeground());
            } else {
                MutableBead.Status status = ((BeadTableModel) table.getModel()).getStatusForRow(row);
                if (column == BeadTableModel.COL_CHECKBOX && status != MutableBead.Status.DEFAULT) {
                    switch (status) {
                    case ACCEPTED:
                        comp.setBackground(Styles.EditorColor.COLOR_ALIGNER_ACCEPTED.getColor());
                        break;
                    case NEEDS_REVIEW:
                        comp.setBackground(Styles.EditorColor.COLOR_ALIGNER_NEEDSREVIEW.getColor());
                        break;
                    case DEFAULT:
                        // Leave color as-is
                    }
                } else if (row == ppRow && column == ppCol) {
                    comp.setBackground(Color.GREEN);
                } else {
                    comp.setBackground(getBeadNumber(table, row) % 2 == 0 ? table.getBackground()
                            : Styles.EditorColor.COLOR_ALIGNER_TABLE_ROW_HIGHLIGHT.getColor());
                    comp.setForeground(table.getForeground());
                }
            }
            Border marginBorder = new EmptyBorder(1, column == 0 ? 5 : 1, 1,
                    column == table.getColumnCount() - 1 ? 5 : 1);
            if (hasFocus) {
                comp.setBorder(new CompoundBorder(marginBorder, FOCUS_BORDER));
            } else {
                comp.setBorder(new CompoundBorder(marginBorder, noFocusBorder));
            }
            comp.setFont(table.getFont());
        }

        private int getBeadNumber(JTable table, int row) {
            return ((BeadTableModel) table.getModel()).getBeadNumberForRow(row);
        }

        void doHighlighting(String text) {
            StyledDocument doc = textArea.getStyledDocument();
            doc.setCharacterAttributes(0, text.length(), new SimpleAttributeSet(), true);
            if (!doHighlight || highlightPattern == null) {
                return;
            }
            Matcher m = highlightPattern.matcher(text);
            while (m.find()) {
                doc.setCharacterAttributes(m.start(), m.end() - m.start(), highlight, true);
            }
        }
    }

    @SuppressWarnings("serial")
    class BeadTableModel extends AbstractTableModel {
        // For debugging purposes, additional columns are defined as COL_CHECKBOX - n.
        // To enable them, set COL_CHECKBOX > 0.
        static final int COL_CHECKBOX = 0;
        static final int COL_SRC = COL_CHECKBOX + 1;
        static final int COL_TRG = COL_SRC + 1;

        private final List<MutableBead> data;

        // Maintain an integer (index) mapping of the contents of each row. This is required when modifying
        // the underlying beads, as all changes are destructive and non-atomic. It also speeds up access.
        List<Float> rowToDistance;
        List<MutableBead> rowToBead;
        List<String> rowToSourceLine;
        List<String> rowToTargetLine;

        public BeadTableModel(List<MutableBead> data) {
            this.data = data;
            makeCache();
        }

        private void makeCache() {
            for (int i = 0; i < data.size(); i++) {
                MutableBead bead = data.get(i);
                // Cull empty beads (can be created by splitting top/bottom line)
                if (bead.isEmpty()) {
                    data.remove(i--);
                    continue;
                }
                // Split beads with 2+-2+
                while (bead.sourceLines.size() > 1 && bead.targetLines.size() > 1) {
                    bead = splitBeadByCount(bead, 1);
                    data.add(++i, bead);
                }
            }
            List<Float> rowToDistance = new ArrayList<>();
            List<MutableBead> rowToBead = new ArrayList<>();
            List<String> rowToSourceLine = new ArrayList<>();
            List<String> rowToTargetLine = new ArrayList<>();
            for (MutableBead bead : data) {
                int beadRows = Math.max(bead.sourceLines.size(), bead.targetLines.size());
                for (int i = 0; i < beadRows; i++) {
                    rowToDistance.add(bead.score);
                    rowToBead.add(bead);
                    rowToSourceLine.add(i < bead.sourceLines.size() ? bead.sourceLines.get(i) : null);
                    rowToTargetLine.add(i < bead.targetLines.size() ? bead.targetLines.get(i) : null);
                }
            }
            this.rowToDistance = rowToDistance;
            this.rowToBead = rowToBead;
            this.rowToSourceLine = rowToSourceLine;
            this.rowToTargetLine = rowToTargetLine;
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return phase == Phase.EDIT && column == COL_CHECKBOX && getValueAt(row, column) != null;
        }

        @Override
        public int getColumnCount() {
            return COL_TRG + 1;
        }

        @Override
        public int getRowCount() {
            return rowToBead.size();
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            switch (columnIndex) {
            case COL_CHECKBOX - 3:
                return Integer.class;
            case COL_CHECKBOX - 2:
                return Integer.class;
            case COL_CHECKBOX - 1:
                // Bead number
                return Integer.class;
            case COL_CHECKBOX:
                return Boolean.class;
            case COL_SRC:
                return String.class;
            case COL_TRG:
                return String.class;
            }
            throw new IllegalArgumentException();
        }

        @Override
        public String getColumnName(int column) {
            switch (column) {
            case COL_CHECKBOX - 3:
                return OStrings.getString("ALIGNER_PANEL_TABLE_COL_ROW");
            case COL_CHECKBOX - 2:
                return OStrings.getString("ALIGNER_PANEL_TABLE_COL_DISTANCE");
            case COL_CHECKBOX - 1:
                // Bead number
                return "";
            case COL_CHECKBOX:
                return OStrings.getString("ALIGNER_PANEL_TABLE_COL_KEEP");
            case COL_SRC:
                return OStrings.getString("ALIGNER_PANEL_TABLE_COL_SOURCE");
            case COL_TRG:
                return OStrings.getString("ALIGNER_PANEL_TABLE_COL_TARGET");
            }
            throw new IllegalArgumentException();
        }

        @Override
        public Object getValueAt(int row, int column) {
            MutableBead bead;
            switch (column) {
            case COL_CHECKBOX - 3:
                return row;
            case COL_CHECKBOX - 2:
                bead = rowToBead.get(row);
                if (row > 0 && bead == rowToBead.get(row - 1)) {
                    return null;
                }
                return rowToDistance.get(row);
            case COL_CHECKBOX - 1:
                bead = rowToBead.get(row);
                if (row > 0 && bead == rowToBead.get(row - 1)) {
                    return null;
                }
                return data.indexOf(bead) + 1;
            case COL_CHECKBOX:
                bead = rowToBead.get(row);
                if (row > 0 && bead == rowToBead.get(row - 1)) {
                    return null;
                }
                return bead.enabled;
            case COL_SRC:
                return rowToSourceLine.get(row);
            case COL_TRG:
                return rowToTargetLine.get(row);
            }
            throw new IllegalArgumentException();
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            if (columnIndex != COL_CHECKBOX) {
                throw new IllegalArgumentException();
            }
            if (!(aValue instanceof Boolean)) {
                throw new IllegalArgumentException();
            }
            rowToBead.get(rowIndex).enabled = (Boolean) aValue;
        }

        /**
         * Move the specified lines located in <code>rows</code> and <code>col</code> into the bead indicated
         * by <code>trgRow</code>.
         * 
         * @param rows
         *            Rows to move
         * @param col
         *            Column of
         * @param trgRow
         * @return An array of two ints indicating the start and end rows of the selection after moving
         */
        int[] move(List<Integer> rows, int col, int trgRow) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            Collections.sort(rows);
            List<String> selected = new ArrayList<>(rows.size());
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            int origRowCount = getRowCount();
            // Bead to be modified selected here
            MutableBead trgBead;
            if (trgRow < 0) {
                // New bead created
                trgBead = new MutableBead();
                data.add(0, trgBead);
            } else if (trgRow > rowToBead.size() - 1) {
                // New bead created
                trgBead = new MutableBead();
                data.add(trgBead);
            } else {
                trgBead = rowToBead.get(trgRow);
            }
            List<String> trgLines = col == COL_SRC ? trgBead.sourceLines : trgBead.targetLines;
            for (int row : rows) {
                String line = lines.get(row);
                if (line == null) {
                    throw new IllegalArgumentException();
                }
                selected.add(line);
                MutableBead bead = rowToBead.get(row);
                if (bead == trgBead) {
                    continue;
                }
                Util.removeByIdentity(col == COL_SRC ? bead.sourceLines : bead.targetLines, line);
                int insertIndex = trgRow > row ? 0 : trgLines.size();
                // XXX: Bead modified here
                trgLines.add(insertIndex, line);
            }
            trgBead.status = Status.DEFAULT;
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
            lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            return new int[] { Util.indexByIdentity(lines, selected.get(0)),
                    Util.indexByIdentity(lines, selected.get(selected.size() - 1)) };
        }

        /**
         * Split the specified bead into two: one with an equal number of source and target lines (e.g. 1-1)
         * and one with the remainder (e.g. 0-1). The new bead is inserted into the underlying data store.
         * 
         * @param bead
         * @return The remainder bead
         */
        private MutableBead splitBead(MutableBead bead) {
            if (bead.isBalanced()) {
                return bead;
            }
            int index = data.indexOf(bead);
            bead = splitBeadByCount(bead, Math.min(bead.sourceLines.size(), bead.targetLines.size()));
            data.add(index + 1, bead);
            return bead;
        }

        /**
         * Split the specified bead into two: the first with the specified count of lines, and the second with
         * the remainder.
         * 
         * @param bead
         * @param count
         * @return The remainder bead
         */
        private MutableBead splitBeadByCount(MutableBead bead, int count) {
            List<String> splitSrc = new ArrayList<>(bead.sourceLines);
            // XXX: Bead modified here
            bead.sourceLines.clear();
            List<String> splitTrg = new ArrayList<>(bead.targetLines);
            bead.targetLines.clear();
            bead.status = Status.DEFAULT;
            for (int i = 0; i < count; i++) {
                if (!splitSrc.isEmpty()) {
                    bead.sourceLines.add(splitSrc.remove(0));
                }
                if (!splitTrg.isEmpty()) {
                    bead.targetLines.add(splitTrg.remove(0));
                }
            }
            // New bead created
            return new MutableBead(splitSrc, splitTrg);
        }

        int getBeadNumberForRow(int row) {
            return data.indexOf(rowToBead.get(row));
        }

        MutableBead.Status getStatusForRow(int row) {
            return rowToBead.get(row).status;
        }

        /**
         * Indicate whether the line at the specified <code>row</code> and <code>col</code> can be moved in
         * the indicated direction. A line is movable if it is not blocked by another line in the same bead.
         * 
         * @param row
         * @param col
         * @param up
         *            Up (toward index=0) when true, down when false
         * @return
         */
        boolean canMove(int row, int col, boolean up) {
            if (!isEditableColumn(col)) {
                return false;
            }
            MutableBead bead = rowToBead.get(row);
            if ((row == 0 && up) || (row == rowToBead.size() - 1 && !up)) {
                return !(col == COL_SRC ? bead.targetLines : bead.sourceLines).isEmpty();
            }
            List<String> lines = col == COL_SRC ? bead.sourceLines : bead.targetLines;
            String line = (col == COL_SRC ? rowToSourceLine : rowToTargetLine).get(row);
            int index = Util.indexByIdentity(lines, line);
            return up ? index == 0 : index == lines.size() - 1;
        }

        /**
         * Indicate whether the line at the specified <code>row</code> and <code>col</code> can be moved to
         * the bead indicated by <code>trgRow</code>. In addition to requiring
         * {@link #canMove(int, int, boolean)} to return true, the bead must be different from the current
         * bead, and no non-empty cells can exist between the current and target rows.
         * 
         * @param trgRow
         * @param row
         * @param col
         * @param up
         *            Up (toward index=0) when true, down when false
         * @return
         */
        boolean canMoveTo(int trgRow, int row, int col, boolean up) {
            if (!canMove(row, col, up) || trgRow == row) {
                return false;
            }
            // Check same bead
            if (trgRow >= 0 && trgRow < rowToBead.size()) {
                MutableBead srcBead = rowToBead.get(row);
                MutableBead trgBead = rowToBead.get(trgRow);
                if (srcBead == trgBead) {
                    return false;
                }
            }
            // Check no non-empty cells in path
            int inc = up ? -1 : 1;
            for (int r = row + inc; r != trgRow && r >= 0 && r < rowToSourceLine.size(); r += inc) {
                String line = (col == COL_SRC ? rowToSourceLine : rowToTargetLine).get(r);
                if (line != null) {
                    return false;
                }
            }
            return true;
        }

        List<MutableBead> getData() {
            return Collections.unmodifiableList(data);
        }

        /**
         * Get a list of rows covered by the bead at <code>row</code>.
         * 
         * @param row
         * @return
         */
        List<Integer> getRowExtentsForBeadAtRow(int row) {
            MutableBead bead = rowToBead.get(row);
            List<Integer> result = new ArrayList<>();
            int firstIndex = rowToBead.indexOf(bead);
            if (firstIndex == -1) {
                throw new IllegalArgumentException();
            }
            for (int i = firstIndex; i < rowToBead.size(); i++) {
                if (rowToBead.get(i) != bead) {
                    break;
                }
                result.add(i);
            }
            return result;
        }

        boolean isEditableColumn(int col) {
            return col == COL_SRC || col == COL_TRG;
        }

        /**
         * Get a list of rows for which the cell in the specified <code>col</code> represents an actual line
         * (is not empty).
         * 
         * @param col
         * @param rows
         * @return
         */
        List<Integer> realCellsInRowSpan(int col, int... rows) {
            List<Integer> result = new ArrayList<Integer>();
            for (int row : rows) {
                if (getValueAt(row, col) != null) {
                    result.add(row);
                }
            }
            return result;
        }

        /**
         * Get the last row of the bead immediately preceding the one at the indicated <code>row</code>.
         * 
         * @param row
         * @return The row, or -1 if there is no previous bead
         */
        int prevBeadFromRow(int row) {
            return nextBeadFromRowByOffset(row, -1);
        }

        /**
         * Get the first row of the bead immediately after the one at the indicated <code>row</code>.
         * 
         * @param row
         * @return The row, or -1 if there is no next bead
         */
        int nextBeadFromRow(int row) {
            return nextBeadFromRowByOffset(row, 1);
        }

        private int nextBeadFromRowByOffset(int row, int offset) {
            MutableBead bead = rowToBead.get(row);
            for (int i = row + offset; i < getRowCount(); i += offset) {
                if (rowToBead.get(i) != bead) {
                    return i;
                }
            }
            return -1;
        }

        /**
         * Merge all lines at the indicated <code>rows</code> and <code>col</code> into the first specified
         * row. This is destructive in that it actually joins the strings together and replaces the existing
         * value.
         * 
         * @param rows
         * @param col
         * @return The resulting row
         */
        int mergeRows(List<Integer> rows, int col) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            int origRowCount = getRowCount();
            List<String> toCombine = new ArrayList<>();
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            toCombine.add(lines.get(rows.get(0)));
            for (int i = 1; i < rows.size(); i++) {
                int row = rows.get(i);
                String line = lines.get(row);
                toCombine.add(line);
                // XXX: Bead modified
                MutableBead bead = rowToBead.get(row);
                Util.removeByIdentity(col == COL_SRC ? bead.sourceLines : bead.targetLines, line);
                bead.status = Status.DEFAULT;
            }
            MutableBead trgBead = rowToBead.get(rows.get(0));
            List<String> trgLines = col == COL_SRC ? trgBead.sourceLines : trgBead.targetLines;
            Language lang = col == COL_SRC ? aligner.srcLang : aligner.trgLang;
            String combined = Util.join(lang, toCombine);
            // XXX: Bead modified
            trgLines.set(Util.indexByIdentity(trgLines, toCombine.get(0)), combined);
            trgBead.status = Status.DEFAULT;
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
            lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            return Util.indexByIdentity(lines, combined);
        }

        /**
         * Replace the line at <code>row</code> and <code>col</code> with the specified <code>split</code>
         * lines, which are inserted in its place. This is destructive in that it removes the original line
         * entirely.
         * 
         * @param row
         * @param col
         * @param split
         * @return A two-member array indicating the first and last resulting rows
         */
        int[] splitRow(int row, int col, String[] split) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            int origRowCount = getRowCount();
            MutableBead trgBead = rowToBead.get(row);
            List<String> trgLines = (col == COL_SRC ? trgBead.sourceLines : trgBead.targetLines);
            String line = (col == COL_SRC ? rowToSourceLine : rowToTargetLine).get(row);
            int insertAt = Util.indexByIdentity(trgLines, line);
            // XXX: Bead modified
            trgLines.set(insertAt++, split[0]);
            for (int i = 1; i < split.length; i++) {
                // XXX: Bead modified
                trgLines.add(insertAt++, split[i]);
            }
            trgBead.status = Status.DEFAULT;
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            return new int[] { Util.indexByIdentity(lines, split[0]),
                    Util.indexByIdentity(lines, split[split.length - 1]) };
        }

        /**
         * Replace the line at <code>row</code> and <code>col</code> with the specified <code>newVal</code>.
         * This is destructive in that it removes the original line entirely.
         * 
         * @param row
         * @param col
         * @param newVal
         */
        void editRow(int row, int col, String newVal) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            MutableBead trgBead = rowToBead.get(row);
            List<String> trgLines = (col == COL_SRC ? trgBead.sourceLines : trgBead.targetLines);
            String line = (col == COL_SRC ? rowToSourceLine : rowToTargetLine).get(row);
            int insertAt = Util.indexByIdentity(trgLines, line);
            // XXX: Bead modified
            trgLines.set(insertAt, newVal);
            makeCache();
        }

        /**
         * Move the lines at the specified <code>rows</code> and <code>col</code> by the specified offset,
         * e.g. +1 or -1 where negative indicates the upwards direction in the table.
         * <p>
         * This is different from {@link #move(List, int, int)} in that the intended effect is to give the
         * impression of each row moving by the offset relative to the opposing column. Because displayed rows
         * don't map directly to lines, that means some rows won't move at all, e.g. if the target row is
         * still the same bead.
         * 
         * @param rows
         * @param col
         * @param offset
         * @return A two-member array indicating the first and last resulting rows
         */
        int[] slide(List<Integer> rows, int col, int offset) {
            if (offset == 0) {
                return new int[0];
            }
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            Collections.sort(rows);
            if (offset > 0) {
                // Handling traversing empty rows when sliding down requires sliding in reverse order.
                Collections.reverse(rows);
            }
            int origRowCount = getRowCount();
            List<String> selected = new ArrayList<>(rows.size());
            for (int row : rows) {
                List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
                String line = lines.get(row);
                if (line == null) {
                    throw new IllegalArgumentException();
                }
                selected.add(line);
                MutableBead bead = rowToBead.get(row);
                int trgRow = row + offset;
                MutableBead trgBead;
                if (trgRow < 0) {
                    // New bead created
                    trgBead = new MutableBead();
                    data.add(0, trgBead);
                } else if (trgRow > rowToBead.size() - 1) {
                    // New bead created
                    trgBead = new MutableBead();
                    data.add(trgBead);
                } else {
                    trgBead = rowToBead.get(trgRow);
                }
                if (trgBead == bead) {
                    if (lines.get(trgRow) != null) {
                        // Already in target bead
                        continue;
                    } else {
                        // Moving down in unbalanced bead where target is blank cell -> split bead and place
                        // into resulting remainder bead
                        trgBead = splitBead(trgBead);
                    }
                }
                // XXX: Bead modified here
                Util.removeByIdentity(col == COL_SRC ? bead.sourceLines : bead.targetLines, line);
                bead.status = Status.DEFAULT;
                List<String> trgLines = col == COL_SRC ? trgBead.sourceLines : trgBead.targetLines;
                int insertIndex = trgRow > row ? 0 : trgLines.size();
                trgLines.add(insertIndex, line);
                trgBead.status = Status.DEFAULT;
            }
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            int[] resultRows = new int[] { Util.indexByIdentity(lines, selected.get(0)),
                    Util.indexByIdentity(lines, selected.get(selected.size() - 1)) };
            // Sort result rows so that callers can expect high-to-low order
            Arrays.sort(resultRows);
            return resultRows;
        }

        /**
         * Get the number of beads contained within the specified rows.
         * 
         * @param rows
         * @return
         */
        int beadsInRowSpan(int... rows) {
            List<MutableBead> beads = new ArrayList<MutableBead>();
            for (int row : rows) {
                MutableBead bead = rowToBead.get(row);
                if (!beads.contains(bead)) {
                    beads.add(bead);
                }
            }
            return beads.size();
        }

        /**
         * Split the lines specified at <code>rows</code> and <code>col</code> into multiple beads.
         * 
         * @param rows
         * @param col
         * @return A two-member array indicating the first and last resulting rows
         */
        void splitBead(int[] rows) {
            int origRowCount = getRowCount();
            MutableBead bead = rowToBead.get(rows[0]);
            int beadIndex = data.indexOf(bead);
            for (int row : rows) {
                String line = rowToSourceLine.get(row);
                List<String> indexFrom = bead.sourceLines;
                if (line == null) {
                    line = rowToTargetLine.get(row);
                    indexFrom = bead.targetLines;
                }
                int index = Util.indexByIdentity(indexFrom, line);
                if (index == -1) {
                    throw new IllegalArgumentException();
                }
                if (index > 0) {
                    bead = splitBeadByCount(bead, index);
                    data.add(++beadIndex, bead);
                }
            }
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
        }

        void toggleBeadsAtRows(int... rows) {
            List<MutableBead> beads = new ArrayList<MutableBead>(rows.length);
            for (int row : rows) {
                MutableBead bead = rowToBead.get(row);
                if (!beads.contains(bead)) {
                    bead.enabled = !bead.enabled;
                    beads.add(bead);
                }
            }
        }

        void toggleAllBeads(boolean value) {
            for (MutableBead bead : data) {
                bead.enabled = value;
            }
        }

        void setStatusAtRow(int row, MutableBead.Status status) {
            MutableBead bead = rowToBead.get(row);
            bead.status = status;
        }

        int nextNonEmptyCell(int row, int col) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            for (int i = row + 1; i < lines.size(); i++) {
                if (lines.get(i) != null) {
                    return i;
                }
            }
            return -1;
        }

        void replaceData(List<MutableBead> newData) {
            data.clear();
            data.addAll(newData);
            makeCache();
            fireTableDataChanged();
        }

        String removeLine(int row, int col) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            MutableBead bead = rowToBead.get(row);
            List<String> lines = col == COL_SRC ? rowToSourceLine : rowToTargetLine;
            String line = lines.get(row);
            // XXX: Bead modified here
            Util.removeByIdentity(col == COL_SRC ? bead.sourceLines : bead.targetLines, line);
            bead.status = Status.DEFAULT;
            return line;
        }

        int insertLines(List<String> lines, int row, int col) {
            if (!isEditableColumn(col)) {
                throw new IllegalArgumentException();
            }
            int origRowCount = getRowCount();
            MutableBead bead = rowToBead.get(row);
            // XXX: Bead modified here
            (col == COL_SRC ? bead.sourceLines : bead.targetLines).add(lines.get(0));
            bead.status = Status.DEFAULT;
            int beadInsertIndex = data.indexOf(bead) + 1;
            List<MutableBead> newBeads = new ArrayList<>();
            for (int i = 1; i < lines.size(); i++) {
                // New bead created
                MutableBead newBead = new MutableBead();
                (col == COL_SRC ? newBead.sourceLines : newBead.targetLines).add(lines.get(i));
                newBeads.add(newBead);
            }
            data.addAll(beadInsertIndex, newBeads);
            makeCache();
            if (origRowCount != getRowCount()) {
                fireTableDataChanged();
            }
            return Util.indexByIdentity(col == COL_SRC ? rowToSourceLine : rowToTargetLine, lines.get(0));
        }
    }

    @SuppressWarnings("serial")
    class AlignTransferHandler extends TransferHandler {

        @Override
        public int getSourceActions(JComponent c) {
            return TransferHandler.MOVE;
        }

        @Override
        protected Transferable createTransferable(JComponent c) {
            if (!(c instanceof JTable)) {
                return null;
            }
            JTable table = (JTable) c;
            return new TableSelection(table.getSelectedRows(), table.getSelectedColumns());
        }

        @Override
        public boolean canImport(TransferSupport support) {
            if (phase != Phase.EDIT) {
                return false;
            }
            if (!support.isDataFlavorSupported(ARRAY2DFLAVOR)) {
                return false;
            }
            try {
                Object o = support.getTransferable().getTransferData(ARRAY2DFLAVOR);
                int[][] sel = (int[][]) o;

                int[] rows = sel[0];
                if (rows.length < 1) {
                    return false;
                }

                int[] cols = sel[1];
                if (cols.length != 1) {
                    return false;
                }

                JTable table = (JTable) support.getComponent();
                BeadTableModel model = (BeadTableModel) table.getModel();

                int col = cols[0];
                if (!model.isEditableColumn(col)) {
                    return false;
                }

                javax.swing.JTable.DropLocation dloc = (javax.swing.JTable.DropLocation) support.getDropLocation();
                if (dloc.getColumn() != col) {
                    return false;
                }
                int trgRow = dloc.getRow();

                List<Integer> realRows = model.realCellsInRowSpan(col, rows);
                if (trgRow < realRows.get(0)) {
                    return model.canMoveTo(trgRow, realRows.get(0), col, true);
                } else if (trgRow > realRows.get(realRows.size() - 1)) {
                    return model.canMoveTo(trgRow, realRows.get(realRows.size() - 1), col, false);
                }
            } catch (Exception e) {
                Log.log(e);
            }
            return false;
        }

        @Override
        public boolean importData(TransferSupport support) {
            if (!canImport(support)) {
                return false;
            }

            try {
                Object o = support.getTransferable().getTransferData(ARRAY2DFLAVOR);
                int[][] sel = (int[][]) o;

                int[] rows = sel[0];
                int[] cols = sel[1];
                int col = cols[0];

                javax.swing.JTable.DropLocation dloc = (javax.swing.JTable.DropLocation) support.getDropLocation();
                int trgRow = dloc.getRow();

                moveRows(rows, col, trgRow);
                return true;
            } catch (Exception e) {
                Log.log(e);
            }
            return false;
        }
    }

    private static final DataFlavor ARRAY2DFLAVOR = new DataFlavor(int[][].class, "2D int array");

    static class TableSelection implements Transferable {
        private static final DataFlavor[] FLAVORS = new DataFlavor[] { ARRAY2DFLAVOR };
        private final int[] rows;
        private final int[] cols;

        public TableSelection(int[] rows, int[] cols) {
            this.rows = rows;
            this.cols = cols;
        }

        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return ARRAY2DFLAVOR.equals(flavor);
        }

        @Override
        public DataFlavor[] getTransferDataFlavors() {
            return FLAVORS;
        }

        @Override
        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
            if (ARRAY2DFLAVOR.equals(flavor)) {
                return new int[][] { rows, cols };
            }
            throw new UnsupportedFlavorException(flavor);
        }
    }

    static class DropLocationListener implements PropertyChangeListener {
        private static final int ERASE_MARGIN = 5;
        private static final int INSET_MARGIN = 3;
        private static final Border BORDER = new RoundedCornerBorder(8, Color.BLUE, RoundedCornerBorder.SIDE_ALL,
                2);

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            DropLocation oldVal = (DropLocation) evt.getOldValue();
            DropLocation newVal = (DropLocation) evt.getNewValue();
            if (equals(oldVal, newVal)) {
                return;
            }
            final JTable table = (JTable) evt.getSource();
            if (oldVal != null) {
                Rectangle rect = rectForTarget(table, oldVal);
                rect.grow(ERASE_MARGIN, ERASE_MARGIN);
                table.paintImmediately(rect);
            }
            if (newVal != null) {
                final Rectangle rect = rectForTarget(table, newVal);
                rect.grow(INSET_MARGIN, INSET_MARGIN);
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        BORDER.paintBorder(table, table.getGraphics(), rect.x, rect.y, rect.width, rect.height);
                    }
                });
            }
        }

        private boolean equals(DropLocation oldVal, DropLocation newVal) {
            if (oldVal == newVal) {
                return true;
            }
            if (oldVal == null || newVal == null) {
                return false;
            }
            return oldVal.getColumn() == newVal.getColumn() && oldVal.getRow() == newVal.getRow();
        }

        private Rectangle rectForTarget(JTable table, DropLocation loc) {
            BeadTableModel model = (BeadTableModel) table.getModel();
            List<Integer> rows = model.getRowExtentsForBeadAtRow(loc.getRow());
            return table.getCellRect(rows.get(0), BeadTableModel.COL_SRC, true)
                    .union(table.getCellRect(rows.get(rows.size() - 1), BeadTableModel.COL_TRG, true));
        }
    }

    static class EnumRenderer<T extends Enum<?>> extends DelegatingComboBoxRenderer<T, String> {
        private final String keyPrefix;

        public EnumRenderer(String keyPrefix) {
            this.keyPrefix = keyPrefix;
        }

        @Override
        protected String getDisplayText(T value) {
            if (value == null) {
                return null;
            }
            try {
                return OStrings.getString(keyPrefix + value.name());
            } catch (MissingResourceException ex) {
                return value.name();
            }
        }
    }
}