001: /*
002: *
003: * JMoney - A Personal Finance Manager
004: * Copyright (c) 2002 Johann Gyger <johann.gyger@switzerland.org>
005: * Copyright (c) 2004 Nigel Westbury <westbury@users.sourceforge.net>
006: *
007: *
008: * This program is free software; you can redistribute it and/or modify
009: * it under the terms of the GNU General Public License as published by
010: * the Free Software Foundation; either version 2 of the License, or
011: * (at your option) any later version.
012: *
013: * This program is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with this program; if not, write to the Free Software
020: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
021: *
022: */
023:
024: package net.sf.jmoney.bookkeepingPages;
025:
026: import java.util.Iterator;
027: import java.util.Vector;
028:
029: import net.sf.jmoney.Constants;
030: import net.sf.jmoney.IBookkeepingPage;
031: import net.sf.jmoney.IBookkeepingPageFactory;
032: import net.sf.jmoney.JMoneyPlugin;
033: import net.sf.jmoney.categoriespanel.CategoriesPanelPlugin;
034: import net.sf.jmoney.model2.Account;
035: import net.sf.jmoney.model2.AccountInfo;
036: import net.sf.jmoney.model2.ExtendableObject;
037: import net.sf.jmoney.model2.IPropertyControl;
038: import net.sf.jmoney.model2.IPropertyDependency;
039: import net.sf.jmoney.model2.IncomeExpenseAccount;
040: import net.sf.jmoney.model2.IncomeExpenseAccountInfo;
041: import net.sf.jmoney.model2.PropertyAccessor;
042: import net.sf.jmoney.model2.ScalarPropertyAccessor;
043: import net.sf.jmoney.model2.Session;
044: import net.sf.jmoney.model2.SessionChangeAdapter;
045: import net.sf.jmoney.model2.SessionChangeListener;
046: import net.sf.jmoney.views.NodeEditor;
047: import net.sf.jmoney.views.SectionlessPage;
048:
049: import org.eclipse.core.commands.ExecutionException;
050: import org.eclipse.core.commands.operations.AbstractOperation;
051: import org.eclipse.core.commands.operations.IOperationHistory;
052: import org.eclipse.core.commands.operations.IUndoableOperation;
053: import org.eclipse.core.runtime.Assert;
054: import org.eclipse.core.runtime.IAdaptable;
055: import org.eclipse.core.runtime.IProgressMonitor;
056: import org.eclipse.core.runtime.IStatus;
057: import org.eclipse.core.runtime.Status;
058: import org.eclipse.jface.action.Action;
059: import org.eclipse.jface.action.IMenuListener;
060: import org.eclipse.jface.action.IMenuManager;
061: import org.eclipse.jface.action.MenuManager;
062: import org.eclipse.jface.action.Separator;
063: import org.eclipse.jface.viewers.ISelectionChangedListener;
064: import org.eclipse.jface.viewers.IStructuredContentProvider;
065: import org.eclipse.jface.viewers.IStructuredSelection;
066: import org.eclipse.jface.viewers.ITreeContentProvider;
067: import org.eclipse.jface.viewers.LabelProvider;
068: import org.eclipse.jface.viewers.SelectionChangedEvent;
069: import org.eclipse.jface.viewers.StructuredSelection;
070: import org.eclipse.jface.viewers.TreeViewer;
071: import org.eclipse.jface.viewers.Viewer;
072: import org.eclipse.jface.viewers.ViewerSorter;
073: import org.eclipse.swt.SWT;
074: import org.eclipse.swt.events.FocusAdapter;
075: import org.eclipse.swt.events.FocusEvent;
076: import org.eclipse.swt.graphics.Image;
077: import org.eclipse.swt.layout.GridData;
078: import org.eclipse.swt.layout.GridLayout;
079: import org.eclipse.swt.widgets.Composite;
080: import org.eclipse.swt.widgets.Label;
081: import org.eclipse.swt.widgets.Menu;
082: import org.eclipse.ui.IMemento;
083: import org.eclipse.ui.IWorkbenchActionConstants;
084: import org.eclipse.ui.IWorkbenchPartSite;
085: import org.eclipse.ui.PartInitException;
086: import org.eclipse.ui.forms.widgets.FormToolkit;
087:
088: /**
089: * @author Nigel Westbury
090: * @author Johann Gyger
091: */
092: public class CategoryPage implements IBookkeepingPageFactory {
093:
094: private static final String PAGE_ID = "net.sf.jmoney.categoriespanel.categories";
095:
096: public void init(IMemento memento) {
097: // No view state to restore
098: }
099:
100: public void saveState(IMemento memento) {
101: // No view state to save
102: }
103:
104: /* (non-Javadoc)
105: * @see net.sf.jmoney.IBookkeepingPageListener#createPages(java.lang.Object, org.eclipse.swt.widgets.Composite)
106: */
107: public IBookkeepingPage createFormPage(NodeEditor editor,
108: IMemento memento) {
109: SectionlessPage formPage = new CategoryFormPage(editor, PAGE_ID);
110:
111: try {
112: editor.addPage(formPage);
113: } catch (PartInitException e) {
114: JMoneyPlugin.log(e);
115: // TODO: cleanly leave out this page.
116: }
117:
118: return formPage;
119: }
120:
121: private class CategoryFormPage extends SectionlessPage {
122: private TreeViewer viewer;
123: // private DrillDownAdapter drillDownAdapter;
124: private Action newAccountAction;
125: private Action newSubAccountAction;
126: private Action deleteAccountAction;
127: private Action editorAction;
128:
129: /**
130: * The account whose property values are in the edit controls below.
131: * If selectedAccount is null then the property controls should
132: * should be disabled.
133: */
134: private IncomeExpenseAccount selectedAccount = null;
135:
136: private Session session;
137:
138: private class PropertyControls {
139:
140: private ScalarPropertyAccessor propertyAccessor;
141: private Label propertyLabel;
142: private IPropertyControl propertyControl;
143:
144: PropertyControls(ScalarPropertyAccessor propertyAccessor,
145: Label propertyLabel,
146: IPropertyControl propertyControl) {
147: this .propertyAccessor = propertyAccessor;
148: this .propertyLabel = propertyLabel;
149: this .propertyControl = propertyControl;
150: }
151:
152: void load(ExtendableObject object) {
153: propertyControl.load(object);
154: setVisibility();
155: }
156:
157: /**
158: * Called whenever a property changes.
159: * The visibility of all property controls with dependencies are updated.
160: */
161: public void setVisibility() {
162: boolean isApplicable = propertyAccessor
163: .isPropertyApplicable(selectedAccount);
164: propertyLabel.setVisible(isApplicable);
165: propertyControl.getControl().setVisible(isApplicable);
166: }
167: }
168:
169: /**
170: * List of the PropertyControls objects for the
171: * properties that can be edited in this panel.
172: */
173: Vector<PropertyControls> propertyList = new Vector<PropertyControls>();
174:
175: private SessionChangeListener listener = new SessionChangeAdapter() {
176: public void objectInserted(ExtendableObject newObject) {
177: if (newObject instanceof IncomeExpenseAccount) {
178: IncomeExpenseAccount newAccount = (IncomeExpenseAccount) newObject;
179: Account parent = newAccount.getParent();
180: if (parent == null) {
181: viewer.refresh(session, false);
182: } else {
183: viewer.refresh(parent, false);
184: }
185: }
186: }
187:
188: public void objectRemoved(ExtendableObject deletedObject) {
189: if (deletedObject instanceof IncomeExpenseAccount) {
190: IncomeExpenseAccount deletedAccount = (IncomeExpenseAccount) deletedObject;
191: viewer.setSelection(null);
192: viewer.remove(deletedAccount);
193: }
194: }
195:
196: public void objectChanged(ExtendableObject changedObject,
197: ScalarPropertyAccessor propertyAccessor,
198: Object oldValue, Object newValue) {
199: if (changedObject instanceof IncomeExpenseAccount) {
200: IncomeExpenseAccount account = (IncomeExpenseAccount) changedObject;
201: if (propertyAccessor == AccountInfo
202: .getNameAccessor()) {
203: Account parent = account.getParent();
204: // We refresh the parent node because the name change
205: // in this node may affect the order of the child nodes.
206: if (parent == null) {
207: viewer.refresh(session, true);
208: } else {
209: viewer.refresh(parent, true);
210: }
211: }
212:
213: if (account.equals(selectedAccount)) {
214: // Update the visibility of controls.
215: for (PropertyControls propertyControls : propertyList) {
216: propertyControls.setVisibility();
217: }
218: }
219: }
220: }
221: };
222:
223: CategoryFormPage(NodeEditor editor, String pageId) {
224: super (
225: editor,
226: pageId,
227: CategoriesPanelPlugin
228: .getResourceString("NavigationTreeModel.categories"),
229: "Income and Expense Categories");
230:
231: this .session = JMoneyPlugin.getDefault().getSession();
232: }
233:
234: public Composite createControl(Object nodeObject,
235: Composite parent, FormToolkit toolkit, IMemento memento) {
236:
237: /**
238: * topLevelControl is a control with grid layout,
239: * onto which all sub-controls should be placed.
240: */
241: Composite topLevelControl = new Composite(parent, SWT.NULL);
242:
243: GridLayout layout = new GridLayout();
244: layout.numColumns = 2;
245: topLevelControl.setLayout(layout);
246:
247: viewer = new TreeViewer(topLevelControl, SWT.SINGLE
248: | SWT.H_SCROLL | SWT.V_SCROLL);
249:
250: GridData gridData = new GridData();
251: gridData.horizontalAlignment = GridData.FILL;
252: gridData.verticalAlignment = GridData.FILL;
253: gridData.grabExcessHorizontalSpace = true;
254: gridData.grabExcessVerticalSpace = true;
255: gridData.horizontalSpan = 2;
256: viewer.getControl().setLayoutData(gridData);
257:
258: // drillDownAdapter = new DrillDownAdapter(viewer);
259: ViewContentProvider contentProvider = new ViewContentProvider();
260: viewer.setContentProvider(contentProvider);
261: viewer.setLabelProvider(new ViewLabelProvider());
262: viewer.setSorter(new NameSorter());
263:
264: viewer.setInput(session);
265:
266: // Listen for changes to the category list.
267: session.getDataManager().addChangeListener(listener,
268: viewer.getControl());
269:
270: // Listen for changes in the selection and update the
271: // edit controls.
272: viewer
273: .addSelectionChangedListener(new ISelectionChangedListener() {
274: public void selectionChanged(
275: SelectionChangedEvent event) {
276: // If a selection already selected, commit any changes
277: if (selectedAccount != null) {
278: // session.registerUndoableChange("change category properties");
279: }
280:
281: // Set the new selection
282: IStructuredSelection selection = (IStructuredSelection) event
283: .getSelection();
284: selectedAccount = (IncomeExpenseAccount) selection
285: .getFirstElement();
286:
287: // Set the values from the account object into the control fields,
288: // or disable the controls if the account is null.
289: for (PropertyControls propertyControls : propertyList) {
290: propertyControls.load(selectedAccount);
291: }
292: }
293: });
294:
295: // Add the properties for category.
296: for (final ScalarPropertyAccessor<?> propertyAccessor : IncomeExpenseAccountInfo
297: .getPropertySet().getScalarProperties3()) {
298: final Label propertyLabel = new Label(topLevelControl,
299: 0);
300: propertyLabel
301: .setText(propertyAccessor.getDisplayName() + ':');
302:
303: IPropertyControl propertyControl = propertyAccessor
304: .createPropertyControl(topLevelControl);
305:
306: createAndAddFocusListener(propertyControl,
307: propertyAccessor);
308:
309: /*
310: * No account is initially set. It is not really obvious in what
311: * state the controls should be when no account is set, so let's
312: * set them invisible (the same state as inapplicable properties
313: * would be set to).
314: */
315: propertyLabel.setVisible(false);
316: propertyControl.getControl().setVisible(false);
317:
318: // Add to our list of controls.
319: propertyList.add(new PropertyControls(propertyAccessor,
320: propertyLabel, propertyControl));
321:
322: toolkit.adapt(propertyLabel, false, false);
323: toolkit.adapt(propertyControl.getControl(), true, true);
324:
325: // Make the control take up the full width
326: GridData gridData5 = new GridData();
327: gridData.horizontalAlignment = GridData.FILL;
328: gridData5.grabExcessHorizontalSpace = true;
329: propertyControl.getControl().setLayoutData(gridData5);
330:
331: // Set the control to have no account set (control
332: // is disabled)
333: propertyControl.load(null);
334: }
335:
336: // Set up the context menus.
337: makeActions();
338: hookContextMenu(fEditor.getSite());
339:
340: return topLevelControl;
341: }
342:
343: private <V> void createAndAddFocusListener(
344: final IPropertyControl propertyControl,
345: final ScalarPropertyAccessor<V> propertyAccessor) {
346: propertyControl.getControl().addFocusListener(
347: new FocusAdapter() {
348:
349: // When a control gets the focus, save the old value here.
350: // This value is used in the change message.
351: V oldValue;
352: String oldValueText;
353:
354: public void focusLost(FocusEvent e) {
355: if (session.getDataManager()
356: .isSessionFiring()) {
357: return;
358: }
359:
360: propertyControl.save();
361:
362: String newValueText = propertyAccessor
363: .formatValueForMessage(selectedAccount);
364: final V newValue = selectedAccount
365: .getPropertyValue(propertyAccessor);
366:
367: String description;
368: if (propertyAccessor == AccountInfo
369: .getNameAccessor()) {
370: description = "rename account from "
371: + oldValueText + " to "
372: + newValueText;
373: } else {
374: description = "change "
375: + propertyAccessor
376: .getDisplayName()
377: + " property" + " in '"
378: + selectedAccount.getName()
379: + "' account" + " from "
380: + oldValueText + " to "
381: + newValueText;
382: }
383:
384: IOperationHistory history = JMoneyPlugin
385: .getDefault().getWorkbench()
386: .getOperationSupport()
387: .getOperationHistory();
388:
389: IUndoableOperation operation = new AbstractOperation(
390: description) {
391: @Override
392: public IStatus execute(
393: IProgressMonitor monitor,
394: IAdaptable info)
395: throws ExecutionException {
396: // Change has already been made, so do nothing here.
397: return Status.OK_STATUS;
398: }
399:
400: @Override
401: public IStatus redo(
402: IProgressMonitor monitor,
403: IAdaptable info)
404: throws ExecutionException {
405: selectedAccount.setPropertyValue(
406: propertyAccessor, oldValue);
407: return Status.OK_STATUS;
408: }
409:
410: @Override
411: public IStatus undo(
412: IProgressMonitor monitor,
413: IAdaptable info)
414: throws ExecutionException {
415: selectedAccount.setPropertyValue(
416: propertyAccessor, newValue);
417: return Status.OK_STATUS;
418: }
419: };
420:
421: operation.addContext(session
422: .getUndoContext());
423: try {
424: history.execute(operation, null, null);
425: } catch (ExecutionException e2) {
426: // TODO Auto-generated catch block
427: e2.printStackTrace();
428: }
429: }
430:
431: public void focusGained(FocusEvent e) {
432: // Save the old value of this property for use in 'undo'.
433: oldValue = selectedAccount
434: .getPropertyValue(propertyAccessor);
435: oldValueText = propertyAccessor
436: .formatValueForMessage(selectedAccount);
437:
438: }
439: });
440: }
441:
442: public void saveState(IMemento memento) {
443: // We could save the current category selection
444: // and the expand/collapse state of each node
445: // but it is not worthwhile.
446: }
447:
448: private void hookContextMenu(IWorkbenchPartSite site) {
449: MenuManager menuMgr = new MenuManager("#PopupMenu");
450: menuMgr.setRemoveAllWhenShown(true);
451: menuMgr.addMenuListener(new IMenuListener() {
452: public void menuAboutToShow(IMenuManager manager) {
453: CategoryFormPage.this .fillContextMenu(manager);
454: }
455: });
456: Menu menu = menuMgr.createContextMenu(viewer.getControl());
457: viewer.getControl().setMenu(menu);
458:
459: site.registerContextMenu(menuMgr, viewer);
460: }
461:
462: private void fillContextMenu(IMenuManager manager) {
463: manager.add(newAccountAction);
464: if (selectedAccount != null) {
465: manager.add(newSubAccountAction);
466: manager.add(deleteAccountAction);
467: }
468:
469: manager.add(new Separator());
470:
471: // Add a menu item for IncomeExpenseAccount editor
472: if (selectedAccount != null && editorAction != null) {
473: manager.add(editorAction);
474: }
475:
476: manager.add(new Separator());
477:
478: // Other plug-ins can contribute their actions here
479: manager.add(new Separator(
480: IWorkbenchActionConstants.MB_ADDITIONS));
481: }
482:
483: private void makeActions() {
484: newAccountAction = new Action() {
485: public void run() {
486: IncomeExpenseAccount account = session
487: .createAccount(IncomeExpenseAccountInfo
488: .getPropertySet());
489: account
490: .setName(CategoriesPanelPlugin
491: .getResourceString("CategoryPanel.newCategory"));
492: // session.registerUndoableChange("add new category");
493: viewer.setSelection(
494: new StructuredSelection(account), true);
495: }
496: };
497: newAccountAction.setText(CategoriesPanelPlugin
498: .getResourceString("CategoryPanel.newCategory"));
499: newAccountAction.setToolTipText("New category tooltip");
500:
501: newSubAccountAction = new Action() {
502: public void run() {
503: if (selectedAccount != null) {
504: IncomeExpenseAccount subAccount = selectedAccount
505: .createSubAccount();
506: subAccount
507: .setName(CategoriesPanelPlugin
508: .getResourceString("CategoryPanel.newCategory"));
509: // session.registerUndoableChange("add new category");
510: viewer.setSelection(new StructuredSelection(
511: subAccount), true);
512: }
513: }
514: };
515: newSubAccountAction.setText(CategoriesPanelPlugin
516: .getResourceString("CategoryPanel.newSubcategory"));
517: newSubAccountAction.setToolTipText("New category tooltip");
518:
519: deleteAccountAction = new Action() {
520: public void run() {
521: if (selectedAccount != null) {
522: session.deleteAccount(selectedAccount);
523: }
524: }
525: };
526: deleteAccountAction.setText(CategoriesPanelPlugin
527: .getResourceString("CategoryPanel.deleteCategory"));
528: deleteAccountAction
529: .setToolTipText("Delete category tooltip");
530:
531: if (!IncomeExpenseAccountInfo.getPropertySet()
532: .getPageFactories().isEmpty()) {
533: editorAction = new Action() {
534: public void run() {
535: IStructuredSelection selection = (IStructuredSelection) viewer
536: .getSelection();
537: for (Object selectedObject : selection.toList()) {
538: Assert
539: .isTrue(selectedObject instanceof ExtendableObject);
540: NodeEditor.openEditor(getSite()
541: .getWorkbenchWindow(),
542: (ExtendableObject) selectedObject);
543: }
544: }
545: };
546: editorAction
547: .setText(JMoneyPlugin
548: .getResourceString("Menu.openCategoryAccountEditor"));
549: } else {
550: // No plug-ins have added any pages that display category information,
551: // so do not show a menu item for this.
552: editorAction = null;
553: }
554: }
555:
556: };
557:
558: class ViewContentProvider implements IStructuredContentProvider,
559: ITreeContentProvider {
560: /**
561: * In fact the input does not change because we create our own node object
562: * that acts as the root node. Certain nodes below the root may get
563: * their data from the model. The accountsNode object does this.
564: */
565: public void inputChanged(Viewer viewer, Object oldInput,
566: Object newInput) {
567: // The input never changes so we don't do anything here.
568: }
569:
570: public void dispose() {
571: }
572:
573: public Object[] getElements(Object parent) {
574: return getChildren(parent);
575: }
576:
577: public Object getParent(Object child) {
578: if (child instanceof Account) {
579: Account parent = ((Account) child).getParent();
580: if (parent == null) {
581: return ((Account) child).getSession();
582: } else {
583: return parent;
584: }
585: }
586: return null;
587: }
588:
589: public Object[] getChildren(Object parent) {
590: // TODO: The nodes are not currently ordered, but they
591: // should be.
592:
593: if (parent instanceof Session) {
594: Iterator iter;
595:
596: iter = ((Session) parent)
597: .getIncomeExpenseAccountIterator();
598: int count = 0;
599: for (; iter.hasNext();) {
600: iter.next();
601: count++;
602: }
603: Object children[] = new Object[count];
604: iter = ((Session) parent)
605: .getIncomeExpenseAccountIterator();
606: int i = 0;
607: for (; iter.hasNext();) {
608: children[i++] = iter.next();
609: }
610: return children;
611: } else if (parent instanceof IncomeExpenseAccount) {
612: return ((IncomeExpenseAccount) parent)
613: .getSubAccountCollection().toArray();
614: } else {
615: throw new RuntimeException("internal error");
616: }
617: }
618:
619: public boolean hasChildren(Object parent) {
620: if (parent instanceof Session) {
621: return ((Session) parent)
622: .getIncomeExpenseAccountIterator().hasNext();
623: } else if (parent instanceof Account) {
624: return !((Account) parent).getSubAccountCollection()
625: .isEmpty();
626: }
627: return false;
628: }
629:
630: }
631:
632: class ViewLabelProvider extends LabelProvider {
633:
634: public String getText(Object obj) {
635: if (obj instanceof Account) {
636: String name = ((Account) obj).getName();
637: return name == null ? "(unknown account name)" : name;
638: } else {
639: return "(unknown object)";
640: }
641: }
642:
643: public Image getImage(Object obj) {
644: if (obj instanceof Account) {
645: return Constants.CATEGORY_ICON;
646: } else {
647: throw new RuntimeException("");
648: }
649: }
650:
651: }
652:
653: class NameSorter extends ViewerSorter {
654: }
655:
656: }
|