001: /*
002: *
003: * JMoney - A Personal Finance Manager
004: * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net>
005: *
006: *
007: * This program is free software; you can redistribute it and/or modify
008: * it under the terms of the GNU General Public License as published by
009: * the Free Software Foundation; either version 2 of the License, or
010: * (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
020: *
021: */
022:
023: package net.sf.jmoney.stocks;
024:
025: import java.util.Collection;
026: import java.util.Collections;
027: import java.util.Comparator;
028: import java.util.Vector;
029:
030: import net.sf.jmoney.model2.Commodity;
031: import net.sf.jmoney.model2.Session;
032: import net.sf.jmoney.stocks.wizards.NewStockWizard;
033:
034: import org.eclipse.jface.wizard.WizardDialog;
035: import org.eclipse.swt.SWT;
036: import org.eclipse.swt.events.KeyAdapter;
037: import org.eclipse.swt.events.KeyEvent;
038: import org.eclipse.swt.events.SelectionAdapter;
039: import org.eclipse.swt.events.SelectionEvent;
040: import org.eclipse.swt.events.SelectionListener;
041: import org.eclipse.swt.events.ShellAdapter;
042: import org.eclipse.swt.events.ShellEvent;
043: import org.eclipse.swt.events.ShellListener;
044: import org.eclipse.swt.graphics.Rectangle;
045: import org.eclipse.swt.layout.FillLayout;
046: import org.eclipse.swt.layout.RowData;
047: import org.eclipse.swt.layout.RowLayout;
048: import org.eclipse.swt.widgets.Button;
049: import org.eclipse.swt.widgets.Composite;
050: import org.eclipse.swt.widgets.Control;
051: import org.eclipse.swt.widgets.Display;
052: import org.eclipse.swt.widgets.List;
053: import org.eclipse.swt.widgets.Shell;
054: import org.eclipse.swt.widgets.Text;
055:
056: /**
057: * A control for selecting a stock.
058: *
059: * This control contains both a text box and a list box that appears when the
060: * text box gains focus.
061: *
062: * @author Nigel Westbury
063: */
064: public class StockControl<A extends Stock> extends Composite {
065:
066: Text textControl;
067:
068: private Session session;
069: private Class<A> stockClass;
070:
071: /**
072: * List of stocks put into stock list.
073: */
074: private Vector<A> allStock;
075:
076: /**
077: * Currently selected stock, or null if no stock selected
078: */
079: private A stock;
080:
081: private Vector<SelectionListener> listeners = new Vector<SelectionListener>();
082:
083: /**
084: * @param parent
085: * @param style
086: */
087: public StockControl(final Composite parent, Session session,
088: final Class<A> stockClass) {
089: super (parent, SWT.NONE);
090: this .session = session;
091: this .stockClass = stockClass;
092:
093: setBackgroundMode(SWT.INHERIT_FORCE);
094:
095: setLayout(new FillLayout(SWT.VERTICAL));
096:
097: textControl = new Text(this , SWT.LEFT);
098:
099: textControl.addKeyListener(new KeyAdapter() {
100:
101: @Override
102: public void keyPressed(KeyEvent e) {
103: if (e.keyCode == SWT.ARROW_DOWN) {
104: openStockShell(parent, stockClass);
105: }
106:
107: }
108:
109: @Override
110: public void keyReleased(KeyEvent e) {
111: // TODO Auto-generated method stub
112:
113: }
114: });
115:
116: // textControl.addFocusListener(new FocusAdapter() {
117: //
118: // @Override
119: // public void focusGained(FocusEvent e) {
120: // System.out.println("stock text control has gained focus");
121: // openStockShell(parent, stockClass);
122: // }
123: //
124: // });
125: }
126:
127: private void openStockShell(final Composite parent,
128: final Class<A> stockClass) {
129: final Shell parentShell = parent.getShell();
130:
131: final Shell shell = new Shell(parent.getShell(), SWT.ON_TOP);
132: shell.setLayout(new RowLayout(SWT.VERTICAL));
133: System.out.println(shell.isDisposed() + ": "
134: + shell.getDisplay());
135:
136: final List listControl = new List(shell, SWT.SINGLE
137: | SWT.V_SCROLL);
138: listControl.setLayoutData(new RowData(SWT.DEFAULT, 100));
139:
140: Button addStockButton = new Button(shell, SWT.PUSH);
141: addStockButton.setText("Add New Stock...");
142: addStockButton.addSelectionListener(new SelectionAdapter() {
143: @Override
144: public void widgetSelected(SelectionEvent e) {
145: NewStockWizard wizard = new NewStockWizard(
146: StockControl.this .session);
147: System.out.println(shell.isDisposed() + ", "
148: + shell.getDisplay());
149: WizardDialog dialog = new WizardDialog(shell, wizard);
150: dialog.setPageSize(600, 300);
151: int result = dialog.open();
152: if (result == WizardDialog.OK) {
153: /*
154: * Having created the new stock, set it as the
155: * selected stock in this control.
156: */
157: setStock(stockClass.cast(wizard.getNewStock()));
158: }
159: }
160: });
161:
162: // Important we use the field for the session and stockClass. We do not use the parameters
163: // (the parameters may be null, but fields should always have been set by
164: // the time control gets focus).
165: allStock = new Vector<A>();
166: addStocks("", StockControl.this .session
167: .getCommodityCollection(), listControl,
168: StockControl.this .stockClass);
169:
170: // shell.setSize(listControl.computeSize(SWT.DEFAULT, listControl.getItemHeight()*10));
171:
172: // Set the currently set stock into the list control.
173: listControl.select(allStock.indexOf(stock));
174:
175: listControl.addSelectionListener(new SelectionAdapter() {
176: @Override
177: public void widgetSelected(SelectionEvent e) {
178: int selectionIndex = listControl.getSelectionIndex();
179: stock = allStock.get(selectionIndex);
180: textControl.setText(stock.getName());
181: fireStockChangeEvent();
182: }
183: });
184:
185: listControl.addKeyListener(new KeyAdapter() {
186: String pattern;
187: int lastTime = 0;
188:
189: @Override
190: public void keyPressed(KeyEvent e) {
191: if (Character.isLetterOrDigit(e.character)) {
192: if ((e.time - lastTime) < 1000) {
193: pattern += Character.toUpperCase(e.character);
194: } else {
195: pattern = String.valueOf(Character
196: .toUpperCase(e.character));
197: }
198: lastTime = e.time;
199:
200: /*
201: *
202: Starting at the currently selected stock,
203: search for a stock starting with these characters.
204: */
205: int startIndex = listControl.getSelectionIndex();
206: if (startIndex == -1) {
207: startIndex = 0;
208: }
209:
210: int match = -1;
211: int i = startIndex;
212: do {
213: if (allStock.get(i).getName().toUpperCase()
214: .startsWith(pattern)) {
215: match = i;
216: break;
217: }
218:
219: i++;
220: if (i == allStock.size()) {
221: i = 0;
222: }
223: } while (i != startIndex);
224:
225: if (match != -1) {
226: stock = allStock.get(match);
227: listControl.select(match);
228: listControl.setTopIndex(match);
229: textControl.setText(stock.getName());
230: }
231:
232: e.doit = false;
233: }
234: }
235: });
236:
237: shell.pack();
238:
239: /*
240: * Position the shell below the text box, unless the
241: * control is so near the bottom of the display that the shell
242: * would go off the bottom of the display, in which case
243: * position the shell above the text box.
244: */
245: Display display = getDisplay();
246: Rectangle rect = display.map(parent, null, getBounds());
247: int calendarShellHeight = shell.getSize().y;
248: if (rect.y + rect.height + calendarShellHeight <= display
249: .getBounds().height) {
250: shell.setLocation(rect.x, rect.y + rect.height);
251: } else {
252: shell.setLocation(rect.x, rect.y - calendarShellHeight);
253: }
254:
255: shell.open();
256:
257: /*
258: * We need to be sure to close the shell when it is no longer active.
259: * Listening for this shell to be deactivated does not work because there
260: * may be child controls which create child shells (third level shells).
261: * We do not want this shell to close if a child shell has been created
262: * and activated. We want to close this shell only if the parent shell
263: * have been activated. Note that if a grandparent shell is activated then
264: * we do not want to close this shell. The parent will be closed anyway
265: * which would automatically close this one.
266: */
267: final ShellListener parentActivationListener = new ShellAdapter() {
268: @Override
269: public void shellActivated(ShellEvent e) {
270: System.out.println("closing shell");
271: shell.close();
272: }
273: };
274:
275: parentShell.addShellListener(parentActivationListener);
276:
277: shell.addShellListener(new ShellAdapter() {
278: @Override
279: public void shellClosed(ShellEvent e) {
280: parentShell
281: .removeShellListener(parentActivationListener);
282: }
283: });
284: }
285:
286: private void fireStockChangeEvent() {
287: for (SelectionListener listener : listeners) {
288: listener.widgetSelected(null);
289: }
290: }
291:
292: private void addStocks(String prefix,
293: Collection<? extends Commodity> stocks, List listControl,
294: Class<A> stockClass) {
295: Vector<A> matchingStocks = new Vector<A>();
296: for (Commodity stock : stocks) {
297: if (stockClass.isAssignableFrom(stock.getClass())) {
298: matchingStocks.add(stockClass.cast(stock));
299: }
300: }
301:
302: // Sort the stocks by name.
303: Collections.sort(matchingStocks, new Comparator<Stock>() {
304: public int compare(Stock stock1, Stock stock2) {
305: return stock1.getName().compareTo(stock2.getName());
306: }
307: });
308:
309: for (A matchingStock : matchingStocks) {
310: allStock.add(matchingStock);
311: listControl.add(prefix + matchingStock.getName());
312: }
313:
314: }
315:
316: /**
317: * @param object
318: */
319: public void setStock(A stock) {
320: this .stock = stock;
321:
322: if (stock == null) {
323: textControl.setText("");
324: } else {
325: textControl.setText(stock.getName());
326: }
327: }
328:
329: /**
330: * @return the stock, or null if no stock has been set in
331: * the control
332: */
333: public A getStock() {
334: return stock;
335: }
336:
337: /**
338: * @param listener
339: */
340: public void addSelectionListener(SelectionListener listener) {
341: listeners.add(listener);
342: }
343:
344: /**
345: * @param listener
346: */
347: public void removeSelectionListener(SelectionListener listener) {
348: listeners.remove(listener);
349: }
350:
351: public Control getControl() {
352: return this ;
353: }
354:
355: /**
356: * Normally the session is set through the constructor. However in some
357: * circumstances (i.e. in the custom cell editors) the session is not
358: * available at construction time and null will be set. This method must
359: * then be called to set the session before the control is used (i.e. before
360: * the control gets focus).
361: */
362: public void setSession(Session session, Class<A> stockClass) {
363: this.session = session;
364: this.stockClass = stockClass;
365: }
366: }
|