1 /* 2 * $Id$ 3 * 4 * Copyright (c) 1996, 2011, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.interview.wizard; 28 29 import java.awt.BorderLayout; 30 import java.awt.Color; 31 import java.awt.Component; 32 import java.awt.Dimension; 33 import java.awt.Font; 34 import java.awt.Graphics; 35 import java.awt.Point; 36 import java.awt.Rectangle; 37 import java.awt.Toolkit; 38 import java.awt.image.BufferedImage; 39 import java.awt.event.ActionEvent; 40 import java.awt.event.ActionListener; 41 import java.awt.event.MouseEvent; 42 import java.awt.event.MouseListener; 43 import java.awt.event.KeyEvent; 44 import java.awt.datatransfer.StringSelection; 45 import java.awt.datatransfer.Transferable; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Set; 49 import java.util.Vector; 50 import javax.accessibility.AccessibleContext; 51 import javax.swing.AbstractListModel; 52 import javax.swing.Icon; 53 import javax.swing.ListCellRenderer; 54 import javax.swing.ListSelectionModel; 55 import javax.swing.JCheckBoxMenuItem; 56 import javax.swing.JComponent; 57 import javax.swing.JLabel; 58 import javax.swing.JList; 59 import javax.swing.JMenu; 60 import javax.swing.JMenuItem; 61 import javax.swing.JPanel; 62 import javax.swing.JPopupMenu; 63 import javax.swing.JViewport; 64 import javax.swing.KeyStroke; 65 import javax.swing.Scrollable; 66 import javax.swing.UIManager; 67 import javax.swing.event.AncestorEvent; 68 import javax.swing.event.AncestorListener; 69 import javax.swing.event.ChangeEvent; 70 import javax.swing.event.ChangeListener; 71 import javax.swing.event.ListSelectionEvent; 72 import javax.swing.event.ListSelectionListener; 73 import javax.swing.event.MenuEvent; 74 import javax.swing.event.MenuListener; 75 import javax.swing.event.PopupMenuEvent; 76 import javax.swing.event.PopupMenuListener; 77 import javax.swing.TransferHandler; 78 79 import com.sun.interview.ErrorQuestion; 80 import com.sun.interview.FinalQuestion; 81 import com.sun.interview.Interview; 82 import com.sun.interview.NullQuestion; 83 import com.sun.interview.Question; 84 85 class PathPanel extends JPanel 86 implements Scrollable 87 { 88 public PathPanel(QuestionPanel questionPanel, Interview interview) { 89 this.questionPanel = questionPanel; //uugh; but needed for autosaving answers before changing questions 90 this.interview = interview; 91 moreText = i18n.getString("path.more"); 92 initGUI(); 93 } 94 95 // ---------- Component stuff --------------------------------------- 96 97 public Dimension getPreferredSize() { 98 return list.getPreferredSize(); // should not be necessary 99 } 100 101 // ---------- Scrollable stuff --------------------------------------- 102 103 public Dimension getPreferredScrollableViewportSize() { 104 return list.getPreferredScrollableViewportSize(); 105 } 106 107 public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { 108 return list.getScrollableBlockIncrement(visibleRect, orientation, direction); 109 } 110 111 public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { 112 return list.getScrollableUnitIncrement(visibleRect, orientation, direction); 113 } 114 115 public boolean getScrollableTracksViewportHeight() { 116 if (getParent() instanceof JViewport) { 117 return getParent().getHeight() > getPreferredSize().height; 118 } 119 return false; 120 } 121 122 public boolean getScrollableTracksViewportWidth() { 123 return true; 124 } 125 126 // ---------- end of Scrollable stuff ----------------------------------- 127 128 boolean getMarkersEnabled() { 129 return markersEnabled; 130 } 131 132 void setMarkersEnabled(boolean on) { 133 if (on != markersEnabled) { 134 markersEnabled = on; 135 pathList.update(); 136 } 137 } 138 139 boolean getMarkersFilterEnabled() { 140 return markersFilterEnabled; 141 } 142 143 void setMarkersFilterEnabled(boolean on) { 144 if (on != markersFilterEnabled) { 145 markersFilterEnabled = on; 146 pathList.update(); 147 } 148 } 149 150 Question getNextVisible() { 151 return pathList.getNextVisible(); 152 } 153 154 Question getPrevVisible() { 155 return pathList.getPrevVisible(); 156 } 157 158 Question getLastVisible() { 159 return pathList.getLastVisible(); 160 } 161 162 JMenu getMarkerMenu() { 163 return createMenu(); 164 } 165 166 private void initGUI() { 167 setName("path"); 168 setFocusable(false); 169 setLayout(new BorderLayout()); 170 pathList = new PathList(); 171 list = new JList<>(pathList); 172 setInfo(list, "path.list", true); 173 list.setCellRenderer(pathList); 174 KeyStroke enterKey = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); 175 list.registerKeyboardAction(pathList, enterKey, JComponent.WHEN_FOCUSED); 176 list.addListSelectionListener(pathList); 177 list.addMouseListener(pathList); 178 //list.setPrototypeCellValue("What is a good default to use?"); 179 180 // would be better if this were configurable 181 list.setFixedCellWidth(2 * DOTS_PER_INCH); 182 183 list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 184 list.setBackground(new Color(255, 255, 255, 0)); 185 list.setOpaque(false); 186 list.setVisibleRowCount(5); 187 list.setTransferHandler(new TransferHandler() { 188 @Override 189 public int getSourceActions(JComponent c) { 190 return COPY; 191 } 192 @Override 193 public Transferable createTransferable(JComponent c) { 194 Object selected = list.getSelectedValue(); 195 if(selected != null) { 196 if(selected instanceof Question) 197 return new StringSelection(((Question)selected).getSummary()); 198 else if(selected instanceof List) { 199 StringBuffer temp = new StringBuffer(); 200 for(Question q: (List<Question>)selected) { 201 temp.append(q.getSummary()); 202 temp.append("\n"); 203 } 204 return new StringSelection(temp.toString()); 205 } else if(selected instanceof String) { 206 return new StringSelection(selected.toString()); 207 } 208 } 209 return null; 210 } 211 }); 212 213 pathList.currentQuestionChanged(interview.getCurrentQuestion()); 214 addAncestorListener(pathList); 215 add(list); 216 } 217 218 private void setInfo(JComponent jc, String uiKey, boolean addToolTip) { 219 jc.setName(uiKey); 220 AccessibleContext ac = jc.getAccessibleContext(); 221 ac.setAccessibleName(i18n.getString(uiKey + ".name")); 222 if (addToolTip) { 223 String tip = i18n.getString(uiKey + ".tip"); 224 jc.setToolTipText(tip); 225 ac.setAccessibleDescription(tip); 226 } 227 else 228 ac.setAccessibleDescription(i18n.getString(uiKey + ".desc")); 229 } 230 231 private QuestionPanel questionPanel; 232 private Interview interview; 233 private PathList pathList; 234 private JList<Object> list; 235 private String moreText; 236 237 // client parameters 238 private boolean markersEnabled; 239 private boolean markersFilterEnabled; 240 private String markerName = null; // theoretically settable 241 242 private static final I18NResourceBundle i18n = I18NResourceBundle.getDefaultBundle(); 243 private static Color INVALID_VALUE_COLOR = i18n.getErrorColor(); 244 private static final int DOTS_PER_INCH = Toolkit.getDefaultToolkit().getScreenResolution(); 245 246 private class PathList 247 extends AbstractListModel<Object> 248 implements ActionListener, AncestorListener, 249 ListCellRenderer<Object>, ListSelectionListener, 250 MouseListener, 251 Interview.Observer 252 { 253 //----- navigation support for WizPane ----------------------- 254 255 Question getNextVisible() { 256 for (int i = currIndex + 1; i < currEntries.length; i++) { 257 Object e = currEntries[i]; 258 if (e instanceof Question) 259 return ((Question) e); 260 } 261 return null; 262 } 263 264 Question getPrevVisible() { 265 for (int i = currIndex - 1; i >= 0; i--) { 266 Object e = currEntries[i]; 267 if (e instanceof Question) 268 return ((Question) e); 269 } 270 return null; 271 } 272 273 Question getLastVisible() { 274 for (int i = currEntries.length - 1; i >= 0; i--) { 275 Object e = currEntries[i]; 276 if (e instanceof Question) 277 return ((Question) e); 278 } 279 return null; 280 } 281 282 //----- state support for menus ----------------------------- 283 284 boolean isQuestionVisible(Question q) { 285 for (Object e : currEntries) { 286 if (e instanceof Question && e == q) 287 return true; 288 else if (e instanceof List && ((List<?>) e).contains(q)) 289 return false; 290 } 291 return false; 292 } 293 294 boolean isQuestionAutoOpened(Question q) { 295 // only return true if the preceding marked question is in the autoOpen set 296 boolean autoOpened = autoOpenSet.contains(null); 297 for (int i = 0; i < currEntries.length; i++) { 298 Object e = currEntries[i]; 299 if (e instanceof Question) { 300 Question qe = (Question) e; 301 if (qe.hasMarker(markerName)) { 302 if (qe == q) 303 return false; 304 autoOpened = autoOpenSet.contains(qe); 305 } 306 else if (qe == q) 307 return autoOpened; 308 } 309 else if (e instanceof List && ((List<?>) e).contains(q)) 310 return false; 311 } 312 return false; 313 } 314 315 //----- actions ------------------------ 316 317 void markCurrentQuestion() { 318 setQuestionMarked(interview.getCurrentQuestion(), true); 319 } 320 321 void unmarkCurrentQuestion() { 322 setQuestionMarked(interview.getCurrentQuestion(), false); 323 } 324 325 private void setQuestionMarked(Question q, boolean on) { 326 questionPanel.saveCurrentResponse(); 327 if (on) 328 q.addMarker(markerName); 329 else 330 q.removeMarker(markerName); 331 332 pathList.update(q); 333 } 334 335 void openCurrentEntry() { 336 openEntry(currIndex); 337 } 338 339 void openEntry(int index) { 340 Object o = currEntries[index]; 341 342 // only a list can be opened 343 if (!(o instanceof List)) 344 return; 345 346 // the marker question for a List is the question in the preceding entry 347 // find that marker question and add it to the autoOpenSet 348 for (int i = 1; i < currEntries.length; i++) { 349 if (currEntries[i] == o) { 350 Object m = currEntries[i - 1]; 351 if (m instanceof Question) 352 autoOpenSet.add((Question) m); 353 update(); 354 } 355 } 356 } 357 358 void closeCurrentEntry() { 359 closeEntry(currIndex); 360 } 361 362 void closeEntry(int index) { 363 Object o = currEntries[index]; 364 365 // only a question can be closed 366 if (!(o instanceof Question)) 367 return; 368 369 // need to figure out the autoOpenSet entry 370 // scan back through entries looking for a marked question 371 // or the first question 372 Question marker = null; 373 for (int i = index; i >= 0; i--) { 374 Object ei = currEntries[i]; 375 if (ei instanceof Question) { 376 Question qi = (Question) ei; 377 if (i == 0 || qi.hasMarker(markerName)) { 378 marker = qi; 379 break; 380 } 381 } 382 } 383 384 // can't close the marker question itself: 385 // must close a question after the marker 386 if (marker == o) 387 return; 388 389 autoOpenSet.remove(marker); 390 update(); 391 } 392 393 //----- from AbstractListModel ----------- 394 395 public int getSize() { 396 return (currEntries == null ? 0 : currEntries.length); 397 } 398 399 public Object getElementAt(int index) { 400 return (index < currEntries.length ? currEntries[index] : null); 401 } 402 403 //----- from ListCellRenderer ----------- 404 405 private JLabel sample = new JLabel() { 406 public Dimension getMaximumSize() { 407 return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); 408 } 409 }; 410 411 public Component getListCellRendererComponent(JList<?> list, Object o, int index, boolean isSelected, boolean cellHasFocus) { 412 if (o instanceof Question) { 413 Question q = (Question)o; 414 Font f; 415 String s; 416 Color c; 417 Color bg = null; // null is default 418 if (q instanceof ErrorQuestion) { 419 f = list.getFont().deriveFont(Font.BOLD); 420 s = " " + q.getSummary(); 421 c = INVALID_VALUE_COLOR; 422 } 423 else if (q instanceof NullQuestion) { 424 int level = ((NullQuestion)q).getLevel(); 425 426 switch (level) { 427 case NullQuestion.LEVEL_NONE: 428 f = list.getFont(); 429 s = " " + q.getSummary(); 430 c = list.getForeground(); 431 bg = null; 432 break; 433 case NullQuestion.LEVEL_1: 434 f = list.getFont().deriveFont(Font.BOLD, 435 list.getFont().getSize() +3); 436 s = q.getSummary(); 437 c = new Color(0x63,0x82,0xBF); 438 bg = new Color(0xDD,0xDD,0xDD); 439 break; 440 case NullQuestion.LEVEL_2: 441 f = list.getFont().deriveFont(Font.BOLD); 442 s = q.getSummary(); 443 c = new Color(0x63,0x82,0xBF); 444 break; 445 case NullQuestion.LEVEL_3: 446 f = list.getFont().deriveFont(Font.PLAIN); 447 s = " " + q.getSummary(); 448 c = list.getForeground(); 449 break; 450 default: // LEVEL_LEGACY handled here 451 f = list.getFont().deriveFont(Font.BOLD); 452 s = " " + q.getSummary(); 453 c = list.getForeground(); 454 } // switch 455 } 456 else { 457 f = list.getFont().deriveFont(Font.PLAIN); 458 s = " " + q.getSummary(); 459 c = list.getForeground(); 460 } 461 sample.setText(s); 462 sample.setFont(f); 463 sample.setForeground(c); 464 if (bg != null) 465 sample.setBackground(bg); 466 else 467 sample.setBackground(list.getBackground()); 468 469 if (markersEnabled) 470 sample.setIcon(q.hasMarker(markerName) ? markerIcon : noMarkerIcon); 471 else 472 sample.setIcon(null); 473 } 474 else if (o instanceof List) { 475 sample.setText(null); 476 sample.setFont(list.getFont()); 477 sample.setForeground(list.getForeground()); 478 sample.setIcon(ellipsisIcon); 479 } 480 else if (o instanceof String) { 481 // prototype value or more... 482 sample.setText(" " + o); 483 sample.setFont(list.getFont().deriveFont(Font.ITALIC)); 484 sample.setForeground(list.getForeground()); 485 sample.setIcon(markersEnabled ? noMarkerIcon : null); 486 } 487 else 488 throw new IllegalArgumentException(); 489 490 // rest of method based on javax.swing.DefaultListCellRenderer 491 if (isSelected) { 492 sample.setBackground(list.getSelectionBackground()); 493 //sample.setForeground(list.getSelectionForeground()); 494 } 495 else { 496 //sample.setBackground(list.getBackground()); 497 //sample.setForeground(list.getForeground()); 498 } 499 sample.setOpaque(true); 500 sample.setEnabled(list.isEnabled()); 501 sample.setBorder((cellHasFocus) ? UIManager.getBorder("List.focusCellHighlightBorder") : null); 502 503 return sample; 504 } 505 506 //----- from ActionListener ----------- 507 508 // invoked by keyboard "enter" 509 public void actionPerformed(ActionEvent e) { 510 //System.err.println("PP.actionPerformed"); 511 JList<?> list = (JList<?>)(e.getSource()); 512 Object o = list.getSelectedValue(); 513 if (o != null && o instanceof Question) { 514 Question q = (Question)o; 515 if (q == interview.getCurrentQuestion()) 516 return; 517 518 //System.err.println("PP.actionPerformed saveCurrentResponse"); 519 questionPanel.saveCurrentResponse(); 520 521 try { 522 //System.err.println("PP.actionPerformed setCurrentQuestion"); 523 interview.setCurrentQuestion(q); 524 } 525 catch (Interview.Fault ex) { 526 // ignore, should never happen; FLW 527 } 528 } 529 } 530 531 //----- from ListSelectionListener ----------- 532 533 // invoked by mouse selection (or by list.setSelectedXXX ??) 534 public void valueChanged(ListSelectionEvent e) { 535 JList<?> list = (JList<?>) (e.getSource()); 536 Object o = list.getSelectedValue(); 537 if (o == null) 538 return; 539 540 // make sure the interview's current question is synchronized with 541 // the list selection 542 if (o instanceof Question) { 543 Question q = (Question) o; 544 if (q == interview.getCurrentQuestion()) 545 return; 546 547 questionPanel.saveCurrentResponse(); 548 549 try { 550 interview.setCurrentQuestion(q); 551 } 552 catch (Interview.Fault ex) { 553 // ignore, should never happen; FLW 554 } 555 } 556 else if (o instanceof List) { 557 List<?> l = (List<?>) o; 558 if (l.contains(interview.getCurrentQuestion())) 559 return; 560 561 questionPanel.saveCurrentResponse(); 562 563 try { 564 Question q = (Question) (l.get(0)); 565 interview.setCurrentQuestion(q); 566 } 567 catch (Interview.Fault ex) { 568 // ignore, should never happen; FLW 569 } 570 571 } 572 else { 573 // if the user tries to select the More string, 574 // reject the request by resetting to the currIndex 575 list.setSelectedIndex(currIndex); 576 } 577 } 578 579 // ---------- from AncestorListener ----------- 580 581 public void ancestorAdded(AncestorEvent e) { 582 interview.addObserver(this); 583 pathUpdated(); 584 } 585 586 public void ancestorMoved(AncestorEvent e) { } 587 588 public void ancestorRemoved(AncestorEvent e) { 589 interview.removeObserver(this); 590 } 591 592 //----- from MouseListener ----------- 593 594 public void mouseEntered(MouseEvent e) { } 595 596 public void mouseExited(MouseEvent e) { } 597 598 public void mousePressed(MouseEvent e) { 599 if (markersEnabled && e.isPopupTrigger() && isOverSelection(e)) 600 showPopupMenu(e); 601 } 602 603 public void mouseReleased(MouseEvent e) { 604 if (markersEnabled && e.isPopupTrigger() && isOverSelection(e)) 605 showPopupMenu(e); 606 } 607 608 private boolean isOverSelection(MouseEvent e) { 609 JList<?> l = (JList<?>) (e.getComponent()); 610 Rectangle r = l.getCellBounds(currIndex, currIndex); 611 return (r.contains(e.getX(), e.getY())); 612 } 613 614 private void showPopupMenu(MouseEvent e) { 615 if (popupMenu == null) 616 popupMenu = createPopupMenu(); 617 popupMenu.show(e.getComponent(), e.getX(), e.getY()); 618 } 619 620 public void mouseClicked(MouseEvent e) { 621 if (!markersEnabled) 622 return; 623 624 Point p = e.getPoint(); 625 int index = list.locationToIndex(p); 626 if (index == -1) 627 return; 628 Object entry = currEntries[index]; 629 630 switch (e.getClickCount()) { 631 case 1: 632 if (p.x < markerIcon.getIconWidth()) { 633 if (entry instanceof Question) { 634 Question q = (Question) entry; 635 setQuestionMarked(q, !q.hasMarker(markerName)); 636 } 637 } 638 break; 639 640 case 2: 641 if (markersFilterEnabled) { 642 if (entry instanceof List) 643 openEntry(index); 644 else 645 closeEntry(index); 646 } 647 break; 648 } 649 } 650 651 //----- from Interview.Observer ----------- 652 653 public void pathUpdated() { 654 update(interview.getPath(), interview.getCurrentQuestion()); 655 } 656 657 public void currentQuestionChanged(Question q) { 658 int prevIndex = currIndex; 659 currQuestion = q; 660 for (int i = 0; i < currEntries.length; i++) { 661 Object o = currEntries[i]; 662 if (o == q || (o instanceof List && ((List<?>) o).contains(q))) { 663 currIndex = i; 664 break; 665 } 666 } 667 fireContentsChanged(this, prevIndex, currIndex); 668 669 list.setSelectedIndex(currIndex); 670 list.ensureIndexIsVisible(currIndex); 671 } 672 673 public void finished() { 674 } 675 676 void update() { 677 update(currPath, currQuestion); 678 } 679 680 void update(Question q) { 681 if (markersFilterEnabled) 682 update(); 683 else { 684 for (int i = 0; i < currEntries.length; i++) { 685 Object e = currEntries[i]; 686 if (e == q) { 687 currMarks[i] = (markersEnabled && q.hasMarker(markerName)); 688 fireContentsChanged(this, i, i); 689 break; 690 } 691 } 692 } 693 } 694 695 private void update(Question[] newPath, Question newCurrQuestion) { 696 boolean oldEnabled = currEnabled; 697 Object[] oldEntries = currEntries; 698 boolean[] oldMarks = currMarks; 699 700 if (!markersFilterEnabled) 701 autoOpenSet.clear(); 702 703 Object[] newEntries = getEntries(newPath); 704 boolean[] newMarks = new boolean[newEntries.length]; 705 if (markersEnabled) { 706 for (int i = 0; i < newEntries.length; i++) { 707 newMarks[i] = (newEntries[i] instanceof Question 708 && ((Question) newEntries[i]).hasMarker(markerName)); 709 } 710 } 711 712 currPath = newPath; 713 currEntries = newEntries; 714 currEnabled = markersEnabled; 715 currMarks = newMarks; 716 717 int shorterEntriesLength = Math.min(oldEntries.length, newEntries.length); 718 719 int firstDiff = 0; 720 if (currEnabled == oldEnabled) { 721 // optimize firstDiff when icons are not changing 722 while (firstDiff < shorterEntriesLength) { 723 Object oldObj = oldEntries[firstDiff]; 724 boolean oldMark = oldMarks[firstDiff]; 725 Object newObj = newEntries[firstDiff]; 726 boolean newMark = newMarks[firstDiff]; 727 if (oldObj instanceof Question ? oldObj == newObj && oldMark == newMark 728 : oldObj instanceof List ? newObj instanceof List 729 : false ) { 730 firstDiff++; 731 } 732 else 733 break; 734 } 735 } 736 737 if (firstDiff != oldEntries.length || firstDiff != newEntries.length) { 738 if (firstDiff != shorterEntriesLength) { 739 //System.err.println("PP.update: change[" + firstDiff + "," + (shorterEntriesLength-1) + "/" + oldEntries.length + "," + newEntries.length + "]" + interview); 740 fireContentsChanged(this, firstDiff, shorterEntriesLength-1); 741 } 742 743 if (shorterEntriesLength != oldEntries.length) { 744 //System.err.println("PP.update: remove[" + shorterEntriesLength + "," + (oldEntries.length-1) + "/" + oldEntries.length + "," + newEntries.length + "]" + interview); 745 fireIntervalRemoved(this, shorterEntriesLength, oldEntries.length-1); 746 } 747 if (shorterEntriesLength != newEntries.length) { 748 //System.err.println("PP.update: add[" + shorterEntriesLength + "," + (newEntries.length-1) + "/" + oldEntries.length + "," + newEntries.length + "]" + interview); 749 fireIntervalAdded(this, shorterEntriesLength, newEntries.length-1); 750 } 751 } 752 753 currQuestion = newCurrQuestion; 754 for (int i = 0; i < currEntries.length; i++) { 755 Object o = currEntries[i]; 756 if (o == currQuestion 757 || (o instanceof List && ((List<?>) o).contains(currQuestion))) { 758 currIndex = i; 759 break; 760 } 761 } 762 763 list.setSelectedIndex(currIndex); 764 list.ensureIndexIsVisible(currIndex); 765 //System.err.println("PP.update: sel:" + currIndex + " " + currQuestion); 766 } 767 768 private Object[] getEntries(Question[] path) { 769 if (path.length == 0) // transient startup condition 770 return path; 771 772 Question last = path[path.length - 1]; 773 boolean needMore = !(last instanceof ErrorQuestion || last instanceof FinalQuestion); 774 // quick check to see if we can simply use the path as is 775 if ( (!markersEnabled || !markersFilterEnabled) && !needMore) 776 return path; 777 778 Vector<Object> v = new Vector<>(); 779 Question lastMarker = null; 780 for (int i = 0; i < path.length; i++) { 781 Question q = path[i]; 782 if (!markersEnabled || !markersFilterEnabled) { 783 v.add(q); 784 } 785 else if (q.hasMarker(markerName) 786 || i == 0 787 || (i == path.length - 1 && q instanceof FinalQuestion)) { 788 lastMarker = q; 789 v.add(q); 790 } 791 else if (autoOpenSet.contains(lastMarker)) { 792 v.add(q); 793 } 794 else { 795 List<Question> l; 796 Object o = v.lastElement(); 797 if (o == null || o instanceof Question) { 798 l = new Vector<>(); 799 v.add(l); 800 } 801 else 802 l = (List<Question>) o; 803 l.add(q); 804 } 805 } 806 807 // auto-expand the final section if it doesn't end in FinalQuestion 808 if (!(last instanceof FinalQuestion) && v.lastElement() instanceof List) { 809 List<?> l = (List<?>) (v.lastElement()); 810 v.setSize(v.size() - 1); 811 v.addAll(l); 812 } 813 814 if (needMore) 815 v.add(moreText); 816 817 Object[] a = new Object[v.size()]; 818 v.copyInto(a); 819 return a; 820 } 821 822 823 824 // currPath and currQuestion give info as obtained from the interview 825 private Question[] currPath = new Question[0]; 826 private Question currQuestion; 827 828 // currEntries and currIndex give displayable entries 829 // entries may be Question, List<Question>, or String 830 private Object[] currEntries = new Object[0]; 831 private int currIndex; 832 833 // currEnabled gives state of markersEnabled as used to construct list 834 private boolean currEnabled; 835 836 // currMarks gives which questions are currently showing a mark 837 private boolean[] currMarks; 838 839 // autoOpenSet gives which non-markered questions should be displayed 840 private Set<Question> autoOpenSet = new HashSet<>(); 841 842 private Icon markerIcon = new MarkerIcon(true); 843 private Icon noMarkerIcon = new MarkerIcon(false); 844 private Icon ellipsisIcon = new EllipsisIcon(); 845 846 private JPopupMenu popupMenu; 847 848 } 849 850 //----------------------------------------------------------------------- 851 852 private JMenu createMenu() { 853 return (JMenu) (new Menu(Menu.JMENU).getComponent()); 854 } 855 856 private JPopupMenu createPopupMenu() { 857 return (JPopupMenu) (new Menu(Menu.JPOPUPMENU).getComponent()); 858 } 859 860 private class Menu 861 implements ActionListener, ChangeListener, MenuListener, PopupMenuListener 862 { 863 864 static final int JMENU = 0, JPOPUPMENU = 1; 865 866 Menu(int type) { 867 this.type = type; 868 869 // check box items (JMenu only) 870 if (type == JMENU) { 871 enableItem = createCheckBoxItem(ENABLE); 872 filterItem = createCheckBoxItem(FILTER); 873 } 874 875 // question items (JMenu and JPopupMenu) 876 markItem = createItem(MARK); 877 unmarkItem = createItem(UNMARK); 878 clearItem = createItem(CLEAR); 879 880 // group items (JMenu and JPopupMenu) 881 openGroupItem = createItem(OPEN_GROUP); 882 closeGroupItem = createItem(CLOSE_GROUP); 883 884 // interview items (JMenu only) 885 if (type == JMENU) { 886 clearMarkedItem = createItem(CLEAR_MARKED); 887 removeAllItem = createItem(REMOVE_MARKERS); 888 } 889 890 if (type == JMENU) { 891 JMenu m = new JMenu(i18n.getString("path.mark.menu")); 892 m.setName("path.mark.menu"); 893 m.getAccessibleContext().setAccessibleDescription(i18n.getString("path.mark.desc")); 894 int mne = i18n.getString("path.mark.mne").charAt(0); 895 m.setMnemonic(mne); 896 m.add(enableItem); 897 m.add(filterItem); 898 m.addSeparator(); 899 m.add(markItem); 900 m.add(unmarkItem); 901 m.add(clearItem); 902 m.addSeparator(); 903 m.add(openGroupItem); 904 m.add(closeGroupItem); 905 m.addSeparator(); 906 m.add(clearMarkedItem); 907 m.add(removeAllItem); 908 m.addMenuListener(this); 909 comp = m; 910 } 911 else { 912 JPopupMenu m = new JPopupMenu(); 913 m.add(markItem); 914 m.add(unmarkItem); 915 m.add(clearItem); 916 // don't put a separator because often all the items above 917 // or all the items below will not be visible 918 m.add(openGroupItem); 919 m.add(closeGroupItem); 920 m.addPopupMenuListener(this); 921 comp = m; 922 } 923 } 924 925 JComponent getComponent() { 926 return comp; 927 } 928 929 private JMenuItem createItem(String name) { 930 JMenuItem mi = new JMenuItem(i18n.getString("path.mark." + name + ".mit")); 931 mi.setName(name); 932 mi.setActionCommand(name); 933 mi.addActionListener(this); 934 setMnemonic(mi, name); 935 return mi; 936 } 937 938 private JCheckBoxMenuItem createCheckBoxItem(String name) { 939 JCheckBoxMenuItem mi = new JCheckBoxMenuItem(i18n.getString("path.mark." + name + ".ckb")); 940 mi.setName(name); 941 mi.addChangeListener(this); 942 setMnemonic(mi, name); 943 return mi; 944 } 945 946 private void updateItems() { 947 948 Question q = interview.getCurrentQuestion(); 949 950 boolean marked = q.hasMarker(markerName); 951 boolean visible = pathList.isQuestionVisible(q); 952 boolean autoOpened = pathList.isQuestionAutoOpened(q); 953 954 if (type == JMENU) { 955 // rules for a menu-bar menu: 956 // keep things more visible, but disable as necessary 957 958 enableItem.setSelected(markersEnabled); 959 960 filterItem.setSelected(markersFilterEnabled); 961 filterItem.setEnabled(markersEnabled); 962 963 markItem.setVisible(!marked); 964 markItem.setEnabled(markersEnabled); 965 966 unmarkItem.setVisible(marked); 967 unmarkItem.setEnabled(markersEnabled); 968 969 clearItem.setVisible(true); 970 clearItem.setEnabled(markersEnabled && !(q instanceof NullQuestion)); 971 972 openGroupItem.setVisible(!markersFilterEnabled || !visible); 973 openGroupItem.setEnabled(markersEnabled && markersFilterEnabled); 974 975 closeGroupItem.setVisible(markersFilterEnabled && visible); 976 closeGroupItem.setEnabled(markersEnabled && markersFilterEnabled); 977 978 clearMarkedItem.setVisible(true); 979 clearMarkedItem.setEnabled(markersEnabled); 980 981 removeAllItem.setVisible(true); 982 removeAllItem.setEnabled(markersEnabled); 983 } 984 else { 985 // rules for a popup menu: 986 // hide inappropriate items, but always enabled 987 // note popup menu only active if markersEnabled 988 989 markItem.setVisible(visible && !marked); 990 991 unmarkItem.setVisible(visible && marked); 992 993 clearItem.setVisible(visible && !(q instanceof NullQuestion)); 994 995 openGroupItem.setVisible(markersFilterEnabled && !visible); 996 closeGroupItem.setVisible(markersFilterEnabled && autoOpened); 997 } 998 } 999 1000 private void setMnemonic(JMenuItem mi, String name) { 1001 int mne = i18n.getString("path.mark." + name + ".mne").charAt(0); 1002 mi.setMnemonic(mne); 1003 } 1004 1005 // ---------- from ActionListener ----------- 1006 1007 public void actionPerformed(ActionEvent e) { 1008 String cmd = e.getActionCommand(); 1009 if (cmd.equals(MARK)) { 1010 pathList.markCurrentQuestion(); 1011 } 1012 else if (cmd.equals(UNMARK)) { 1013 pathList.unmarkCurrentQuestion(); 1014 } 1015 else if (cmd.equals(CLEAR)) { 1016 Question q = interview.getCurrentQuestion(); 1017 q.clear(); 1018 // have to redisplay question explicitly, 1019 // because there is no notification that the 1020 // value of the current question has been changed. 1021 questionPanel.showQuestion(q); 1022 } 1023 else if (cmd.equals(OPEN_GROUP)) { 1024 pathList.openCurrentEntry(); 1025 } 1026 else if (cmd.equals(CLOSE_GROUP)) { 1027 pathList.closeCurrentEntry(); 1028 } 1029 else if (cmd.equals(CLEAR_MARKED)) { 1030 Question q = interview.getCurrentQuestion(); 1031 interview.clearMarkedResponses(markerName); 1032 // If the previously current question is still current, 1033 // we need to redisplay it to make sure that it shows 1034 // the updated value 1035 if (q == interview.getCurrentQuestion()) 1036 questionPanel.showQuestion(q); 1037 } 1038 else if (cmd.equals(REMOVE_MARKERS)) { 1039 // show a confirm dialog? 1040 interview.removeMarkers(markerName); 1041 if (getMarkersFilterEnabled() == true) 1042 setMarkersFilterEnabled(false); // will cause update 1043 else 1044 pathList.update(); 1045 } 1046 } 1047 1048 // ---------- from ChangeListener ----------- 1049 1050 public void stateChanged(ChangeEvent e) { 1051 Object src = e.getSource(); 1052 if (src == enableItem) { 1053 questionPanel.saveCurrentResponse(); 1054 boolean on = enableItem.isSelected(); 1055 setMarkersEnabled(on); 1056 } 1057 else if (src == filterItem) { 1058 questionPanel.saveCurrentResponse(); 1059 boolean on = filterItem.isSelected(); 1060 setMarkersFilterEnabled(on); 1061 } 1062 } 1063 1064 // ---------- from MenuListener ----------- 1065 1066 public void menuSelected(MenuEvent e) { 1067 updateItems(); 1068 } 1069 1070 public void menuDeselected(MenuEvent e) { 1071 } 1072 1073 public void menuCanceled(MenuEvent e) { 1074 } 1075 1076 // ---------- from PopupMenuListener ----------- 1077 1078 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 1079 updateItems(); 1080 } 1081 1082 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 1083 } 1084 1085 public void popupMenuCanceled(PopupMenuEvent e) { 1086 } 1087 1088 private int type; 1089 private JComponent comp; 1090 private JCheckBoxMenuItem enableItem; 1091 private JCheckBoxMenuItem filterItem; 1092 private JMenuItem markItem; 1093 private JMenuItem unmarkItem; 1094 private JMenuItem clearItem; 1095 private JMenuItem openGroupItem; 1096 private JMenuItem closeGroupItem; 1097 private JMenuItem clearMarkedItem; 1098 private JMenuItem removeAllItem; 1099 1100 private static final String ENABLE = "enable"; 1101 private static final String FILTER = "filter"; 1102 private static final String MARK = "mark"; 1103 private static final String UNMARK = "unmark"; 1104 private static final String CLEAR = "clear"; 1105 private static final String OPEN_GROUP = "open"; 1106 private static final String CLOSE_GROUP = "close"; 1107 1108 private static final String CLEAR_MARKED = "clearMarked"; 1109 private static final String REMOVE_MARKERS = "remove"; 1110 } 1111 1112 //----------------------------------------------------------------------- 1113 1114 private static class MarkerIcon implements Icon 1115 { 1116 MarkerIcon(boolean on) { 1117 this.on = on; 1118 } 1119 1120 public int getIconWidth() { 1121 return iconWidth; 1122 } 1123 1124 public int getIconHeight() { 1125 return iconHeight; 1126 } 1127 1128 public void paintIcon(Component c, Graphics g, int x, int y) { 1129 if (on) { 1130 if (image == null) { 1131 image = new BufferedImage(getIconWidth(), getIconHeight(), 1132 BufferedImage.TYPE_INT_ARGB); 1133 paintMe(image); 1134 } 1135 g.drawImage(image, x, y, null); 1136 } 1137 } 1138 1139 private void paintMe(BufferedImage image) { 1140 Graphics g = image.getGraphics(); 1141 1142 int x0 = 0; 1143 int y0 = 0; 1144 1145 int x1 = Math.min(iconWidth, iconHeight); 1146 int y1 = x1; 1147 1148 int[] xx = { 1149 x0 + iconIndent, 1150 x1, 1151 x1, 1152 x1 - iconIndent, 1153 x0, 1154 x0 + iconIndent, 1155 }; 1156 1157 int[] yy = { 1158 y0, 1159 y1 - iconIndent, 1160 y1, 1161 y1, 1162 y0 + iconIndent, 1163 y0 + iconIndent 1164 }; 1165 1166 g.setColor(new Color(102, 102, 153));//g.setColor(MetalLookAndFeel.getPrimaryControlDarkShadow()); 1167 g.fillPolygon(xx, yy, xx.length); 1168 } 1169 1170 private boolean on; 1171 private BufferedImage image; 1172 private static final int iconWidth = 8; 1173 private static final int iconHeight = 16; 1174 private static final int iconIndent = 3; 1175 } 1176 1177 //----------------------------------------------------------------------- 1178 1179 private static class EllipsisIcon implements Icon 1180 { 1181 public int getIconWidth() { 1182 return iconWidth; 1183 } 1184 1185 public int getIconHeight() { 1186 return iconHeight; 1187 } 1188 1189 public void paintIcon(Component c, Graphics g, int x, int y) { 1190 if (image == null) { 1191 image = new BufferedImage(getIconWidth(), getIconHeight(), 1192 BufferedImage.TYPE_INT_ARGB); 1193 paintMe(image); 1194 } 1195 g.drawImage(image, x, y, null); 1196 } 1197 1198 private void paintMe(BufferedImage image) { 1199 Graphics g = image.getGraphics(); 1200 g.setColor(Color.black); 1201 1202 for (int iy = 0; iy < dotHeight; iy++) { 1203 int y = (iconHeight - dotHeight) / 2 + iy; 1204 for (int ix = 0; ix < dots; ix++) { 1205 int x = dotIndent + ix * (dotWidth + dotSep); 1206 g.drawLine(x, y, x + dotWidth - 1, y); 1207 } 1208 } 1209 } 1210 1211 private BufferedImage image; 1212 private static final int iconWidth = 48; 1213 private static final int iconHeight = 6; 1214 private static final int dots = 3; 1215 private static final int dotWidth = 2; 1216 private static final int dotHeight = 1; 1217 private static final int dotSep = 4; 1218 private static final int dotIndent = 20; 1219 } 1220 1221 }