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 }