001: /*=============================================================================
002: * Copyright Texas Instruments, Inc., 2001-2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.chimera.plugin;
020:
021: import ti.exceptions.ProgrammingErrorException;
022: import ti.chimera.*;
023: import ti.chimera.registry.*;
024: import ti.chimera.service.*;
025:
026: import java.util.*;
027: import java.lang.ref.*;
028: import javax.swing.*; // import javax.swing.event.*;
029: import java.awt.Rectangle;
030: import java.awt.Container;
031: import java.awt.BorderLayout;
032: import java.awt.event.WindowListener;
033:
034: /**
035: * The <code>WindowManagerPlugin</code> handles diplaying <code>View</code>s,
036: * tool-bars, and handling the menu-bar. All the API methods to add/remove
037: * menu-bar-items/docks/toolbars are thread safe, so they can be used, without
038: * having to worry about swing's lack of thread safeness.
039: * <p>
040: * This plugin, and the <i>window manager</i> service that it provides is
041: * only half of the equasion as far as window management. The other half
042: * is the "window manager mode" which is what actually displays the UI.
043: * <center><img src="WindowManagerArchitecture.png"></center>
044: * The devision is labor is that the this plugin handles writing data into
045: * the registry, creating {@link Dialog} objects, etc., and the window
046: * manager implementation responds to data written into the registry in
047: * order to realize the UI. There are actually multiple implementations
048: * of the window manager implementation... window mode and desktop mode.
049: *
050: * @author Rob Clark
051: * @version 0.1
052: * @see ti.chimera.service.WindowManager ti.chimera.service.WindowManager -
053: * the service interface implemented by this plugin
054: */
055: public class WindowManagerPlugin extends Plugin {
056: private Registry registry;
057:
058: /**
059: * Counter used to generate a unique id for each toolbar, which is used
060: * as part of the path for storing toolbars in the registry.
061: */
062: private int toolBarIdx = 0;
063:
064: /**
065: * The current mode service. Hang on to this to keep the plugin that
066: * implements the mode active while it is in use.
067: */
068: private WindowMode windowMode;
069:
070: /*=======================================================================*/
071: /**
072: * Class Constructor.
073: *
074: * @param main the main application
075: */
076: public WindowManagerPlugin(Main main) {
077: super (main, "Window Manager");
078:
079: if ("true".equals(System.getProperty("ti.chimera.debug.swing"))) {
080: System.err.println("enabling ThreadCheckingRepaintManager");
081: RepaintManager
082: .setCurrentManager(new com.clientjava.examples.badswingthread.ThreadCheckingRepaintManager(
083: true));
084: }
085:
086: registry = main.getRegistry();
087:
088: try {
089: registry.link(new Node(Boolean.FALSE,
090: NodeContract.BOOLEAN_CONTRACT,
091: "is the user interface visible?"),
092: "/Window Manager/visible");
093: } catch (RegistryException e) {
094: throw new ProgrammingErrorException(e);
095: }
096:
097: registry.subscribeToValue("/Preferences/Window Manager/Mode",
098: null, new NodeSubscriber() {
099:
100: public void publish(Node node, Object value) {
101: if (windowMode != null)
102: windowMode.stop();
103:
104: windowMode = (WindowMode) (registry
105: .getService((String) value));
106:
107: windowMode.start();
108: }
109:
110: });
111:
112: /* create the service... note doing it this way prevents the service
113: * from being GC'd, which will cause the plugin to never stop... in
114: * this case this is the behavior we want, but for most plugins it
115: * shouldn't be done this way.
116: */
117: final Service service = new WindowManager() {
118:
119: private Node toolBarNode = registry.mkdir("/ToolBars");
120:
121: /**
122: * Set the mode. The mode implements the actual display of the
123: * user interface to the user.
124: *
125: * @param mode the service implementing the mode which
126: * realizes the display of dialogs/toolbars/menubar
127: */
128: public void setMode(WindowMode mode) {
129: setValue("/Preferences/Window Manager/Mode", mode
130: .getName());
131: }
132:
133: /**
134: * Set the Look & Feel. Swing supports multiple look&feels, depending
135: * on the platform. (For example: Metal, Motif, Windows, MacOSX, plus
136: * any 3rd party L&F that the user has installed.)
137: *
138: * @param lnfName the full name of the class implementing the L&F
139: */
140: public void setLookAndFeel(String lnfName) {
141: setValue("/Preferences/Window Manager/Look And Feel",
142: lnfName);
143: }
144:
145: private void setValue(String path, Object value) {
146: try {
147: registry.resolve(path).setValue(value);
148: } catch (RegistryException e) {
149: throw new ProgrammingErrorException(e);
150: }
151: }
152:
153: private Object getValue(String path) {
154: try {
155: return registry.resolve(path).getValue();
156: } catch (RegistryException e) {
157: throw new ProgrammingErrorException(e);
158: }
159: }
160:
161: /**
162: * Get a dialog with the specified title. This method should be used,
163: * rather than creating a <code>JDialog</code>, because this will behave
164: * properly if the window manager is in <code>DESKTOP_MODE</code>.
165: *
166: * @param title the title of the dialog
167: * @return a dialog
168: */
169: public ti.chimera.Dialog getDialog(final String title) {
170: final ti.chimera.Dialog[] d = new ti.chimera.Dialog[1];
171: try {
172: ((SwingWorker) (getMain().getRegistry()
173: .getService("swing worker"))).run(
174: new Runnable() {
175: public void run() {
176: d[0] = new WMDialog(title);
177: }
178: }, null);
179: } catch (Throwable t) {
180: throw new ProgrammingErrorException(t);
181: }
182: return d[0];
183: }
184:
185: // XXX temporary, until Dock get registrified:
186: public void addDock(final Dock dock) {
187: Dialog dialog = getDialog(dock.getName());
188: dock.setDialog(dialog);
189: dock.setVisible(true);
190:
191: dialog.addCloseRunnable(new Runnable() {
192: public void run() {
193: dock.close();
194: }
195: });
196: dialog.getContentPane().add(dock);
197:
198: // if the current bounds are invalid, pack:
199: if (dialog.getBounds() == null)
200: dialog.pack();
201: // work around for OBJS155: --RDC
202: else
203: dialog.setBounds(dialog.getBounds());
204:
205: dialog.setVisible(true);
206: }
207:
208: // XXX temporary, until Dock get registrified:
209: public void removeDock(Dock dock) {
210: Dialog dialog = dock.getDialog();
211:
212: if (dialog != null) {
213: dialog.getContentPane().remove(dock);
214: dialog.dispose();
215: dock.setDialog(null);
216: } else {
217: getMain().error("WindowManager",
218: "dock not in dialog");
219: }
220: }
221:
222: // XXX temporary, until Dock get registrified:
223: public void dockUpdated(Dock dock) {
224: }
225:
226: /**
227: * Add a tool-bar.
228: *
229: * @param toolBar the tool-bar to add
230: */
231: public void addToolBar(JToolBar toolBar) {
232: try {
233: synchronized (registry) {
234: registry.link(new Node(toolBar,
235: new TypeNodeContract(JToolBar.class),
236: "a toolbar"), "/ToolBars/"
237: + (toolBarIdx++));
238: }
239: } catch (RegistryException e) {
240: throw new ProgrammingErrorException(e);
241: }
242: }
243:
244: /**
245: * Remove a tool-bar.
246: *
247: * @param toolBar the tool-bar to remove
248: */
249: public void removeToolBar(JToolBar toolBar) {
250: try {
251: synchronized (registry) {
252: DirectoryTable dt = (DirectoryTable) (registry
253: .resolve("/ToolBars").getValue());
254:
255: for (Iterator itr = dt.getChildNames(); itr
256: .hasNext();) {
257: String name = (String) (itr.next());
258: if (dt.get(name).getValue().equals(toolBar)) {
259: registry.unlink("/ToolBars/" + name);
260: return;
261: }
262: }
263: }
264: } catch (RegistryException e) {
265: throw new ProgrammingErrorException(e);
266: }
267: }
268:
269: private int numSeparators = 0;
270:
271: /**
272: * Add a menu bar item. The path of the action is "/" seperated "path" of
273: * the action, such as "/File/Open". Multiple actions with the same path
274: * and name can exist, in which case when the user selects that entry from
275: * the pull-down menus, the <code>actionPerformed</code> methods will be
276: * in the order that the actions where added. If the action is null, a
277: * separator will be added.
278: *
279: * @param path which sub-menu the item should go under
280: * @param a the menu bar action to add.
281: * @see #removeMenuBarItem
282: */
283: public void addMenuBarItem(String path, Action a) {
284: try {
285: synchronized (registry) {
286: String name;
287:
288: if (a == null)
289: name = "." + (numSeparators++);
290: else
291: name = (String) (a.getValue(Action.NAME));
292:
293: String rpath = "/MenuBar/" + path + "/" + name;
294: LinkedList actionList;
295: Node node;
296:
297: if (registry.exists(rpath)) {
298: node = registry.resolve(rpath);
299: actionList = (LinkedList) (node.getValue());
300: } else {
301: registry.link(node = new Node(
302: actionList = new LinkedList(),
303: new TypeNodeContract(
304: LinkedList.class),
305: "a list of menubar item actions"),
306: rpath);
307: }
308:
309: if (a != null) {
310: actionList.add(a);
311: node.setValue(actionList); // ensure MenuBar gets notification of change
312: }
313: }
314: } catch (RegistryException e) {
315: throw new ProgrammingErrorException(e);
316: }
317: }
318:
319: /**
320: * Remove a menu bar item.
321: *
322: * @param path which sub-menu the item should go under
323: * @param a the menu bar action to remove.
324: * @see #addMenuBarItem
325: */
326: public void removeMenuBarItem(String path, Action a) {
327: try {
328: synchronized (registry) {
329: String rpath = "/MenuBar/" + path + "/"
330: + a.getValue(Action.NAME);
331:
332: if (registry.exists(rpath)) {
333: Node node = registry.resolve(rpath);
334: LinkedList actionList = (LinkedList) (node
335: .getValue());
336: actionList.remove(a);
337: if (actionList.size() == 0)
338: registry.unlink(rpath);
339: else
340: node.setValue(actionList); // ensure MenuBar gets notification of change
341: }
342: }
343: } catch (RegistryException e) {
344: throw new ProgrammingErrorException(e);
345: }
346: }
347:
348: /**
349: * Show or hide the user interface.
350: *
351: * @param b <code>true</code> to show the user interface, or
352: * <code>false</code> to hide it
353: */
354: public void setVisible(boolean b) {
355: setValue("/Window Manager/visible", b ? Boolean.TRUE
356: : Boolean.FALSE);
357: }
358:
359: /**
360: * Show or hide the user interface.
361: *
362: * @param b <code>true</code> to show the user interface, or
363: * <code>false</code> to hide it
364: */
365: public boolean isVisible() {
366: return ((Boolean) getValue("/Window Manager/visible"))
367: .booleanValue();
368: }
369: };
370:
371: registerServiceFactory(new ServiceFactory() {
372:
373: public Service createService() {
374: return service;
375: }
376:
377: });
378:
379: }
380:
381: private Main getMain() {
382: return main;
383: }
384:
385: /*=======================================================================*/
386: /**
387: * The implementation of the dialog interface
388: */
389: private class WMDialog implements Dialog {
390: /**
391: * The dialog's title, must be unique, as it is used as a "key" to
392: * uniquely identify a dialog.
393: */
394: private String title;
395:
396: private Node boundsNode = new PersistentNode(null,
397: new TypeNodeContract(Rectangle.class),
398: "the position of this dialog");
399:
400: private Node closeRunnableListNode = new Node(new LinkedList(),
401: new TypeNodeContract(LinkedList.class),
402: "the list of runnables to run when dialog is closed");
403:
404: private Node contentPaneNode = new Node(new JPanel(
405: new BorderLayout()), new TypeNodeContract(
406: Container.class), "the content pane");
407:
408: private Node controlNode = new Node(null,
409: NodeContract.STRING_CONTRACT,
410: "The control command pipe. Write <i>center</i>, "
411: + "<i>toFront</i>, <i>pack</i> to this pipe.");
412:
413: private Node windowListenerListNode = new Node(
414: new LinkedList(),
415: new TypeNodeContract(LinkedList.class),
416: "the list of window listeners");
417:
418: private Node instanceNode = new Node(
419: this ,
420: new TypeNodeContract(Dialog.class),
421: "the dialog instance; this is immutable, so it is safe to getValue() instead of subscribing."); // XXX maybe add an immutable contract???
422:
423: private Node visibleNode = new Node(Boolean.FALSE,
424: NodeContract.BOOLEAN_CONTRACT,
425: "is this dialog visible?");
426:
427: /**
428: * Table of node paths and nodes...
429: */
430: private Object[][] nodeTable = new Object[][] {
431: { "bounds", boundsNode },
432: { "closeRunnableList", closeRunnableListNode },
433: { "contentPane", contentPaneNode },
434: { "control", controlNode },
435: { "windowListenerList", windowListenerListNode },
436: { "instance", instanceNode },
437: { "visible", visibleNode } };
438:
439: /**
440: * Class Constructor.
441: */
442: WMDialog(String title) {
443: synchronized (registry) {
444: /* If there already exists a dialog with the same title, then add a number
445: * to the end of the dialog name to make it unique.
446: */
447: if (registry.exists("/Dialogs/" + title)) {
448: int idx = 1;
449: while (registry.exists("/Dialogs/" + title + " "
450: + idx))
451: idx++;
452: title += " " + idx;
453: }
454: }
455:
456: this .title = title;
457:
458: //createRegistryNodes(); // XXX
459: }
460:
461: private void createRegistryNodes() {
462: /* Setup registry entries:
463: */
464: try {
465: for (int i = 0; i < nodeTable.length; i++) {
466: registry.link((Node) (nodeTable[i][1]), "/Dialogs/"
467: + title + "/" + nodeTable[i][0]);
468: }
469: } catch (RegistryException e) {
470: throw new ProgrammingErrorException(e);
471: }
472:
473: addCloseRunnable(new Runnable() {
474: public void run() {
475: synchronized (WMDialog.this ) {
476: if (modalQueue != null)
477: modalQueue.enqueue("done"); // doesn't matter what we enqueue
478: }
479: }
480: });
481:
482: // wait for impl to be ready:
483: final ti.swing.SwingQueue waitQueue = new ti.swing.SwingQueue();
484: controlNode.subscribe(new NodeSubscriber() {
485:
486: public void publish(Node node, Object value) {
487: if ("ready".equals(value)) {
488: waitQueue.enqueue("ready");
489: node.unsubscribe(this );
490: }
491: }
492:
493: });
494: waitQueue.dequeue();
495: }
496:
497: /**
498: * Get the title of this dialog.
499: *
500: * @return a string
501: */
502: public String getTitle() {
503: return title;
504: }
505:
506: /**
507: * Called to set the visible state of this dialog.
508: *
509: * @param visible the new visible state
510: */
511: public void setVisible(boolean visible) {
512: if (visible && !registry.exists("/Dialogs/" + title))
513: createRegistryNodes();
514: visibleNode
515: .setValue(visible ? Boolean.TRUE : Boolean.FALSE);
516: if (!visible && (modalQueue != null))
517: modalQueue.enqueue("done");
518: }
519:
520: /**
521: * Get the bounds of this dialog, ie the position and size of the window.
522: *
523: * @return a rectangle
524: */
525: public Rectangle getBounds() {
526: return (Rectangle) (boundsNode.getValue());
527: }
528:
529: /**
530: * Set the bounds of this dialog, ie. the size and position of the window.
531: *
532: * @param r the new bounds
533: */
534: public void setBounds(Rectangle r) {
535: boundsNode.setValue(r);
536: }
537:
538: /**
539: * Return the content-pane. This is the component that the contents of
540: * the window are to be added to.
541: *
542: * @return a container
543: */
544: public Container getContentPane() {
545: return (Container) (contentPaneNode.getValue());
546: }
547:
548: /**
549: * Bring this dialog to the front.
550: */
551: public void toFront() {
552: setVisible(true); // work-around for #362
553: controlNode.setValue("toFront");
554: }
555:
556: /**
557: * Center this dialog on the screen.
558: */
559: public void center() {
560: setVisible(true); // work-around for #362
561: controlNode.setValue("center");
562: }
563:
564: /**
565: * Cause the dialog to be sized to fit the preferred size of it's sub-
566: * components.
567: */
568: public void pack() {
569: setVisible(true); // work-around for #362
570: controlNode.setValue("pack");
571: }
572:
573: /**
574: * Dispose of this dialog.
575: */
576: public synchronized void dispose() {
577: /* Clear registry entries:
578: */
579: if (registry.exists("/Dialogs/" + title)) {
580: try {
581: registry.unlink("/Dialogs/" + title, true);
582: } catch (RegistryException e) {
583: throw new ProgrammingErrorException(e);
584: }
585: }
586: }
587:
588: // protected void finalize()
589: // {
590: // dispose();
591: // }
592:
593: /**
594: * Show as modal dialog. Do not return until dialog closed.
595: */
596: public synchronized void showModal() {
597: modalQueue = new ti.swing.SwingQueue();
598:
599: // make dialog visible:
600: setVisible(true);
601:
602: // wait for dialog to be closed:
603: modalQueue.dequeue();
604:
605: modalQueue = null;
606: }
607:
608: /**
609: * When dialog is shown modally, we block on this until someone (either
610: * close-runnable or setVisible(false) writes to this.
611: */
612: private ti.swing.SwingQueue modalQueue;
613:
614: /**
615: * Add a runnable that will be invoked when this dialog is closed. This
616: * gives the user of the dialog a way to perform cleanup when the dialog
617: * is closed.
618: *
619: * @param r the runnable
620: */
621: public void addCloseRunnable(Runnable r) {
622: LinkedList closeRunnableList = ((LinkedList) (closeRunnableListNode
623: .getValue()));
624: synchronized (closeRunnableList) {
625: closeRunnableList.add(r);
626: }
627: }
628:
629: /**
630: * Remove the runnable from the list of runnables that will get run when
631: * this dialog is closed.
632: *
633: * @param r the runnable
634: */
635: public void removeCloseRunnable(Runnable r) {
636: LinkedList closeRunnableList = ((LinkedList) (closeRunnableListNode
637: .getValue()));
638: synchronized (closeRunnableList) {
639: closeRunnableList.remove(r);
640: }
641: }
642:
643: /**
644: * Adds the specified window listener to receive window events.
645: *
646: * @param l the listener
647: */
648: public void addWindowListener(WindowListener l) {
649: ((LinkedList) (windowListenerListNode.getValue())).add(l);
650: }
651:
652: /**
653: * Removes the specified window listener so that it no longer receives
654: * window events from this component
655: *
656: * @param l the listener
657: */
658: public void removeWindowListener(WindowListener l) {
659: ((LinkedList) (windowListenerListNode.getValue()))
660: .remove(l);
661: }
662: }
663: }
664:
665: /*
666: * Local Variables:
667: * tab-width: 2
668: * indent-tabs-mode: nil
669: * mode: java
670: * c-indentation-style: java
671: * c-basic-offset: 2
672: * eval: (c-set-offset 'substatement-open '0)
673: * eval: (c-set-offset 'case-label '+)
674: * eval: (c-set-offset 'inclass '+)
675: * eval: (c-set-offset 'inline-open '0)
676: * End:
677: */
|