org.omegat.gui.issues.IssuesPanelController.java Source code

Java tutorial

Introduction

Here is the source code for org.omegat.gui.issues.IssuesPanelController.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.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")
    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();
        }
    }
}