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