long getReturnTime(GraphObject to) {
   for (GraphLink lnk : out_links) {
     if (lnk.getType() == LinkType.RETURN) {
       GraphObject go = lnk.getToObject();
       if (go == to) return lnk.getTime();
       else {
         long rt = go.getReturnTime(to);
         if (rt != 0) return Math.min(lnk.getTime(), rt);
       }
     }
   }
   return 0;
 }
 @Override
 public String getToolTipText(MouseEvent e) {
   BddtHistoryItem itm = getItemAtPoint(e.getX(), e.getY());
   if (itm != null) {
     StringBuffer buf = new StringBuffer();
     BumpThreadStack stk = itm.getStack();
     BumpStackFrame frm = stk.getFrame(0);
     if (frm == null) return null;
     buf.append(frm.getMethod() + " at " + frm.getLineNumber());
     return buf.toString();
   }
   GraphObject go = getObjectAtPoint(e.getX(), e.getY());
   if (go != null) {
     return go.getName();
   }
   return null;
 }
  /** ***************************************************************************** */
  @Override
  public void handlePopupMenu(MouseEvent e) {
    JPopupMenu popup = new JPopupMenu();
    Point pt = SwingUtilities.convertPoint(getContentPane().getParent(), e.getPoint(), draw_area);

    BddtHistoryItem itm = getItemAtPoint(pt.x, pt.y);
    if (itm != null) {
      popup.add(new GotoSourceAction(itm));
      popup.add(new GotoStackAction(itm));
    } else {
      GraphObject go = getObjectAtPoint(pt.x, pt.y);
      if (go != null && go.getValue() != null) {
        popup.add(new GotoValueAction(go));
      }
    }

    popup.add(getFloatBubbleAction());

    popup.show(draw_area, pt.x, pt.y);
  }
  private void drawObject(Graphics2D g, int idx, GraphObject go) {
    double x0 = left_right_margin + (object_width + object_hspacing) * idx;
    Rectangle2D r = new Rectangle2D.Double(x0, top_bottom_margin, object_width, object_height);
    graph_locations.put(go, x0 + object_width / 2);

    g.setColor(Color.WHITE);
    g.fill(r);
    g.setColor(Color.BLACK);
    g.draw(r);
    g.setColor(Color.RED);
    SwingText.drawText(go.getName(), g, r);

    double x1 = x0 + object_width / 2;
    double y1 = top_bottom_margin + object_height;
    double y2 = time_end;
    Line2D tl = new Line2D.Double(x1, y1, x1, y2);
    g.setColor(Color.BLACK);
    g.draw(tl);

    for (GraphBlock gb : go.getBlocks()) {
      drawBlock(g, x1, gb);
    }
  }
 @Override
 public int compare(GraphObject t0, GraphObject t1) {
   long x0 = t1.getReturnTime(t0);
   long x1 = t0.getReturnTime(t1);
   if (x0 == x1) { // either same or == 0
     long d = t0.getStartTime() - t1.getStartTime();
     if (d < 0) return -1;
     else if (d > 0) return 1;
     else if (t0.getName() == null) return -1;
     else if (t1.getName() == null) return 1;
     else return t0.getName().compareTo(t1.getName());
   } else if (x0 == 0) return -1;
   else if (x1 == 0) return 1;
   else if (x0 < x1) return 1;
   else return -1;
 }
  private void drawLinks(Graphics2D g, GraphObject go) {
    double x0 = graph_locations.get(go);

    for (GraphLink gl : go.getLinks()) {
      GraphObject got = gl.getToObject();
      double x1 = graph_locations.get(got);
      double y0 = getTimeY(gl.getTime());
      double x3, x4;
      if (x0 < x1) {
        x3 = x0 + active_width / 2;
        x4 = x1 - active_width / 2;
      } else {
        x3 = x0 - active_width / 2;
        x4 = x1 + active_width / 2;
      }
      Stroke sk = type_strokes.get(gl.getType());
      Color c = getThreadBlockColor(gl.getThread());
      g.setColor(c);
      g.setStroke(sk);
      Line2D ln = new Line2D.Double(x3, y0, x4, y0);
      g.draw(ln);
      drawArrow(g, x3, y0, x4, y0);
    }
  }
  /** ***************************************************************************** */
  private BddtHistoryItem getItemAtPoint(int x, int y) {
    if (object_width == 0) return null;

    double t =
        history_graph.getStartTime()
            + (y - time_start)
                / (time_end - time_start)
                * (history_graph.getEndTime() - history_graph.getStartTime() + 1);
    if (t < history_graph.getStartTime() || t > history_graph.getEndTime()) return null;

    GraphObject gobj = getObjectAtPoint(x, y);

    // correlate with blocks
    if (gobj != null) {
      GraphBlock gb0 = null;
      double dtime = 0;
      for (GraphBlock gb : gobj.getBlocks()) {
        double dt = gb.getEndTime() - gb.getStartTime();
        if (t >= gb.getStartTime() && t <= gb.getEndTime()) {
          if (gb0 == null || dt < dtime) {
            gb0 = gb;
            dtime = dt;
          }
        }
      }
      if (gb0 != null) {
        BddtHistoryItem bi0 = null;
        dtime = 0;
        for (BddtHistoryItem bhi : gb0.getItems()) {
          double dt = t - bhi.getTime();
          if (dt >= 0) {
            if (bi0 == null || dt < dtime) {
              bi0 = bhi;
              dtime = dt;
            }
          }
        }
        if (bi0 != null) return bi0;
      }
    }

    // correlate with links
    double delta =
        (history_graph.getEndTime() - history_graph.getStartTime()) / (time_end - time_start) * 3;

    double dtime = 0;
    GraphLink gl0 = null;
    for (int i = 0; i < history_graph.getNumObjects(); ++i) {
      GraphObject go = history_graph.getObject(i);
      double x0 = graph_locations.get(go);
      for (GraphLink gl : go.getLinks()) {
        double x1 = graph_locations.get(gl.getToObject());
        if (x > x0 && x < x1) {
          double dt = Math.abs(t - gl.getTime());
          if (gl0 == null || dt < dtime) {
            gl0 = gl;
            dtime = dt;
          }
        }
      }
    }
    if (gl0 != null && dtime <= delta) {
      return gl0.getItem();
    }

    return null;
  }
    void addThreadItems(Iterable<BddtHistoryItem> itms, long since) {
      GraphObject lastobj = null;
      BddtHistoryItem lastitem = null;
      for (BddtHistoryItem hi : itms) {
        GraphObject go = getObject(hi);
        if (since == 0 || hi.getTime() > since) {
          last_time = Math.max(last_time, hi.getTime());
          if (start_time == 0) start_time = last_time;
          else start_time = Math.min(start_time, hi.getTime());

          if (go == null) continue;
          if (lastobj == null) { // first time
            go.startBlock(hi);
          } else if (lastobj == go) { // step inside the same object
            go.extendBlock(hi);
          } else if (lastitem != null && hi.isInside(lastitem)) { // step/call into a new object
            go.startBlock(hi);
            lastobj.addLink(go, LinkType.ENTER, hi);
          } else if (lastitem != null && lastitem.isInside(hi)) { // return to prior object
            go.extendBlock(hi);
            // end prior block??
            lastobj.addLink(go, LinkType.RETURN, hi);
          } else {
            lastobj.finish(hi);
            go.startBlock(hi);
            lastobj.addLink(go, LinkType.NEXT, hi);
          }
        }
        lastobj = go;
        lastitem = hi;
      }
    }
 GotoValueAction(GraphObject go) {
   super("Show this value");
   for_value = go.getValue();
 }