001/* A panel containing customizable controls for a Ptolemy II model. 002 003 Copyright (c) 1998-2014 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 */ 027package ptolemy.actor.gui.run; 028 029import java.awt.Component; 030import java.awt.LayoutManager; 031import java.awt.Window; 032import java.awt.event.ActionEvent; 033import java.awt.event.ActionListener; 034import java.io.ByteArrayInputStream; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.UnsupportedEncodingException; 038import java.util.HashMap; 039import java.util.Iterator; 040 041import javax.swing.JButton; 042import javax.swing.JLabel; 043import javax.swing.JPanel; 044import javax.xml.parsers.DocumentBuilder; 045import javax.xml.parsers.DocumentBuilderFactory; 046 047import org.mlc.swing.layout.ContainerLayout; 048import org.mlc.swing.layout.LayoutConstraintsManager; 049import org.w3c.dom.Document; 050import org.w3c.dom.Node; 051import org.w3c.dom.NodeList; 052 053import ptolemy.actor.CompositeActor; 054import ptolemy.actor.Manager; 055import ptolemy.actor.gui.AWTContainer; 056import ptolemy.actor.gui.Configurer; 057import ptolemy.actor.gui.Placeable; 058import ptolemy.actor.injection.PortablePlaceable; 059import ptolemy.gui.CloseListener; 060import ptolemy.kernel.ComponentEntity; 061import ptolemy.kernel.util.Attribute; 062import ptolemy.kernel.util.ConfigurableAttribute; 063import ptolemy.kernel.util.IllegalActionException; 064import ptolemy.kernel.util.InternalErrorException; 065import ptolemy.kernel.util.NamedObj; 066import ptolemy.util.CancelException; 067import ptolemy.util.MessageHandler; 068 069/////////////////////////////////////////////////////////////////// 070//// CustomizableRunPane 071 072/** 073 074 A panel for interacting with an executing Ptolemy II model. 075 This panel can be customized by inserting 076 077 FIXME: more 078 079 @see Placeable 080 @author Edward A. Lee 081 @version $Id$ 082 @since Ptolemy II 8.0 083 @Pt.ProposedRating Yellow (eal) 084 @Pt.AcceptedRating Red (cxh) 085 */ 086@SuppressWarnings("serial") 087public class CustomizableRunPane extends JPanel implements CloseListener { 088 089 /** Construct a panel for interacting with the specified Ptolemy II model. 090 * @param model The model to control. 091 * @param xml The XML specification of the layout, or null to use the default. 092 * @exception IllegalActionException If the XML to be parsed has errors. 093 */ 094 public CustomizableRunPane(CompositeActor model, String xml) 095 throws IllegalActionException { 096 super(); 097 098 _model = model; 099 100 // If no XML is specified, then see whether the model has one. 101 if (xml == null) { 102 Attribute layoutAttribute = _model 103 .getAttribute("_runLayoutAttribute"); 104 if (layoutAttribute instanceof ConfigurableAttribute) { 105 try { 106 xml = ((ConfigurableAttribute) layoutAttribute).value(); 107 } catch (IOException e) { 108 throw new InternalErrorException(e); 109 } 110 } 111 } 112 113 if (xml == null) { 114 xml = _defaultLayout(); 115 } 116 // Parse the XML 117 InputStream stream; 118 try { 119 stream = new ByteArrayInputStream(xml.getBytes("UTF-8")); 120 } catch (UnsupportedEncodingException e1) { 121 throw new InternalErrorException(e1); 122 } 123 Document dataDocument = null; 124 try { 125 DocumentBuilderFactory factory = DocumentBuilderFactory 126 .newInstance(); 127 // Without the following, then carriage returns in the XML 128 // mess up the parsing, incredibly enough! 129 factory.setIgnoringElementContentWhitespace(true); 130 DocumentBuilder documentBuilder = factory.newDocumentBuilder(); 131 dataDocument = documentBuilder.parse(stream); 132 } catch (Exception e) { 133 throw new IllegalActionException(model, e, 134 "Unable to parse layout specification."); 135 } 136 Node root = dataDocument.getDocumentElement(); 137 _layoutConstraintsManager = LayoutConstraintsManager 138 .getLayoutConstraintsManager(root); 139 140 ContainerLayout layout = _layoutConstraintsManager.createLayout("top", 141 this); 142 this.setLayout(layout); 143 144 // Walk through the XML, creating an interface as specified in it. 145 // This assumes a very specific structure to the XML. 146 NodeList components = root.getChildNodes(); 147 // Go through the subpanels. 148 for (int i = 0; i < components.getLength(); i++) { 149 Node subpanelNode = components.item(i); 150 if (!subpanelNode.getNodeName().equals("container")) { 151 continue; 152 } 153 String subpanelName = subpanelNode.getAttributes() 154 .getNamedItem("name").getNodeValue(); 155 if (subpanelName.equals("top")) { 156 NodeList nodeList = components.item(i).getChildNodes(); 157 for (int j = 0; j < nodeList.getLength(); j++) { 158 Node node = nodeList.item(j); 159 // Skip over nodes that are not cellconstraints. 160 if (!node.getNodeName().equals("cellconstraints")) { 161 continue; 162 } 163 // The attributes will be null if the node represents the contents text. 164 if (node.getAttributes() != null) { 165 String name = node.getAttributes().getNamedItem("name") 166 .getNodeValue(); 167 _addComponent(name, this); 168 } 169 } 170 } else { 171 if (_subpanels == null) { 172 throw new IllegalActionException(_model, 173 "Panel 'top' is required to be first. Found instead: " 174 + subpanelName); 175 } 176 JPanel panel = _subpanels.get(subpanelName); 177 if (panel == null) { 178 // FIXME: FormsLayout has a bug where if a subpanel that contains 179 // a subpanel is deleted, then the subsubpanel layout is not deleted. 180 try { 181 MessageHandler.warning( 182 "A layout is given for a subpanel named '" 183 + subpanelName 184 + "', but there is no instance of this subpanel."); 185 } catch (CancelException e) { 186 throw new IllegalActionException(_model, "Canceled"); 187 } 188 continue; 189 } 190 NodeList nodeList = subpanelNode.getChildNodes(); 191 for (int j = 0; j < nodeList.getLength(); j++) { 192 Node node = nodeList.item(j); 193 // The attributes will be null if the node represents the contents text. 194 if (node.getAttributes() != null) { 195 String name = node.getAttributes().getNamedItem("name") 196 .getNodeValue(); 197 _addComponent(name, panel); 198 } 199 } 200 } 201 } 202 } 203 204 /////////////////////////////////////////////////////////////////// 205 //// public methods //// 206 207 /** Return the layout constraints manager for this pane. 208 * @return A layout constraints manager. 209 */ 210 public LayoutConstraintsManager getLayoutConstraintsManager() { 211 return _layoutConstraintsManager; 212 } 213 214 /** If the model has a manager and is executing, then 215 * pause execution by calling the pause() method of the manager. 216 * If there is no manager, do nothing. 217 */ 218 public void pauseRun() { 219 Manager manager = _model.getManager(); 220 if (manager != null) { 221 manager.pause(); 222 } 223 } 224 225 /** If the model has a manager and is executing, then 226 * resume execution by calling the resume() method of the manager. 227 * If there is no manager, do nothing. 228 */ 229 public void resumeRun() { 230 Manager manager = _model.getManager(); 231 if (manager != null) { 232 manager.resume(); 233 } 234 } 235 236 /** If the model has a manager and is not already running, 237 * then execute the model in a new thread. Otherwise, do nothing. 238 */ 239 public void startRun() { 240 Manager manager = _model.getManager(); 241 if (manager != null) { 242 try { 243 manager.startRun(); 244 } catch (IllegalActionException ex) { 245 // Model is already running. Ignore. 246 } 247 } 248 } 249 250 /** If the model has a manager and is executing, then 251 * stop execution by calling the stop() method of the manager. 252 * If there is no manager, do nothing. 253 */ 254 public void stopRun() { 255 Manager manager = _model.getManager(); 256 if (manager != null) { 257 manager.stop(); 258 } 259 } 260 261 /** Notify the contained instances of PtolemyQuery that the window 262 * has been closed, and remove all Placeable displays by calling 263 * place(null). This method is called if this pane is contained 264 * within a container that supports such notification. 265 * @param window The window that closed. 266 * @param button The name of the button that was used to close the window. 267 */ 268 @Override 269 public void windowClosed(Window window, String button) { 270 // FIXME: This is not getting invoked. Need to override 271 // TableauFrame above with an override to _close(). 272 // Better yet, should use ModelFrame instead of TableauFrame, 273 // but then ModelPane needs to be converted to an interface 274 // so that this pane can be used instead. 275 if (_model != null) { 276 _closeDisplays(); 277 } 278 } 279 280 /////////////////////////////////////////////////////////////////// 281 //// public variables //// 282 283 /////////////////////////////////////////////////////////////////// 284 //// protected methods //// 285 286 /** Return a component with the specified name. The name is of the form 287 * TYPE:DETAIL, where TYPE defines the type of component to add 288 * and DETAIL specifies details. 289 * @param name The name. 290 * @return A component, or null if the specification is not recognized. 291 * @exception IllegalActionException If there is an error in the name. 292 */ 293 protected Component _getComponent(String name) 294 throws IllegalActionException { 295 // Figure out what type of component to create. 296 if (name.startsWith("Placeable:")) { 297 // Display of an actor that implements the Placeable 298 // interface is given with "Placeable:NAME" where 299 // NAME is the name of the actor relative to the model. 300 String actorName = name.substring(10); 301 ComponentEntity entity = _model.getEntity(actorName); 302 if (!(entity instanceof Placeable 303 || entity instanceof PortablePlaceable)) { 304 throw new IllegalActionException(_model, 305 "Entity that does not implement Placeable is specified in a display."); 306 } 307 // Regrettably, it seems we need an intermediate JPanel. 308 JPanel dummy = new JPanel(); 309 if (entity instanceof Placeable) { 310 ((Placeable) entity).place(dummy); 311 } else if (entity instanceof PortablePlaceable) { 312 ((PortablePlaceable) entity).place(new AWTContainer(dummy)); 313 } 314 return dummy; 315 } else if (name.startsWith("Label")) { 316 // Default is the text after the colon in the name, if 317 // there is one. 318 int colon = name.indexOf(":"); 319 String label = "Label"; 320 if (name.length() > 5 && colon > 4) { 321 label = name.substring(colon + 1); 322 } 323 return new JLabel(label); 324 } else if (name.startsWith("GoButton")) { 325 // Go button is specified with "GoButton", where the label 326 // on the button is given by either the "label" or "text" 327 // Java Bean property, or if there is none, by the default 328 // label "Go". 329 JButton goButton = new JButton("Go"); 330 goButton.setToolTipText("Execute the model"); 331 goButton.addActionListener(new ActionListener() { 332 @Override 333 public void actionPerformed(ActionEvent event) { 334 startRun(); 335 } 336 }); 337 return goButton; 338 } else if (name.startsWith("PauseButton")) { 339 // Button is specified with "PauseButton", where the label 340 // on the button is given by either the "label" or "text" 341 // Java Bean property, or if there is none, by the default 342 // label "Pause". 343 JButton pauseButton = new JButton("Pause"); 344 pauseButton.setToolTipText("Pause the model"); 345 pauseButton.addActionListener(new ActionListener() { 346 @Override 347 public void actionPerformed(ActionEvent event) { 348 pauseRun(); 349 } 350 }); 351 return pauseButton; 352 } else if (name.startsWith("ResumeButton")) { 353 // Button is specified with "ResumeButton", where the label 354 // on the button is given by either the "label" or "text" 355 // Java Bean property, or if there is none, by the default 356 // label "Resume". 357 JButton button = new JButton("Resume"); 358 button.setToolTipText("Resume the model"); 359 button.addActionListener(new ActionListener() { 360 @Override 361 public void actionPerformed(ActionEvent event) { 362 resumeRun(); 363 } 364 }); 365 return button; 366 } else if (name.startsWith("StopButton")) { 367 // Button is specified with "StopButton", where the label 368 // on the button is given by either the "label" or "text" 369 // Java Bean property, or if there is none, by the default 370 // label "Stop". 371 JButton button = new JButton("Stop"); 372 button.setToolTipText("Stop the model"); 373 button.addActionListener(new ActionListener() { 374 @Override 375 public void actionPerformed(ActionEvent event) { 376 stopRun(); 377 } 378 }); 379 return button; 380 } else if (name.equals("ConfigureTopLevel")) { 381 // A parameter editor for top-level parameters 382 return new Configurer(_model); 383 } else if (name.equals("ConfigureDirector")) { 384 // A parameter editor for the director 385 if (_model.getDirector() == null) { 386 throw new IllegalActionException(_model, 387 "Does not have a director. A director is needed to have a contol panel."); 388 } 389 return new Configurer(_model.getDirector()); 390 } else if (name.startsWith("ConfigureEntity:")) { 391 // A parameter editor for an entity is specified with 392 // "ConfigureEntity:NAME", where NAME is the name of 393 // the entity to configure relative to the top level. 394 String entityName = name.substring(16); 395 ComponentEntity entity = _model.getEntity(entityName); 396 if (entity == null) { 397 throw new IllegalActionException(_model, 398 "Nonexistent entity: " + entityName); 399 } 400 return new Configurer(entity); 401 } else if (name.startsWith("Subpanel:")) { 402 // Subpanel is specified with "Subpanel:NAME", where NAME 403 // is the name of the the subpanel. 404 JPanel subpanel = new JPanel(); 405 LayoutManager layout = _layoutConstraintsManager.createLayout(name, 406 subpanel); 407 subpanel.setLayout(layout); 408 if (_subpanels == null) { 409 _subpanels = new HashMap<String, JPanel>(); 410 } 411 _subpanels.put(name, subpanel); 412 return subpanel; 413 } else { 414 // FIXME: When a component is dragged from once 415 // cell to another, the FormsLayout package messes 416 // up and puts in a spurious entry in the XML 417 // with entity name matching the enclosing panel 418 // and layout constraints matching the result of 419 // the drag. Why? 420 try { 421 MessageHandler.warning( 422 "Unrecognized entry in control panel layout: " + name); 423 } catch (CancelException e) { 424 throw new IllegalActionException(_model, "Canceled"); 425 } 426 return null; 427 } 428 } 429 430 /////////////////////////////////////////////////////////////////// 431 //// protected variables //// 432 433 /** The associated model. */ 434 protected CompositeActor _model; 435 436 /////////////////////////////////////////////////////////////////// 437 //// private methods //// 438 439 /** Add a component with the specified name. The name is of the form 440 * TYPE:DETAIL, where TYPE defines the type of component to add 441 * and DETAIL specifies details. 442 * @param name The name. 443 * @param panel The panel into which to add the component. 444 * @exception IllegalActionException If there is an error in the XML. 445 */ 446 private void _addComponent(String name, JPanel panel) 447 throws IllegalActionException { 448 Component component = _getComponent(name); 449 if (component != null) { 450 panel.add(component, name); 451 } 452 } 453 454 /** Close any open displays by calling place(null). 455 */ 456 private void _closeDisplays() { 457 if (_model != null) { 458 Iterator atomicEntities = _model.allAtomicEntityList().iterator(); 459 460 while (atomicEntities.hasNext()) { 461 Object object = atomicEntities.next(); 462 463 if (object instanceof Placeable) { 464 ((Placeable) object).place(null); 465 } else if (object instanceof PortablePlaceable) { 466 ((PortablePlaceable) object).place(null); 467 } 468 } 469 } 470 } 471 472 /** Create a default layout for the associated model. */ 473 private String _defaultLayout() { 474 StringBuffer xml = new StringBuffer( 475 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 476 xml.append("<containers>\n"); 477 478 // Top-level panel has two columns and one row. 479 xml.append("<container name=\"top\" " 480 + "columnSpecs=\"default,3dlu,default:grow\" " 481 + "rowSpecs=\"default\">\n"); 482 xml.append( 483 "<cellconstraints name=\"Subpanel:ControlPanel\" gridX=\"1\" gridY=\"1\" " 484 + "gridWidth=\"1\" gridHeight=\"1\" horizontalAlignment=\"default\" " 485 + "verticalAlignment=\"top\" topInset=\"0\" bottomInset=\"0\" " 486 + "rightInset=\"0\" leftInset=\"0\"/>\n"); 487 xml.append( 488 "<cellconstraints name=\"Subpanel:PlaceablePanel\" gridX=\"3\" gridY=\"1\" " 489 + "gridWidth=\"1\" gridHeight=\"1\" horizontalAlignment=\"default\" " 490 + "verticalAlignment=\"top\" topInset=\"0\" bottomInset=\"0\" " 491 + "rightInset=\"0\" leftInset=\"0\"/>\n"); 492 xml.append("</container>\n"); 493 494 // Subpanel with the run control buttons, the top-level parameters, 495 // and the director parameters. 496 xml.append("<container name=\"Subpanel:ControlPanel\" " 497 + "columnSpecs=\"default\" " 498 + "rowSpecs=\"default,5dlu,default,5dlu,default,5dlu,default,5dlu," 499 + "default,5dlu,default,5dlu,default\">\n"); 500 xml.append( 501 "<cellconstraints name=\"Label:Run Control\" gridX=\"1\" gridY=\"1\" " 502 + "gridWidth=\"1\" gridHeight=\"1\" " 503 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 504 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 505 xml.append( 506 "<cellconstraints name=\"Subpanel:Run Control\" gridX=\"1\" gridY=\"3\" " 507 + "gridWidth=\"1\" gridHeight=\"1\" " 508 + "horizontalAlignment=\"default\" verticalAlignment=\"top\" " 509 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 510 // Place a configurer for the top-level settables here. 511 xml.append( 512 "<cellconstraints name=\"Label:Top-Level Parameters\" gridX=\"1\" gridY=\"5\" " 513 + "gridWidth=\"1\" gridHeight=\"1\" " 514 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 515 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 516 xml.append( 517 "<cellconstraints name=\"ConfigureTopLevel\" gridX=\"1\" gridY=\"7\" " 518 + "gridWidth=\"1\" gridHeight=\"1\" horizontalAlignment=\"default\" " 519 + "verticalAlignment=\"top\" topInset=\"0\" bottomInset=\"0\" " 520 + "rightInset=\"0\" leftInset=\"0\"/>\n"); 521 // Place a configurer for the director settables here. 522 xml.append( 523 "<cellconstraints name=\"Label:Director Parameters\" gridX=\"1\" gridY=\"9\" " 524 + "gridWidth=\"1\" gridHeight=\"1\" " 525 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 526 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 527 xml.append( 528 "<cellconstraints name=\"ConfigureDirector\" gridX=\"1\" gridY=\"11\" " 529 + "gridWidth=\"1\" gridHeight=\"1\" horizontalAlignment=\"default\" " 530 + "verticalAlignment=\"top\" topInset=\"0\" bottomInset=\"0\" " 531 + "rightInset=\"0\" leftInset=\"0\"/>\n"); 532 xml.append("</container>\n"); 533 534 // Subpanel with the run control buttons. 535 xml.append("<container name=\"Subpanel:Run Control\" " 536 + "columnSpecs=\"default,3dlu,default,3dlu,default,3dlu,default\" " 537 + "rowSpecs=\"default\">\n"); 538 xml.append("<cellconstraints name=\"GoButton\" gridX=\"1\" gridY=\"1\" " 539 + "gridWidth=\"1\" gridHeight=\"1\" " 540 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 541 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 542 xml.append( 543 "<cellconstraints name=\"PauseButton\" gridX=\"3\" gridY=\"1\" " 544 + "gridWidth=\"1\" gridHeight=\"1\" " 545 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 546 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 547 xml.append( 548 "<cellconstraints name=\"ResumeButton\" gridX=\"5\" gridY=\"1\" " 549 + "gridWidth=\"1\" gridHeight=\"1\" " 550 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 551 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 552 xml.append( 553 "<cellconstraints name=\"StopButton\" gridX=\"7\" gridY=\"1\" " 554 + "gridWidth=\"1\" gridHeight=\"1\" " 555 + "horizontalAlignment=\"default\" verticalAlignment=\"default\" " 556 + "topInset=\"0\" bottomInset=\"0\" rightInset=\"0\" leftInset=\"0\"/>\n"); 557 xml.append("</container>\n"); 558 559 // Subpanel for each object that implements Placeable. 560 xml.append("<container name=\"Subpanel:PlaceablePanel\" "); 561 xml.append( 562 "columnSpecs=\"default,3dlu,default,3dlu,default,3dlu,default\" "); 563 // FIXME: Need some way to resize plot windows here... 564 xml.append("rowSpecs=\"default"); 565 StringBuffer constraints = new StringBuffer(); 566 if (_model != null) { 567 Iterator atomicEntities = _model.allAtomicEntityList().iterator(); 568 int row = 1; 569 while (atomicEntities.hasNext()) { 570 Object object = atomicEntities.next(); 571 if (object instanceof Placeable 572 || object instanceof PortablePlaceable) { 573 if (row > 1) { 574 xml.append(",5dlu,default"); 575 } 576 constraints.append("<cellconstraints name=\"Placeable:"); 577 constraints.append(((NamedObj) object).getName(_model)); 578 constraints.append("\" gridX=\"3\" gridY=\""); 579 constraints.append(row); 580 constraints.append( 581 "\" gridWidth=\"1\" gridHeight=\"1\" horizontalAlignment=\"default\" " 582 + "verticalAlignment=\"default\" topInset=\"0\" bottomInset=\"0\" " 583 + "rightInset=\"0\" leftInset=\"0\"/>\n"); 584 row = row + 2; 585 } 586 } 587 } 588 // End the row specs. 589 xml.append("\">\n"); 590 // Add the constraints. 591 xml.append(constraints); 592 xml.append("</container>\n"); 593 594 xml.append("</containers>\n"); 595 return xml.toString(); 596 } 597 598 /////////////////////////////////////////////////////////////////// 599 //// private variables //// 600 601 /** The layout constraint manager. */ 602 private LayoutConstraintsManager _layoutConstraintsManager; 603 604 /** A collection of subpanels. */ 605 private HashMap<String, JPanel> _subpanels; 606}