1 package com.jbidwatcher.ui; 2 /* 3 * Copyright (c) 2000-2007, CyberFOX Software, Inc. All Rights Reserved. 4 * 5 * Developed by mrs (Morgan Schweers) 6 */ 7 8 import com.cyberfox.util.platform.Platform; 9 import com.jbidwatcher.auction.*; 10 import com.jbidwatcher.util.config.*; 11 import com.jbidwatcher.util.Currency; 12 import com.jbidwatcher.util.Constants; 13 import com.jbidwatcher.util.queue.MQFactory; 14 import com.jbidwatcher.ui.util.*; 15 import com.jbidwatcher.ui.table.TableColumnController; 16 import com.jbidwatcher.ui.table.CSVExporter; 17 import com.jbidwatcher.ui.table.TableSorter; 18 import com.jbidwatcher.ui.table.AuctionTable; 19 import com.jbidwatcher.util.queue.PlainMessageQueue; 20 21 import javax.imageio.ImageIO; 22 import javax.swing.*; 23 import javax.swing.event.ListSelectionEvent; 24 import javax.swing.event.ListSelectionListener; 25 import javax.swing.event.TableModelEvent; 26 import javax.swing.event.TableModelListener; 27 import javax.swing.table.TableColumn; 28 import java.awt.*; 29 import java.awt.event.ActionListener; 30 import java.awt.dnd.DropTarget; 31 import java.io.IOException; 32 import java.util.*; 33 import java.util.List; 34 35 public class AuctionsUIModel { 36 private Auctions _dataModel; 37 private JTable _table; 38 private JScrollPane _scroller; 39 /** @noinspection FieldCanBeLocal*/ 40 private DropTarget[] _targets; /* This can't be local, otherwise it gets GC'ed, which is bad. */ 41 private Color _bgColor; 42 private JPrintable _print; 43 private CSVExporter _export; 44 private JPanel mPanel; 45 46 private static final myTableCellRenderer _myRenderer = new myTableCellRenderer(); 47 private TableSorter _tSort; 48 49 /** 50 * @brief Construct a new UI model for a provided auction list. 51 * @param newAuctionList - The auction list to use as a 'backing 52 * store' for displaying lists of auctions. 53 * @param tableContextMenu - The context menu to present for this table. 54 * @param frameContextMenu - The context menu to present for whitespace outside the table. 55 * @param cornerButtonListener - The button to sit above the scrollbar. 56 * @param monitor 57 */ AuctionsUIModel(Auctions newAuctionList, JContext tableContextMenu, final JContext frameContextMenu, ActionListener cornerButtonListener)58 public AuctionsUIModel(Auctions newAuctionList, JContext tableContextMenu, final JContext frameContextMenu, ActionListener cornerButtonListener) { 59 _dataModel = newAuctionList; 60 61 _targets = new DropTarget[2]; 62 63 _tSort = new TableSorter(_dataModel.getName(), "Time left", new auctionTableModel(_dataModel.getList())); 64 65 _table = new AuctionTable(_dataModel.getName(), _tSort); 66 if(newAuctionList.isCompleted()) { 67 if(_table.convertColumnIndexToView(TableColumnController.END_DATE) == -1) { 68 _table.addColumn(new TableColumn(TableColumnController.END_DATE, Constants.DEFAULT_COLUMN_WIDTH, _myRenderer, null)); 69 } 70 } 71 if(JConfig.queryConfiguration("show_shipping", "false").equals("true")) { 72 if(_table.convertColumnIndexToView(TableColumnController.SHIPPING_INSURANCE) == -1) { 73 _table.addColumn(new TableColumn(TableColumnController.SHIPPING_INSURANCE)); 74 } 75 JConfig.killAll("show_shipping"); 76 } 77 78 // provide sufficient vertical height in the rows for micro-thumbnails list view 79 adjustRowHeight(); 80 81 _table.addMouseListener(tableContextMenu); 82 _tSort.addMouseListenerToHeaderInTable(_table); 83 if(Platform.isMac() || JConfig.queryConfiguration("ui.useCornerButton", "true").equals("true")) { 84 _scroller = new JScrollPane(_table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 85 } else { 86 _scroller = new JScrollPane(_table, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); 87 } 88 89 // This is a button to manage the custom columns for the current tab. 90 if(JConfig.queryConfiguration("ui.useCornerButton", "true").equals("true")) { 91 JButton cornerButton = new JButton("*"); 92 cornerButton.addActionListener(cornerButtonListener); 93 _scroller.setCorner(ScrollPaneConstants.UPPER_RIGHT_CORNER, cornerButton); 94 } 95 96 _bgColor = UIManager.getColor("window"); 97 _scroller.setViewport(new JViewport() { 98 private Image image; 99 100 { 101 setBackground(_bgColor); 102 addMouseListener(frameContextMenu); 103 setView(_table); 104 105 try { 106 image = ImageIO.read(JConfig.getResource("/jbidwatch.jpg")); 107 } catch (IOException e) { 108 image = null; 109 } 110 } 111 112 public void paintComponent(Graphics g) { 113 super.paintComponent(g); 114 if(image != null && _table.getRowCount() == 0) { 115 int imageW = image.getWidth(null); 116 int imageH = image.getHeight(null); 117 118 Graphics2D g2d = (Graphics2D) g; 119 AlphaComposite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); 120 Composite oldComp = g2d.getComposite(); 121 g2d.setComposite(comp); 122 int xloc = getWidth()/2 - imageW/2 - 2; 123 int yloc = getHeight()/2 - imageH/2 - 2; 124 125 g2d.drawImage(image, xloc, yloc, this); 126 127 g2d.setComposite(oldComp); 128 } 129 } 130 }); 131 132 JDropListener _dropEar; 133 if(newAuctionList.isCompleted()) { 134 _dropEar = new JDropListener(new TargetDrop()); 135 } else { 136 _dropEar = new JDropListener(new TargetDrop(_dataModel.getName(), new ImageDropResolver() { 137 public void handle(String imgUrl, Point location) { 138 int rowPoint = _table.rowAtPoint(location); 139 AuctionEntry whichAuction = (AuctionEntry)_table.getValueAt(rowPoint, -1); 140 DeletedEntry.deleteThumbnails(whichAuction.getIdentifier()); 141 whichAuction.getAuction().setThumbnailURL(imgUrl); 142 ((PlainMessageQueue)MQFactory.getConcrete("thumbnail")).enqueueObject(whichAuction.getAuction()); 143 } 144 })); 145 } 146 _targets[0] = new DropTarget(_scroller.getViewport(), _dropEar); 147 _targets[1] = new DropTarget(_table, _dropEar); 148 149 _targets[0].setActive(true); 150 _targets[1].setActive(true); 151 152 _print = new JPrintable(_table); 153 _export = new CSVExporter(_table); 154 _table.setDefaultRenderer(String.class, _myRenderer); 155 _table.setDefaultRenderer(Icon.class, _myRenderer); 156 157 mPanel = new JPanel(); 158 mPanel.setLayout(new BorderLayout()); 159 mPanel.add(_scroller, BorderLayout.CENTER); 160 addSumMonitor(_table, _tSort); 161 JPanel statusPanel = new TabStatusPanel(_dataModel.getName()); 162 mPanel.add(statusPanel, BorderLayout.NORTH); 163 } 164 getPanel()165 public JPanel getPanel() { 166 return mPanel; 167 } 168 addSumMonitor(JTable table, TableSorter sort)169 private void addSumMonitor(JTable table, TableSorter sort) { 170 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { 171 public void valueChanged(ListSelectionEvent event) { 172 updateSum(); 173 } 174 }); 175 176 sort.addTableModelListener(new TableModelListener() { 177 public void tableChanged(TableModelEvent tableModelEvent) { 178 updateSum(); 179 } 180 }); 181 } 182 updateSum()183 private void updateSum() { 184 int[] rowList = _table.getSelectedRows(); 185 String total = sum(rowList); 186 187 if(total == null) { 188 MQFactory.getConcrete("Swing").enqueue("PRICE "); // A blank space to clear the price 189 } else { 190 MQFactory.getConcrete("Swing").enqueue("PRICE " + rowList.length + " / " + total); 191 } 192 } 193 194 /** 195 * @brief Pick and return a value from the entry that best describes 196 * how much COULD be spent on it by the buyer. 197 * 198 * For an item not bid on, it's the current bid price. For an item 199 * the user has bid on, it's their maximum bid. For an item the 200 * user has a snipe set for, it's the maximum of their snipe bid. 201 * If the item is closed, it's just the current bid price. 202 * 203 * @param checkEntry - The AuctionEntry to operate on. 204 * 205 * @return - A currency value containing either the current bid, the 206 * users high bid, or the users snipe bid. 207 */ getBestBidValue(AuctionEntry checkEntry)208 private static Currency getBestBidValue(AuctionEntry checkEntry) { 209 return checkEntry.bestValue(); 210 } 211 212 // A single accessor... getTableSorter()213 public TableSorter getTableSorter() { return _tSort; } 214 addUSD(Currency inCurr, AuctionEntry ae)215 private static Currency addUSD(Currency inCurr, AuctionEntry ae) { 216 boolean newCurrency = (inCurr == null || inCurr.isNull()); 217 Currency currentUSPrice = ae.getCurrentUSPrice(); 218 try { 219 if(ae.getShippingWithInsurance().isNull()) { 220 if(newCurrency) { 221 return currentUSPrice; 222 } 223 return inCurr.add(currentUSPrice); 224 } 225 226 if(newCurrency) { 227 inCurr = currentUSPrice.add(Currency.convertToUSD(currentUSPrice, ae.getCurrentPrice(), ae.getShippingWithInsurance())); 228 } else { 229 inCurr = inCurr.add(currentUSPrice.add(Currency.convertToUSD(currentUSPrice, ae.getCurrentPrice(), ae.getShippingWithInsurance()))); 230 } 231 } catch(Currency.CurrencyTypeException cte) { 232 JConfig.log().handleException("This should have been cleaned up.", cte); 233 } 234 return inCurr; 235 } 236 addNonUSD(Currency inCurr, AuctionEntry ae)237 private static Currency addNonUSD(Currency inCurr, AuctionEntry ae) { 238 boolean newCurrency = inCurr == null || inCurr.isNull(); 239 try { 240 if(ae.getShippingWithInsurance().isNull()) { 241 if(newCurrency) { 242 return getBestBidValue(ae); 243 } 244 return inCurr.add(getBestBidValue(ae)); 245 } 246 247 if(newCurrency) { 248 inCurr = getBestBidValue(ae).add(ae.getShippingWithInsurance()); 249 } else { 250 inCurr = inCurr.add(getBestBidValue(ae).add(ae.getShippingWithInsurance())); 251 } 252 } catch(Currency.CurrencyTypeException cte) { 253 JConfig.log().handleException("This should have been cleaned up.", cte); 254 } 255 256 return inCurr; 257 } 258 sum(int[] rowList)259 protected String sum(int[] rowList) { 260 boolean approx = false, i18n = true; 261 Currency accum = null; 262 Currency withShipping = null; 263 Currency withRealShipping = null; 264 Currency realAccum = null; 265 266 try { 267 for (int aRowList : rowList) { 268 AuctionEntry ae2; 269 try { 270 ae2 = (AuctionEntry) _table.getValueAt(aRowList, -1); 271 } catch (ClassCastException cce) { 272 ae2 = null; 273 } catch (IndexOutOfBoundsException bounds) { 274 ae2 = null; 275 approx = true; 276 } 277 if (ae2 != null) { 278 Currency currentUSPrice = ae2.getCurrentUSPrice(); 279 280 if (accum == null) { 281 accum = currentUSPrice; 282 realAccum = getBestBidValue(ae2); 283 withShipping = addUSD(withShipping, ae2); 284 withRealShipping = addNonUSD(withRealShipping, ae2); 285 } else { 286 if (!currentUSPrice.isNull() && !accum.isNull() && currentUSPrice.getCurrencyType() != Currency.NONE) { 287 accum = accum.add(currentUSPrice); 288 withShipping = addUSD(withShipping, ae2); 289 290 // If we're still trying to do the internationalization 291 // thing, then try to keep track of the 'real' total. 292 if (i18n) { 293 //noinspection NestedTryStatement 294 try { 295 realAccum = realAccum.add(getBestBidValue(ae2)); 296 withRealShipping = addNonUSD(withRealShipping, ae2); 297 } catch (Currency.CurrencyTypeException cte) { 298 // We can't handle multiple non-USD currency types, so 299 // we stop trying to do the internationalization thing. 300 i18n = false; 301 } 302 } 303 } 304 } 305 if (ae2.getCurrentPrice().getCurrencyType() != Currency.US_DOLLAR) approx = true; 306 } 307 } 308 } catch(Currency.CurrencyTypeException e) { 309 JConfig.log().handleException("Sum currency exception!", e); 310 return null; 311 } catch(ArrayIndexOutOfBoundsException ignored) { 312 JConfig.log().logDebug("Selection of " + rowList.length + " items changed out from under 'sum'."); 313 return null; 314 } catch(NullPointerException npe) { 315 JConfig.log().logDebug("sum got NPE - this is common during delete operations"); 316 return null; 317 } catch(Exception e) { 318 JConfig.log().handleException("Sum serious exception!", e); 319 return null; 320 } 321 322 if(accum == null || accum.isNull()) { 323 return null; 324 } 325 326 String sAndH = "s/h"; 327 if(!Locale.getDefault().equals(Locale.US)) sAndH = "p/p"; 328 329 // If we managed to do the i18n thing through it all, and we have 330 // some real values, return it. 331 if(i18n && realAccum != null) { 332 StringBuffer result = new StringBuffer(realAccum.toString()); 333 if(withRealShipping != null && !realAccum.equals(withRealShipping)) { 334 result.append(" (").append(withRealShipping).append(" with ").append(sAndH).append(')'); 335 } 336 return result.toString(); 337 } 338 339 if(approx) { 340 String result; 341 if(withShipping != null && !accum.equals(withShipping)) { 342 result = "About " + accum.toString() + " (" + withShipping + " with " + sAndH + ')'; 343 } else { 344 result = "About " + accum.toString(); 345 } 346 return result; 347 } 348 349 if(withShipping != null && !accum.equals(withShipping)) { 350 return accum.toString() + " (" + withShipping + " with " + sAndH + ')'; 351 } 352 353 return accum.toString(); 354 } 355 356 /** 357 * @brief Sets the background color for this tab to the passed in color. 358 * 359 * @param bgColor - The color to set the background to. 360 */ setBackground(Color bgColor)361 public void setBackground(Color bgColor) { 362 _scroller.getViewport().setBackground(bgColor); 363 _table.setBackground(bgColor); 364 _bgColor = bgColor; 365 } 366 367 /** 368 * @brief Return the background color this was set to. 369 * 370 * @return - The color, if any, this tab was set to. 371 */ getBackground()372 public Color getBackground() { 373 return _bgColor; 374 } 375 376 /** 377 * Delete an auction entry, using that auction entry to match against. 378 * This also tells the auction entry to unregister itself! 379 * 380 * @param inEntry - The auction entry to delete. 381 */ delEntry(EntryInterface inEntry)382 public void delEntry(EntryInterface inEntry) { 383 _tSort.delete(inEntry); 384 } 385 386 /** 387 * Add an AuctionEntry that has already been created, denying 388 * duplicates, but allowing duplicates where both have useful 389 * information that is not the same. 390 * 391 * @param aeNew - The new auction entry to add to the tables. 392 */ addEntry(EntryInterface aeNew)393 public void addEntry(EntryInterface aeNew) { 394 if (aeNew != null) { 395 if (_tSort.insert(aeNew) == -1) { 396 JConfig.log().logMessage("JBidWatch: Bad auction entry, cannot add!"); 397 } 398 } 399 } 400 toggleField(String field)401 public boolean toggleField(String field) { 402 boolean rval; 403 int modelColumn = TableColumnController.getInstance().getColumnNumber(field); 404 if(_table.convertColumnIndexToView(modelColumn) == -1) { 405 TableColumn newColumn = new TableColumn(modelColumn, Constants.DEFAULT_COLUMN_WIDTH, _myRenderer, null); 406 if(modelColumn == TableColumnController.THUMBNAIL) newColumn.setMinWidth(75); 407 _table.addColumn(newColumn); 408 rval = true; 409 } else { 410 _table.removeColumn(_table.getColumn(field)); 411 _tSort.removeColumn(field, _table); 412 rval = false; 413 } 414 415 adjustRowHeight(); 416 417 return rval; 418 } 419 420 // hack and a half - but adding a row height attribute for columns seems like overkill adjustRowHeight()421 public void adjustRowHeight() { 422 Font def = myTableCellRenderer.getDefaultFont(); 423 Graphics g = _table.getGraphics(); 424 int defaultHeight; 425 426 if(def == null || g == null) { 427 defaultHeight = Constants.DEFAULT_ROW_HEIGHT; 428 } else { 429 FontMetrics metrics = g.getFontMetrics(def); 430 defaultHeight = metrics.getMaxAscent() + metrics.getMaxDescent() + metrics.getLeading()+4; 431 } 432 433 int thumbnailIndex = _table.convertColumnIndexToView(TableColumnController.THUMBNAIL); 434 if (thumbnailIndex != -1) { 435 defaultHeight = Math.max(Constants.MICROTHUMBNAIL_ROW_HEIGHT, defaultHeight); 436 } 437 _table.setRowHeight(Math.max(defaultHeight, Constants.DEFAULT_ROW_HEIGHT)); 438 if(def != null) { 439 _table.getTableHeader().setFont(def); 440 } 441 } 442 getColumns()443 public List<String> getColumns() { 444 ArrayList<String> al = new ArrayList<String>(); 445 for(int i = 0; i<_table.getColumnCount(); i++) { 446 al.add(_table.getColumnName(i)); 447 } 448 449 return al; 450 } 451 export(String fname)452 public boolean export(String fname) { 453 return _export.export(fname); 454 } 455 456 /** 457 * @brief Print this table. 458 * 459 */ print()460 public void print() { 461 _print.doPrint(); 462 } 463 464 /** 465 * @brief Convert current column widths into display properties to 466 * be saved for a future session. 467 * 468 * @param addToProps - The properties object to add the column widths to. 469 * @param name - The category name to get the info from. 470 */ getColumnWidthsToProperties(Properties addToProps, String name)471 public void getColumnWidthsToProperties(Properties addToProps, String name) { 472 for(int j = 0; j<_table.getColumnCount(); j++) { 473 TableColumn ct; 474 try { 475 ct = _table.getColumn(_table.getColumnName(j)); 476 } catch(IllegalArgumentException iae) { 477 JConfig.log().logMessage("Column can't be retrieved from the table: " + _table.getColumnName(j)); 478 ct = null; 479 } 480 // ColumnProps cp = new ColumnProps(_dataModel.getColumnName(j), j, ct.getWidth()); 481 //noinspection StringContatenationInLoop 482 if(ct != null) addToProps.setProperty(name + '.' + _table.getColumnName(j), Integer.toString(j) + '.' + Integer.toString(ct.getWidth())); 483 } 484 } 485 getColumnWidthsToProperties(Properties addToProps)486 public void getColumnWidthsToProperties(Properties addToProps) { 487 getColumnWidthsToProperties(addToProps, _dataModel.getName()); 488 } 489 sort()490 public void sort() { 491 _tSort.sort(); 492 } 493 redrawAll()494 public void redrawAll() { 495 _tSort.tableChanged(new TableModelEvent(_tSort)); 496 } 497 } 498