简体   繁体   中英

JFace TreeViewer Sorting - Updated

Linking in from Sort rows in JFace Treeviewer where I asked @greg-449 why my comparator doesn't work when (like the question on that page) I just wanted to sort by the label provider.

My TreeViewer is somewhat extended with some generic features used in my application. There are then at 3 derivations extending it, but essentially the example below of it being an abstract class is because it's not intended to be directly instantiated, and the rest of the Eclipse plugin code must choose a concrete implementation for various view parts. However, I need sorting functionality in all of them, so this is probably where it should go.

I anonymized the code below, but it is in essence the abstract wrapper around TreeViewer which in all derivations has 3 columns. The first column (index 0) is always a tree of some type, so the nodes are expandable and this produces more visible rows of data in the 2nd and 3rd columns (indexes 1 and 2). These columns contain text data only.

As such, what I hope to achieve is a sortable control, where clicking header column with index 0 will clear any sorting and render the data as it was initially loaded, while clicking any other headers will do the following:

  1. Sort Ascending (UP) if it was NOT already the sort column
  2. Invert sort direction if same sort column is repeatedly clicked

Here's what I have tried, starting with the ViewerComparator class mentioned in the link at he top of this post:

    public abstract class MyTreeViewer extends TreeViewer {
    
        public static final int ACTIVE_TOTAL_COLUMN_WIDTH = 120;
        public static final int FIRST_COLUMN_WIDTH = 180;
    
        private static final String SPECIAL_FIELD = "specialField";
        private TreeColumn sortColumn = null;
    
        final RMService rmService;
        final PService pService;
    
        public MyTreeViewer(Composite parent) {
    
            this(parent, SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION);
        }
    
        public MyTreeViewer(Composite parent, int style) {
    
            super(parent, style);
    
            this.rmService = UIActivator.getDefault().getInjector().getInstance(RMService.class);
            this.pService = UIActivator.getDefault().getInjector().getInstance(PService.class);
    
            this.setUseHashlookup(true);
    
            this.addSelectionChangedListener(new ISelectionChangedListener() {
    
                @Override
                public void selectionChanged(SelectionChangedEvent event) {
                    if (!event.getSelection().isEmpty()) {
                        Tree tree = getTree();
                        if (tree != null) {
                            List<MyTreeItem> treeItems = Lists.newArrayList();
                            for (int i = 0; i < tree.getSelectionCount(); i++) {
                                TreeItem item = tree.getSelection()[i];
                                Object obj = item.getData();
                                if (obj instanceof MyTreeItem) {
                                    treeItems.add((MyTreeItem) obj);
                                }
                            }
                            handleSelectionChanged(treeItems);
                        }
                    }
                }
            });
    
            this.setComparator(new ViewerComparator());
        }
    
        protected abstract void handleSelectionChanged(Collection<MyTreeItem> treeItemList);
    
        public void initTree(List<ViewField> fields, IMemento memento) {
    
            TableLayout tableLayout = new TableLayout();
            Tree tree = getTree();
    
            for (ViewField field : fields) {
    
                TreeViewerColumn column = new TreeViewerColumn(this, SWT.NONE);
                if (SystemPropertiesLoader.OPERATING_SYSTEM_NAME.equalsIgnoreCase(IConstants.LINUX_OS)) {
                    column.getColumn().setResizable(false);
                    column.getColumn().setMoveable(false);
                } else {
                    column.getColumn().setResizable(true);
                    column.getColumn().setMoveable(true);
                }
                column.getColumn().setData(SPECIAL_FIELD, field);
                column.getColumn().setText(field.getFieldName());
    
                tableLayout.addColumnData(new ColumnPixelData(field.getWidth(), false));
    
                column.getColumn().addSelectionListener(new SelectionListener() {
                    @Override
                    public void widgetSelected(SelectionEvent selectionEvent) {
                        if (selectionEvent.getSource() instanceof TreeColumn) {
                            TreeColumn column = (TreeColumn)selectionEvent.getSource();
                            Tree tree = getTree();
                            if (column.getText().equalsIgnoreCase("") && sortColumn != null) {
                                // file column clicked - use it to clear any sort order
                                sortColumn = null;
                                tree.setSortColumn(sortColumn);
                                tree.setSortDirection(0);
                                refresh();
                            } else {
                                sortColumn = column;
                                tree.setSortColumn(sortColumn);
                                tree.setSortDirection(invertSortDirection(tree.getSortDirection()));
                                refresh();
                            }
                        }
                    }
    
                    @Override
                    public void widgetDefaultSelected(SelectionEvent selectionEvent) {
                        // not currently used, but required as part of implementation of SelectionListener interface
                    }
                });
            }
            tree.setLayout(tableLayout);
            tree.setLinesVisible(false);
            tree.setHeaderVisible(true);
            tree.layout(true);
    
            // prevent expanding/collapsing tree item on dbl click
            tree.addListener(SWT.MeasureItem, new Listener() {
    
                @Override
                public void handleEvent(Event event) {
                    // nothing
                }
            });
        }
    
        private int invertSortDirection(int sortDirection) {
            if (sortDirection != SWT.UP && sortDirection != SWT.DOWN) {
                return SWT.UP;
            } else if (sortDirection == SWT.UP) {
                return SWT.DOWN;
            }
            return SWT.UP;
        }
    
        @Override
        public void refresh() {
            super.refresh();
        }
    
        @Override
        public void refresh(boolean updateLabels) {
            super.refresh(updateLabels);
        }
    }

I inherited this code, so there are some peculiar things that fixed bugs that I won't touch without knowing it won't produce a regression in QA, such as the crude way preventing expanding/collapsing tree item on double-click is implemented. In fact, the only part of this particular code I amended thus far is the insertion of the addSelectionListener closure for handling column header clicks, and the invertSortDirection method.

What happens when I run this and click on the headers is as I expect. I see the UI caret indicating the sort direction on column index 1 or 2, but I do not see the data sorted. Clicking the header of column index 0 will clear the sort column and the order. If the data was sorted, I'd like the viewer to refresh in the UI to its original loaded state before any column ordering is requested.

On the prior question (linked at top), I interpreted that if sorting by label text was required, I should just add the this.setComparator(new ViewerComparator()); line. I've no idea what I would override or change if I have to write a class that extends ViewComparator .

None of the derived classes from the above code implement a listener on a column. I can trace the code, and the above code for handling header clicks does execute.

So, do I need to extend ViewComparator and what should I be changing in it to get the desired behaviour, if I do?

(I can also probably do away with the TreeColumn sortColumn field since the tree itself 'remembers' this. The Google Guice injected services are used by derivations of this abstract tree viewer)

UPDATE 1:

My intention was to show a derived class of this generic viewer defined above, but after I examined this, it was clear it shows little of use for the current issue of why the sort does not occur.

I had found what I thought was the 'smoking gun' of why the sort does not occur in the custom label provider itself from one of my predecessors that I've inherited the project from. Here's the label provider:

public class MyCustomLabelProvider extends LabelProvider implements ITableLabelProvider {

    final IDecoratorManager decorator;

    public MyCustomLabelProvider() {
        decorator = PlatformUI.getWorkbench().getDecoratorManager();
    }

    @Override
    public Image getImage(Object element) {
        return super.getImage(element);
    }

    @Override
    public String getText(Object element) {
        return null;
    }

    @Override
    public Image getColumnImage(Object element, int columnIndex) {
        if (element instanceof MyTreeItem) {
            if (columnIndex == 0) {
                final MyTreeItem item = (MyTreeItem)element;
                switch (item.getType()) {
                    // snipped cases returning different images
                }
            }
        }
        return null;
    }

    @Override
    public String getColumnText(Object element, int columnIndex) {
        if (element instanceof MyTreeItem) {
            return showColumns((MyTreeItem) element, columnIndex);
        }
        return null;
    }

    private String showColumns(MyTreeItem element, int columnIndex) {
        if (columnIndex == 0) {
            return element.getName();
        }
        if (columnIndex == 1) {
            return String.valueOf(element.getCustomProperty1());
        }
        if (columnIndex == 2) {
            return String.valueOf(element.getCustomProperty2());
        }
        return "";
    }
}

Via tracing the ViewerComparator code, the program eventually calls getText which always returning null .

The ViewerComparator transpires only to attempt to grab the label text, which due to the above is null , which it amends to an empty String. It then uses the Java String compareTo method for the comparison. Since both are "" , there is no comparison result to signal the elements order needs to be swapped.

I wondered about changing the getText method to somehow obtain the original column index of the clicked column and to have logic in it to determine which property to read from my underlying data object used to populate a row in the viewer. For me, this transpired to not work because the underlying object I used has non-String properties that are used to populate 2 of the 3 columns.

User greg-449 had indicated in comments I would need to extend and override the ViewerComparator to make my own version, but until I got this far, or until he stated...

The standard ViewerComparator only supports one column

... it had not been clear why. The originally linked post doesn't have that clarification. Or at least, not at time of writing this.

At the point he mentioned that, I had not indicated the issue as resolved, just that I thought I had found the issue. I had simply started with the ViewerComparator , traced running code, and found the reason the existing comparator could not re-order the items, assuming that would likely be the end of it once I update code.

I would go further to what greg-449 said, in that even if you have a single column, the default ViewerComparator will not support comparing elements where the property of the underlying data object is not a Java String . You need to implement your own comparator for that. Thus is became clear why greg-449 suggested that at the outset of me stating I had 3 columns - but I remained confused because I simply thought I had text data on the 2 columns I wanted to be sortable, even if that text is actually converted from an int . I just thought the linked post I read was probably apt because it mentioned sorting on text, without mentioning the limitation of a single column, or Java String data types.

So moving on, I have now got my code working as I wanted. More importantly, I did not have to change the getText method, since this was called specifically by the default ViewerComparator and I found an approach to follow where this wasn't needed. I shall be posting these in the answer.

So in addition to my updates in the question...

I tried to find the example of a different comparator that greg-449 referenced, but the specific package was not available in my environment if I tried to import it.

I then searched for a means to writing a custom comparator and found a Vogella post: https://www.vogella.com/tutorials/EclipseJFaceTable/article.html#sort-content-of-table-columns

I noticed that the column index issue I mentioned above was handled by creating a handler per column in the Vogella code. I mostly copied it, with slight changes to how it performs the comparison to meet my requirements:

public class MyCustomViewerComparator extends ViewerComparator {
    private int propertyIndex;
    private static final int DESCENDING = 1;
    private int direction;

    public MyCustomViewerComparator() {
        this.propertyIndex = 0;
        direction = DESCENDING;
    }

    public int getDirection() {
        return direction == 1 ? SWT.DOWN : SWT.UP;
    }

    public void setColumn(int column) {
        if (column == this.propertyIndex) {
            // Same column as last sort; toggle the direction
            direction = 1 - direction;
        } else {
            // New column; do an ascending sort
            this.propertyIndex = column;
            direction = DESCENDING;
        }
    }

    @Override
    public int compare(Viewer viewer, Object e1, Object e2) {
        MyTreeItem p1 = (MyTreeItem) e1;
        MyTreeItem p2 = (MyTreeItem) e2;
        int rc = 0;
        switch (propertyIndex) {
            case 1:
                rc = p1.getCustomProperty1() - p2.getCustomProperty1();
                break;
            case 2:
                rc = p1.getCustomProperty2() - p2.getCustomProperty2();
                break;
            default:
                rc = 0;
        }
        // If descending order, flip the direction
        if (direction == DESCENDING) {
            rc = -rc;
        }
        return rc;
    }
}

As can be seen, we have a standard compare function that returns a negative number, zero when matching, or a positive number, that indicates which order two items should be in. Or as the standard JavaDoc states:

the value {@code 0} if the argument string is equal to * this string; a value less than {@code 0} if this string * is lexicographically less than the string argument; and a * value greater than {@code 0} if this string is * lexicographically greater than the string argument.

Just in my version, the compare is using int values from my data object instead.

In addition to this, if the user clicked the column header of the column that by default is column index 0, I wanted it to clear any sort and return to the format the viewer was initially loaded as.

To achieve this, I simply changed the SelectionListener I originally used in MyTreeViewer (see second code listing in question) for each column , and made it use the following from the Vogella example (again tweaked):

    private SelectionAdapter getSelectionAdapter(final TreeColumn column,
                                                 final int index) {
        SelectionAdapter selectionAdapter = new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                comparator.setColumn(index);
                if (index == 0) {
                    viewer.getTree().setSortDirection(0);
                    viewer.getTree().setSortColumn(null);
                } else {
                    int dir = comparator.getDirection();
                    viewer.getTree().setSortDirection(dir);
                    viewer.getTree().setSortColumn(column);
                }
                viewer.refresh();
            }
        };
        return selectionAdapter;
    }

The if (index == 0) check in the above, clears the sorting for me when true, otherwise sorts on the whichever other column header has been clicked. Since the column index pertains to how the columns were set up, they are not affected by having movable columns in the UI. You can change the visible column ordering without deleterious effects.

From what I've learnt, it's only the logic for comparing elements that is instrumental and would need overriding in a custom comparator. Outside of this, the only thing to recognize is you then want the comparator specifically wired to each column, by implementing the SelectionAdapter interface and adding this to the column, while also being able to track the column indexes. The Vogella link does show this. Basically you will see on the comparator that there exists a setColumn method, and this is used in the SelectionAdapter to set the column index to be tested in the compare .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM