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.fields;
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.Account;
031: import net.sf.jmoney.model2.Session;
032:
033: import org.eclipse.swt.SWT;
034: import org.eclipse.swt.events.FocusEvent;
035: import org.eclipse.swt.events.FocusListener;
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.graphics.Rectangle;
044: import org.eclipse.swt.layout.FillLayout;
045: import org.eclipse.swt.layout.RowData;
046: import org.eclipse.swt.layout.RowLayout;
047: import org.eclipse.swt.widgets.Composite;
048: import org.eclipse.swt.widgets.Control;
049: import org.eclipse.swt.widgets.Display;
050: import org.eclipse.swt.widgets.List;
051: import org.eclipse.swt.widgets.Shell;
052: import org.eclipse.swt.widgets.Text;
053: import org.eclipse.ui.IMemento;
054:
055: /**
056: * A control for entering accounts.
057: *
058: * This control contains both a text box and a list box that appears when the
059: * text box gains focus.
060: *
061: * @author Nigel Westbury
062: */
063: public class AccountControl<A extends Account> extends
064: AccountComposite<A> {
065:
066: Text textControl;
067:
068: private Session session;
069: private Class<A> accountClass;
070:
071: /**
072: * List of accounts put into account list.
073: */
074: private Vector<A> allAccounts;
075:
076: /**
077: * Currently selected account, or null if no account selected
078: */
079: private A account;
080:
081: private Vector<SelectionListener> listeners = new Vector<SelectionListener>();
082:
083: /**
084: * @param parent
085: * @param style
086: */
087: public AccountControl(final Composite parent, Session session,
088: Class<A> accountClass) {
089: super (parent, SWT.NONE);
090: this .session = session;
091: this .accountClass = accountClass;
092:
093: setBackgroundMode(SWT.INHERIT_FORCE);
094:
095: setLayout(new FillLayout(SWT.VERTICAL));
096:
097: textControl = new Text(this , SWT.LEFT);
098:
099: textControl.addFocusListener(new FocusListener() {
100:
101: Shell shell;
102: boolean closingShell = false;
103:
104: public void focusGained(FocusEvent e) {
105: if (closingShell) {
106: return;
107: }
108:
109: shell = new Shell(parent.getShell(), SWT.ON_TOP);
110: shell.setLayout(new RowLayout());
111:
112: final List listControl = new List(shell, SWT.SINGLE
113: | SWT.V_SCROLL);
114: listControl
115: .setLayoutData(new RowData(SWT.DEFAULT, 100));
116:
117: // Important we use the field for the session and accountClass. We do not use the parameters
118: // (the parameters may be null, but fields should always have been set by
119: // the time control gets focus).
120: allAccounts = new Vector<A>();
121: addAccounts("", AccountControl.this .session
122: .getAccountCollection(), listControl,
123: AccountControl.this .accountClass);
124:
125: // shell.setSize(listControl.computeSize(SWT.DEFAULT, listControl.getItemHeight()*10));
126:
127: // Set the currently set account into the list control.
128: listControl.select(allAccounts.indexOf(account));
129:
130: listControl
131: .addSelectionListener(new SelectionAdapter() {
132: @Override
133: public void widgetSelected(SelectionEvent e) {
134: int selectionIndex = listControl
135: .getSelectionIndex();
136: account = allAccounts
137: .get(selectionIndex);
138: textControl.setText(account.getName());
139: fireAccountChangeEvent();
140: }
141: });
142:
143: listControl.addKeyListener(new KeyAdapter() {
144: String pattern;
145: int lastTime = 0;
146:
147: @Override
148: public void keyPressed(KeyEvent e) {
149: if (Character.isLetterOrDigit(e.character)) {
150: if ((e.time - lastTime) < 1000) {
151: pattern += Character
152: .toUpperCase(e.character);
153: } else {
154: pattern = String.valueOf(Character
155: .toUpperCase(e.character));
156: }
157: lastTime = e.time;
158:
159: /*
160: *
161: Starting at the currently selected account,
162: search for an account starting with these characters.
163: */
164: int startIndex = listControl
165: .getSelectionIndex();
166: if (startIndex == -1) {
167: startIndex = 0;
168: }
169:
170: int match = -1;
171: int i = startIndex;
172: do {
173: if (allAccounts.get(i).getName()
174: .toUpperCase().startsWith(
175: pattern)) {
176: match = i;
177: break;
178: }
179:
180: i++;
181: if (i == allAccounts.size()) {
182: i = 0;
183: }
184: } while (i != startIndex);
185:
186: if (match != -1) {
187: account = allAccounts.get(match);
188: listControl.select(match);
189: listControl.setTopIndex(match);
190: textControl.setText(account.getName());
191: }
192:
193: e.doit = false;
194: }
195: }
196: });
197:
198: shell.pack();
199:
200: /*
201: * Position the shell below the text box, unless the account
202: * control is so near the bottom of the display that the shell
203: * would go off the bottom of the display, in which case
204: * position the shell above the text box.
205: */
206: Display display = getDisplay();
207: Rectangle rect = display.map(parent, null, getBounds());
208: int calendarShellHeight = shell.getSize().y;
209: if (rect.y + rect.height + calendarShellHeight <= display
210: .getBounds().height) {
211: shell.setLocation(rect.x, rect.y + rect.height);
212: } else {
213: shell.setLocation(rect.x, rect.y
214: - calendarShellHeight);
215: }
216:
217: shell.open();
218:
219: shell.addShellListener(new ShellAdapter() {
220: @Override
221: public void shellDeactivated(ShellEvent e) {
222: closingShell = true;
223: shell.close();
224: closingShell = false;
225: }
226: });
227: }
228:
229: public void focusLost(FocusEvent e) {
230: // shell.close();
231: // listControl = null;
232: }
233: });
234: }
235:
236: private void fireAccountChangeEvent() {
237: for (SelectionListener listener : listeners) {
238: listener.widgetSelected(null);
239: }
240: }
241:
242: private void addAccounts(String prefix,
243: Collection<? extends Account> accounts, List listControl,
244: Class<A> accountClass) {
245: Vector<A> matchingAccounts = new Vector<A>();
246: for (Account account : accounts) {
247: if (accountClass.isAssignableFrom(account.getClass())) {
248: matchingAccounts.add(accountClass.cast(account));
249: }
250: }
251:
252: // Sort the accounts by name.
253: Collections.sort(matchingAccounts, new Comparator<Account>() {
254: public int compare(Account account1, Account account2) {
255: return account1.getName().compareTo(account2.getName());
256: }
257: });
258:
259: for (A matchingAccount : matchingAccounts) {
260: allAccounts.add(matchingAccount);
261: listControl.add(prefix + matchingAccount.getName());
262: addAccounts(prefix + matchingAccount.getName() + ":",
263: matchingAccount.getSubAccountCollection(),
264: listControl, accountClass);
265: }
266:
267: }
268:
269: /**
270: * @param object
271: */
272: @Override
273: public void setAccount(A account) {
274: this .account = account;
275:
276: if (account == null) {
277: textControl.setText("");
278: } else {
279: textControl.setText(account.getName());
280: }
281: }
282:
283: /**
284: * @return the account, or null if no account has been set in
285: * the control
286: */
287: @Override
288: public A getAccount() {
289: return account;
290: }
291:
292: /**
293: * @param listener
294: */
295: public void addSelectionListener(SelectionListener listener) {
296: listeners.add(listener);
297: }
298:
299: /**
300: * @param listener
301: */
302: public void removeSelectionListener(SelectionListener listener) {
303: listeners.remove(listener);
304: }
305:
306: public Control getControl() {
307: return this ;
308: }
309:
310: @Override
311: public void rememberChoice() {
312: // We don't remember choices, so nothing to do
313: }
314:
315: @Override
316: public void init(IMemento memento) {
317: // No state to restore
318: }
319:
320: @Override
321: public void saveState(IMemento memento) {
322: // No state to save
323: }
324:
325: /**
326: * Normally the session is set through the constructor. However in some
327: * circumstances (i.e. in the custom cell editors) the session is not
328: * available at construction time and null will be set. This method must
329: * then be called to set the session before the control is used (i.e. before
330: * the control gets focus).
331: */
332: public void setSession(Session session, Class<A> accountClass) {
333: this.session = session;
334: this.accountClass = accountClass;
335: }
336: }
|