/* * Copyright 2013 Hewlett-Packard Development Company, L.P * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hp.alm.ali.idea.ui; import com.hp.alm.ali.idea.model.ItemsProvider; import com.hp.alm.ali.idea.model.KeyValue; import com.hp.alm.ali.idea.ui.dialog.MyDialog; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.IconLoader; import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.labels.LinkLabel; import com.intellij.ui.components.labels.LinkListener; import com.intellij.ui.table.JBTable; import com.intellij.util.ui.StatusText; import com.intellij.util.ui.UIUtil; import net.coderazzi.filters.gui.IFilterEditor; import net.coderazzi.filters.gui.IFilterHeaderObserver; import net.coderazzi.filters.gui.TableFilterHeader; import org.apache.commons.lang.ArrayUtils; import javax.swing.BorderFactory; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JToggleButton; import javax.swing.ListSelectionModel; import javax.swing.RowSorter; import javax.swing.SortOrder; import javax.swing.UIDefaults; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.RowSorterListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; public class MultipleItemsDialog<K, E extends KeyValue<K>> extends MyDialog implements IFilterHeaderObserver { private JLabel tooMany; private JLabel selected; private JToggleButton toggleSelected; private JButton okButton; private boolean ok; private MultipleItemsDialogModel<K, E> model; private JBTable table; private TableFilterHeader header; private MySelectionModel mySelectionModel; private MyListSelectionListener myListSelectionListener; public MultipleItemsDialog(Project project, String title, final boolean multiple, final List<E> fieldList, final List<K> selectedFields) { this(project, "Select " + title, new MultipleItemsDialogModel<K, E>(title, multiple, new ItemsProvider.Eager<E>(fieldList), selectedFields, null)); } public MultipleItemsDialog(Project project, String title, final MultipleItemsDialogModel<K, E> model) { super(project, title, true); this.model = model; mySelectionModel = new MySelectionModel(); myListSelectionListener = new MyListSelectionListener(); tooMany = new JLabel("Too many results, narrow your search"); tooMany.setBorder(BorderFactory.createEtchedBorder()); tooMany.setVisible(false); selected = new JLabel("Showing currently selected items"); selected.setVisible(false); toggleSelected = new JToggleButton(); toggleSelected.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { model.setShowingSelected(toggleSelected.isSelected()); if(!model.isShowingSelected() && !model.getSelectedFields().isEmpty()) { updateSelectionFromModel(); } else if(model.isShowingSelected()) { header.getFilterEditor(1).setContent(""); } } }); updateSelected(); table = new JBTable() { @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { int column = convertColumnIndexToModel(columnIndex); mySelectionModel.setFirstColumnEvent(column == 0); super.changeSelection(rowIndex, columnIndex, toggle, extend); } }; table.setRowSelectionAllowed(true); table.setColumnSelectionAllowed(false); table.setAutoCreateColumnsFromModel(false); table.setModel(model); final MyTableRowSorter sorter = new MyTableRowSorter(model); table.setRowSorter(sorter); table.setDefaultRenderer(Boolean.class, new MyRenderer()); table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); table.setSelectionModel(mySelectionModel); sorter.setIgnoreAddRowSorterListener(true); // prevent auto-selection (functionality not accessible via proper API) header = new TableFilterHeader(table); sorter.setIgnoreAddRowSorterListener(false); sorter.setSortKeys(Arrays.asList(new RowSorter.SortKey(1, SortOrder.ASCENDING))); JPanel panel = new JPanel(new BorderLayout()); JPanel toolbar = new JPanel(new BorderLayout()); toolbar.setBorder(BorderFactory.createEtchedBorder()); panel.add(toolbar, BorderLayout.NORTH); toolbar.add(toggleSelected, BorderLayout.EAST); if(model.isMultiple()) { table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.getColumnModel().addColumn(createColumn(0, model, 45, 45)); header.getFilterEditor(0).setEditable(false); header.getFilterEditor(0).setUserInteractionEnabled(false); final LinkListener selectUnselect = new LinkListener() { public void linkSelected(LinkLabel aSource, Object aLinkData) { if(model.isShowingSelected()) { if(!Boolean.TRUE.equals(aLinkData)) { List<Integer> ixs = new ArrayList<Integer>(); for (int i = 0; i < sorter.getViewRowCount(); i++) { ixs.add(sorter.convertRowIndexToModel(i)); } // make sure indexes are not affected by removal by starting from the last Collections.sort(ixs); Collections.reverse(ixs); for(int ix: ixs) { model.setValueAt(aLinkData, ix, 0); } } } else { if(Boolean.TRUE.equals(aLinkData)) { mySelectionModel.doAddSelectionInterval(0, table.getRowCount() - 1); } else { mySelectionModel.removeSelectionInterval(0, table.getRowCount() - 1); } } } }; JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT)); left.add(new LinkLabel("Select All", IconLoader.getIcon("/actions/selectall.png"), selectUnselect, true)); left.add(new LinkLabel("Unselect All", IconLoader.getIcon("/actions/unselectall.png"), selectUnselect, false)); toolbar.add(left, BorderLayout.WEST); } else { table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); } table.getColumnModel().addColumn(createColumn(1, model, 450, null)); table.getSelectionModel().addListSelectionListener(myListSelectionListener); model.addTableModelListener(new TableModelListener() { @Override public void tableChanged(TableModelEvent e) { selected.setVisible(model.isShowingSelected()); tooMany.setVisible(model.hasMore() && !model.isShowingSelected()); updateSelected(); } }); JPanel contentPanel = new JPanel(new BorderLayout()); contentPanel.add(selected, BorderLayout.NORTH); contentPanel.add(new JBScrollPane(table), BorderLayout.CENTER); contentPanel.add(tooMany, BorderLayout.SOUTH); panel.add(contentPanel, BorderLayout.CENTER); JPanel buttons = new JPanel(); okButton = new JButton("OK"); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { ok = true; close(true); } }); buttons.add(okButton); JButton cancel = new JButton("Cancel"); cancel.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { close(false); } }); buttons.add(cancel); panel.add(buttons, BorderLayout.SOUTH); getContentPane().add(panel, BorderLayout.CENTER); pack(); setResizable(false); centerOnOwner(); requestPropertyFilterFocus(header); load(true, null); } private void updateSelectionFromModel() { // try to highlight selected item when switching back from selected view myListSelectionListener.setIgnoreUpdateEvent(true); try { for (int i = 0; i < table.getRowCount(); i++) { int modelRow = table.convertRowIndexToModel(i); E value = model.getFields().get(modelRow); if(model.getSelectedFields().contains(value.getKey())) { mySelectionModel.doAddSelectionInterval(i, i); if(!model.isMultiple()) { break; } } } } finally { myListSelectionListener.setIgnoreUpdateEvent(false); } } private void updateOk() { okButton.setEnabled(model.isMultiple() || !model.getSelectedFields().isEmpty()); } private void updateSelected() { int size = model.getSelectedFields().size(); if(size > 0) { toggleSelected.setVisible(true); if(model.isMultiple()) { toggleSelected.setText("Show Selected (" + size + ")"); } else { toggleSelected.setText("Show Selected"); } } else { toggleSelected.setSelected(false); toggleSelected.setVisible(false); model.setShowingSelected(false); } } @Override public void tableFilterEditorCreated(TableFilterHeader tableFilterHeader, IFilterEditor iFilterEditor, TableColumn tableColumn) { } @Override public void tableFilterEditorExcluded(TableFilterHeader tableFilterHeader, IFilterEditor iFilterEditor, TableColumn tableColumn) { } @Override public void tableFilterUpdated(TableFilterHeader tableFilterHeader, IFilterEditor iFilterEditor, TableColumn tableColumn) { String filter = iFilterEditor.getContent().toString(); if(!filter.isEmpty() && !filter.endsWith("*")) { // enforce prefix semantics to remain compatible with instant search that is used when all items fit into the first (filter-less) request iFilterEditor.setContent(filter + "*"); } else { load(false, filter); } } private void load(final boolean first, final String filter) { final StatusText emptyText = table.getEmptyText(); final String originalValue = emptyText.getText(); emptyText.setText("Loading..."); okButton.setEnabled(false); ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { model.load(filter); UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { if (model.hasMore() && first) { // truncated: if query changes execute fetch data again header.addHeaderObserver(MultipleItemsDialog.this); header.setInstantFiltering(false); header.setFilterOnUpdates(false); } emptyText.setText(originalValue); updateSelectionFromModel(); updateOk(); } }); } }); } private void requestPropertyFilterFocus(TableFilterHeader header) { IFilterEditor editor = header.getFilterEditor(model.getColumnCount() - 1); if(editor instanceof JComponent) { if(((JComponent) editor).getComponentCount() == 2) { ((JComponent) editor).getComponent(1).requestFocus(); } } } private TableColumn createColumn(int index, TableModel model, int preferredWidth, Integer minWidth) { TableColumn column = new TableColumn(index); column.setPreferredWidth(preferredWidth); if(minWidth != null) { column.setMinWidth(minWidth); } column.setHeaderValue(model.getColumnName(index)); return column; } public boolean isOk() { return ok; } private class MyRenderer extends JCheckBox implements TableCellRenderer { private Color selectedColor; private Color unselectedColor; public MyRenderer() { setHorizontalAlignment(JCheckBox.CENTER); UIDefaults defaults = javax.swing.UIManager.getDefaults(); selectedColor = defaults.getColor("List.selectionBackground"); unselectedColor = defaults.getColor("List.background"); } public Component getTableCellRendererComponent(JTable jTable, Object value, boolean isSelected, boolean hasFocus, int rowIndex, int colIndex) { setSelected(Boolean.TRUE.equals(value)); setBackground(table.isRowSelected(rowIndex)? selectedColor: unselectedColor); return this; } } private static class MyTableRowSorter extends TableRowSorter<TableModel> { private boolean ignoreAddRowSorterListener; public MyTableRowSorter(TableModel model) { super(model); } @Override public void addRowSorterListener(RowSorterListener l) { if(!ignoreAddRowSorterListener) { super.addRowSorterListener(l); } } public void setIgnoreAddRowSorterListener(boolean ignore) { this.ignoreAddRowSorterListener = ignore; } } private class MyListSelectionListener implements ListSelectionListener { private boolean ignoreUpdateEvent; @Override public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting() && !model.isShowingSelected()) { update(); } } public void setIgnoreUpdateEvent(boolean ignoreUpdateEvent) { this.ignoreUpdateEvent = ignoreUpdateEvent; update(); } private void update() { if(!ignoreUpdateEvent) { HashSet<Integer> selectedRows = new HashSet<Integer>(Arrays.asList(ArrayUtils.toObject(table.getSelectedRows()))); for (int i = 0; i < table.getRowCount(); i++) { int modelRow = table.convertRowIndexToModel(i); model.setValueAt(selectedRows.contains(i), modelRow, 0); } updateOk(); } } } private class MySelectionModel extends DefaultListSelectionModel { private boolean firstColumnEvent; private boolean passThrough; public void setFirstColumnEvent(boolean firstColumnEvent) { this.firstColumnEvent = firstColumnEvent; this.passThrough = false; } @Override public void setSelectionInterval(int index0, int index1) { if(!passThrough && firstColumnEvent) { toggleRowSelection(index1); } else { super.setSelectionInterval(index0, index1); } } private void toggleRowSelection(int rowIndex) { // avoid special handling on recursive calls passThrough = true; // when clicking on the checkbox column only consider the row where user clicked (index1) K key = model.getFields().get(table.convertRowIndexToModel(rowIndex)).getKey(); if(model.getSelectedFields().contains(key)) { super.removeSelectionInterval(rowIndex, rowIndex); } else { super.addSelectionInterval(rowIndex, rowIndex); } } @Override public void addSelectionInterval(int index0, int index1) { if(!passThrough && firstColumnEvent) { toggleRowSelection(index1); } else { super.addSelectionInterval(index0, index1); } } public void doAddSelectionInterval(int index0, int index1) { super.addSelectionInterval(index0, index1); } } }