示例#1
0
  public byte[] bytesForContentItem(int index) {
    Integer offset = contentOffset(index);
    Integer total = contentNumBytes(index);

    if (offset == null || total == null) return null;

    return Utility.subArray(bytes, offset, total);
  }
  private void footerJvmAction() {
    long jvmUsed = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
    long jvmMax = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax();

    double jvmFrac = ((double) jvmUsed) / ((double) jvmMax);
    display.jvmAvailLabel.setText(Utility.progressString(progressSize, jvmFrac));
    //
    display.footerExtraLabel.setText(
        String.format(
            "   %d logs, %sused, %smax",
            AllGroups.getLogCount(),
            Utility.byteString(jvmUsed, true, true, false, false),
            Utility.byteString(jvmMax, true, true, false, false)));

    // Debug.print("jvm %d %s", jvmFrac,
    // String.format("%d logs, %s used memory, %s max",
    // AllGroups.getLogCount(), Utility.byteString(jvmUsed, true, true,
    // false, false),
    // Utility.byteString(jvmMax, true, true, false, false))
    // );
  }
  private void footerDiskAction() {
    long diskSpace;
    long diskUsed;
    try {
      diskSpace = usedFileStore.getTotalSpace();
      diskUsed = diskSpace - usedFileStore.getUnallocatedSpace();
    } catch (IOException e) {
      e.printStackTrace();
      throw new Error(e);
    }

    double diskFrac = ((double) diskUsed) / ((double) diskSpace);
    display.diskAvailLabel.setText(Utility.progressString(progressSize, diskFrac));
  }
  private void setupFooter() {
    try {
      usedFileStore = Files.getFileStore(ToolSettings.NBITES_DIR_PATH);
    } catch (Exception e) {
      e.printStackTrace();
      ;
      throw new Error(e);
    }

    Debug.warn("Tool footer using fileStore: %s", usedFileStore.name());

    display.diskAvailLabel.setText(Utility.progressString(progressSize, 0.5));
    display.jvmAvailLabel.setText(Utility.progressString(progressSize, 0.5));

    footerJvmTimer =
        new Timer(
            1000, // ms
            new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                footerJvmAction();
              }
            });

    footerJvmTimer.start();

    footerDiskTimer =
        new Timer(
            1000,
            new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                footerDiskAction();
              }
            });
    footerDiskTimer.start();
  }
示例#5
0
public class Group {

  private final long unique_id = Utility.getNextIndex(this);

  public final ArrayList<LogReference> logs = new ArrayList<>();

  public void add(LogReference... logReferences) {
    this.add(Arrays.asList(logReferences));
  }

  /* this method takes ownership of the logrefs.  if you do not wish this, access logs directly */
  public void add(List<LogReference> logReferences) {
    synchronized (logs) {
      for (LogReference lr : logReferences) {
        logs.add(lr);
        lr.container = this;
      }
    }
  }

  public void remove(LogReference... logReferences) {
    this.remove(Arrays.asList(logReferences));
  }

  public void remove(List<LogReference> logReferences) {
    synchronized (logs) {
      for (LogReference lr : logReferences) {
        if (logs.remove(lr)) {
          lr.container = null;
        }
      }
    }
  }

  public enum GroupSource {
    UNDEFINED,
    ROBOT_STREAM,
    FILESYSTEM
  };

  public static final GroupSource UNDEFINED = GroupSource.UNDEFINED;
  public static final GroupSource ROBOT_STREAM = GroupSource.ROBOT_STREAM;
  public static final GroupSource FILESYSTEM = GroupSource.FILESYSTEM;

  public final GroupSource source;
  private final String info;

  public String getGroupSourceAddress() {
    return source == ROBOT_STREAM ? info : null;
  }

  public Path getGroupSourcePath() {
    return source == FILESYSTEM ? Paths.get(info) : null;
  }

  public String getGroupInfo() {
    return info;
  }

  private Group(GroupSource src, String info) {
    this.source = src;
    this.info = info;
  }

  public static Group groupForStream(String robotAddress) {
    Group ret = new Group(ROBOT_STREAM, robotAddress);
    AllGroups.add(ret);
    return ret;
  }

  public static Group groupFromPath(Path path) {
    Group ret = new Group(FILESYSTEM, path.toString());
    AllGroups.add(ret);
    return ret;
  }

  public static Group groupNamed(String name) {
    Group ret = new Group(UNDEFINED, name);
    AllGroups.add(ret);
    return ret;
  }

  @Override
  public String toString() {
    return String.format(
        "Group{i%d,#%d}(source:%s, info:%s)", unique_id, logs.size(), source.toString(), info);
  }

  public String guiString() {
    return String.format(
        "group %d: (%d logs) source: %s: %s", unique_id, logs.size(), source, info);
  }
}
public class ToolDisplayHandler
    implements IOFirstResponder, Events.LogsFound, Events.LogRefsFound, Events.GroupAdded {

  private final long id = Utility.getNextIndex(this);
  private final ToolDisplayHandler outerThis = this;

  private static final Debug.DebugSettings debug = Debug.createSettings(Debug.INFO);

  @Override
  public String toString() {
    return this.getClass().getSimpleName() + "-" + id;
  }

  protected ToolDisplayHandler() {
    ToolMessage.displayInfo("creating new display and handler: %s", this);
    display = new ToolDisplay();
    listener = new TitleListener();

    setupUtilitiesTab();
    setupControlTab();
    setupLogsTab();
    setupLogDisplay();
    setupFooter();

    Center.listen(Events.LogsFound.class, this, true);
    Center.listen(Events.LogRefsFound.class, this, true);
    Center.listen(Events.GroupAdded.class, this, true);

    final String boundsKey = this.toString();
    Center.listen(
        new NBToolShutdownListener() {
          @Override
          public void nbtoolShutdownCallback() {
            DisplaySettings end =
                new DisplaySettings(
                    display.getBounds(), viewProfile, display.topLevelSplit.getDividerLocation());

            UserSettings.BOUNDS_MAP.put(boundsKey, end);
          }
        });

    DisplaySettings ds = UserSettings.BOUNDS_MAP.get(boundsKey);
    if (ds != null) {
      display.setBounds(ds.bounds);
      display.topLevelSplit.setDividerLocation(ds.splitLocation);
      viewProfile = ds.profile == null ? ViewProfile.DEFAULT_PROFILE : ds.profile;
    }

    display.setTitle("nbtool v" + ToolSettings.VERSION + "." + ToolSettings.MINOR_VERSION);
    display.setMinimumSize(MIN_SIZE);

    if (id == 0) {
      display.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    } else {
      display.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
    }

    setupKeyBindings();

    display.topLevelSplit.setContinuousLayout(false);
    display.topLevelSplit.setAutoscrolls(false);

    Dimension min = ToolSettings.DEFAULT_BOUNDS.getSize();
    min.width = 0;

    display.leftSideTabs.setMinimumSize(min);
    display.displayTabs.setMinimumSize(min);

    display.topLevelSplit.requestFocus();
  }

  public void show(boolean vis) {
    display.setVisible(vis);
  }

  public boolean isVisible() {
    return display.isVisible();
  }

  // ^^ EXTERNAL
  // ------------------------------------------
  // vv INTERNAL

  private static final Dimension MIN_SIZE = new Dimension(800, 600);

  private final ToolDisplay display;
  private RobotConnection robot = null;
  private Group lastGroup = null;
  private ViewProfile viewProfile = ViewProfile.DEFAULT_PROFILE;

  private static <T> void updateComboBoxAndSettings(JComboBox<T> box, OrderedSet<T> set, T newest) {
    if (newest != null) set.update(newest);
    box.setModel(new DefaultComboBoxModel<T>(set.vector()));

    if (newest != null) box.setSelectedItem(newest);
    else if (set.vector().isEmpty()) {
      box.setSelectedIndex(-1);
    } else {
      box.setSelectedIndex(0);
    }
  }

  private void setupKeyBindings() {

    AbstractAction switchTabsAction =
        new AbstractAction() {
          @Override
          public void actionPerformed(ActionEvent e) {
            char key = e.getActionCommand().charAt(0);
            if (Character.isDigit(key)) {
              int n = (Character.getNumericValue(key) - 1);
              if (n >= 0 && n < display.displayTabs.getTabCount()) {
                display.displayTabs.setSelectedIndex(n);
              }

              return;
            }
          }
        };

    AbstractAction loadAction =
        new AbstractAction() {
          @Override
          public void actionPerformed(ActionEvent e) {
            if (display.leftSideTabs.getSelectedComponent() == display.controlTab)
              display.loadButton.doClick();
          }
        };

    AbstractAction toggleLeftSideAction =
        new AbstractAction() {
          @Override
          public void actionPerformed(ActionEvent e) {
            if (display.topLevelSplit.getDividerLocation() > 10) {
              display.topLevelSplit.setDividerLocation(0);
            } else {
              display.topLevelSplit.setDividerLocation(0.5);
              display.topLevelSplit.requestFocus();
            }
          }
        };

    display.topLevelSplit.getActionMap().put("switchTabsAction", switchTabsAction);
    display.topLevelSplit.getActionMap().put("loadAction", loadAction);
    display.topLevelSplit.getActionMap().put("toggleLeftSideAction", toggleLeftSideAction);

    for (char c = '1'; c <= '9'; ++c) {
      display
          .topLevelSplit
          .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
          .put(KeyStroke.getKeyStroke(c), "switchTabsAction");
    }

    display
        .topLevelSplit
        .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "loadAction");

    display
        .topLevelSplit
        .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
        .put(KeyStroke.getKeyStroke('`'), "toggleLeftSideAction");
  }

  private void setupViewProfileBox() {
    Collection<ViewProfile> set = ViewProfile.PROFILES.values();
    display.viewProfileBox.setModel(new DefaultComboBoxModel<>(set.toArray(new ViewProfile[0])));

    if (set.contains(viewProfile)) {
      display.viewProfileBox.setSelectedItem(viewProfile);
    } else {
      ViewProfile def = ViewProfile.DEFAULT_PROFILE;
      display.viewProfileBox.setSelectedItem(def);
      viewProfile = def;
    }
  }

  private void setupUtilitiesTab() {
    display.debugLevelBox.setModel(
        new DefaultComboBoxModel<Debug.LogLevel>(Debug.LogLevel.values()));
    display.debugLevelBox.setEditable(false);
    display.debugLevelBox.setSelectedIndex(
        Arrays.asList(Debug.LogLevel.values()).indexOf(Debug.level));
    display.debugLevelBox.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            int sel = display.debugLevelBox.getSelectedIndex();
            Debug.level = Debug.LogLevel.values()[sel];
            UserSettings.logLevel = Debug.level;

            Debug.print("changed Debug.level to: %s", Debug.level);
          }
        });

    setupViewProfileBox();

    display.viewProfileBox.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            ViewProfile prof = (ViewProfile) display.viewProfileBox.getSelectedItem();
            if (prof != null) {
              viewProfile = prof;
            }
          }
        });

    Center.listen(
        Events.ViewProfileSetChanged.class,
        new ViewProfileSetChanged() {
          @Override
          public void viewProfileSetChanged(Object src) {
            setupViewProfileBox();
          }
        },
        true);

    display.venueField.setText(UserSettings.venue);

    display.venueField.addKeyListener(
        new KeyAdapter() {
          @Override
          public void keyTyped(KeyEvent e) {
            UserSettings.venue = display.venueField.getText();
          }
        });

    JPanel utilityHolder = new JPanel();
    // utilityHolder.setLayout(new BoxLayout(utilityHolder,
    // BoxLayout.Y_AXIS));
    utilityHolder.setLayout(new GridLayout(0, 1));
    for (UtilityParent up : UtilityManager.utilities) {
      utilityHolder.add(new UtilityPanel(up));
    }
    // utilityHolder.add(Box.createVerticalGlue());

    display.utilityScrollPane.setViewportView(utilityHolder);
  }

  private void controlSelectAction() {
    Path selected = PathChooser.chooseDirPath(display, null);
    if (selected != null) {
      if (FileIO.isValidLogFolder(selected)) {
        updateComboBoxAndSettings(display.pathBox, UserSettings.loadPathes, selected);
      } else {
        ToolMessage.displayError("invalid path to logs: %s", selected);
      }
    }
  }

  private void controlLoadAction() {
    if (robot == null) {
      Path selected = (Path) display.pathBox.getSelectedItem();
      if (selected == null) {
        Debug.error("null path");
        ToolMessage.display("load action: null path", Color.RED);
        return;
      }

      if (FileIO.isValidLogFolder(selected)) {
        updateComboBoxAndSettings(display.pathBox, UserSettings.loadPathes, selected);

        lastGroup = Group.groupFromPath(selected);
        LogReference[] added;
        try {
          added = FileIO.readAllRefsFromPath(selected, true);
        } catch (Throwable e) {
          ToolMessage.displayError(
              "error {%s} (see below) reading Log refs from %s", e.getMessage(), selected);
          e.printStackTrace();

          return;
        }
        lastGroup.add(added);

        // Log[] addedLogs = new Log[added.length];
        // for (int i = 0; i < added.length; ++i)
        // addedLogs[i] = added[i].get();

        ToolMessage.displayInfo("loaded %d logs into %s", added.length, lastGroup);

        Events.GGroupAdded.generate(this, lastGroup);
        Events.GLogRefsFound.generate(this, added);
        // Events.GLogsFound.generate(this, addedLogs);

        display.leftSideTabs.setSelectedComponent(display.logTab);
      } else {
        Debug.error("invalid Log folder: " + selected.toString());
        ToolMessage.display("invalid Log folder: " + selected.toString(), Color.RED);
        return;
      }

    } else {
      Debug.error("cannot load session while streaming (%s)", robot);
      ToolMessage.display("cannot load session while streaming", Color.ORANGE);
    }
  }

  private void controlRequestFlags() {
    LogRPC.requestFlags(
        new IOFirstResponder() {
          @Override
          public void ioFinished(IOInstance instance) {
            assert (false);
          }

          @Override
          public void ioReceived(IOInstance inst, int ret, Log... out) {
            if (inst != robot) {
              Debug.error(
                  "got requestFlags return from instance %s, expected from %s!", inst, robot);
            } else {
              assert (out.length == 1);
              Log flags = out[0];
              assert (flags.logClass.equals(SharedConstants.LogClass_Flags()));

              RobotFlag[] parsedFlags = RobotFlag.parseLog(flags);

              JPanel container = new JPanel();
              container.setLayout(new GridLayout(parsedFlags.length, 1));
              for (RobotFlag val : parsedFlags) {
                container.add(new FlagPanel(robot, val));
              }

              container.setMinimumSize(container.getPreferredSize());
              display.rpcScrollPane.setViewportView(container);
            }
          }

          @Override
          public boolean ioMayRespondOnCenterThread(IOInstance inst) {
            return false;
          }
        },
        robot);
  }

  private class ControlConnectRunnable extends Center.EventRunnable {
    private final String robotAddress;

    ControlConnectRunnable(String s) {
      this.robotAddress = s;
    }

    @Override
    protected void run() {
      Debug.warn("trying to connect to %s", robotAddress);
      robot = RobotConnection.connectToRobot(robotAddress, outerThis);

      if (robot == null) {
        ToolMessage.displayError("connection failed to: %s", robotAddress);

        SwingUtilities.invokeLater(
            new Runnable() {
              @Override
              public void run() {
                display.connectButton.setEnabled(true);
              }
            });

        return;
      } else {
        ToolMessage.displayWarn("SUCCESS: connected to %s (%s)", robotAddress, robot);

        SwingUtilities.invokeLater(
            new Runnable() {
              @Override
              public void run() {
                display.connectButton.setEnabled(true);

                display.connectButton.setText("disconnect");
                display.loadButton.setEnabled(false);
                updateComboBoxAndSettings(
                    display.robotAddressBox, UserSettings.addresses, robotAddress);

                controlRequestFlags();

                lastGroup = Group.groupForStream(robotAddress);
                Events.GGroupAdded.generate(this, lastGroup);
              }
            });
      }
    }
  }

  private void controlConnectAction() {
    if (robot == null) {
      assert (display.connectButton.getText().equals("connect"));
    } else {
      assert (display.connectButton.getText().equals("disconnect"));
      Debug.info("trying to kill %s", robot);

      final RobotConnection _robot = robot;
      Center.addEvent(
          new Center.EventRunnable() {
            @Override
            protected void run() {
              _robot.kill();
            }
          });

      return;
    }

    String address = (String) display.robotAddressBox.getSelectedItem();
    if (address == null) {
      ToolMessage.displayError("choose valid address");
      return;
    }

    address = address.trim();

    if (display.localCheckBox.isSelected() && !address.endsWith(".local")) {
      address += ".local";
    }

    display.connectButton.setEnabled(false);
    Center.addEvent(new ControlConnectRunnable(address));
  }

  private void setupKeepSlider() {
    Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
    labelTable.put(new Integer(0), new JLabel("0.0"));
    labelTable.put(new Integer(1), new JLabel("0.01"));
    labelTable.put(new Integer(2), new JLabel("0.1"));
    labelTable.put(new Integer(3), new JLabel("0.2"));
    labelTable.put(new Integer(4), new JLabel("1.0"));

    display.keepSlider.setSnapToTicks(true);
    display.keepSlider.setMinimum(0);
    display.keepSlider.setMaximum(labelTable.size() - 1);
    display.keepSlider.setLabelTable(labelTable);
    display.keepSlider.setPaintLabels(true);
    display.keepSlider.setPaintTicks(true);
    display.keepSlider.setValue(labelTable.size() - 1);
  }

  private int keepMod() {
    int val = display.keepSlider.getValue();
    switch (val) {
      case 0:
        return 0;
      case 1:
        return 100;
      case 2:
        return 10;
      case 3:
        return 5;
      case 4:
        return 1;
      default:
        debug.error("bad keepSlider value: %d", val);
        return -1;
    }
  }

  private boolean shouldKeep(long index) {
    int km = keepMod();
    return (km > 0) && ((index % km) == 0);
  }

  @SuppressWarnings("unchecked")
  private void setupControlTab() {
    updateComboBoxAndSettings(display.pathBox, UserSettings.loadPathes, null);
    updateComboBoxAndSettings(display.robotAddressBox, UserSettings.addresses, null);

    final String last = "don't stream";
    Vector<String> streamVector = new Vector<String>();
    for (Pair<String, Criteria> pair : LogSearching.CRITERIA) {
      streamVector.add("stream: " + pair.a);
    }
    streamVector.add(last);

    display.streamComboBox.setModel(new DefaultComboBoxModel<String>(streamVector));
    display.streamComboBox.setSelectedItem(last);

    display.selectButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            controlSelectAction();
          }
        });

    display.loadButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            controlLoadAction();
          }
        });

    display.connectButton.addActionListener(
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            controlConnectAction();
          }
        });

    setupKeepSlider();

    // Stop streaming if the user selects a log...
    Center.listen(
        Events.LogSelected.class,
        new Events.LogSelected() {
          @Override
          public void logSelected(Object source, Log first, List<Log> alsoSelected) {
            debug.info("Log{%s} selected, %s disabling streaming!", first, outerClassThis);
            display.streamComboBox.setSelectedItem(last);
          }
        },
        true);

    display.leftSideTabs.setSelectedComponent(display.controlTab);
    ;
  }

  private ArrayList<LogReference> currentlyDisplayedFrom(Group group) {
    ArrayList<LogReference> refs = new ArrayList<>((group).logs);
    LogSearching.Criteria criteria =
        LogSearching.criteriaAt(display.sasStreamProfile.getSelectedIndex());
    if (criteria != null) {
      for (int i = 0; i < refs.size(); ) {
        if (!criteria.fits(refs.get(i).description)) refs.remove(i);
        else ++i;
      }
    }

    LogSorting.Sort sort = (LogSorting.Sort) display.sasOrderBox.getSelectedItem();
    if (sort != null) {
      Collections.sort(refs, sort.comparator);
    }

    return refs;
  }

  private boolean resemblesGeneratedFilename(String name, LogReference ref) {
    String[] checks =
        new String[] {
          String.format("%s_%s_v%d", ref.host_name, ref.logClass, ToolSettings.VERSION),
          String.format("temp_log_id"),
          String.format("log_%s_", ref.logClass)
        };

    for (String check : checks) {
      if (name.startsWith(check)) return true;
    }

    return false;
  }

  private class LogTreeModel
      implements TreeModel, TreeSelectionListener, LogDNDSource, TreeCellRenderer {

    private final DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();

    @Override
    public Component getTreeCellRendererComponent(
        JTree tree,
        Object value,
        boolean selected,
        boolean expanded,
        boolean leaf,
        int row,
        boolean hasFocus) {
      JLabel rendered =
          (JLabel)
              renderer.getTreeCellRendererComponent(
                  tree, value, selected, expanded, leaf, row, hasFocus);

      if (value instanceof Group) {
        Group group = (Group) value;
        rendered.setText(group.guiString());
      } else if (value instanceof LogReference) {
        LogReference reference = (LogReference) value;

        if (!reference.temporary()) {

          //					rendered.setText(reference.loadPath().getFileName().toString());
          //					rendered.setText("" + reference.loadPath().getFileName().toString().length());

          Path lp = reference.loadPath();
          String last = lp.getFileName().toString();

          if (last.length() < 20 || !resemblesGeneratedFilename(last, reference)) {
            rendered.setText(last);
          } else {
            rendered.setText(reference.guiString());
          }

        } else {
          rendered.setText(reference.guiString());
        }
      }

      return rendered;
    }

    @Override
    public void valueChanged(TreeSelectionEvent e) {
      if (!e.isAddedPath()) {
        // path unselected, ignore
        return;
      }

      TreePath[] selected = display.logTree.getSelectionPaths();
      switch (selected[0].getPathCount()) {
        case 0:
          Debug.error("null selection!");
          break;
        case 1:
          Debug.error("root selected!");
          break;
        case 2:
          {
            Group group = (Group) selected[0].getLastPathComponent();
            displayGroup(group);
            Events.GGroupSelected.generate(this, group);
          }
          return;
        case 3:
          {
            List<Log> all = new LinkedList<>();
            for (TreePath path : selected) {
              if (path.getPathCount() != 3) continue; // it isn't a Log selection

              LogReference ref = (LogReference) path.getLastPathComponent();
              all.add(ref.get());
            }

            Log first = all.remove(0);
            displayLog(first, all);
            Events.GLogSelected.generate(outerClassThis, first, all);
          }
          return;
        default:
          Debug.error(
              "selection count %d %s",
              selected[0].getPathCount(), selected[0].getLastPathComponent());
      }
    }

    @Override
    public Object getRoot() {
      return this;
    }

    @Override
    public Object getChild(Object parent, int index) {
      if (parent == this) {
        return AllGroups.get(index);
      } else if (parent instanceof Group) {
        Group group = (Group) parent;
        return currentlyDisplayedFrom(group).get(index);
      } else return null;
    }

    @Override
    public int getChildCount(Object parent) {
      if (parent == this) {
        return AllGroups.getGroupCount();
      } else if (parent instanceof Group) {
        return currentlyDisplayedFrom((Group) parent).size();
      } else {
        return 0;
      }
    }

    @Override
    public boolean isLeaf(Object node) {
      return node.getClass() == LogReference.class;
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {
      Debug.error("logTree should not be editable...");
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
      if (parent == this) {
        return AllGroups.allGroups.indexOf(child);
      } else if (parent instanceof Group) {
        return currentlyDisplayedFrom((Group) parent).indexOf(child);
      } else {
        Debug.error("parent %s not container!", parent);
        return -1;
      }
    }

    private final ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();

    @Override
    public void addTreeModelListener(TreeModelListener l) {
      listeners.add(l);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
      listeners.remove(l);
    }

    @Override
    public Log[] supplyLogsForDrag() {
      ArrayList<Log> refs = new ArrayList<>();
      for (TreePath tp : display.logTree.getSelectionPaths()) {
        if (tp.getPathCount() == 3) {
          LogReference reference = (LogReference) tp.getLastPathComponent();
          refs.add(reference.get());
        }
      }

      return refs.toArray(new Log[0]);
    }

    protected void deleteCurrent() {
      int num_logs_deleted = 0;
      Set<Group> groups_changed = new HashSet<>();

      for (TreePath tp : display.logTree.getSelectionPaths()) {
        if (tp.getPathCount() == 3) {
          Group group = (Group) tp.getPath()[1];
          LogReference reference = (LogReference) tp.getLastPathComponent();

          Debug.warn("deleting {%s} from {%s}", reference, group);
          group.remove(reference);

          ++num_logs_deleted;
          groups_changed.add(group);
        } else {
          Debug.warn("cannot delete {%s}", tp.getLastPathComponent());
        }
      }

      for (Group group : groups_changed) {
        TreeModelEvent removeEvent = new TreeModelEvent(this, new Object[] {this, group});

        for (TreeModelListener listener : listeners) listener.treeStructureChanged(removeEvent);
      }

      ToolMessage.displayAndPrint("deleted %d logs", num_logs_deleted);
    }

    protected void showGroupAdded(Group group) {
      // TreeModelEvent tme = new TreeModelEvent(this, new Object[]{root},
      // new int[]{AllGroups.allGroups.indexOf(group)},
      // new Object[]{group});
      Debug.event("LogModel group added");

      TreeModelEvent changed = new TreeModelEvent(this, new Object[] {this});
      for (TreeModelListener listener : listeners) listener.treeStructureChanged(changed);
    }

    protected void showReferenceAdded(LogReference reference) {
      Debug.event("LogModel reference added");

      TreeModelEvent tme = new TreeModelEvent(this, new Object[] {this, reference.container});
      for (TreeModelListener l : listeners) {
        l.treeStructureChanged(tme);
      }
    }

    protected void treeReSorted() {
      Debug.event("LogModel tree resorted");

      TreeModelEvent tme = new TreeModelEvent(this, new Object[] {this});
      for (TreeModelListener l : listeners) {
        l.treeStructureChanged(tme);
      }
    }
  }

  private LogTreeModel model;
  private final ToolDisplayHandler outerClassThis = this;

  private void setupLogsTab() {
    String last = "don't filter";
    Vector<String> streamVector = new Vector<String>();
    for (Pair<String, Criteria> pair : LogSearching.CRITERIA) {
      streamVector.add("search for: " + pair.a);
    }
    streamVector.add(last);

    ActionListener tellModel =
        new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
            model.treeReSorted();
          }
        };

    display.sasStreamProfile.setModel(new DefaultComboBoxModel<String>(streamVector));
    display.sasStreamProfile.setSelectedItem(last);
    display.sasStreamProfile.addActionListener(tellModel);

    display.sasOrderBox.setModel(new DefaultComboBoxModel<>(LogSorting.Sort.values()));
    display.sasOrderBox.setSelectedItem(LogSorting.Sort.BY_ARRIVAL);
    display.sasOrderBox.addActionListener(tellModel);

    model = new LogTreeModel();
    display.logTree.setModel(model);
    display.logTree.setCellRenderer(model);

    display.logTree.setEditable(false);
    display.logTree.setRootVisible(false);
    display.logTree.setScrollsOnExpand(true);
    display
        .logTree
        .getSelectionModel()
        .setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
    display.logTree.addTreeSelectionListener(model);
    LogDND.makeComponentSource(display.logTree, model);

    display.logTree.addKeyListener(
        new KeyAdapter() {
          @Override
          public void keyReleased(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
              model.deleteCurrent();
            }
          }
        });
  }

  private void displayLog(Log log) {
    displayLog(log, new LinkedList<Log>());
  }

  private void displayLog(Log log, List<Log> also) {
    tabDisplayer.setContents(viewProfile, log, also);
  }

  private void displayGroup(Group g) {
    tabDisplayer.setContents(g);
  }

  private ToolDisplayTabs tabDisplayer = null;

  private void setupLogDisplay() {
    LogDND.makeComponentTarget(
        display.displayTabs,
        new LogDNDTarget() {
          @Override
          public void takeLogsFromDrop(Log[] log) {
            if (log.length < 1) return;
            List<Log> list = new LinkedList<>(Arrays.asList(log));
            displayLog(list.remove(0), list);
          }
        });

    tabDisplayer = new ToolDisplayTabs(display);
  }

  TitleListener listener = null;

  class TitleListener implements Events.CrossStatus, Events.RobotConnectionStatus {

    TitleListener() {
      Debug.event("TitleListener() listening...");
      Center.listen(Events.CrossStatus.class, this, true);
      Center.listen(Events.RobotConnectionStatus.class, this, true);
    }

    int numCross = 0;
    int numRobot = 0;

    @Override
    public void robotStatus(RobotConnection inst, boolean up) {
      numRobot += up ? 1 : -1;
      set();
    }

    @Override
    public void nbCrossFound(CrossInstance inst, boolean up) {
      numCross += up ? 1 : -1;
      set();
    }

    private void set() {
      display.setTitle(
          String.format("nbtool - %d CrossInstance, %d RobotConnection", numCross, numRobot));
    }
  }

  private Timer footerJvmTimer = null;
  private Timer footerDiskTimer = null;
  private final int progressSize = 50;

  private FileStore usedFileStore = null;

  private void setupFooter() {
    try {
      usedFileStore = Files.getFileStore(ToolSettings.NBITES_DIR_PATH);
    } catch (Exception e) {
      e.printStackTrace();
      ;
      throw new Error(e);
    }

    Debug.warn("Tool footer using fileStore: %s", usedFileStore.name());

    display.diskAvailLabel.setText(Utility.progressString(progressSize, 0.5));
    display.jvmAvailLabel.setText(Utility.progressString(progressSize, 0.5));

    footerJvmTimer =
        new Timer(
            1000, // ms
            new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                footerJvmAction();
              }
            });

    footerJvmTimer.start();

    footerDiskTimer =
        new Timer(
            1000,
            new ActionListener() {
              @Override
              public void actionPerformed(ActionEvent e) {
                footerDiskAction();
              }
            });
    footerDiskTimer.start();
  }

  private void footerJvmAction() {
    long jvmUsed = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
    long jvmMax = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax();

    double jvmFrac = ((double) jvmUsed) / ((double) jvmMax);
    display.jvmAvailLabel.setText(Utility.progressString(progressSize, jvmFrac));
    //
    display.footerExtraLabel.setText(
        String.format(
            "   %d logs, %sused, %smax",
            AllGroups.getLogCount(),
            Utility.byteString(jvmUsed, true, true, false, false),
            Utility.byteString(jvmMax, true, true, false, false)));

    // Debug.print("jvm %d %s", jvmFrac,
    // String.format("%d logs, %s used memory, %s max",
    // AllGroups.getLogCount(), Utility.byteString(jvmUsed, true, true,
    // false, false),
    // Utility.byteString(jvmMax, true, true, false, false))
    // );
  }

  private void footerDiskAction() {
    long diskSpace;
    long diskUsed;
    try {
      diskSpace = usedFileStore.getTotalSpace();
      diskUsed = diskSpace - usedFileStore.getUnallocatedSpace();
    } catch (IOException e) {
      e.printStackTrace();
      throw new Error(e);
    }

    double diskFrac = ((double) diskUsed) / ((double) diskSpace);
    display.diskAvailLabel.setText(Utility.progressString(progressSize, diskFrac));
  }

  /*
   * Events interface implementations...
   */

  @Override
  public void groupAdded(Object source, Group group) {
    model.showGroupAdded(group);
  }

  @Override
  public void logsFound(Object source, Log... found) {
    for (Log log : found) {
      LogReference ref = log.getReference();
      if (ref != null) {
        model.showReferenceAdded(ref);
      }
    }
  }

  @Override
  public void logRefsFound(Object source, LogReference... found) {
    for (LogReference ref : found) {
      model.showReferenceAdded(ref);
    }
  }

  /*
   * IO implementation...
   */

  @Override
  public void ioFinished(IOInstance instance) {
    if (instance == robot) {
      ToolMessage.displayWarn("robot %s disconnected!", instance);
      display.rpcScrollPane.setViewportView(new JLabel("no connection"));
      display.connectButton.setText("connect");
      display.loadButton.setEnabled(true);
      robot = null;
    } else {
      Debug.error("informed of dead connection %s but robot=%s", instance, robot);
    }
  }

  @Override
  public void ioReceived(IOInstance inst, int ret, Log... out) {
    if (inst == robot) {

      // Deal with streaming...
      LogSearching.Criteria criteria =
          LogSearching.criteriaAt(display.streamComboBox.getSelectedIndex());

      if (criteria != null) {
        Log pass = null;

        for (Log l : out) {
          if (criteria.fits(l.getFullDescription())) pass = l;
        }

        if (pass != null) displayLog(pass);
      }

      // Deal with adding to groups (i.e. keeping)...
      assert (lastGroup != null);
      for (Log l : out) {
        if (shouldKeep(l.jvm_unique_id)) lastGroup.add(LogReference.referenceFromLog(l));
      }

      Events.GLogsFound.generate(this, out);
    } else {
      Debug.error("%s got %d surprising logs from %s", this, out.length, inst);
    }
  }

  @Override
  public boolean ioMayRespondOnCenterThread(IOInstance inst) {
    return false;
  }
}