| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
| TableAssociation |
|
| 2.268292682926829;2.268 |
| 1 | 0 | /* |
| 2 | Wotonomy: OpenStep design patterns for pure Java applications. |
|
| 3 | Copyright (C) 2000 Intersect Software Corporation |
|
| 4 | ||
| 5 | This library is free software; you can redistribute it and/or |
|
| 6 | modify it under the terms of the GNU Lesser General Public |
|
| 7 | License as published by the Free Software Foundation; either |
|
| 8 | version 2.1 of the License, or (at your option) any later version. |
|
| 9 | ||
| 10 | This library is distributed in the hope that it will be useful, |
|
| 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
| 13 | Lesser General Public License for more details. |
|
| 14 | ||
| 15 | You should have received a copy of the GNU Lesser General Public |
|
| 16 | License along with this library; if not, see http://www.gnu.org |
|
| 17 | */ |
|
| 18 | ||
| 19 | package net.wotonomy.ui.swing; |
|
| 20 | ||
| 21 | import java.awt.EventQueue; |
|
| 22 | import java.awt.Graphics; |
|
| 23 | import java.awt.Rectangle; |
|
| 24 | import java.awt.Toolkit; |
|
| 25 | import java.awt.datatransfer.Clipboard; |
|
| 26 | import java.awt.datatransfer.StringSelection; |
|
| 27 | import java.awt.event.ActionEvent; |
|
| 28 | import java.awt.event.ActionListener; |
|
| 29 | import java.awt.event.FocusEvent; |
|
| 30 | import java.awt.event.FocusListener; |
|
| 31 | import java.awt.event.KeyEvent; |
|
| 32 | import java.awt.event.MouseAdapter; |
|
| 33 | import java.awt.event.MouseEvent; |
|
| 34 | import java.util.Enumeration; |
|
| 35 | ||
| 36 | import javax.swing.CellEditor; |
|
| 37 | import javax.swing.JComponent; |
|
| 38 | import javax.swing.JTable; |
|
| 39 | import javax.swing.KeyStroke; |
|
| 40 | import javax.swing.event.ListSelectionEvent; |
|
| 41 | import javax.swing.event.ListSelectionListener; |
|
| 42 | import javax.swing.table.AbstractTableModel; |
|
| 43 | import javax.swing.table.JTableHeader; |
|
| 44 | import javax.swing.table.TableColumn; |
|
| 45 | import javax.swing.table.TableModel; |
|
| 46 | ||
| 47 | import net.wotonomy.control.EOSortOrdering; |
|
| 48 | import net.wotonomy.foundation.NSArray; |
|
| 49 | import net.wotonomy.foundation.NSMutableArray; |
|
| 50 | import net.wotonomy.foundation.internal.WotonomyException; |
|
| 51 | import net.wotonomy.ui.EOAssociation; |
|
| 52 | import net.wotonomy.ui.EODisplayGroup; |
|
| 53 | ||
| 54 | /** |
|
| 55 | * TableAssociation binds one or more TableColumnAssociations |
|
| 56 | * to a display group. You should not instantiate this class |
|
| 57 | * directly; use TableColumnAssociation.setTable() instead. |
|
| 58 | * Note that TableAssociation inserts itself as the controlled |
|
| 59 | * JTable's TableModel. |
|
| 60 | * |
|
| 61 | * Bindings are: |
|
| 62 | * <ul> |
|
| 63 | * <li>source: a property convertable to a string for |
|
| 64 | * display in the cells of the table column</li> |
|
| 65 | * <li>enabled: a property convertable to a string for |
|
| 66 | * display in the cells of the table column. |
|
| 67 | * Note that you can bind this aspect to a key equal to |
|
| 68 | * "true" or "false" and leave the display group null.</li> |
|
| 69 | * </ul> |
|
| 70 | * |
|
| 71 | * @author michael@mpowers.net |
|
| 72 | * @author $Author: cgruber $ |
|
| 73 | * @version $Revision: 904 $ |
|
| 74 | */ |
|
| 75 | 0 | public class TableAssociation extends EOAssociation |
| 76 | implements ActionListener, ListSelectionListener, FocusListener |
|
| 77 | { |
|
| 78 | 0 | static final NSArray aspects = |
| 79 | 0 | new NSArray( new Object[] { |
| 80 | 0 | SourceAspect, EnabledAspect |
| 81 | } ); |
|
| 82 | 0 | static final NSArray aspectSignatures = |
| 83 | 0 | new NSArray( new Object[] { |
| 84 | 0 | AttributeToOneAspectSignature |
| 85 | } ); |
|
| 86 | 0 | static final NSArray objectKeysTaken = |
| 87 | 0 | new NSArray( new Object[] { |
| 88 | 0 | "tableModel", "tableHeader" |
| 89 | } ); |
|
| 90 | ||
| 91 | // key command to copy contents to clipboard |
|
| 92 | static public final String COPY = "COPY"; |
|
| 93 | ||
| 94 | EODisplayGroup source; |
|
| 95 | EODisplayGroup sortTarget; // used by TreeColumnAssociation |
|
| 96 | NSMutableArray columns; |
|
| 97 | JTableHeader tableHeader; |
|
| 98 | ||
| 99 | boolean pleaseIgnore; |
|
| 100 | boolean selectionPaintedImmediately; |
|
| 101 | boolean selectionTracking; |
|
| 102 | ||
| 103 | /** |
|
| 104 | * Constructor specifying the object to be controlled by this |
|
| 105 | * association. Throws an exception if the object is not |
|
| 106 | * a TableColumn. setTable() must be called before |
|
| 107 | * establishing the connection. |
|
| 108 | */ |
|
| 109 | public TableAssociation ( Object anObject ) |
|
| 110 | { |
|
| 111 | 0 | super( anObject ); |
| 112 | 0 | source = null; |
| 113 | 0 | columns = new NSMutableArray(); |
| 114 | 0 | JTable aTable = ((JTable)anObject); |
| 115 | 0 | aTable.addFocusListener( this ); |
| 116 | 0 | aTable.setModel( |
| 117 | 0 | new TableAssociationModel( this ) ); |
| 118 | // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X |
|
| 119 | ||
| 120 | // why did sun make this harder to use? |
|
| 121 | //aTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( |
|
| 122 | // KeyStroke.getKeyStroke( KeyEvent.VK_C, java.awt.Event.CTRL_MASK ), COPY); |
|
| 123 | //aTable.getActionMap().put(COPY, this); |
|
| 124 | ||
| 125 | 0 | aTable.registerKeyboardAction( this, COPY, |
| 126 | 0 | KeyStroke.getKeyStroke( KeyEvent.VK_C, |
| 127 | 0 | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), |
| 128 | 0 | JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); |
| 129 | 0 | aTable.registerKeyboardAction( this, COPY, |
| 130 | 0 | KeyStroke.getKeyStroke( KeyEvent.VK_X, |
| 131 | 0 | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), |
| 132 | 0 | JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); |
| 133 | 0 | tableHeader = new SortedTableHeader(); |
| 134 | 0 | tableHeader.setColumnModel( aTable.getColumnModel() ); |
| 135 | 0 | aTable.setTableHeader( tableHeader ); |
| 136 | 0 | tableHeader.addMouseListener( new TableHeaderListener() ); |
| 137 | 0 | pleaseIgnore = false; |
| 138 | 0 | selectionPaintedImmediately = false; |
| 139 | 0 | selectionTracking = false; |
| 140 | 0 | } |
| 141 | ||
| 142 | /** |
|
| 143 | * Returns a List of aspect signatures whose contents |
|
| 144 | * correspond with the aspects list. Each element is |
|
| 145 | * a string whose characters represent a capability of |
|
| 146 | * the corresponding aspect. <ul> |
|
| 147 | * <li>"A" attribute: the aspect can be bound to |
|
| 148 | * an attribute.</li> |
|
| 149 | * <li>"1" to-one: the aspect can be bound to a |
|
| 150 | * property that returns a single object.</li> |
|
| 151 | * <li>"M" to-one: the aspect can be bound to a |
|
| 152 | * property that returns multiple objects.</li> |
|
| 153 | * </ul> |
|
| 154 | * An empty signature "" means that the aspect can |
|
| 155 | * bind without needing a key. |
|
| 156 | * This implementation returns "A1M" for each |
|
| 157 | * element in the aspects array. |
|
| 158 | */ |
|
| 159 | public static NSArray aspectSignatures () |
|
| 160 | { |
|
| 161 | 0 | return aspectSignatures; |
| 162 | } |
|
| 163 | ||
| 164 | /** |
|
| 165 | * Returns a List that describes the aspects supported |
|
| 166 | * by this class. Each element in the list is the string |
|
| 167 | * name of the aspect. This implementation returns an |
|
| 168 | * empty list. |
|
| 169 | */ |
|
| 170 | public static NSArray aspects () |
|
| 171 | { |
|
| 172 | 0 | return aspects; |
| 173 | } |
|
| 174 | ||
| 175 | /** |
|
| 176 | * Returns a List of EOAssociation subclasses that, |
|
| 177 | * for the objects that are usable for this association, |
|
| 178 | * are less suitable than this association. |
|
| 179 | */ |
|
| 180 | public static NSArray associationClassesSuperseded () |
|
| 181 | { |
|
| 182 | 0 | return new NSArray(); |
| 183 | } |
|
| 184 | ||
| 185 | /** |
|
| 186 | * Returns whether this class can control the specified |
|
| 187 | * object. |
|
| 188 | */ |
|
| 189 | public static boolean isUsableWithObject ( Object anObject ) |
|
| 190 | { |
|
| 191 | 0 | return ( anObject instanceof JTable ); |
| 192 | } |
|
| 193 | ||
| 194 | /** |
|
| 195 | * Returns a List of properties of the controlled object |
|
| 196 | * that are controlled by this class. For example, |
|
| 197 | * "stringValue", or "selected". |
|
| 198 | */ |
|
| 199 | public static NSArray objectKeysTaken () |
|
| 200 | { |
|
| 201 | 0 | return objectKeysTaken; |
| 202 | } |
|
| 203 | ||
| 204 | /** |
|
| 205 | * Returns the aspect that is considered primary |
|
| 206 | * or default. This is typically "value" or somesuch. |
|
| 207 | */ |
|
| 208 | public static String primaryAspect () |
|
| 209 | { |
|
| 210 | 0 | return ValueAspect; |
| 211 | } |
|
| 212 | ||
| 213 | /** |
|
| 214 | * Returns whether this association can bind to the |
|
| 215 | * specified display group on the specified key for |
|
| 216 | * the specified aspect. |
|
| 217 | */ |
|
| 218 | public boolean canBindAspect ( |
|
| 219 | String anAspect, EODisplayGroup aDisplayGroup, String aKey) |
|
| 220 | { |
|
| 221 | 0 | return ( aspects.containsObject( anAspect ) ); |
| 222 | } |
|
| 223 | ||
| 224 | /** |
|
| 225 | * Binds the specified aspect of this association to the |
|
| 226 | * specified key on the specified display group. |
|
| 227 | */ |
|
| 228 | public void bindAspect ( |
|
| 229 | String anAspect, EODisplayGroup aDisplayGroup, String aKey ) |
|
| 230 | { |
|
| 231 | 0 | if ( SourceAspect.equals( anAspect ) ) |
| 232 | { |
|
| 233 | 0 | source = aDisplayGroup; |
| 234 | } |
|
| 235 | 0 | super.bindAspect( anAspect, aDisplayGroup, aKey ); |
| 236 | 0 | } |
| 237 | ||
| 238 | /** |
|
| 239 | * Establishes a connection between this association |
|
| 240 | * and the controlled object. Subclasses should begin |
|
| 241 | * listening for events from their controlled object here. |
|
| 242 | */ |
|
| 243 | public void establishConnection () |
|
| 244 | { |
|
| 245 | 0 | if ( source == null ) |
| 246 | { |
|
| 247 | 0 | throw new WotonomyException( "No display group: " + |
| 248 | "please ensure that the TableColumnAssociation " + |
|
| 249 | "has a display group bound to the ValueAspect" ); |
|
| 250 | } |
|
| 251 | 0 | super.establishConnection(); |
| 252 | 0 | selectFromDisplayGroup(); |
| 253 | 0 | addAsListener(); |
| 254 | 0 | } |
| 255 | ||
| 256 | /** |
|
| 257 | * Breaks the connection between this association and |
|
| 258 | * its object. Override to stop listening for events |
|
| 259 | * from the object. |
|
| 260 | */ |
|
| 261 | public void breakConnection () |
|
| 262 | { |
|
| 263 | 0 | removeAsListener(); |
| 264 | 0 | super.breakConnection(); |
| 265 | 0 | } |
| 266 | ||
| 267 | protected void addAsListener() |
|
| 268 | { |
|
| 269 | 0 | component().getSelectionModel() |
| 270 | 0 | .addListSelectionListener( this ); |
| 271 | 0 | } |
| 272 | ||
| 273 | protected void removeAsListener() |
|
| 274 | { |
|
| 275 | 0 | component().getSelectionModel() |
| 276 | 0 | .removeListSelectionListener( this ); |
| 277 | 0 | } |
| 278 | ||
| 279 | /** |
|
| 280 | * Forces this association to cause the object to |
|
| 281 | * stop editing and validate the user's input. |
|
| 282 | * @return false if there were problems validating, |
|
| 283 | * or true to continue. |
|
| 284 | */ |
|
| 285 | public boolean endEditing () |
|
| 286 | { |
|
| 287 | // stop any cell editing |
|
| 288 | 0 | CellEditor editor = component().getCellEditor(); |
| 289 | 0 | if ( editor != null ) |
| 290 | { |
|
| 291 | 0 | return editor.stopCellEditing(); |
| 292 | } |
|
| 293 | 0 | return true; |
| 294 | } |
|
| 295 | ||
| 296 | /** |
|
| 297 | * Called when either the selection or the contents |
|
| 298 | * of an associated display group have changed. |
|
| 299 | */ |
|
| 300 | public void subjectChanged () |
|
| 301 | { |
|
| 302 | 0 | if ( source != null ) |
| 303 | { |
|
| 304 | 0 | if ( source.contentsChanged() ) |
| 305 | { |
|
| 306 | 0 | removeAsListener(); |
| 307 | 0 | ((TableAssociationModel)component().getModel()). |
| 308 | 0 | fireTableDataChanged(); |
| 309 | 0 | selectFromDisplayGroup(); |
| 310 | 0 | addAsListener(); |
| 311 | ||
| 312 | // if we caused this change, do nothing |
|
| 313 | 0 | if ( pleaseIgnore ) |
| 314 | { |
|
| 315 | 0 | pleaseIgnore = false; |
| 316 | 0 | } |
| 317 | else // otherwise, update the sort indicator |
|
| 318 | { |
|
| 319 | 0 | tableHeader.repaint(); |
| 320 | ||
| 321 | // cancel any cell editing |
|
| 322 | 0 | CellEditor editor = component().getCellEditor(); |
| 323 | 0 | if ( editor != null ) |
| 324 | { |
|
| 325 | 0 | editor.cancelCellEditing(); |
| 326 | } |
|
| 327 | } |
|
| 328 | 0 | } |
| 329 | else |
|
| 330 | 0 | if ( source.selectionChanged() ) |
| 331 | { |
|
| 332 | 0 | removeAsListener(); |
| 333 | 0 | selectFromDisplayGroup(); |
| 334 | 0 | addAsListener(); |
| 335 | } |
|
| 336 | } |
|
| 337 | ||
| 338 | 0 | } |
| 339 | ||
| 340 | private void selectFromDisplayGroup() |
|
| 341 | { |
|
| 342 | 0 | JTable component = component(); |
| 343 | ||
| 344 | int index; |
|
| 345 | 0 | component.getSelectionModel().clearSelection(); |
| 346 | 0 | Enumeration e = |
| 347 | 0 | source.selectionIndexes().objectEnumerator(); |
| 348 | ||
| 349 | 0 | while ( e.hasMoreElements() ) |
| 350 | { // add selections one-by-one to support non-contiguous |
|
| 351 | 0 | index = ((Number)e.nextElement()).intValue(); |
| 352 | 0 | component.getSelectionModel().addSelectionInterval( |
| 353 | 0 | index, index ); // adds one row |
| 354 | 0 | } |
| 355 | 0 | } |
| 356 | ||
| 357 | // interface ListSelectionListener |
|
| 358 | ||
| 359 | public void valueChanged(ListSelectionEvent e) |
|
| 360 | { |
|
| 361 | 0 | if ( source != null ) |
| 362 | { |
|
| 363 | 0 | if ( selectionTracking || !e.getValueIsAdjusting() ) |
| 364 | { |
|
| 365 | 0 | int[] selectedIndices = component().getSelectedRows(); |
| 366 | 0 | final NSMutableArray indexList = new NSMutableArray(); |
| 367 | 0 | for ( int i = 0; i < selectedIndices.length; i++ ) |
| 368 | { |
|
| 369 | 0 | indexList.addObject( new Integer( selectedIndices[i] ) ); |
| 370 | } |
|
| 371 | ||
| 372 | // invoke later so the component is repainted before |
|
| 373 | // any potentially lengthy second-order effects happen: |
|
| 374 | // this improves user-perceived responsiveness of big apps |
|
| 375 | 0 | Runnable select = new Runnable() |
| 376 | { |
|
| 377 | 0 | public void run() |
| 378 | { |
|
| 379 | 0 | pleaseIgnore = true; |
| 380 | 0 | source.setSelectionIndexes( indexList ); |
| 381 | 0 | } |
| 382 | }; |
|
| 383 | 0 | if ( selectionPaintedImmediately ) |
| 384 | { |
|
| 385 | 0 | EventQueue.invokeLater( select ); |
| 386 | 0 | } |
| 387 | else |
|
| 388 | { |
|
| 389 | 0 | select.run(); |
| 390 | } |
|
| 391 | } |
|
| 392 | } |
|
| 393 | 0 | } |
| 394 | ||
| 395 | /** |
|
| 396 | * Determines whether the selection should be painted |
|
| 397 | * immediately after the user clicks and therefore |
|
| 398 | * before the children display group is updated. |
|
| 399 | * When the children group is bound to many associations |
|
| 400 | * or is bound to a master-detail association, updating |
|
| 401 | * the display group can take a perceptibly long time. |
|
| 402 | * This property defaults to false. |
|
| 403 | * @see #setSelectionPaintedImmediately |
|
| 404 | */ |
|
| 405 | public boolean isSelectionPaintedImmediately() |
|
| 406 | { |
|
| 407 | 0 | return selectionPaintedImmediately; |
| 408 | } |
|
| 409 | ||
| 410 | /** |
|
| 411 | * Sets whether the selection should be painted immediately. |
|
| 412 | * Setting this property to true will let the table paint |
|
| 413 | * first before the display group is updated. |
|
| 414 | */ |
|
| 415 | public void setSelectionPaintedImmediately( boolean isImmediate ) |
|
| 416 | { |
|
| 417 | 0 | selectionPaintedImmediately = isImmediate; |
| 418 | 0 | } |
| 419 | ||
| 420 | /** |
|
| 421 | * Determines whether the selection is actively tracking |
|
| 422 | * the selection as the user moves the mouse. |
|
| 423 | * If true, selection will not be updated while the |
|
| 424 | * list selection event returns true for isValueAdjusting(). |
|
| 425 | * This property defaults to false. |
|
| 426 | * @see #setSelectionTracking |
|
| 427 | */ |
|
| 428 | public boolean isSelectionTracking() |
|
| 429 | { |
|
| 430 | 0 | return selectionTracking; |
| 431 | } |
|
| 432 | ||
| 433 | /** |
|
| 434 | * Sets whether the selection is actively tracking |
|
| 435 | * the selection as the user moves the mouse. |
|
| 436 | * Setting this property to true will update the display |
|
| 437 | * group with each change to the selection. |
|
| 438 | * This means that any tree selection listers will |
|
| 439 | * also be notified before the display group is updated |
|
| 440 | * and will have to invokeLater if they want to see the |
|
| 441 | * updated display group. |
|
| 442 | */ |
|
| 443 | public void setSelectionTracking( boolean isTracking ) |
|
| 444 | { |
|
| 445 | 0 | selectionTracking = isTracking; |
| 446 | 0 | } |
| 447 | ||
| 448 | // convenience |
|
| 449 | 0 | private JTable component() |
| 450 | { |
|
| 451 | 0 | return (JTable) object(); |
| 452 | } |
|
| 453 | ||
| 454 | /** |
|
| 455 | * Copies the contents of the table to the clipboard as a tab-delimited string. |
|
| 456 | */ |
|
| 457 | public void copyToClipboard() |
|
| 458 | { |
|
| 459 | 0 | Toolkit toolkit = Toolkit.getDefaultToolkit(); |
| 460 | 0 | Clipboard clipboard = toolkit.getSystemClipboard(); |
| 461 | 0 | StringSelection selection = |
| 462 | 0 | new StringSelection( getTabDelimitedString() ); |
| 463 | 0 | clipboard.setContents( selection, selection ); |
| 464 | 0 | } |
| 465 | ||
| 466 | /** |
|
| 467 | * Converts the contents of the table to a tab-delimited string. |
|
| 468 | * @return A String containing the text contents of the table. |
|
| 469 | */ |
|
| 470 | public String getTabDelimitedString() |
|
| 471 | { |
|
| 472 | 0 | StringBuffer result = new StringBuffer(64); |
| 473 | ||
| 474 | 0 | TableModel model = component().getModel(); |
| 475 | 0 | int cols = model.getColumnCount(); |
| 476 | 0 | int rows = model.getRowCount(); |
| 477 | ||
| 478 | 0 | Object o = null; |
| 479 | 0 | for ( int y = 0; y < rows; y++ ) |
| 480 | { |
|
| 481 | 0 | for ( int x = 0; x < cols; x++ ) |
| 482 | { |
|
| 483 | 0 | o = model.getValueAt( y, x ); |
| 484 | 0 | if ( o == null ) o = ""; |
| 485 | 0 | result.append( o ); |
| 486 | 0 | result.append( '\t' ); |
| 487 | } |
|
| 488 | 0 | result.append( '\n' ); |
| 489 | } |
|
| 490 | ||
| 491 | 0 | return result.toString(); |
| 492 | } |
|
| 493 | ||
| 494 | // interface ActionEventListener |
|
| 495 | ||
| 496 | public void actionPerformed(ActionEvent evt) |
|
| 497 | { |
|
| 498 | 0 | String cmd = evt.getActionCommand(); |
| 499 | ||
| 500 | 0 | if ( COPY.equals( cmd ) ) |
| 501 | { |
|
| 502 | 0 | copyToClipboard(); |
| 503 | 0 | return; |
| 504 | } |
|
| 505 | 0 | } |
| 506 | ||
| 507 | /** |
|
| 508 | * Used to render the little triangle over the sorted column(s). |
|
| 509 | */ |
|
| 510 | 0 | class SortedTableHeader extends JTableHeader |
| 511 | { |
|
| 512 | public void paint(Graphics g) |
|
| 513 | { |
|
| 514 | 0 | super.paint( g ); |
| 515 | Rectangle r; |
|
| 516 | TableColumnAssociation association; |
|
| 517 | 0 | int size = columns.size(); |
| 518 | NSArray orderings; |
|
| 519 | 0 | if ( sortTarget != null ) |
| 520 | { |
|
| 521 | 0 | orderings = sortTarget.sortOrderings(); |
| 522 | 0 | } |
| 523 | else |
|
| 524 | { |
|
| 525 | 0 | orderings = source.sortOrderings(); |
| 526 | } |
|
| 527 | 0 | for ( int i = 0; i < size; i++ ) |
| 528 | { |
|
| 529 | 0 | r = getHeaderRect( component().convertColumnIndexToView( i ) ); |
| 530 | 0 | association = (TableColumnAssociation) columns.objectAtIndex( i ); |
| 531 | 0 | association.drawSortIndicator( r, g, orderings ); |
| 532 | } |
|
| 533 | 0 | } |
| 534 | } |
|
| 535 | ||
| 536 | /** |
|
| 537 | * Used to listen for clicks on the table header. |
|
| 538 | */ |
|
| 539 | 0 | class TableHeaderListener extends MouseAdapter |
| 540 | { |
|
| 541 | public void mouseClicked( MouseEvent evt ) |
|
| 542 | { |
|
| 543 | 0 | EODisplayGroup displayGroup = sortTarget; |
| 544 | 0 | if ( displayGroup == null ) displayGroup = source; |
| 545 | ||
| 546 | 0 | if ( evt.getClickCount() > 0 ) |
| 547 | { |
|
| 548 | 0 | int columnClicked = tableHeader.columnAtPoint( evt.getPoint() ); |
| 549 | 0 | if ( columnClicked != -1 ) |
| 550 | { |
|
| 551 | 0 | columnClicked = component().convertColumnIndexToModel( columnClicked ); |
| 552 | 0 | TableColumnAssociation association = (TableColumnAssociation) |
| 553 | 0 | columns.objectAtIndex( columnClicked ); |
| 554 | 0 | if ( association.isSortable() ) |
| 555 | { |
|
| 556 | 0 | NSMutableArray newOrder = |
| 557 | 0 | new NSMutableArray( displayGroup.sortOrderings() ); |
| 558 | ||
| 559 | // click once = asc, twice = desc, thrice = clear |
|
| 560 | EOSortOrdering sortOrdering; |
|
| 561 | 0 | int index = association.getIndexOfMatchingOrdering( newOrder ); |
| 562 | 0 | if ( index == -1 ) sortOrdering = null; |
| 563 | 0 | else if ( index == 1 ) sortOrdering = association.getSortOrdering( false ); |
| 564 | 0 | else sortOrdering = association.getSortOrdering( true ); |
| 565 | ||
| 566 | 0 | pleaseIgnore = true; |
| 567 | 0 | tableHeader.repaint(); |
| 568 | ||
| 569 | // stop any cell editing |
|
| 570 | 0 | CellEditor editor = component().getCellEditor(); |
| 571 | 0 | if ( editor != null ) |
| 572 | { |
|
| 573 | 0 | editor.stopCellEditing(); |
| 574 | } |
|
| 575 | ||
| 576 | // remove existing key |
|
| 577 | 0 | if ( index != 0 ) |
| 578 | { |
|
| 579 | 0 | newOrder.removeObjectAtIndex( Math.abs( index ) - 1 ); |
| 580 | } |
|
| 581 | ||
| 582 | // put new key at front of list |
|
| 583 | 0 | if ( sortOrdering != null ) |
| 584 | { |
|
| 585 | 0 | newOrder.insertObjectAtIndex( sortOrdering, 0 ); |
| 586 | } |
|
| 587 | ||
| 588 | 0 | displayGroup.setSortOrderings( newOrder ); |
| 589 | 0 | displayGroup.updateDisplayedObjects(); |
| 590 | } |
|
| 591 | } |
|
| 592 | } |
|
| 593 | 0 | } |
| 594 | } |
|
| 595 | ||
| 596 | /** |
|
| 597 | * Notifies of beginning of edit. |
|
| 598 | */ |
|
| 599 | public void focusGained(FocusEvent evt) |
|
| 600 | { |
|
| 601 | Object o; |
|
| 602 | EODisplayGroup displayGroup; |
|
| 603 | 0 | Enumeration e = aspects().objectEnumerator(); |
| 604 | 0 | while ( e.hasMoreElements() ) |
| 605 | { |
|
| 606 | 0 | displayGroup = |
| 607 | 0 | displayGroupForAspect( e.nextElement().toString() ); |
| 608 | 0 | if ( displayGroup != null ) |
| 609 | { |
|
| 610 | 0 | displayGroup.associationDidBeginEditing( this ); |
| 611 | 0 | } |
| 612 | } |
|
| 613 | 0 | } |
| 614 | ||
| 615 | /** |
|
| 616 | * Updates object on focus lost and notifies of end of edit. |
|
| 617 | */ |
|
| 618 | public void focusLost(FocusEvent evt) |
|
| 619 | { |
|
| 620 | 0 | if ( ! component().isEditing() ) |
| 621 | { |
|
| 622 | Object o; |
|
| 623 | EODisplayGroup displayGroup; |
|
| 624 | 0 | Enumeration e = aspects().objectEnumerator(); |
| 625 | 0 | while ( e.hasMoreElements() ) |
| 626 | { |
|
| 627 | 0 | displayGroup = |
| 628 | 0 | displayGroupForAspect( e.nextElement().toString() ); |
| 629 | 0 | if ( displayGroup != null ) |
| 630 | { |
|
| 631 | 0 | displayGroup.associationDidEndEditing( this ); |
| 632 | 0 | } |
| 633 | } |
|
| 634 | } |
|
| 635 | 0 | } |
| 636 | ||
| 637 | /** |
|
| 638 | * Used as the model for the controlled table. |
|
| 639 | * Package access so TableColumnAssociation can recognize |
|
| 640 | * it and use the addColumnAssociation() method. |
|
| 641 | * Extends AbstractTableModel just so we get event |
|
| 642 | * broadcasting for free. |
|
| 643 | */ |
|
| 644 | 0 | static class TableAssociationModel extends AbstractTableModel |
| 645 | { |
|
| 646 | private TableAssociation parent; |
|
| 647 | ||
| 648 | 0 | private TableAssociationModel( TableAssociation aParent ) |
| 649 | 0 | { |
| 650 | 0 | parent = aParent; |
| 651 | 0 | } |
| 652 | ||
| 653 | public TableAssociation getAssociation() |
|
| 654 | { |
|
| 655 | 0 | return parent; |
| 656 | } |
|
| 657 | ||
| 658 | /** |
|
| 659 | * Adds the column to the list of ColumnAssociations, |
|
| 660 | * and adds the corresponding column to the table |
|
| 661 | * at the next available index, setting the value of |
|
| 662 | * the column's model index accordingly. |
|
| 663 | * Establishes connection if no columns are currently |
|
| 664 | * associated. |
|
| 665 | */ |
|
| 666 | public void addColumnAssociation( |
|
| 667 | TableColumnAssociation aColumnAssociation ) |
|
| 668 | { |
|
| 669 | // establish connection if necessary |
|
| 670 | 0 | if ( parent.columns.size() == 0 ) |
| 671 | { |
|
| 672 | 0 | parent.establishConnection(); |
| 673 | } |
|
| 674 | ||
| 675 | 0 | int newIndex = parent.columns.count(); |
| 676 | 0 | parent.columns.add( aColumnAssociation ); |
| 677 | ||
| 678 | // add column to table |
|
| 679 | 0 | TableColumn column = (TableColumn) aColumnAssociation.object(); |
| 680 | 0 | column.setModelIndex( newIndex ); |
| 681 | 0 | parent.component().addColumn( column ); |
| 682 | ||
| 683 | 0 | } |
| 684 | ||
| 685 | /** |
|
| 686 | * Removes the column from the list of ColumnAssociations, |
|
| 687 | * and removes the corresponding column from the table. |
|
| 688 | * Breaks connection if no more columns are associated. |
|
| 689 | */ |
|
| 690 | public void removeColumnAssociation( |
|
| 691 | TableColumnAssociation aColumnAssociation ) |
|
| 692 | { |
|
| 693 | 0 | int index = parent.columns.indexOfIdenticalObject( aColumnAssociation ); |
| 694 | 0 | if ( index == NSArray.NotFound ) return; |
| 695 | ||
| 696 | 0 | parent.columns.removeObjectAtIndex( index ); |
| 697 | ||
| 698 | // remove column from table |
|
| 699 | 0 | TableColumn column = (TableColumn) aColumnAssociation.object(); |
| 700 | 0 | parent.component().removeColumn( column ); |
| 701 | ||
| 702 | // break connection if necessary |
|
| 703 | 0 | if ( parent.columns.size() == 0 ) |
| 704 | { |
|
| 705 | 0 | parent.breakConnection(); |
| 706 | } |
|
| 707 | 0 | } |
| 708 | ||
| 709 | public int getRowCount() |
|
| 710 | { |
|
| 711 | 0 | if ( parent.source == null ) return 0; |
| 712 | 0 | return parent.source.displayedObjects().count(); |
| 713 | } |
|
| 714 | ||
| 715 | public int getColumnCount() |
|
| 716 | { |
|
| 717 | 0 | return parent.columns.count(); |
| 718 | } |
|
| 719 | ||
| 720 | /** |
|
| 721 | * Attempts to retrieve the header value from the specified column, |
|
| 722 | * or returns " " if the value is null. |
|
| 723 | */ |
|
| 724 | public String getColumnName(int columnIndex) |
|
| 725 | { |
|
| 726 | 0 | TableColumnAssociation association = (TableColumnAssociation) |
| 727 | 0 | parent.columns.objectAtIndex( columnIndex ); |
| 728 | 0 | Object value = ((TableColumn)association.object()).getHeaderValue(); |
| 729 | 0 | if ( value != null ) return value.toString(); |
| 730 | 0 | return " "; |
| 731 | } |
|
| 732 | ||
| 733 | /** |
|
| 734 | * Returns the class of the first item in the |
|
| 735 | * display group bound to the column. |
|
| 736 | */ |
|
| 737 | public Class getColumnClass(int columnIndex) |
|
| 738 | { |
|
| 739 | Object value; |
|
| 740 | 0 | int count = getRowCount(); |
| 741 | 0 | for( int i = 0; i < count; i++ ) |
| 742 | { //First row in column is null find one that is not. |
|
| 743 | 0 | value = ((TableColumnAssociation)parent.columns. |
| 744 | 0 | objectAtIndex( columnIndex ) ).valueAtIndex( i ); |
| 745 | 0 | if ( value != null ) return value.getClass(); |
| 746 | } |
|
| 747 | 0 | return Object.class; |
| 748 | } |
|
| 749 | ||
| 750 | /** |
|
| 751 | * Calls the column association's isEditableAtRow method. |
|
| 752 | */ |
|
| 753 | public boolean isCellEditable(int rowIndex, |
|
| 754 | int columnIndex) |
|
| 755 | { |
|
| 756 | 0 | return |
| 757 | 0 | ((TableColumnAssociation)parent.columns.objectAtIndex( |
| 758 | 0 | columnIndex ) ).isEditableAtRow( rowIndex ); |
| 759 | } |
|
| 760 | ||
| 761 | /** |
|
| 762 | * Calls the column association's valueAtIndex method. |
|
| 763 | */ |
|
| 764 | public Object getValueAt(int rowIndex, |
|
| 765 | int columnIndex) |
|
| 766 | { |
|
| 767 | 0 | return |
| 768 | 0 | ((TableColumnAssociation)parent.columns.objectAtIndex( |
| 769 | 0 | columnIndex ) ).valueAtIndex( rowIndex ); |
| 770 | } |
|
| 771 | ||
| 772 | /** |
|
| 773 | * Calls the column association's setValueAtIndex method. |
|
| 774 | */ |
|
| 775 | public void setValueAt(Object aValue, |
|
| 776 | int rowIndex, |
|
| 777 | int columnIndex) |
|
| 778 | { |
|
| 779 | 0 | Object existingValue = getValueAt( rowIndex, columnIndex ); |
| 780 | ||
| 781 | // don't update display group if value is the same as before |
|
| 782 | 0 | if ( aValue == existingValue ) return; // handles null case |
| 783 | 0 | if ( existingValue != null ) // both are not null |
| 784 | { |
|
| 785 | 0 | Object newValue = aValue; |
| 786 | ||
| 787 | // if different types |
|
| 788 | 0 | if ( newValue.getClass() != existingValue.getClass() ) |
| 789 | { |
|
| 790 | // convert to string since most editors do anyway |
|
| 791 | 0 | newValue = newValue.toString(); |
| 792 | 0 | existingValue = existingValue.toString(); |
| 793 | } |
|
| 794 | 0 | if ( newValue.equals( existingValue ) ) |
| 795 | { |
|
| 796 | // same value - do nothing |
|
| 797 | 0 | return; |
| 798 | } |
|
| 799 | } |
|
| 800 | ||
| 801 | 0 | ((TableColumnAssociation)parent.columns.objectAtIndex( |
| 802 | 0 | columnIndex ) ).setValueAtIndex( aValue, rowIndex ); |
| 803 | 0 | } |
| 804 | ||
| 805 | } |
|
| 806 | ||
| 807 | } |
|
| 808 | ||
| 809 | /* |
|
| 810 | * $Log$ |
|
| 811 | * Revision 1.2 2006/02/18 23:19:05 cgruber |
|
| 812 | * Update imports and maven dependencies. |
|
| 813 | * |
|
| 814 | * Revision 1.1 2006/02/16 13:22:22 cgruber |
|
| 815 | * Check in all sources in eclipse-friendly maven-enabled packages. |
|
| 816 | * |
|
| 817 | * Revision 1.31 2003/08/06 23:07:52 chochos |
|
| 818 | * general code cleanup (mostly, removing unused imports) |
|
| 819 | * |
|
| 820 | * Revision 1.30 2002/11/16 16:33:31 mpowers |
|
| 821 | * Now using platform-specific accelerator key for shortcuts. |
|
| 822 | * |
|
| 823 | * Revision 1.29 2002/05/24 14:41:29 mpowers |
|
| 824 | * Throwing an exception for clarity. |
|
| 825 | * |
|
| 826 | * Revision 1.28 2002/04/10 21:20:04 mpowers |
|
| 827 | * Better handling for tree nodes when working with editing contexts. |
|
| 828 | * Better handling for invalidation. No longer broadcasting events |
|
| 829 | * when nodes have not been "registered" in the tree. |
|
| 830 | * |
|
| 831 | * Revision 1.27 2002/03/05 23:18:28 mpowers |
|
| 832 | * Added documentation. |
|
| 833 | * Added isSelectionPaintedImmediate and isSelectionTracking attributes |
|
| 834 | * to TableAssociation. |
|
| 835 | * Added getTableAssociation to TableColumnAssociation. |
|
| 836 | * |
|
| 837 | * Revision 1.25 2002/03/04 22:49:53 mpowers |
|
| 838 | * Now working with TreeColumnAssociation to delegate sorting behavior. |
|
| 839 | * |
|
| 840 | * Revision 1.23 2002/03/04 03:58:17 mpowers |
|
| 841 | * Refined table header click behavior. |
|
| 842 | * |
|
| 843 | * Revision 1.22 2002/03/01 23:41:39 mpowers |
|
| 844 | * Added facility to get table association from model. |
|
| 845 | * |
|
| 846 | * Revision 1.21 2002/03/01 20:41:22 mpowers |
|
| 847 | * Cleaned up unused code. |
|
| 848 | * |
|
| 849 | * Revision 1.20 2002/03/01 15:42:00 mpowers |
|
| 850 | * Table column headers now always show their sort indicator. |
|
| 851 | * A third table-column click clears the sort for that column. |
|
| 852 | * |
|
| 853 | * Revision 1.19 2002/02/28 23:01:39 mpowers |
|
| 854 | * TableColumnAssociations add and remove themselves from the TableAssociation |
|
| 855 | * when their connection is established and broken respectively. |
|
| 856 | * TableAssociations now break connection if they have no column associations. |
|
| 857 | * |
|
| 858 | * Revision 1.18 2002/02/28 22:45:27 mpowers |
|
| 859 | * Now registers as editing association when table gets focus, so that |
|
| 860 | * endEditing can appropriate commit any table cell editors. |
|
| 861 | * |
|
| 862 | * Revision 1.17 2001/10/26 20:01:50 mpowers |
|
| 863 | * We're again cancelling instead of stopping editing on subjectChanged. |
|
| 864 | * I believe that the introduction of NSRunLoop has allowed us to return |
|
| 865 | * to the correct behavior. |
|
| 866 | * |
|
| 867 | * Revision 1.16 2001/09/14 13:40:26 mpowers |
|
| 868 | * User-initiated selection changes are now handled on the next event loop |
|
| 869 | * so that the component repaints the new selection before any potentially |
|
| 870 | * lengthy logic is triggered by the selection change. |
|
| 871 | * |
|
| 872 | * Revision 1.15 2001/07/25 15:03:01 mpowers |
|
| 873 | * getColumnName now checks for a column header value before defaulting " ". |
|
| 874 | * |
|
| 875 | * Revision 1.14 2001/06/05 19:09:25 mpowers |
|
| 876 | * Fixed broken build. |
|
| 877 | * |
|
| 878 | * Revision 1.13 2001/06/05 19:08:12 mpowers |
|
| 879 | * Better determination of column class. Previously, if the first object |
|
| 880 | * was null, Object.class was used. Now we get the first non-null object. |
|
| 881 | * |
|
| 882 | * Revision 1.12 2001/05/02 17:39:01 mpowers |
|
| 883 | * Now selects from display group after initial population. |
|
| 884 | * |
|
| 885 | * Revision 1.11 2001/03/29 23:05:22 mpowers |
|
| 886 | * Fixes for editing table cells. |
|
| 887 | * |
|
| 888 | * Revision 1.10 2001/03/09 22:09:22 mpowers |
|
| 889 | * Now better handling jdk1.1 for rendering the column header. |
|
| 890 | * |
|
| 891 | * Revision 1.9 2001/02/27 02:10:38 mpowers |
|
| 892 | * No longer updating values to the display group if the value |
|
| 893 | * has not changed. |
|
| 894 | * |
|
| 895 | * Revision 1.8 2001/02/17 16:52:05 mpowers |
|
| 896 | * Changes in imports to support building with jdk1.1 collections. |
|
| 897 | * |
|
| 898 | * Revision 1.7 2001/02/16 17:47:17 mpowers |
|
| 899 | * Now cancelling any cell editing on subjectChanged(). |
|
| 900 | * |
|
| 901 | * Revision 1.6 2001/01/12 19:11:56 mpowers |
|
| 902 | * Fixed table column click sorting. |
|
| 903 | * |
|
| 904 | * Revision 1.5 2001/01/12 17:20:30 mpowers |
|
| 905 | * Moved EOSortOrdering creation to ColumnAssociation. |
|
| 906 | * |
|
| 907 | * Revision 1.4 2001/01/11 21:55:57 mpowers |
|
| 908 | * Implemented sort indicator for table column headers. |
|
| 909 | * |
|
| 910 | * Revision 1.3 2001/01/11 20:34:26 mpowers |
|
| 911 | * Implemented EOSortOrdering and added support in framework. |
|
| 912 | * Added header-click to sort table columns. |
|
| 913 | * |
|
| 914 | * Revision 1.2 2001/01/08 20:40:51 mpowers |
|
| 915 | * JTables in 1.3 clear their selection when the data model changes, |
|
| 916 | * which sends a list selection event. We now reset the selection again |
|
| 917 | * after the data changes so we're compatible across 1.2 and 1.3. |
|
| 918 | * |
|
| 919 | * Revision 1.1.1.1 2000/12/21 15:49:00 mpowers |
|
| 920 | * Contributing wotonomy. |
|
| 921 | * |
|
| 922 | * Revision 1.7 2000/12/20 16:25:41 michael |
|
| 923 | * Added log to all files. |
|
| 924 | * |
|
| 925 | * |
|
| 926 | */ |
|
| 927 |