]>
Commit | Line | Data |
---|---|---|
add40ab2 | 1 | package org.tianocore.frameworkwizard.platform.ui; |
2 | ||
3 | import java.awt.*; | |
4 | import java.awt.event.*; | |
5 | import java.util.*; | |
6 | import java.util.List; | |
7 | ||
8 | import javax.swing.*; | |
9 | import javax.swing.event.TableModelEvent; | |
10 | import javax.swing.event.TableModelListener; | |
11 | import javax.swing.table.*; | |
12 | ||
13 | ||
14 | public class TableSorter extends AbstractTableModel { | |
15 | /** | |
16 | * | |
17 | */ | |
18 | private static final long serialVersionUID = 1L; | |
19 | ||
20 | protected TableModel tableModel; | |
21 | ||
22 | public static final int DESCENDING = -1; | |
23 | public static final int NOT_SORTED = 0; | |
24 | public static final int ASCENDING = 1; | |
25 | ||
26 | private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED); | |
27 | ||
28 | ||
29 | private Row[] viewToModel; | |
30 | private int[] modelToView; | |
31 | ||
32 | private JTableHeader tableHeader; | |
33 | private MouseListener mouseListener; | |
34 | private TableModelListener tableModelListener; | |
35 | ||
36 | private List<Directive> sortingColumns = new ArrayList<Directive>(); | |
37 | ||
38 | public TableSorter() { | |
39 | this.mouseListener = new MouseHandler(); | |
40 | this.tableModelListener = new TableModelHandler(); | |
41 | } | |
42 | ||
43 | public TableSorter(TableModel tableModel) { | |
44 | this(); | |
45 | setTableModel(tableModel); | |
46 | } | |
47 | ||
48 | ||
49 | private void clearSortingState() { | |
50 | viewToModel = null; | |
51 | modelToView = null; | |
52 | } | |
53 | ||
54 | public TableModel getTableModel() { | |
55 | return tableModel; | |
56 | } | |
57 | ||
58 | public void setTableModel(TableModel tableModel) { | |
59 | if (this.tableModel != null) { | |
60 | this.tableModel.removeTableModelListener(tableModelListener); | |
61 | } | |
62 | ||
63 | this.tableModel = tableModel; | |
64 | if (this.tableModel != null) { | |
65 | this.tableModel.addTableModelListener(tableModelListener); | |
66 | } | |
67 | ||
68 | clearSortingState(); | |
69 | fireTableStructureChanged(); | |
70 | } | |
71 | ||
72 | public JTableHeader getTableHeader() { | |
73 | return tableHeader; | |
74 | } | |
75 | ||
76 | public void setTableHeader(JTableHeader tableHeader) { | |
77 | if (this.tableHeader != null) { | |
78 | this.tableHeader.removeMouseListener(mouseListener); | |
79 | TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer(); | |
80 | if (defaultRenderer instanceof SortableHeaderRenderer) { | |
81 | this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer); | |
82 | } | |
83 | } | |
84 | this.tableHeader = tableHeader; | |
85 | if (this.tableHeader != null) { | |
86 | this.tableHeader.addMouseListener(mouseListener); | |
87 | this.tableHeader.setDefaultRenderer( | |
88 | new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer())); | |
89 | } | |
90 | } | |
91 | ||
92 | public boolean isSorting() { | |
93 | return sortingColumns.size() != 0; | |
94 | } | |
95 | ||
96 | private Directive getDirective(int column) { | |
97 | for (int i = 0; i < sortingColumns.size(); i++) { | |
98 | Directive directive = (Directive)sortingColumns.get(i); | |
99 | if (directive.column == column) { | |
100 | return directive; | |
101 | } | |
102 | } | |
103 | return EMPTY_DIRECTIVE; | |
104 | } | |
105 | ||
106 | public int getSortingStatus(int column) { | |
107 | return getDirective(column).direction; | |
108 | } | |
109 | ||
110 | private void sortingStatusChanged() { | |
111 | clearSortingState(); | |
112 | fireTableDataChanged(); | |
113 | if (tableHeader != null) { | |
114 | tableHeader.repaint(); | |
115 | } | |
116 | } | |
117 | ||
118 | public void setSortingStatus(int column, int status) { | |
119 | Directive directive = getDirective(column); | |
120 | if (directive != EMPTY_DIRECTIVE) { | |
121 | sortingColumns.remove(directive); | |
122 | } | |
123 | if (status != NOT_SORTED) { | |
124 | sortingColumns.add(new Directive(column, status)); | |
125 | } | |
126 | sortingStatusChanged(); | |
127 | } | |
128 | ||
129 | protected Icon getHeaderRendererIcon(int column, int size) { | |
130 | Directive directive = getDirective(column); | |
131 | if (directive == EMPTY_DIRECTIVE) { | |
132 | return null; | |
133 | } | |
134 | return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive)); | |
135 | } | |
136 | ||
137 | private void cancelSorting() { | |
138 | sortingColumns.clear(); | |
139 | sortingStatusChanged(); | |
140 | } | |
141 | ||
142 | private Row[] getViewToModel() { | |
143 | if (viewToModel == null) { | |
144 | int tableModelRowCount = tableModel.getRowCount(); | |
145 | viewToModel = new Row[tableModelRowCount]; | |
146 | for (int row = 0; row < tableModelRowCount; row++) { | |
147 | viewToModel[row] = new Row(row); | |
148 | } | |
149 | ||
150 | if (isSorting()) { | |
151 | Arrays.sort(viewToModel); | |
152 | } | |
153 | } | |
154 | return viewToModel; | |
155 | } | |
156 | ||
157 | public int modelIndex(int viewIndex) { | |
158 | return getViewToModel()[viewIndex].modelIndex; | |
159 | } | |
160 | ||
1bf9fa5a | 161 | public int[] getModelToView() { |
add40ab2 | 162 | if (modelToView == null) { |
163 | int n = getViewToModel().length; | |
164 | modelToView = new int[n]; | |
165 | for (int i = 0; i < n; i++) { | |
166 | modelToView[modelIndex(i)] = i; | |
167 | } | |
168 | } | |
169 | return modelToView; | |
170 | } | |
171 | ||
172 | // TableModel interface methods | |
173 | ||
174 | public int getRowCount() { | |
175 | return (tableModel == null) ? 0 : tableModel.getRowCount(); | |
176 | } | |
177 | ||
178 | public int getColumnCount() { | |
179 | return (tableModel == null) ? 0 : tableModel.getColumnCount(); | |
180 | } | |
181 | ||
182 | public String getColumnName(int column) { | |
183 | return tableModel.getColumnName(column); | |
184 | } | |
185 | ||
186 | public Class<?> getColumnClass(int column) { | |
187 | return tableModel.getColumnClass(column); | |
188 | } | |
189 | ||
190 | public boolean isCellEditable(int row, int column) { | |
191 | return tableModel.isCellEditable(modelIndex(row), column); | |
192 | } | |
193 | ||
194 | public Object getValueAt(int row, int column) { | |
195 | return tableModel.getValueAt(modelIndex(row), column); | |
196 | } | |
197 | ||
198 | public void setValueAt(Object aValue, int row, int column) { | |
199 | tableModel.setValueAt(aValue, modelIndex(row), column); | |
200 | } | |
201 | ||
202 | // Helper classes | |
203 | ||
204 | private class Row implements Comparable { | |
205 | private int modelIndex; | |
206 | ||
207 | public Row(int index) { | |
208 | this.modelIndex = index; | |
209 | } | |
210 | ||
211 | public int compareTo(Object o) { | |
212 | int row1 = modelIndex; | |
213 | int row2 = ((Row) o).modelIndex; | |
214 | ||
215 | for (Iterator it = sortingColumns.iterator(); it.hasNext();) { | |
216 | Directive directive = (Directive) it.next(); | |
217 | int column = directive.column; | |
218 | Object o1 = tableModel.getValueAt(row1, column); | |
219 | Object o2 = tableModel.getValueAt(row2, column); | |
220 | ||
221 | int comparison = 0; | |
222 | // Define null less than everything, except null. | |
223 | if (o1 == null && o2 == null) { | |
224 | comparison = 0; | |
225 | } else if (o1 == null) { | |
226 | comparison = -1; | |
227 | } else if (o2 == null) { | |
228 | comparison = 1; | |
229 | } else { | |
230 | comparison = o1.toString().compareTo(o2.toString());; | |
231 | } | |
232 | if (comparison != 0) { | |
233 | return directive.direction == DESCENDING ? -comparison : comparison; | |
234 | } | |
235 | } | |
236 | return 0; | |
237 | } | |
238 | } | |
239 | ||
240 | private class TableModelHandler implements TableModelListener { | |
241 | public void tableChanged(TableModelEvent e) { | |
242 | // If we're not sorting by anything, just pass the event along. | |
243 | if (!isSorting()) { | |
244 | clearSortingState(); | |
245 | fireTableChanged(e); | |
246 | return; | |
247 | } | |
248 | ||
249 | // If the table structure has changed, cancel the sorting; the | |
250 | // sorting columns may have been either moved or deleted from | |
251 | // the model. | |
252 | if (e.getFirstRow() == TableModelEvent.HEADER_ROW) { | |
253 | cancelSorting(); | |
254 | fireTableChanged(e); | |
255 | return; | |
256 | } | |
257 | ||
258 | // We can map a cell event through to the view without widening | |
259 | // when the following conditions apply: | |
260 | // | |
261 | // a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and, | |
262 | // b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and, | |
263 | // c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and, | |
264 | // d) a reverse lookup will not trigger a sort (modelToView != null) | |
265 | // | |
266 | // Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS. | |
267 | // | |
268 | // The last check, for (modelToView != null) is to see if modelToView | |
269 | // is already allocated. If we don't do this check; sorting can become | |
270 | // a performance bottleneck for applications where cells | |
271 | // change rapidly in different parts of the table. If cells | |
272 | // change alternately in the sorting column and then outside of | |
273 | // it this class can end up re-sorting on alternate cell updates - | |
274 | // which can be a performance problem for large tables. The last | |
275 | // clause avoids this problem. | |
276 | int column = e.getColumn(); | |
277 | if (e.getFirstRow() == e.getLastRow() | |
278 | && column != TableModelEvent.ALL_COLUMNS | |
279 | && getSortingStatus(column) == NOT_SORTED | |
280 | && modelToView != null) { | |
281 | int viewIndex = getModelToView()[e.getFirstRow()]; | |
282 | fireTableChanged(new TableModelEvent(TableSorter.this, | |
283 | viewIndex, viewIndex, | |
284 | column, e.getType())); | |
285 | return; | |
286 | } | |
287 | ||
288 | // Something has happened to the data that may have invalidated the row order. | |
289 | clearSortingState(); | |
290 | fireTableDataChanged(); | |
291 | return; | |
292 | } | |
293 | } | |
294 | ||
295 | private class MouseHandler extends MouseAdapter { | |
296 | public void mouseClicked(MouseEvent e) { | |
297 | JTableHeader h = (JTableHeader) e.getSource(); | |
298 | TableColumnModel columnModel = h.getColumnModel(); | |
299 | int viewColumn = columnModel.getColumnIndexAtX(e.getX()); | |
300 | int column = columnModel.getColumn(viewColumn).getModelIndex(); | |
301 | if (column != -1) { | |
302 | int status = getSortingStatus(column); | |
303 | if (!e.isControlDown()) { | |
304 | cancelSorting(); | |
305 | } | |
306 | // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or | |
307 | // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed. | |
308 | status = status + (e.isShiftDown() ? -1 : 1); | |
309 | status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1} | |
310 | setSortingStatus(column, status); | |
311 | } | |
312 | } | |
313 | } | |
314 | ||
315 | private static class Arrow implements Icon { | |
316 | private boolean descending; | |
317 | private int size; | |
318 | private int priority; | |
319 | ||
320 | public Arrow(boolean descending, int size, int priority) { | |
321 | this.descending = descending; | |
322 | this.size = size; | |
323 | this.priority = priority; | |
324 | } | |
325 | ||
326 | public void paintIcon(Component c, Graphics g, int x, int y) { | |
327 | Color color = c == null ? Color.GRAY : c.getBackground(); | |
328 | // In a compound sort, make each succesive triangle 20% | |
329 | // smaller than the previous one. | |
330 | int dx = (int)(size/2*Math.pow(0.8, priority)); | |
331 | int dy = descending ? dx : -dx; | |
332 | // Align icon (roughly) with font baseline. | |
333 | y = y + 5*size/6 + (descending ? -dy : 0); | |
334 | int shift = descending ? 1 : -1; | |
335 | g.translate(x, y); | |
336 | ||
337 | // Right diagonal. | |
338 | g.setColor(color.darker()); | |
339 | g.drawLine(dx / 2, dy, 0, 0); | |
340 | g.drawLine(dx / 2, dy + shift, 0, shift); | |
341 | ||
342 | // Left diagonal. | |
343 | g.setColor(color.brighter()); | |
344 | g.drawLine(dx / 2, dy, dx, 0); | |
345 | g.drawLine(dx / 2, dy + shift, dx, shift); | |
346 | ||
347 | // Horizontal line. | |
348 | if (descending) { | |
349 | g.setColor(color.darker().darker()); | |
350 | } else { | |
351 | g.setColor(color.brighter().brighter()); | |
352 | } | |
353 | g.drawLine(dx, 0, 0, 0); | |
354 | ||
355 | g.setColor(color); | |
356 | g.translate(-x, -y); | |
357 | } | |
358 | ||
359 | public int getIconWidth() { | |
360 | return size; | |
361 | } | |
362 | ||
363 | public int getIconHeight() { | |
364 | return size; | |
365 | } | |
366 | } | |
367 | ||
368 | private class SortableHeaderRenderer implements TableCellRenderer { | |
369 | private TableCellRenderer tableCellRenderer; | |
370 | ||
371 | public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) { | |
372 | this.tableCellRenderer = tableCellRenderer; | |
373 | } | |
374 | ||
375 | public Component getTableCellRendererComponent(JTable table, | |
376 | Object value, | |
377 | boolean isSelected, | |
378 | boolean hasFocus, | |
379 | int row, | |
380 | int column) { | |
381 | Component c = tableCellRenderer.getTableCellRendererComponent(table, | |
382 | value, isSelected, hasFocus, row, column); | |
383 | if (c instanceof JLabel) { | |
384 | JLabel l = (JLabel) c; | |
385 | l.setHorizontalTextPosition(JLabel.LEFT); | |
386 | int modelColumn = table.convertColumnIndexToModel(column); | |
387 | l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize())); | |
388 | } | |
389 | return c; | |
390 | } | |
391 | } | |
392 | ||
393 | private static class Directive { | |
394 | private int column; | |
395 | private int direction; | |
396 | ||
397 | public Directive(int column, int direction) { | |
398 | this.column = column; | |
399 | this.direction = direction; | |
400 | } | |
401 | } | |
402 | } |