/**************************************************************************
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;
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;
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;
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;
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();
}
}
}
}