[77] | 1 | package com.framsticks.gui; |
---|
| 2 | |
---|
[88] | 3 | import com.framsticks.core.Entity; |
---|
[84] | 4 | import com.framsticks.core.Instance; |
---|
[77] | 5 | import com.framsticks.core.Path; |
---|
| 6 | import com.framsticks.gui.view.*; |
---|
| 7 | import com.framsticks.gui.view.TreeCellRenderer; |
---|
[84] | 8 | import com.framsticks.util.dispatching.Dispatcher; |
---|
[88] | 9 | import com.framsticks.util.dispatching.JoinableCollection; |
---|
[85] | 10 | import com.framsticks.util.lang.ScopeEnd; |
---|
[84] | 11 | import com.framsticks.util.swing.KeyboardModifier; |
---|
[88] | 12 | import com.framsticks.util.swing.MenuConstructor; |
---|
| 13 | |
---|
[77] | 14 | import org.apache.log4j.Logger; |
---|
| 15 | |
---|
| 16 | import javax.swing.*; |
---|
[84] | 17 | import javax.swing.event.TreeModelEvent; |
---|
| 18 | import javax.swing.event.TreeModelListener; |
---|
[77] | 19 | import javax.swing.event.TreeSelectionEvent; |
---|
| 20 | import javax.swing.event.TreeSelectionListener; |
---|
| 21 | import javax.swing.tree.*; |
---|
[84] | 22 | |
---|
[77] | 23 | import java.awt.*; |
---|
[78] | 24 | import java.awt.datatransfer.StringSelection; |
---|
[84] | 25 | import java.awt.event.*; |
---|
[77] | 26 | import java.util.HashMap; |
---|
| 27 | import java.util.Map; |
---|
[85] | 28 | import com.framsticks.util.dispatching.RunAt; |
---|
[77] | 29 | |
---|
| 30 | /** |
---|
| 31 | * @author Piotr Sniegowski |
---|
| 32 | */ |
---|
[84] | 33 | @SuppressWarnings("serial") |
---|
[88] | 34 | public class Frame extends JoinableCollection<Instance> implements Entity, Dispatcher<Frame> { |
---|
[77] | 35 | |
---|
[84] | 36 | private static final Logger log = Logger.getLogger(Frame.class.getName()); |
---|
[77] | 37 | |
---|
[84] | 38 | protected final Browser browser; |
---|
[77] | 39 | |
---|
[88] | 40 | protected final Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); |
---|
[77] | 41 | |
---|
[88] | 42 | protected CardLayout cardPanelLayout; |
---|
| 43 | protected JPanel cardPanel; |
---|
[77] | 44 | |
---|
[88] | 45 | protected final String title; |
---|
| 46 | protected JFrame swing; |
---|
[84] | 47 | protected JScrollPane treeScrollPane; |
---|
| 48 | protected JTree tree; |
---|
| 49 | protected DefaultTreeModel treeModel; |
---|
| 50 | protected javax.swing.tree.MutableTreeNode rootNode; |
---|
| 51 | //final Instance instance; |
---|
| 52 | protected JPanel treePanel; |
---|
| 53 | protected JPopupMenu treePopupMenu; |
---|
| 54 | protected JMenuItem treePopupMenuHeader; |
---|
[77] | 55 | |
---|
[84] | 56 | TreeNode currentlyPoppedTreeNode; |
---|
| 57 | protected JLabel statusBar; |
---|
| 58 | protected JPanel mainPanel; |
---|
| 59 | protected JPanel leftPanel; |
---|
| 60 | protected JPanel normalWorkPanel; |
---|
| 61 | protected CardLayout mainPanelLayout; |
---|
[77] | 62 | |
---|
[84] | 63 | protected JMenuBar menuBar; |
---|
| 64 | protected JMenu fileMenu; |
---|
| 65 | protected JMenu editMenu; |
---|
| 66 | protected JMenu viewMenu; |
---|
| 67 | protected JMenu windowMenu; |
---|
| 68 | protected JMenu helpMenu; |
---|
[77] | 69 | |
---|
[88] | 70 | protected final Map<Instance, InstanceAtFrame> instancesAtFrames = new HashMap<Instance, InstanceAtFrame>(); |
---|
[77] | 71 | |
---|
[84] | 72 | public Frame(String title, Browser browser) { |
---|
[88] | 73 | this.title = title; |
---|
| 74 | this.browser = browser; |
---|
| 75 | } |
---|
| 76 | |
---|
| 77 | public void configure() { |
---|
| 78 | swing = new JFrame(title); |
---|
| 79 | swing.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); |
---|
| 80 | swing.addWindowListener(new WindowAdapter() { |
---|
| 81 | @Override |
---|
| 82 | public void windowClosing(WindowEvent e) { |
---|
| 83 | log.info("received closing"); |
---|
| 84 | joinableFinish(); |
---|
| 85 | } |
---|
| 86 | }); |
---|
[84] | 87 | /** this is done to remove the current value label from above the slider, |
---|
| 88 | * because it cannot put to work properly with floating-point value sliders, |
---|
| 89 | * nor it can be removed in normal way through JSlider methods */ |
---|
| 90 | UIManager.put("Slider.paintValue", false); |
---|
| 91 | log.debug("creating " + this); |
---|
[77] | 92 | |
---|
[88] | 93 | statusBar = new JLabel("not connected"); |
---|
[77] | 94 | |
---|
[88] | 95 | Container contentPane = swing.getContentPane(); |
---|
[84] | 96 | contentPane.setLayout(new BorderLayout()); |
---|
[77] | 97 | |
---|
[84] | 98 | treePopupMenu = new JPopupMenu("title"); |
---|
| 99 | treePopupMenu.setName("popup"); |
---|
| 100 | treePopupMenuHeader = new JMenuItem(); |
---|
| 101 | treePopupMenuHeader.setForeground(Color.BLUE); |
---|
| 102 | treePopupMenu.add(treePopupMenuHeader); |
---|
| 103 | treePopupMenu.addSeparator(); |
---|
| 104 | //TODO: add to favourites |
---|
| 105 | //TODO: remove from favourites |
---|
| 106 | //TODO: open in new window as root |
---|
| 107 | //TODO: refresh |
---|
| 108 | //TODO: open in console |
---|
[77] | 109 | |
---|
[84] | 110 | treePopupMenu.add(new JMenuItem("Refresh")); |
---|
| 111 | treePopupMenu.add(new JMenuItem("Open in new frame as root")); |
---|
[88] | 112 | addNodeActionToTreePopupMenu("Copy path to clipboard", new NodeAction() { |
---|
| 113 | @Override |
---|
| 114 | public void actionPerformed(TreeNode treeNode) { |
---|
| 115 | Path path = treeNode.getInstancePath(); |
---|
| 116 | StringSelection selection = new StringSelection(path.toString()); |
---|
| 117 | swing.getToolkit().getSystemClipboard().setContents(selection, selection); |
---|
| 118 | } |
---|
| 119 | }); |
---|
[84] | 120 | //this.add(createMenuItem("Add to favourites", null)); |
---|
| 121 | //this.add(createMenuItem("Remove from favourites", null)); |
---|
| 122 | |
---|
| 123 | treePanel = new JPanel(); |
---|
| 124 | treePanel.setLayout(new BorderLayout()); |
---|
| 125 | |
---|
| 126 | treeModel = new DefaultTreeModel(null); |
---|
| 127 | treeModel.addTreeModelListener(new TreeModelListener() { |
---|
| 128 | |
---|
[78] | 129 | @Override |
---|
[84] | 130 | public void treeNodesChanged(TreeModelEvent arg0) { |
---|
| 131 | log.trace("treeNodesChanged: " + arg0); |
---|
[78] | 132 | } |
---|
[77] | 133 | |
---|
[84] | 134 | @Override |
---|
| 135 | public void treeNodesInserted(TreeModelEvent arg0) { |
---|
| 136 | // log.trace("treeNodesInserted: " + arg0); |
---|
| 137 | } |
---|
[77] | 138 | |
---|
[84] | 139 | @Override |
---|
| 140 | public void treeNodesRemoved(TreeModelEvent arg0) { |
---|
| 141 | log.trace("treeNodesRemoved: " + arg0); |
---|
| 142 | } |
---|
[77] | 143 | |
---|
[84] | 144 | @Override |
---|
| 145 | public void treeStructureChanged(TreeModelEvent arg0) { |
---|
| 146 | log.trace("treeStructureChanged: " + arg0); |
---|
| 147 | } |
---|
| 148 | }); |
---|
[77] | 149 | |
---|
[84] | 150 | tree = new JTree(treeModel); |
---|
| 151 | tree.setName("tree"); |
---|
| 152 | tree.setRootVisible(false); |
---|
| 153 | tree.setExpandsSelectedPaths(true); |
---|
| 154 | tree.setSelectionModel(new DefaultTreeSelectionModel()); |
---|
| 155 | ToolTipManager.sharedInstance().registerComponent(tree); |
---|
[77] | 156 | |
---|
[84] | 157 | tree.addTreeSelectionListener(new TreeSelectionListener() { |
---|
| 158 | @Override |
---|
| 159 | public void valueChanged(TreeSelectionEvent e) { |
---|
| 160 | chooseTreeNode(e.getNewLeadSelectionPath()); |
---|
| 161 | } |
---|
| 162 | }); |
---|
[77] | 163 | |
---|
[84] | 164 | tree.setExpandsSelectedPaths(true); |
---|
| 165 | tree.setEditable(false); |
---|
| 166 | tree.setDoubleBuffered(true); |
---|
| 167 | tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); |
---|
| 168 | tree.setShowsRootHandles(true); |
---|
| 169 | tree.setRowHeight(26); |
---|
| 170 | tree.setDoubleBuffered(true); |
---|
| 171 | tree.addMouseListener(new MouseAdapter() { |
---|
| 172 | @Override |
---|
| 173 | public void mousePressed(MouseEvent e) { |
---|
| 174 | assert isActive(); |
---|
| 175 | showPopup(e); |
---|
| 176 | } |
---|
[77] | 177 | |
---|
[84] | 178 | @Override |
---|
| 179 | public void mouseReleased(MouseEvent e) { |
---|
| 180 | assert isActive(); |
---|
| 181 | showPopup(e); |
---|
| 182 | } |
---|
| 183 | }); |
---|
[77] | 184 | |
---|
[88] | 185 | new KeyboardModifier(tree, JComponent.WHEN_FOCUSED).join(KeyStroke.getKeyStroke('h'), KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)).join(KeyStroke.getKeyStroke('j'), KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0)).join(KeyStroke.getKeyStroke('k'), KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0)).join(KeyStroke.getKeyStroke('l'), KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0)); |
---|
[77] | 186 | |
---|
[84] | 187 | tree.setCellRenderer(new TreeCellRenderer()); |
---|
[77] | 188 | |
---|
[84] | 189 | treeScrollPane = new JScrollPane(tree); |
---|
| 190 | treeScrollPane.setBorder(BorderFactory.createEmptyBorder()); |
---|
[77] | 191 | |
---|
[84] | 192 | treePanel.add(treeScrollPane); |
---|
[77] | 193 | |
---|
[84] | 194 | rootNode = new DefaultMutableTreeNode(); |
---|
| 195 | rootNode.setUserObject("root"); |
---|
| 196 | treeModel.setRoot(rootNode); |
---|
[77] | 197 | |
---|
[84] | 198 | normalWorkPanel = new JPanel(); |
---|
| 199 | normalWorkPanel.setLayout(new BorderLayout()); |
---|
| 200 | normalWorkPanel.setName("browser"); |
---|
[77] | 201 | |
---|
[84] | 202 | mainPanel = new JPanel(); |
---|
| 203 | mainPanel.setName("main"); |
---|
| 204 | mainPanelLayout = new CardLayout(); |
---|
| 205 | mainPanel.setLayout(mainPanelLayout); |
---|
| 206 | mainPanel.add(normalWorkPanel, "browser"); |
---|
[77] | 207 | |
---|
[84] | 208 | menuBar = new JMenuBar(); |
---|
[77] | 209 | |
---|
[84] | 210 | fileMenu = menuBar.add(new JMenu("File")); |
---|
| 211 | editMenu = menuBar.add(new JMenu("Edit")); |
---|
| 212 | viewMenu = menuBar.add(new JMenu("View")); |
---|
| 213 | windowMenu = menuBar.add(new JMenu("Window")); |
---|
| 214 | helpMenu = menuBar.add(new JMenu("Help")); |
---|
[77] | 215 | |
---|
[84] | 216 | contentPane.add(menuBar, BorderLayout.NORTH); |
---|
| 217 | contentPane.add(mainPanel, BorderLayout.CENTER); |
---|
| 218 | contentPane.add(statusBar, BorderLayout.SOUTH); |
---|
[77] | 219 | |
---|
[84] | 220 | leftPanel = new JPanel(); |
---|
| 221 | leftPanel.setLayout(new BorderLayout()); |
---|
| 222 | //leftPanel.add(new ViewerTest(), BorderLayout.PAGE_START); |
---|
[88] | 223 | // JPanel leftTopPanel = createLeftTopPanel(); |
---|
| 224 | // if (leftTopPanel != null) { |
---|
| 225 | // leftPanel.add(leftTopPanel, BorderLayout.PAGE_START); |
---|
| 226 | // } |
---|
[84] | 227 | leftPanel.add(treePanel, BorderLayout.CENTER); |
---|
| 228 | leftPanel.setBackground(Color.WHITE); |
---|
| 229 | leftPanel.setForeground(Color.WHITE); |
---|
[77] | 230 | |
---|
[88] | 231 | cardPanel = new JPanel(); |
---|
[84] | 232 | cardPanel.setName("card"); |
---|
| 233 | JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, cardPanel); |
---|
| 234 | split.setPreferredSize(browser.defaultFrameDimension); |
---|
| 235 | split.setMaximumSize(screenDimension); |
---|
| 236 | split.setOneTouchExpandable(true); |
---|
| 237 | split.setDividerLocation(250); |
---|
| 238 | split.setDividerSize(5); |
---|
| 239 | split.setName("split"); |
---|
[77] | 240 | |
---|
[84] | 241 | normalWorkPanel.add(split); |
---|
[77] | 242 | |
---|
[84] | 243 | //this.setVisible(true); |
---|
| 244 | mainPanelLayout.show(mainPanel, "browser"); |
---|
[77] | 245 | |
---|
[88] | 246 | cardPanelLayout = new CardLayout(); |
---|
[84] | 247 | cardPanel.setLayout(cardPanelLayout); |
---|
[77] | 248 | |
---|
[88] | 249 | swing.pack(); |
---|
[84] | 250 | tree.requestFocusInWindow(); |
---|
[77] | 251 | |
---|
[88] | 252 | log.debug("frame configured: " + this); |
---|
[77] | 253 | |
---|
[88] | 254 | new MenuConstructor(fileMenu).add(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), new AbstractAction("Close") { |
---|
| 255 | @Override |
---|
| 256 | public void actionPerformed(ActionEvent actionEvent) { |
---|
| 257 | interrupt(); |
---|
| 258 | } |
---|
| 259 | }); |
---|
| 260 | |
---|
[84] | 261 | } |
---|
[77] | 262 | |
---|
[84] | 263 | protected JPanel createLeftTopPanel() { |
---|
| 264 | return null; |
---|
| 265 | } |
---|
[77] | 266 | |
---|
[88] | 267 | public void addRootPath(Path path) { |
---|
[84] | 268 | assert isActive(); |
---|
[88] | 269 | Instance instance = path.getInstance(); |
---|
| 270 | assert browser.getInstances().containsValue(instance); |
---|
[77] | 271 | |
---|
[88] | 272 | InstanceAtFrame e = new InstanceAtFrame(instance, this); |
---|
| 273 | instance.addListener(e); |
---|
| 274 | instancesAtFrames.put(instance, e); |
---|
[84] | 275 | TreeNode node = new TreeNode(e, path); |
---|
| 276 | e.rootTreeNode = node; |
---|
| 277 | treeModel.insertNodeInto(node, rootNode, rootNode.getChildCount()); |
---|
| 278 | tree.expandPath(new TreePath(rootNode)); |
---|
| 279 | } |
---|
[77] | 280 | |
---|
[84] | 281 | @Override |
---|
| 282 | public boolean isActive() { |
---|
| 283 | return SwingDispatcher.instance.isActive(); |
---|
| 284 | } |
---|
[77] | 285 | |
---|
[84] | 286 | @Override |
---|
[85] | 287 | public void invokeLater(RunAt<? extends Frame> runnable) { |
---|
| 288 | SwingDispatcher.getInstance().invokeLater(runnable); |
---|
[84] | 289 | } |
---|
[77] | 290 | |
---|
[84] | 291 | public Action addActionToTreePopupMenu(Action action) { |
---|
| 292 | assert isActive(); |
---|
| 293 | treePopupMenu.add(action); |
---|
| 294 | return action; |
---|
| 295 | } |
---|
[77] | 296 | |
---|
[84] | 297 | public Action addNodeActionToTreePopupMenu(String title, final NodeAction nodeAction) { |
---|
| 298 | assert isActive(); |
---|
| 299 | return addActionToTreePopupMenu(new AbstractAction(title) { |
---|
| 300 | @Override |
---|
| 301 | public void actionPerformed(ActionEvent e) { |
---|
| 302 | TreeNode treeNode = getCurrentlyPoppedTreeNode(); |
---|
| 303 | if (treeNode == null) { |
---|
| 304 | return; |
---|
| 305 | } |
---|
| 306 | nodeAction.actionPerformed(treeNode); |
---|
| 307 | } |
---|
| 308 | }); |
---|
| 309 | } |
---|
[77] | 310 | |
---|
[84] | 311 | public void showPanel(Panel panel) { |
---|
| 312 | assert isActive(); |
---|
| 313 | assert panel != null; |
---|
| 314 | cardPanelLayout.show(cardPanel, panel.getUniqueName()); |
---|
| 315 | } |
---|
[77] | 316 | |
---|
[84] | 317 | private void showPopup(MouseEvent e) { |
---|
| 318 | assert isActive(); |
---|
| 319 | if (!e.isPopupTrigger()) { |
---|
| 320 | return; |
---|
| 321 | } |
---|
| 322 | currentlyPoppedTreeNode = findTreeNodeByTreePath(tree.getPathForLocation(e.getX(), e.getY())); |
---|
| 323 | if (currentlyPoppedTreeNode == null) { |
---|
| 324 | return; |
---|
| 325 | } |
---|
| 326 | treePopupMenu.show(e.getComponent(), e.getX(), e.getY()); |
---|
| 327 | //currentlyPoppedPanel.getNode().getFramsClass().getName() |
---|
| 328 | //treePopupMenuHeader.setText(currentlyPoppedTreeNode.getNode().getPath()); |
---|
| 329 | } |
---|
[77] | 330 | |
---|
[88] | 331 | /** |
---|
| 332 | * @return the swing |
---|
| 333 | */ |
---|
| 334 | public JFrame getSwing() { |
---|
| 335 | return swing; |
---|
| 336 | } |
---|
| 337 | |
---|
[84] | 338 | public TreeNode getCurrentlyPoppedTreeNode() { |
---|
| 339 | assert isActive(); |
---|
| 340 | return currentlyPoppedTreeNode; |
---|
| 341 | } |
---|
[77] | 342 | |
---|
[84] | 343 | public void clear() { |
---|
| 344 | treeModel.setRoot(null); |
---|
| 345 | cardPanel.removeAll(); |
---|
| 346 | cardPanel.updateUI(); |
---|
| 347 | tree.setEnabled(false); |
---|
| 348 | } |
---|
[77] | 349 | |
---|
[85] | 350 | public ScopeEnd startChange(final DefaultMutableTreeNode node) { |
---|
[84] | 351 | assert isActive(); |
---|
| 352 | final TreePath selection = tree.getSelectionPath(); |
---|
[85] | 353 | return new ScopeEnd() { |
---|
[84] | 354 | @Override |
---|
[85] | 355 | public void close() { |
---|
[84] | 356 | assert isActive(); |
---|
| 357 | treeModel.nodeChanged(node); |
---|
| 358 | tree.setSelectionPath(selection); |
---|
| 359 | } |
---|
| 360 | }; |
---|
| 361 | } |
---|
| 362 | |
---|
| 363 | public void selectTreeNode(final TreeNode treeNode) { |
---|
| 364 | assert isActive(); |
---|
[88] | 365 | /* final Panel panel = treeNode.getOrCreatePanel(); |
---|
| 366 | if (panel == null) { |
---|
| 367 | return; |
---|
| 368 | } |
---|
| 369 | panel.setCurrentTreeNode(treeNode); |
---|
| 370 | treeNode.updateData(); |
---|
| 371 | showPanel(panel);*/ |
---|
[84] | 372 | } |
---|
[77] | 373 | |
---|
[84] | 374 | public TreeNode findTreeNodeByTreePath(TreePath treePath) { |
---|
| 375 | assert isActive(); |
---|
| 376 | if (treePath == null) { |
---|
| 377 | return null; |
---|
| 378 | } |
---|
| 379 | if (!(treePath.getLastPathComponent() instanceof TreeNode)) { |
---|
| 380 | return null; |
---|
| 381 | } |
---|
[88] | 382 | return (TreeNode) treePath.getLastPathComponent(); |
---|
[84] | 383 | } |
---|
[77] | 384 | |
---|
[84] | 385 | public void chooseTreeNode(TreePath treePath) { |
---|
| 386 | assert isActive(); |
---|
| 387 | final TreeNode treeNode = findTreeNodeByTreePath(treePath); |
---|
| 388 | if (treeNode == null) { |
---|
| 389 | return; |
---|
| 390 | } |
---|
| 391 | treeNode.select(); |
---|
| 392 | } |
---|
[77] | 393 | |
---|
[84] | 394 | public void goTo(Path path) { |
---|
| 395 | assert isActive(); |
---|
[88] | 396 | final TreePath treePath = instancesAtFrames.get(path.getInstance()).getTreePath(path, false); |
---|
[84] | 397 | log.info("go to path: " + path + "(" + treePath + ")"); |
---|
[85] | 398 | |
---|
| 399 | new RunAt<Frame>(this) { |
---|
[84] | 400 | @Override |
---|
| 401 | public void run() { |
---|
[85] | 402 | log.info("executed"); |
---|
[84] | 403 | tree.setSelectionPath(treePath); |
---|
| 404 | tree.makeVisible(treePath); |
---|
| 405 | assert tree.isVisible(treePath); |
---|
| 406 | } |
---|
[85] | 407 | }; |
---|
[77] | 408 | |
---|
[84] | 409 | } |
---|
[77] | 410 | |
---|
[84] | 411 | public void addNode(TreeNode child, DefaultMutableTreeNode parent) { |
---|
| 412 | assert isActive(); |
---|
[85] | 413 | |
---|
| 414 | try (ScopeEnd e = startChange(parent)) { |
---|
| 415 | treeModel.insertNodeInto(child, parent, parent.getChildCount()); |
---|
| 416 | } |
---|
[84] | 417 | } |
---|
| 418 | |
---|
| 419 | @Override |
---|
| 420 | public String toString() { |
---|
[88] | 421 | return title + "@" + browser.getName(); |
---|
[84] | 422 | } |
---|
| 423 | |
---|
[88] | 424 | @Override |
---|
| 425 | protected void joinableInterrupt() { |
---|
| 426 | assert isActive(); |
---|
| 427 | super.joinableInterrupt(); |
---|
| 428 | |
---|
| 429 | invokeLater(new RunAt<Frame>() { |
---|
| 430 | @Override |
---|
| 431 | public void run() { |
---|
| 432 | finish(); |
---|
| 433 | } |
---|
| 434 | }); |
---|
| 435 | } |
---|
| 436 | |
---|
| 437 | @Override |
---|
| 438 | protected void joinableFinish() { |
---|
| 439 | assert isActive(); |
---|
| 440 | log.debug("disposing frame " + this); |
---|
| 441 | swing.dispose(); |
---|
| 442 | } |
---|
| 443 | |
---|
| 444 | // @Override |
---|
| 445 | // public boolean isDone() { |
---|
| 446 | // return super.isDone() && !swing.isDisplayable(); |
---|
| 447 | // } |
---|
| 448 | |
---|
| 449 | |
---|
| 450 | @Override |
---|
| 451 | public String getName() { |
---|
| 452 | return title; |
---|
| 453 | } |
---|
| 454 | |
---|
| 455 | |
---|
[77] | 456 | } |
---|