package name.abuchen.portfolio.ui.util.viewers; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.layout.TreeColumnLayout; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.ViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.Widget; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.ConfigurationStore; import name.abuchen.portfolio.ui.util.ConfigurationStore.ConfigurationStoreOwner; import name.abuchen.portfolio.util.TextUtil; public class ShowHideColumnHelper implements IMenuListener, ConfigurationStoreOwner { @FunctionalInterface public interface Listener { void onConfigurationPicked(); } private abstract static class ViewerPolicy { /** * The changeListener is attached to various SWT events (ordering, * resizing or moving columns) in order to store the updated * configuration. */ private org.eclipse.swt.widgets.Listener changeListener; abstract ColumnViewer getViewer(); abstract int getColumnCount(); abstract Widget[] getColumns(); abstract Widget getColumn(int index); abstract Widget getSortColumn(); abstract Widget getColumnWidget(ViewerColumn column); abstract int[] getColumnOrder(); abstract int getSortDirection(); abstract int getWidth(Widget col); abstract void create(Column column, Object option, Integer direction, int width); void setCommonParameters(Column column, ViewerColumn viewerColumn, Integer direction) { viewerColumn.setLabelProvider(column.getLabelProvider()); if (column.getSorter() != null) { if (direction != null) column.getSorter().attachTo(getViewer(), viewerColumn, direction); else column.getSorter().attachTo(getViewer(), viewerColumn); // add selection listener *after* attaching the viewer sorter // because the viewer sorter will add a listener that actually // changes the sort order if (changeListener != null) getColumnWidget(viewerColumn).addListener(SWT.Selection, changeListener); } if (column.getEditingSupport() != null) { viewerColumn.setEditingSupport( new ColumnEditingSupportWrapper(getViewer(), column.getEditingSupport())); } if (changeListener != null) { getColumnWidget(viewerColumn).addListener(SWT.Resize, changeListener); getColumnWidget(viewerColumn).addListener(SWT.Move, changeListener); } } void setRedraw(boolean redraw) { getViewer().getControl().setRedraw(redraw); } public void setChangeListener(org.eclipse.swt.widgets.Listener changeListener) { this.changeListener = changeListener; } } private static class TableViewerPolicy extends ViewerPolicy { private TableViewer table; private TableColumnLayout layout; public TableViewerPolicy(TableViewer table, TableColumnLayout layout) { this.table = table; this.layout = layout; } @Override public ColumnViewer getViewer() { return table; } @Override public int getColumnCount() { return table.getTable().getColumnCount(); } @Override public Widget[] getColumns() { return table.getTable().getColumns(); } @Override public Widget getColumn(int index) { return table.getTable().getColumn(index); } @Override public Widget getSortColumn() { return table.getTable().getSortColumn(); } @Override Widget getColumnWidget(ViewerColumn column) { return ((TableViewerColumn) column).getColumn(); } @Override public int[] getColumnOrder() { return table.getTable().getColumnOrder(); } @Override public int getSortDirection() { return table.getTable().getSortDirection(); } @Override public int getWidth(Widget col) { return ((TableColumn) col).getWidth(); } @Override public void create(Column column, Object option, Integer direction, int width) { TableViewerColumn col = new TableViewerColumn(table, column.getStyle()); TableColumn tableColumn = col.getColumn(); tableColumn.setMoveable(true); tableColumn.setWidth(width); tableColumn.setData(Column.class.getName(), column); if (option == null) { tableColumn.setText(column.getLabel()); tableColumn.setToolTipText(wordwrap(column.getToolTipText())); } else { tableColumn.setText(column.getOptions().getColumnLabel(option)); String description = column.getOptions().getDescription(option); tableColumn.setToolTipText(wordwrap(description != null ? description : column.getToolTipText())); tableColumn.setData(OPTIONS_KEY, option); } layout.setColumnData(tableColumn, new ColumnPixelData(width)); setCommonParameters(column, col, direction); } private String wordwrap(String text) { // other platforms such as Mac and Linux natively wrap tool tip // labels, but not Windows return Platform.OS_WIN32.equals(Platform.getOS()) ? TextUtil.wordwrap(text) : text; } } private static class TreeViewerPolicy extends ViewerPolicy { private TreeViewer tree; private TreeColumnLayout layout; public TreeViewerPolicy(TreeViewer tree, TreeColumnLayout layout) { this.tree = tree; this.layout = layout; } @Override public ColumnViewer getViewer() { return tree; } @Override public int getColumnCount() { return tree.getTree().getColumnCount(); } @Override public Widget[] getColumns() { return tree.getTree().getColumns(); } @Override public Widget getColumn(int index) { return tree.getTree().getColumn(index); } @Override public Widget getSortColumn() { return tree.getTree().getSortColumn(); } @Override Widget getColumnWidget(ViewerColumn column) { return ((TreeViewerColumn) column).getColumn(); } @Override public int[] getColumnOrder() { return tree.getTree().getColumnOrder(); } @Override public int getSortDirection() { return tree.getTree().getSortDirection(); } @Override public int getWidth(Widget col) { return ((TreeColumn) col).getWidth(); } @Override public void create(Column column, Object option, Integer direction, int width) { TreeViewerColumn col = new TreeViewerColumn(tree, column.getStyle()); TreeColumn treeColumn = col.getColumn(); treeColumn.setMoveable(true); treeColumn.setWidth(width); treeColumn.setData(Column.class.getName(), column); if (option == null) { treeColumn.setText(column.getLabel()); treeColumn.setToolTipText(column.getToolTipText()); } else { treeColumn.setText(column.getOptions().getColumnLabel(option)); String description = column.getOptions().getDescription(option); treeColumn.setToolTipText(description != null ? description : column.getToolTipText()); treeColumn.setData(OPTIONS_KEY, option); } layout.setColumnData(treeColumn, new ColumnPixelData(width)); setCommonParameters(column, col, direction); } } /* package */static final String OPTIONS_KEY = Column.class.getName() + "_OPTION"; //$NON-NLS-1$ private static final Pattern CONFIG_PATTERN = Pattern.compile("^([^=]*)=(?:([^\\|]*)\\|)?(?:(\\d*)\\$)?(\\d*)$"); //$NON-NLS-1$ private final String identifier; private List<Column> columns = new ArrayList<>(); private Map<String, Column> id2column = new HashMap<>(); private IPreferenceStore preferences; private ConfigurationStore store; private List<Listener> listeners = new ArrayList<>(); private ViewerPolicy policy; private Menu contextMenu; public ShowHideColumnHelper(String identifier, IPreferenceStore preferences, TreeViewer viewer, TreeColumnLayout layout) { this(identifier, null, preferences, new TreeViewerPolicy(viewer, layout)); } public ShowHideColumnHelper(String identifier, IPreferenceStore preferences, TableViewer viewer, TableColumnLayout layout) { this(identifier, null, preferences, viewer, layout); } public ShowHideColumnHelper(String identifier, Client client, IPreferenceStore preferences, TableViewer viewer, TableColumnLayout layout) { this(identifier, client, preferences, new TableViewerPolicy(viewer, layout)); } private ShowHideColumnHelper(String identifier, Client client, IPreferenceStore preferences, ViewerPolicy policy) { this.identifier = identifier; this.policy = policy; this.preferences = preferences; if (client != null) { this.store = new ConfigurationStore(identifier, client, preferences, this); this.policy.setChangeListener(e -> store.updateActive(serialize())); } this.policy.getViewer().getControl().addDisposeListener(e -> ShowHideColumnHelper.this.widgetDisposed()); } private void widgetDisposed() { if (store != null) store.updateActive(serialize()); else preferences.setValue(identifier, serialize()); if (contextMenu != null) contextMenu.dispose(); if (store != null) store.dispose(); } public String getConfigurationName() { return store != null ? store.getActiveName() : null; } public void addListener(Listener l) { this.listeners.add(l); } public void showSaveMenu(Shell shell) { if (store == null) throw new UnsupportedOperationException(); store.showMenu(shell); } public void showHideShowColumnsMenu(Shell shell) { if (contextMenu == null) { MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(this); contextMenu = menuMgr.createContextMenu(shell); } contextMenu.setVisible(true); } @Override public void menuAboutToShow(final IMenuManager manager) { final Map<Column, List<Object>> visible = new HashMap<>(); for (Widget col : policy.getColumns()) { Column column = (Column) col.getData(Column.class.getName()); visible.computeIfAbsent(column, k -> new ArrayList<Object>()).add(col.getData(OPTIONS_KEY)); } Map<String, IMenuManager> groups = new HashMap<>(); for (final Column column : columns) { IMenuManager managerToAdd = manager; // create a sub-menu for each group label if (column.getGroupLabel() != null) { managerToAdd = groups.computeIfAbsent(column.getGroupLabel(), l -> { MenuManager m = new MenuManager(l); manager.add(m); return m; }); } if (column.hasOptions()) { List<Object> options = visible.getOrDefault(column, Collections.emptyList()); MenuManager subMenu = new MenuManager(column.getMenuLabel()); for (Object option : column.getOptions().getOptions()) { boolean isVisible = options.contains(option); String label = column.getOptions().getMenuLabel(option); addShowHideAction(subMenu, column, label, isVisible, option); if (isVisible) options.remove(option); } for (Object option : options) { String label = column.getOptions().getMenuLabel(option); addShowHideAction(subMenu, column, label, true, option); } if (column.getOptions().canCreateNewOptions()) addCreateOptionAction(subMenu, column); managerToAdd.add(subMenu); } else { addShowHideAction(managerToAdd, column, column.getMenuLabel(), visible.containsKey(column), null); } } addMenuAddGroup(groups, visible); manager.add(new Separator()); manager.add(new Action(Messages.MenuResetColumns) { @Override public void run() { doResetColumns(); } }); } private void addCreateOptionAction(MenuManager manager, Column column) { manager.add(new Separator()); manager.add(new Action(Messages.MenuCreateColumnConfig) { @Override public void run() { Object option = column.getOptions().createNewOption(Display.getCurrent().getActiveShell()); if (option != null) { policy.create(column, option, column.getDefaultSortDirection(), column.getDefaultWidth()); policy.getViewer().refresh(true); if (store != null) store.updateActive(serialize()); } } }); } private void addShowHideAction(IMenuManager manager, final Column column, String label, final boolean isChecked, final Object option) { Action action = new Action(label) { @Override public void run() { if (isChecked) { if (column.isRemovable()) destroyColumnWithOption(column, option); } else { policy.create(column, option, column.getDefaultSortDirection(), column.getDefaultWidth()); policy.getViewer().refresh(true); } if (store != null) store.updateActive(serialize()); } }; action.setChecked(isChecked); manager.add(action); } public void destroyColumnWithOption(Column column, Object option) { for (Widget widget : policy.getColumns()) { if (column.equals(widget.getData(Column.class.getName())) // && (option == null || option.equals(widget.getData(OPTIONS_KEY)))) { try { policy.setRedraw(false); Widget sortColumn = policy.getSortColumn(); if (widget.equals(sortColumn)) policy.getViewer().setComparator(null); widget.dispose(); } finally { policy.getViewer().refresh(); policy.setRedraw(true); } break; } } } private void addMenuAddGroup(Map<String, IMenuManager> groups, final Map<Column, List<Object>> visible) { for (final Entry<String, IMenuManager> entry : groups.entrySet()) { IMenuManager manager = entry.getValue(); manager.add(new Separator()); manager.add(new Action(Messages.MenuAddAll) { @Override public void run() { doAddGroup(entry.getKey(), visible); } }); manager.add(new Action(Messages.MenuRemoveAll) { @Override public void run() { doRemoveGroup(entry.getKey()); } }); } } private void doAddGroup(String group, Map<Column, List<Object>> visible) { try { policy.setRedraw(false); for (Column column : columns) { if (!group.equals(column.getGroupLabel())) continue; if (visible.containsKey(column)) continue; if (column.hasOptions()) { for (Object element : column.getOptions().getOptions()) policy.create(column, element, column.getDefaultSortDirection(), column.getDefaultWidth()); } else { policy.create(column, null, column.getDefaultSortDirection(), column.getDefaultWidth()); } } } finally { policy.getViewer().refresh(); policy.setRedraw(true); } } private void doRemoveGroup(String group) { try { policy.setRedraw(false); for (Widget col : policy.getColumns()) { Column column = (Column) col.getData(Column.class.getName()); if (group.equals(column.getGroupLabel())) col.dispose(); } } finally { policy.getViewer().refresh(); policy.setRedraw(true); } } public void addColumn(Column column) { // columns used to be identified by index only if (column.getId() == null) column.setId(Integer.toString(columns.size())); columns.add(column); id2column.put(column.getId(), column); } public void createColumns() { createFromColumnConfig(); if (policy.getColumnCount() == 0) { columns.stream().filter(c -> c.isVisible()) .forEach(c -> policy.create(c, null, c.getDefaultSortDirection(), c.getDefaultWidth())); } } private void createFromColumnConfig() { // if a configuration store is used, then migrate the preferences into // the store. This is done once. Unfortunately, if the user then does // not save the file subsequently, the configuration is lost (e.g. the // order and size of the displayed columns). Therefore the key is saved // for manual recovery. // if no configuration store is used (i.e. column configuration cannot // be saved), we continue to use the preferences to store configuration String configInPreferences = preferences.getString(identifier); if (store != null && !configInPreferences.isEmpty()) { preferences.setToDefault(identifier); preferences.setValue("__backup__" + identifier, configInPreferences); //$NON-NLS-1$ store.insertMigratedConfiguration(configInPreferences); } String config = store != null ? store.getActive() : configInPreferences; createFromColumnConfig(config); } private void createFromColumnConfig(String config) { if (config == null || config.trim().length() == 0) return; try { policy.setRedraw(false); // turn of sorting in case new columns define no viewer comparator policy.getViewer().setComparator(null); int count = policy.getColumnCount(); StringTokenizer tokens = new StringTokenizer(config, ";"); //$NON-NLS-1$ while (tokens.hasMoreTokens()) { try { Matcher matcher = CONFIG_PATTERN.matcher(tokens.nextToken()); if (!matcher.matches()) continue; // index Column col = id2column.get(matcher.group(1)); if (col == null) continue; // option Object option = null; if (col.hasOptions()) { String o = matcher.group(2); option = col.getOptions().valueOf(o); if (option == null) continue; } // direction String d = matcher.group(3); Integer direction = d != null ? Integer.parseInt(d) : null; // width int width = Integer.parseInt(matcher.group(4)); policy.create(col, option, direction, width); } catch (RuntimeException e) { PortfolioPlugin.log(e); } } for (int ii = 0; ii < count; ii++) policy.getColumn(0).dispose(); } finally { policy.setRedraw(true); } } private void doResetColumns() { try { // first add, then remove columns // (otherwise rendering of first column is broken) policy.setRedraw(false); // turn of sorting in case new columns define no viewer comparator policy.getViewer().setComparator(null); int count = policy.getColumnCount(); for (Column column : columns) { // columns w/ options are not created by default if (column.isVisible() && !column.hasOptions()) policy.create(column, null, column.getDefaultSortDirection(), column.getDefaultWidth()); } for (int ii = 0; ii < count; ii++) policy.getColumn(0).dispose(); } finally { policy.getViewer().refresh(); policy.setRedraw(true); } if (store != null) store.updateActive(serialize()); } private String serialize() { StringBuilder buf = new StringBuilder(); Widget sortedColumn = policy.getSortColumn(); for (int index : policy.getColumnOrder()) { Widget col = policy.getColumn(index); Column column = (Column) col.getData(Column.class.getName()); buf.append(column.getId()).append('='); if (column.hasOptions()) buf.append(column.getOptions().toString(col.getData(OPTIONS_KEY))).append('|'); if (col.equals(sortedColumn)) buf.append(policy.getSortDirection()).append('$'); buf.append(policy.getWidth(col)).append(';'); } return buf.toString(); } @Override public void beforeConfigurationPicked() { store.updateActive(serialize()); } @Override public void onConfigurationPicked(String data) { if (data == null) doResetColumns(); else createFromColumnConfig(data); listeners.stream().forEach(l -> l.onConfigurationPicked()); policy.getViewer().refresh(); } }