/************************************************************************** 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.issues; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionEvent; 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.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import javax.swing.AbstractAction; import javax.swing.AbstractListModel; import javax.swing.DefaultListModel; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JTable; import javax.swing.KeyStroke; import javax.swing.ListModel; import javax.swing.RowFilter; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import javax.swing.text.JTextComponent; import org.apache.commons.io.FilenameUtils; import org.omegat.core.Core; import org.omegat.core.CoreEvents; import org.omegat.core.data.DataUtils; import org.omegat.core.data.IProject; import org.omegat.core.data.SourceTextEntry; import org.omegat.core.data.TMXEntry; import org.omegat.util.Log; import org.omegat.util.OStrings; import org.omegat.util.Platform; import org.omegat.util.Preferences; import org.omegat.util.StreamUtil; import org.omegat.util.StringUtil; import org.omegat.util.gui.DataTableStyling; import org.omegat.util.gui.OSXIntegration; import org.omegat.util.gui.ResourcesUtil; import org.omegat.util.gui.StaticUIUtils; import org.omegat.util.gui.TableColumnSizer; /** * A controller to orchestrate the {@link IssuesPanel}. * * @author Aaron Madlon-Kay * */ public class IssuesPanelController implements IIssues { static final String ACTION_KEY_JUMP_TO_SELECTED_ISSUE = "jumpToSelectedIssue"; static final String ACTION_KEY_FOCUS_ON_TYPES_LIST = "focusOnTypesList"; static final String ALL_FILES_PATTERN = ".*"; static final String NO_INSTRUCTIONS = ""; static final double INNER_SPLIT_INITIAL_RATIO = 0.25d; static final double OUTER_SPLIT_INITIAL_RATIO = 0.5d; static final Icon SETTINGS_ICON = new ImageIcon(ResourcesUtil.getBundledImage("appbar.settings.active.png")); static final Icon SETTINGS_ICON_INACTIVE = new ImageIcon( ResourcesUtil.getBundledImage("appbar.settings.inactive.png")); static final Icon SETTINGS_ICON_PRESSED = new ImageIcon( ResourcesUtil.getBundledImage("appbar.settings.pressed.png")); static final Icon SETTINGS_ICON_INVISIBLE = new Icon() { @Override public void paintIcon(Component c, Graphics g, int x, int y) { } @Override public int getIconWidth() { return SETTINGS_ICON.getIconWidth(); } @Override public int getIconHeight() { return SETTINGS_ICON.getIconHeight(); } }; final Window parent; JFrame frame; IssuesPanel panel; TableColumnSizer colSizer; String filePattern; String instructions; int mouseoverCol = -1; int mouseoverRow = -1; int selectedEntry = -1; String selectedType = null; IssueLoader loader; public IssuesPanelController(Window parent) { this.parent = parent; } @SuppressWarnings("serial") synchronized void init() { if (frame != null) { // Regenerate menu bar to reflect current prefs frame.setJMenuBar(generateMenuBar()); return; } frame = new JFrame(OStrings.getString("ISSUES_WINDOW_TITLE")); StaticUIUtils.setEscapeClosable(frame); StaticUIUtils.setWindowIcon(frame); if (Platform.isMacOSX()) { OSXIntegration.enableFullScreen(frame); } panel = new IssuesPanel(); frame.add(panel); frame.setJMenuBar(generateMenuBar()); frame.setPreferredSize(new Dimension(600, 400)); frame.pack(); frame.setLocationRelativeTo(parent); panel.innerSplitPane.setDividerLocation(INNER_SPLIT_INITIAL_RATIO); panel.outerSplitPane.setDividerLocation(OUTER_SPLIT_INITIAL_RATIO); StaticUIUtils.persistGeometry(frame, Preferences.ISSUES_WINDOW_GEOMETRY_PREFIX, () -> { Preferences.setPreference(Preferences.ISSUES_WINDOW_DIVIDER_LOCATION_BOTTOM, panel.outerSplitPane.getDividerLocation()); }); try { int bottomDL = Integer .parseInt(Preferences.getPreference(Preferences.ISSUES_WINDOW_DIVIDER_LOCATION_BOTTOM)); panel.outerSplitPane.setDividerLocation(bottomDL); } catch (NumberFormatException e) { // Ignore } frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { reset(); } }); if (Preferences.isPreference(Preferences.PROJECT_FILES_USE_FONT)) { String fontName = Preferences.getPreference(Preferences.TF_SRC_FONT_NAME); int fontSize = Integer.parseInt(Preferences.getPreference(Preferences.TF_SRC_FONT_SIZE)); setFont(new Font(fontName, Font.PLAIN, fontSize)); } panel.table.getSelectionModel().addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { viewSelectedIssueDetail(); selectedEntry = getSelectedIssue().map(IIssue::getSegmentNumber).orElse(-1); } }); panel.table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ACTION_KEY_JUMP_TO_SELECTED_ISSUE); panel.table.getActionMap().put(ACTION_KEY_JUMP_TO_SELECTED_ISSUE, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { jumpToSelectedIssue(); } }); // Swap focus between the Types list and Issues table; don't allow // tabbing within the table because it's pointless. Maybe this would be // better accomplished by adjusting the focus traversal policy? panel.table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK), ACTION_KEY_FOCUS_ON_TYPES_LIST); panel.table.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), ACTION_KEY_FOCUS_ON_TYPES_LIST); panel.table.getActionMap().put(ACTION_KEY_FOCUS_ON_TYPES_LIST, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { if (panel.typeList.isVisible()) { panel.typeList.requestFocusInWindow(); } } }); panel.closeButton.addActionListener(e -> StaticUIUtils.closeWindowByEvent(frame)); MouseAdapter adapter = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) { jumpToSelectedIssue(); } else if (e.getButton() == MouseEvent.BUTTON1 && mouseoverCol == IssueColumn.ACTION_BUTTON.index) { doPopup(e); } } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { doPopup(e); } } @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { doPopup(e); } } @Override public void mouseExited(MouseEvent e) { updateRollover(); } @Override public void mouseMoved(MouseEvent e) { updateRollover(); } private void doPopup(MouseEvent e) { getIssueAt(e.getPoint()).ifPresent(issue -> showPopupMenu(e.getComponent(), e.getPoint(), issue)); } }; panel.table.addMouseListener(adapter); panel.table.addMouseMotionListener(adapter); panel.typeList.addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { updateFilter(); selectedType = getSelectedType().orElse(null); } }); panel.jumpButton.addActionListener(e -> jumpToSelectedIssue()); panel.reloadButton.addActionListener(e -> refreshData(selectedEntry, selectedType)); panel.showAllButton.addActionListener(e -> showAll()); colSizer = TableColumnSizer.autoSize(panel.table, IssueColumn.DESCRIPTION.index, true); CoreEvents.registerProjectChangeListener(e -> { switch (e) { case CLOSE: SwingUtilities.invokeLater(() -> { filePattern = ALL_FILES_PATTERN; instructions = NO_INSTRUCTIONS; reset(); frame.setVisible(false); }); break; case MODIFIED: if (frame.isVisible()) { SwingUtilities.invokeLater(() -> refreshData(selectedEntry, selectedType)); } break; default: } }); CoreEvents.registerFontChangedEventListener(f -> { if (!Preferences.isPreference(Preferences.PROJECT_FILES_USE_FONT)) { f = new JTable().getFont(); } setFont(f); viewSelectedIssueDetail(); }); } JMenuBar generateMenuBar() { JMenuBar menuBar = new JMenuBar(); JMenu menu = menuBar.add(new JMenu(OStrings.getString("ISSUES_WINDOW_MENU_OPTIONS"))); { // Tags item is hard-coded because it is not disableable and is implemented differently from all // others. JCheckBoxMenuItem tagsItem = new JCheckBoxMenuItem( OStrings.getString("ISSUES_WINDOW_MENU_OPTIONS_TOGGLE_PROVIDER", OStrings.getString("ISSUES_TAGS_PROVIDER_NAME"))); tagsItem.setSelected(true); tagsItem.setEnabled(false); menu.add(tagsItem); } Set<String> disabledProviders = IssueProviders.getDisabledProviderIds(); IssueProviders.getIssueProviders().stream().sorted(Comparator.comparing(IIssueProvider::getId)) .forEach(provider -> { String label = StringUtil.format( OStrings.getString("ISSUES_WINDOW_MENU_OPTIONS_TOGGLE_PROVIDER"), provider.getName()); JCheckBoxMenuItem item = new JCheckBoxMenuItem(label); item.addActionListener(e -> { IssueProviders.setProviderEnabled(provider.getId(), item.isSelected()); refreshData(selectedEntry, selectedType); }); item.setSelected(!disabledProviders.contains(provider.getId())); menu.add(item); }); menu.addSeparator(); { JCheckBoxMenuItem askItem = new JCheckBoxMenuItem( OStrings.getString("ISSUES_WINDOW_MENU_DONT_ASK")); askItem.setSelected(Preferences.isPreference(Preferences.ISSUE_PROVIDERS_DONT_ASK)); askItem.addActionListener(e -> Preferences.setPreference(Preferences.ISSUE_PROVIDERS_DONT_ASK, askItem.isSelected())); menu.add(askItem); } return menuBar; } void updateRollover() { // Rows here are all in terms of the view, not the model. Point point = panel.table.getMousePosition(); int oldRow = mouseoverRow; int oldCol = mouseoverCol; int newRow = point == null ? -1 : panel.table.rowAtPoint(point); int newCol = point == null ? -1 : panel.table.columnAtPoint(point); boolean doRepaint = newRow != oldRow || newCol != oldCol; mouseoverRow = newRow; mouseoverCol = newCol; if (doRepaint) { Rectangle rect = panel.table.getCellRect(oldRow, IssueColumn.ACTION_BUTTON.index, true); panel.table.repaint(rect); rect = panel.table.getCellRect(newRow, IssueColumn.ACTION_BUTTON.index, true); panel.table.repaint(rect); } } void setFont(Font font) { panel.typeList.setFont(font); DataTableStyling.applyFont(panel.table, font); panel.messageLabel.setFont(font); } void viewSelectedIssueDetail() { Optional<IIssue> issue = getSelectedIssue(); issue.map(IIssue::getDetailComponent).ifPresent(comp -> { if (Preferences.isPreference(Preferences.PROJECT_FILES_USE_FONT)) { Font font = Core.getMainWindow().getApplicationFont(); StaticUIUtils.visitHierarchy(comp, c -> c instanceof JTextComponent, c -> c.setFont(font)); } panel.outerSplitPane.setBottomComponent(comp); }); panel.jumpButton.setEnabled(issue.isPresent()); } void jumpToSelectedIssue() { getSelectedIssue().map(IIssue::getSegmentNumber).ifPresent(i -> { Core.getEditor().gotoEntry(i); JFrame mwf = Core.getMainWindow().getApplicationFrame(); mwf.setState(JFrame.NORMAL); mwf.toFront(); }); } Optional<IIssue> getIssueAt(Point p) { return getIssueAtRow(panel.table.rowAtPoint(p)); } Optional<IIssue> getSelectedIssue() { return getIssueAtRow(panel.table.getSelectedRow()); } Optional<IIssue> getIssueAtRow(int row) { if (row < 0) { return Optional.empty(); } TableModel model = panel.table.getModel(); if (!(model instanceof IssuesTableModel) || model.getRowCount() == 0) { return Optional.empty(); } IssuesTableModel imodel = (IssuesTableModel) model; int realSelection = panel.table.getRowSorter().convertRowIndexToModel(row); return Optional.of(imodel.getIssueAt(realSelection)); } Optional<String> getSelectedType() { return getTypeAtRow(panel.typeList.getSelectedIndex()); } Optional<String> getTypeAtRow(int row) { if (row < 0) { return Optional.empty(); } ListModel<String> model = panel.typeList.getModel(); if (!(model instanceof TypeListModel)) { return Optional.empty(); } TypeListModel tModel = (TypeListModel) model; return Optional.of(tModel.getTypeAt(row)); } void showPopupMenu(Component source, Point p, IIssue issue) { List<? extends JMenuItem> items = issue.getMenuComponents(); if (items.isEmpty()) { return; } JPopupMenu menu = new JPopupMenu(); items.forEach(menu::add); menu.show(source, p.x, p.y); } @Override public void showAll() { show(ALL_FILES_PATTERN, NO_INSTRUCTIONS, -1); } @Override public void showAll(String instructions) { show(ALL_FILES_PATTERN, instructions, -1); } @Override public void showForFiles(String filePattern) { show(filePattern, NO_INSTRUCTIONS, -1); } @Override public void showForFiles(String filePattern, String instructions) { show(filePattern, instructions, -1); } @Override public void showForFiles(String filePattern, int jumpToEntry) { show(filePattern, NO_INSTRUCTIONS, jumpToEntry); } private void show(String filePattern, String instructions, int jumpToEntry) { this.filePattern = filePattern; this.instructions = instructions; init(); SwingUtilities.invokeLater(() -> refreshData(jumpToEntry, null)); } void reset() { if (loader != null) { loader.cancel(true); loader = null; } frame.setTitle(OStrings.getString("ISSUES_WINDOW_TITLE")); panel.table.setModel(new DefaultTableModel()); panel.typeList.setModel(new DefaultListModel<>()); panel.outerSplitPane.setBottomComponent(panel.messageLabel); panel.messageLabel.setText(OStrings.getString("ISSUES_LOADING")); StaticUIUtils.setHierarchyEnabled(panel, false); panel.closeButton.setEnabled(true); panel.showAllButtonPanel.setVisible(!isShowingAllFiles()); panel.instructionsPanel.setVisible(!instructions.equals(NO_INSTRUCTIONS)); panel.instructionsTextArea.setText(instructions); } synchronized void refreshData(int jumpToEntry, String jumpToType) { reset(); if (!frame.isVisible()) { // Don't call setVisible if already visible, because the window will // steal focus frame.setVisible(true); } frame.setState(JFrame.NORMAL); panel.progressBar.setValue(0); panel.progressBar.setMaximum(Core.getProject().getAllEntries().size()); panel.progressBar.setVisible(true); panel.progressBar.setEnabled(true); loader = new IssueLoader(jumpToEntry, jumpToType); loader.execute(); } class IssueLoader extends SwingWorker<List<IIssue>, Integer> { private final int jumpToEntry; private final String jumpToType; private int progress = 0; public IssueLoader(int jumpToEntry, String jumpToType) { this.jumpToEntry = jumpToEntry; this.jumpToType = jumpToType; } @Override protected List<IIssue> doInBackground() throws Exception { long start = System.currentTimeMillis(); Stream<IIssue> tagErrors = Core.getTagValidation().listInvalidTags(filePattern).stream() .map(TagIssue::new); List<IIssueProvider> providers = IssueProviders.getEnabledProviders(); Stream<IIssue> providerIssues = Core.getProject().getAllEntries().parallelStream() .filter(StreamUtil.patternFilter(filePattern, ste -> ste.getKey().file)) .filter(this::progressFilter).map(this::makeEntryPair) .filter(Objects::nonNull).flatMap(e -> providers.stream() .flatMap(provider -> provider.getIssues(e.getKey(), e.getValue()).stream())); List<IIssue> result = Stream.concat(tagErrors, providerIssues).collect(Collectors.toList()); Logger.getLogger(IssuesPanelController.class.getName()).log(Level.FINEST, () -> String.format("Issue detection took %.3f s", (System.currentTimeMillis() - start) / 1000f)); return result; } Map.Entry<SourceTextEntry, TMXEntry> makeEntryPair(SourceTextEntry ste) { IProject project = Core.getProject(); if (!project.isProjectLoaded()) { return null; } TMXEntry tmxEntry = project.getTranslationInfo(ste); if (!tmxEntry.isTranslated()) { return null; } if (isShowingAllFiles() && DataUtils.isDuplicate(ste, tmxEntry)) { return null; } return new AbstractMap.SimpleImmutableEntry<SourceTextEntry, TMXEntry>(ste, tmxEntry); } boolean progressFilter(SourceTextEntry ste) { boolean continu = !isCancelled(); if (continu) { publish(ste.entryNum()); } return continu; } @Override protected void process(List<Integer> chunks) { if (!chunks.isEmpty()) { panel.progressBar.setValue(progress += chunks.size()); } } @Override protected void done() { if (isCancelled()) { return; } List<IIssue> allIssues = Collections.emptyList(); try { allIssues = get(); } catch (InterruptedException | ExecutionException e) { Log.log(e); JOptionPane.showMessageDialog(parent, e.getMessage(), OStrings.getString("ERROR_TITLE"), JOptionPane.ERROR_MESSAGE); frame.setVisible(false); return; } catch (CancellationException e) { return; } if (allIssues.isEmpty()) { panel.messageLabel.setText(OStrings.getString("ISSUES_NO_ISSUES_FOUND")); } panel.progressBar.setVisible(false); StaticUIUtils.setHierarchyEnabled(panel, true); panel.typeList.setModel(new TypeListModel(allIssues)); panel.table.setModel(new IssuesTableModel(allIssues)); TableRowSorter<?> sorter = (TableRowSorter<?>) panel.table.getRowSorter(); sorter.setSortable(IssueColumn.ICON.index, false); sorter.toggleSortOrder(IssueColumn.SEG_NUM.index); panel.typeList.setSelectedIndex(0); // Hide Types list if we have fewer than 3 items ("All" and at least // two others) boolean typeListIsVisible = panel.typeList.getModel().getSize() > 2; panel.typeListScrollPanel.setVisible(typeListIsVisible); if (typeListIsVisible) { SwingUtilities.invokeLater(() -> { int width = panel.typeListScrollPanel.getPreferredSize().width + 10; panel.innerSplitPane.setDividerLocation(width); }); } colSizer.reset(); colSizer.adjustTableColumns(); if (jumpToType != null) { ((TypeListModel) panel.typeList.getModel()).indexOfType(jumpToType) .ifPresent(panel.typeList::setSelectedIndex); } if (jumpToEntry >= 0) { IntStream.range(0, panel.table.getRowCount()) .filter(row -> (int) panel.table.getValueAt(row, IssueColumn.SEG_NUM.index) >= jumpToEntry) .findFirst().ifPresent(jump -> panel.table.changeSelection(jump, 0, false, false)); } panel.table.requestFocusInWindow(); } } void updateFilter() { int selection = panel.typeList.getSelectedIndex(); if (selection < 0) { return; } TypeListModel model = ((TypeListModel) panel.typeList.getModel()); String type = model.getTypeAt(selection); @SuppressWarnings("unchecked") TableRowSorter<IssuesTableModel> sorter = (TableRowSorter<IssuesTableModel>) panel.table.getRowSorter(); sorter.setRowFilter(new RowFilter<IssuesTableModel, Integer>() { @Override public boolean include(RowFilter.Entry<? extends IssuesTableModel, ? extends Integer> entry) { return type == ALL_TYPES || entry.getStringValue(IssueColumn.TYPE.index).equals(type); } }); int totalItems = panel.table.getModel().getRowCount(); if (type == ALL_TYPES) { updateTitle(totalItems); } else { updateTitle((int) model.getCountAt(selection), totalItems); } panel.table.changeSelection(0, 0, false, false); } void updateTitle(int totalItems) { if (isShowingAllFiles()) { frame.setTitle(StringUtil.format(OStrings.getString("ISSUES_WINDOW_TITLE_TEMPLATE"), totalItems)); } else { String filePath = filePattern.replace("\\Q", "").replace("\\E", ""); frame.setTitle(StringUtil.format(OStrings.getString("ISSUES_WINDOW_TITLE_FILE_TEMPLATE"), FilenameUtils.getName(filePath), totalItems)); } } void updateTitle(int shownItems, int totalItems) { if (isShowingAllFiles()) { frame.setTitle(StringUtil.format(OStrings.getString("ISSUES_WINDOW_TITLE_FILTERED_TEMPLATE"), shownItems, totalItems)); } else { String filePath = filePattern.replace("\\Q", "").replace("\\E", ""); frame.setTitle(StringUtil.format(OStrings.getString("ISSUES_WINDOW_TITLE_FILE_FILTERED_TEMPLATE"), FilenameUtils.getName(filePath), shownItems, totalItems)); } } boolean isShowingAllFiles() { return ALL_FILES_PATTERN.equals(filePattern); } enum IssueColumn { SEG_NUM(0, OStrings.getString("ISSUES_TABLE_COLUMN_ENTRY_NUM"), Integer.class), ICON(1, "", Icon.class), TYPE(2, OStrings.getString("ISSUES_TABLE_COLUMN_TYPE"), String.class), DESCRIPTION(3, OStrings.getString("ISSUES_TABLE_COLUMN_DESCRIPTION"), String.class), ACTION_BUTTON(4, "", Icon.class); private final int index; private final String label; private final Class<?> clazz; private IssueColumn(int index, String label, Class<?> clazz) { this.index = index; this.label = label; this.clazz = clazz; } static IssueColumn get(int index) { return values()[index]; } } Icon getActionMenuIcon(IIssue issue, int modelRow, int col) { // The row argument is in terms of the model while mouseoverRow is in // terms of the view, so convert first. int viewRow = panel.table.getRowSorter().convertRowIndexToView(modelRow); if (!issue.hasMenuComponents()) { return SETTINGS_ICON_INVISIBLE; } else if (panel.table.getSelectedRow() == viewRow) { // Show "pressed" version here for better contrast against the table // selection highlight. return SETTINGS_ICON_PRESSED; } else if (viewRow == mouseoverRow && col == mouseoverCol) { return SETTINGS_ICON; } else if (viewRow == mouseoverRow) { return SETTINGS_ICON_INACTIVE; } else { return SETTINGS_ICON_INVISIBLE; } } @SuppressWarnings("serial") class IssuesTableModel extends AbstractTableModel { private final List<IIssue> issues; public IssuesTableModel(List<IIssue> issues) { this.issues = issues; } @Override public int getRowCount() { return issues.size(); } @Override public int getColumnCount() { return IssueColumn.values().length; } @Override public String getColumnName(int column) { return IssueColumn.get(column).label; } @Override public Object getValueAt(int rowIndex, int columnIndex) { IIssue iss = issues.get(rowIndex); switch (IssueColumn.get(columnIndex)) { case SEG_NUM: return iss.getSegmentNumber(); case ICON: return iss.getIcon(); case TYPE: return iss.getTypeName(); case DESCRIPTION: return iss.getDescription(); case ACTION_BUTTON: return getActionMenuIcon(iss, rowIndex, columnIndex); } throw new IllegalArgumentException("Unknown column requested: " + columnIndex); } public IIssue getIssueAt(int rowIndex) { return issues.get(rowIndex); } @Override public Class<?> getColumnClass(int columnIndex) { return IssueColumn.get(columnIndex).clazz; } } static final String ALL_TYPES = new String(OStrings.getString("ISSUES_TYPE_ALL")); @SuppressWarnings("serial") static class TypeListModel extends AbstractListModel<String> { private final List<Map.Entry<String, Long>> types; public TypeListModel(List<IIssue> issues) { this.types = calculateData(issues); } List<Map.Entry<String, Long>> calculateData(List<IIssue> issues) { Map<String, Long> counts = issues.stream() .map(IIssue::getTypeName) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); List<Map.Entry<String, Long>> result = new ArrayList<>(); result.add(new AbstractMap.SimpleImmutableEntry<String, Long>(ALL_TYPES, (long) issues.size())); counts.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEach(result::add); return result; } @Override public int getSize() { return types.size(); } @Override public String getElementAt(int index) { Map.Entry<String, Long> entry = types.get(index); return StringUtil.format(OStrings.getString("ISSUES_TYPE_SUMMARY_TEMPLATE"), entry.getKey(), entry.getValue()); } String getTypeAt(int index) { return types.get(index).getKey(); } long getCountAt(int index) { return types.get(index).getValue(); } OptionalInt indexOfType(String type) { return IntStream.range(0, types.size()).filter(i -> types.get(i).getKey().equals(type)).findFirst(); } } }