1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.ui.column; 34 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Comparator; 38 import java.util.LinkedHashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.function.Consumer; 42 import java.util.stream.Collectors; 43 import java.util.stream.Stream; 44 45 import org.eclipse.jface.preference.JFacePreferences; 46 import org.eclipse.jface.resource.JFaceResources; 47 import org.eclipse.jface.viewers.ColumnLabelProvider; 48 import org.eclipse.jface.viewers.ColumnViewer; 49 import org.eclipse.jface.viewers.TableViewer; 50 import org.eclipse.jface.viewers.TableViewerColumn; 51 import org.eclipse.jface.viewers.TreeViewer; 52 import org.eclipse.jface.viewers.TreeViewerColumn; 53 import org.eclipse.jface.viewers.Viewer; 54 import org.eclipse.jface.viewers.ViewerColumn; 55 import org.eclipse.jface.viewers.ViewerComparator; 56 import org.eclipse.swt.SWT; 57 import org.eclipse.swt.SWTException; 58 import org.eclipse.swt.events.ControlEvent; 59 import org.eclipse.swt.events.ControlListener; 60 import org.eclipse.swt.events.DisposeEvent; 61 import org.eclipse.swt.events.DisposeListener; 62 import org.eclipse.swt.graphics.Color; 63 import org.eclipse.swt.graphics.Font; 64 import org.eclipse.swt.widgets.Event; 65 import org.eclipse.swt.widgets.Item; 66 import org.eclipse.swt.widgets.Listener; 67 import org.eclipse.swt.widgets.Table; 68 import org.eclipse.swt.widgets.Tree; 69 import org.eclipse.swt.widgets.Widget; 70 71 import org.openjdk.jmc.ui.column.TableSettings.ColumnSettings; 72 73 public class ColumnManager { 74 75 public interface IColumnState { 76 IColumn getColumn(); 77 78 Integer getWidth(); 79 80 Boolean isHidden(); 81 82 Boolean isSortAscending(); 83 84 boolean isVisible(); 85 } 86 87 public static class SelectionState { 88 private int scrollIndex; 89 private int[] selectionIndices; 90 91 private SelectionState(int scrollIndex, int[] selectionIndices) { 92 this.scrollIndex = scrollIndex; 93 this.selectionIndices = selectionIndices; 94 } 95 96 private int[] getSelectionIndices() { 97 return selectionIndices; 98 } 99 100 private int getScrollIndex() { 101 return scrollIndex; 102 } 103 104 } 105 106 private static final int DEFAULT_WIDTH = 150; 107 private static final boolean DEFAULT_HIDDEN = false; 108 private static final boolean DEFAULT_SORT_ASC = true; 109 110 private final List<ColumnEntry> addedColumns; 111 private final ColumnViewer viewer; 112 private ColumnEntry sortColumn; 113 114 private final Listener customDrawer = new Listener() { 115 116 @Override 117 public void handleEvent(Event event) { 118 Item[] columnWidgets = getColumnWidgets(); 119 if (event.index < columnWidgets.length) { 120 Widget colWidget = columnWidgets[event.index]; 121 for (ColumnEntry ce : addedColumns) { 122 if (ce.ui != null && colWidget == getColumnWidget(ce.ui)) { 123 Listener drawer; 124 if ((drawer = ce.getColumn().getColumnDrawer()) != null) { 125 drawer.handleEvent(event); 126 } 127 } 128 } 129 } 130 } 131 }; 132 private final Consumer<ColumnComparator> onSortChange; 133 134 static class ColumnEntry implements ControlListener, IColumnState { 135 private final IColumn impl; 136 private Boolean hidden; 137 private Integer width; 138 private Boolean sortAscending; 139 private ViewerColumn ui; 140 141 public ColumnEntry(IColumn columnImpl, Boolean hidden, Integer width, Boolean sortAscending) { 142 impl = columnImpl; 143 this.hidden = hidden; 144 this.width = width; 145 this.sortAscending = sortAscending; 146 } 147 148 @Override 149 public IColumn getColumn() { 150 return impl; 151 } 152 153 @Override 154 public Integer getWidth() { 155 return width; 156 } 157 158 @Override 159 public Boolean isHidden() { 160 return hidden; 161 } 162 163 @Override 164 public boolean isVisible() { 165 return ui != null; 166 } 167 168 @Override 169 public Boolean isSortAscending() { 170 return sortAscending; 171 } 172 173 boolean isSortAscendingPreferred() { 174 return sortAscending == null ? DEFAULT_SORT_ASC : sortAscending; 175 } 176 177 Item create(ColumnViewer viewer, int columnIndex) { 178 Item columnWidget; 179 int colWidth = width == null ? DEFAULT_WIDTH : width; 180 if (viewer instanceof TableViewer) { 181 TableViewerColumn vc = new TableViewerColumn((TableViewer) viewer, getColumn().getStyle(), columnIndex); 182 ui = vc; // set before usage since viewerColumn is used to implement isVisible() 183 vc.getColumn().setMoveable(true); 184 vc.getColumn().addControlListener(this); 185 vc.getColumn().setToolTipText(getColumn().getDescription()); 186 vc.getColumn().setWidth(colWidth); 187 columnWidget = vc.getColumn(); 188 189 } else { 190 TreeViewerColumn vc = new TreeViewerColumn(((TreeViewer) viewer), getColumn().getStyle(), columnIndex); 191 ui = vc; // set before usage since viewerColumn is used to implement isVisible() 192 vc.getColumn().setMoveable(true); 193 vc.getColumn().addControlListener(this); 194 vc.getColumn().setToolTipText(getColumn().getDescription()); 195 vc.getColumn().setWidth(colWidth); 196 columnWidget = vc.getColumn(); 197 } 198 ui.setEditingSupport(getColumn().getEditingSupport()); 199 ui.setLabelProvider(getColumn().getLabelProvider()); 200 columnWidget.setText(getColumn().getName()); 201 return columnWidget; 202 } 203 204 private void doHide() { 205 ViewerColumn vc = ui; 206 ui = null; // clear before dispose since viewerColumn is used to implement isVisible() 207 if (vc instanceof TableViewerColumn) { 208 TableViewerColumn tc = ((TableViewerColumn) vc); 209 ColumnViewer viewer = vc.getViewer(); 210 try { 211 // Workaround. disable redraw, or TableViewer will throw exception when removing the last column 212 viewer.getControl().setRedraw(false); 213 tc.getColumn().dispose(); 214 } finally { 215 try { 216 viewer.getControl().setRedraw(true); 217 } catch (SWTException e) { 218 // Workaround. for some reason the table sometimes complains about the table is 219 // disposed even though it seem to be working perfectly. 220 } 221 } 222 } else if (vc != null) { 223 ((TreeViewerColumn) vc).getColumn().dispose(); 224 } 225 } 226 227 @Override 228 public void controlMoved(ControlEvent e) { 229 230 } 231 232 @Override 233 public void controlResized(ControlEvent e) { 234 width = getColumnWidth(ui); 235 // FIXME: Workaround to avoid drawing dug on windows when having hooked EraseItem listener and the first tree column draws a custom background or is right aligned 236 ui.getViewer().getControl().redraw(); 237 } 238 239 } 240 241 private static Boolean getNullForDefault(boolean hidden) { 242 return hidden == DEFAULT_HIDDEN ? null : hidden; 243 } 244 245 public static ColumnManager build(TableViewer viewer, List<IColumn> columns, TableSettings ts) { 246 return build((ColumnViewer) viewer, columns, ts, viewer::setComparator); 247 } 248 249 public static ColumnManager build( 250 TableViewer viewer, List<IColumn> columns, TableSettings ts, Consumer<ColumnComparator> onSortChange) { 251 return build((ColumnViewer) viewer, columns, ts, onSortChange); 252 } 253 254 public static ColumnManager build(TreeViewer viewer, List<IColumn> columns, TableSettings ts) { 255 return build((ColumnViewer) viewer, columns, ts, viewer::setComparator); 256 } 257 258 public static ColumnManager build( 259 TreeViewer viewer, List<IColumn> columns, TableSettings ts, Consumer<ColumnComparator> onSortChange) { 260 return build((ColumnViewer) viewer, columns, ts, onSortChange); 261 } 262 263 private static ColumnManager build( 264 ColumnViewer viewer, List<IColumn> columns, TableSettings ts, Consumer<ColumnComparator> onSortChange) { 265 List<ColumnEntry> entries = new ArrayList<>(); 266 String sortColumn = null; 267 if (ts != null) { 268 Map<String, IColumn> columnsMap = new LinkedHashMap<>(); // preserve order for columns with no settings 269 for (IColumn c : columns) { 270 columnsMap.put(c.getId(), c); 271 } 272 if (ts.getOrderBy() != null && columnsMap.containsKey(ts.getOrderBy())) { 273 sortColumn = ts.getOrderBy(); 274 } 275 // Add settings in order 276 for (ColumnSettings cc : ts.getColumns()) { 277 IColumn c = columnsMap.remove(cc.getId()); 278 if (c != null) { 279 entries.add(new ColumnEntry(c, cc.isHidden(), cc.getWidth(), cc.isSortAscending())); 280 } 281 } 282 // Add columns with no settings as hidden 283 for (IColumn c : columnsMap.values()) { 284 entries.add(new ColumnEntry(c, getNullForDefault(true), null, null)); 285 } 286 } else { 287 // No settings, add all columns as visible 288 for (IColumn c : columns) { 289 entries.add(new ColumnEntry(c, getNullForDefault(false), null, null)); 290 } 291 } 292 return new ColumnManager(viewer, sortColumn, entries, onSortChange); 293 } 294 295 ColumnManager(ColumnViewer viewer, String sortColumnId, List<ColumnEntry> columns, 296 Consumer<ColumnComparator> onSortChange) { 297 this.viewer = viewer; 298 addedColumns = columns; 299 this.onSortChange = onSortChange; 300 setLinesVisible(true); 301 setHeaderVisible(true); 302 viewer.setLabelProvider(new ColumnLabelProvider() { 303 @Override 304 public String getText(Object element) { 305 return Messages.ALL_COLUMNS_HIDDEN_LABEL; 306 } 307 308 @Override 309 public Font getFont(Object element) { 310 return JFaceResources.getFontRegistry().getItalic(JFaceResources.DEFAULT_FONT); 311 } 312 313 @Override 314 public Color getForeground(Object element) { 315 return JFaceResources.getColorRegistry().get(JFacePreferences.QUALIFIER_COLOR); 316 } 317 }); 318 viewer.getControl().addDisposeListener(new DisposeListener() { 319 320 @Override 321 public void widgetDisposed(DisposeEvent e) { 322 updateColumnOrder(); 323 } 324 }); 325 for (ColumnEntry ce : addedColumns) { 326 if (ce.hidden == null ? !DEFAULT_HIDDEN : !ce.hidden) { 327 createColumnUi(ce); 328 } 329 } 330 updateEraseItemListener(); 331 if (sortColumnId != null) { 332 setSortColumn(sortColumnId); 333 } 334 } 335 336 private void updateEraseItemListener() { 337 for (ColumnEntry c : addedColumns) { 338 if (c.isVisible() && c.getColumn().getColumnDrawer() != null) { 339 // Custom drawer found, ensure the the listener is added 340 for (Listener l : viewer.getControl().getListeners(SWT.EraseItem)) { 341 if (l == customDrawer) { 342 return; 343 } 344 } 345 viewer.getControl().addListener(SWT.EraseItem, customDrawer); 346 return; 347 } 348 } 349 350 // No custom drawer found, ensure the the listener is not added 351 viewer.getControl().removeListener(SWT.EraseItem, customDrawer); 352 } 353 354 public ColumnViewer getViewer() { 355 return viewer; 356 } 357 358 public void setColumnHidden(String columnId, boolean hidden) { 359 updateColumnOrder(); 360 ColumnEntry columnEntry = getColumnEntry(columnId); 361 columnEntry.hidden = hidden; 362 if (hidden) { 363 columnEntry.doHide(); 364 updateEraseItemListener(); 365 } else { 366 createColumnUi(columnEntry); 367 updateEraseItemListener(); 368 getViewer().refresh(); // Need to populate the added column 369 } 370 getViewer().getControl().getParent().layout(); 371 } 372 373 private void createColumnUi(ColumnEntry columnEntry) { 374 Item columnWidget = columnEntry.create(viewer, countVisibleColumnsBefore(columnEntry)); 375 columnWidget.addListener(SWT.Selection, e -> changeOrFlipSortColumn(columnEntry)); 376 } 377 378 private int countVisibleColumnsBefore(ColumnEntry columnEntry) { 379 int i = 0; 380 for (ColumnEntry c : addedColumns) { 381 if (c == columnEntry) { 382 return i; 383 } else if (c.isVisible()) { 384 i++; 385 } 386 } 387 return i; 388 } 389 390 private void updateColumnOrder() { 391 if (viewer instanceof TableViewer) { 392 Table table = ((TableViewer) viewer).getTable(); 393 updateColumnOrder(table.getColumns(), table.getColumnOrder()); 394 } else { 395 Tree tree = ((TreeViewer) viewer).getTree(); 396 updateColumnOrder(tree.getColumns(), tree.getColumnOrder()); 397 } 398 } 399 400 private void updateColumnOrder(Widget[] columns, int[] order) { 401 int visibleIndex = 0; 402 for (int i = 0; i < addedColumns.size(); i++) { 403 ColumnEntry e = addedColumns.get(i); 404 if (e.isVisible()) { 405 Widget v1 = getColumnWidget(e.ui); 406 Widget v2 = columns[order[visibleIndex++]]; 407 if (v2 != v1) { 408 // swap position for v1 and v2; 409 for (int j = i; j < addedColumns.size(); j++) { 410 ColumnEntry e2 = addedColumns.get(j); 411 if (e2.ui != null && v2 == getColumnWidget(e2.ui)) { 412 addedColumns.set(i, e2); 413 addedColumns.set(j, e); 414 415 } 416 } 417 } 418 } 419 } 420 } 421 422 /** 423 * Change the current sort column. If this column is already the sort column, then switch sort 424 * order. 425 * 426 * @param entry 427 * Column to sort on. May not be null. 428 */ 429 private void changeOrFlipSortColumn(ColumnEntry entry) { 430 if (sortColumn == entry) { 431 setSortColumn(entry, !entry.isSortAscendingPreferred()); 432 } else { 433 setSortColumn(entry, entry.isSortAscendingPreferred()); 434 } 435 } 436 437 public void clearSortColumn() { 438 sortColumn = null; 439 setSortColumn(null, SWT.UP); 440 } 441 442 public void setSortColumn(String columnId) { 443 ColumnEntry columnEntry = getColumnEntry(columnId); 444 setSortColumn(columnEntry, columnEntry.isSortAscendingPreferred()); 445 } 446 447 public void setSortColumn(String columnId, boolean sortAscending) { 448 setSortColumn(getColumnEntry(columnId), sortAscending); 449 } 450 451 public ColumnComparator getColumnComparator() { 452 return sortColumn == null ? null : new ColumnComparator(sortColumn.impl, sortColumn.isSortAscendingPreferred()); 453 } 454 455 private void setSortColumn(ColumnEntry entry, boolean sortAscending) { 456 sortColumn = entry; 457 entry.sortAscending = sortAscending; 458 setSortColumn(entry.ui, sortAscending ? SWT.UP : SWT.DOWN); 459 } 460 461 private ColumnEntry getColumnEntry(String columnId) { 462 return addedColumns.stream().filter(ce -> ce.impl.getId().equals(columnId)).findAny().get(); 463 } 464 465 private void setSortColumn(ViewerColumn vc, int direction) { 466 if (viewer instanceof TableViewer) { 467 Table table = ((TableViewer) viewer).getTable(); 468 table.setSortColumn(vc == null ? null : ((TableViewerColumn) vc).getColumn()); 469 table.setSortDirection(direction); 470 } else { 471 Tree tree = ((TreeViewer) viewer).getTree(); 472 tree.setSortColumn(vc == null ? null : ((TreeViewerColumn) vc).getColumn()); 473 tree.setSortDirection(direction); 474 } 475 onSortChange.accept(getColumnComparator()); 476 } 477 478 private static Item getColumnWidget(ViewerColumn vc) { 479 if (vc instanceof TableViewerColumn) { 480 return ((TableViewerColumn) vc).getColumn(); 481 } else { 482 return ((TreeViewerColumn) vc).getColumn(); 483 } 484 } 485 486 private Item[] getColumnWidgets() { 487 if (viewer instanceof TableViewer) { 488 return ((TableViewer) viewer).getTable().getColumns(); 489 } else { 490 return ((TreeViewer) viewer).getTree().getColumns(); 491 } 492 } 493 494 private static int getColumnWidth(ViewerColumn vc) { 495 if (vc instanceof TableViewerColumn) { 496 return ((TableViewerColumn) vc).getColumn().getWidth(); 497 } else { 498 return ((TreeViewerColumn) vc).getColumn().getWidth(); 499 } 500 } 501 502 private void setHeaderVisible(boolean visible) { 503 if (viewer instanceof TableViewer) { 504 ((TableViewer) viewer).getTable().setHeaderVisible(visible); 505 } else { 506 ((TreeViewer) viewer).getTree().setHeaderVisible(visible); 507 } 508 } 509 510 private void setLinesVisible(boolean visible) { 511 if (viewer instanceof TableViewer) { 512 ((TableViewer) viewer).getTable().setLinesVisible(visible); 513 } else { 514 ((TreeViewer) viewer).getTree().setLinesVisible(visible); 515 } 516 } 517 518 public Stream<? extends IColumnState> getColumnStates() { 519 if (!viewer.getControl().isDisposed()) { 520 updateColumnOrder(); 521 } 522 return addedColumns.stream(); 523 } 524 525 public TableSettings getSettings() { 526 List<ColumnSettings> cols = getColumnStates().map(ColumnManager::buildColumnConfig) 527 .collect(Collectors.toList()); 528 return new TableSettings(sortColumn == null ? null : sortColumn.getColumn().getId(), cols); 529 } 530 531 private static ColumnSettings buildColumnConfig(IColumnState state) { 532 return new ColumnSettings(state.getColumn().getId(), state.isHidden(), state.getWidth(), 533 state.isSortAscending()); 534 } 535 536 public static class ColumnComparator extends ViewerComparator implements Comparator<Object> { 537 538 private final IColumn column; 539 private final boolean sortAscending; 540 541 private ColumnComparator(IColumn column, boolean sortAscending) { 542 this.column = column; 543 this.sortAscending = sortAscending; 544 } 545 546 @Override 547 public int compare(Viewer viewer, Object e1, Object e2) { 548 return compare(e1, e2); 549 } 550 551 @Override 552 public int compare(Object row1, Object row2) { 553 int compare = 0; 554 Comparator<Object> comparator = column.getComparator(); 555 if (comparator != null) { 556 compare = comparator.compare(row1, row2); 557 } else { 558 ColumnLabelProvider clp = column.getLabelProvider(); 559 String l1 = clp.getText(row1); 560 String l2 = clp.getText(row2); 561 compare = l1 == null ? (l2 == null ? 0 : -1) : (l2 == null ? 1 : l1.compareTo(l2)); 562 } 563 return sortAscending ? compare : -compare; 564 } 565 566 public IColumn getColumn() { 567 return column; 568 } 569 570 public boolean isSortAscending() { 571 return sortAscending; 572 } 573 } 574 575 public int getVisibilityIndex(IColumn column) { 576 for (ColumnEntry c : addedColumns) { 577 if (c.impl == column) { 578 if (c.ui != null) { 579 return Arrays.asList(getColumnWidgets()).indexOf(getColumnWidget(c.ui)); 580 } 581 break; 582 } 583 } 584 return -1; 585 } 586 587 public SelectionState getSelectionState() { 588 Table table = (Table) getViewer().getControl(); 589 return new SelectionState(table.getTopIndex(), table.getSelectionIndices()); 590 } 591 592 public void setSelectionState(SelectionState state) { 593 if (state == null) { 594 return; 595 } 596 Table table = (Table) getViewer().getControl(); 597 table.setSelection(state.getSelectionIndices()); 598 // Workaround to fire selection events for listeners of the TableViewer 599 getViewer().setSelection(getViewer().getSelection()); 600 table.setTopIndex(state.getScrollIndex()); 601 } 602 603 }