visual switcher - take 2
authorKirill Kalishev <kirill.kalishev@jetbrains.com>
Thu, 22 Apr 2010 13:51:08 +0000 (17:51 +0400)
committerKirill Kalishev <kirill.kalishev@jetbrains.com>
Thu, 22 Apr 2010 13:51:08 +0000 (17:51 +0400)
platform/lang-impl/src/com/intellij/execution/ui/layout/impl/GridCellImpl.java
platform/lang-impl/src/com/intellij/execution/ui/layout/impl/GridImpl.java
platform/lang-impl/src/com/intellij/execution/ui/layout/impl/RunnerContentUi.java
platform/lang-impl/src/com/intellij/execution/ui/layout/impl/RunnerLayoutUiImpl.java
platform/platform-api/src/com/intellij/ui/switcher/SwitchAction.java
platform/platform-api/src/com/intellij/ui/switcher/SwitchProvider.java
platform/platform-api/src/com/intellij/ui/switcher/SwitchTarget.java
platform/platform-api/src/com/intellij/ui/switcher/SwitchingSession.java
platform/platform-api/src/com/intellij/ui/tabs/JBTabs.java
platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java

index 3aea1d66aa76f70f94f18bd6c2c06d975de93c02..fc3d1ccfceb12911c48e2b84d37a8c68933bec93 100644 (file)
@@ -30,6 +30,7 @@ import com.intellij.openapi.wm.WindowManager;
 import com.intellij.ui.components.panels.NonOpaquePanel;
 import com.intellij.ui.components.panels.Wrapper;
 import com.intellij.ui.content.Content;
+import com.intellij.ui.switcher.SwitchTarget;
 import com.intellij.ui.tabs.JBTabs;
 import com.intellij.ui.tabs.TabInfo;
 import com.intellij.ui.tabs.TabsListener;
@@ -46,6 +47,8 @@ import javax.swing.border.EmptyBorder;
 import java.awt.*;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Set;
 
 public class GridCellImpl implements GridCell, Disposable {
@@ -246,6 +249,18 @@ public class GridCellImpl implements GridCell, Disposable {
     return myMinimizedContents.contains(content);
   }
 
+  public java.util.List<SwitchTarget> getTargets(boolean onlyVisible) {
+    return myTabs.getTargets(onlyVisible);
+  }
+
+  public SwitchTarget getTargetForSelection() {
+    return myTabs.getCurrentTarget();
+  }
+
+  public boolean contains(Component c) {
+    return myTabs.getComponent().isAncestorOf(c);
+  }
+
   private static class ProviderWrapper extends NonOpaquePanel implements DataProvider {
 
     Content myContent;
index 170f6c8465b0ccdef287f8f253ae38f30d1408d5..1cfe36b9a7068dc78979071cda51170781b31d88 100644 (file)
@@ -26,6 +26,8 @@ import com.intellij.openapi.util.ActionCallback;
 import com.intellij.openapi.util.Disposer;
 import com.intellij.ui.components.panels.Wrapper;
 import com.intellij.ui.content.Content;
+import com.intellij.ui.switcher.SwitchProvider;
+import com.intellij.ui.switcher.SwitchTarget;
 import com.intellij.util.containers.HashMap;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.Nullable;
@@ -222,6 +224,7 @@ public class GridImpl extends Wrapper implements Grid, Disposable, CellTransform
     return getCellFor(content).isMinimized(content);
   }
 
+
   static class Placeholder extends Wrapper implements NullableComponent {
 
     private JComponent myContent;
@@ -403,4 +406,30 @@ public class GridImpl extends Wrapper implements Grid, Disposable, CellTransform
   public String getSessionName() {
     return mySessionName;
   }
+  public SwitchTarget getCellFor(Component c) {
+    Component eachParent = c;
+    while (eachParent != null) {
+      for (GridCellImpl eachCell : myContent2Cell.values()) {
+        if (eachCell.contains(eachParent)) {
+          return eachCell.getTargetForSelection();
+        }
+      }
+
+      eachParent = eachParent.getParent();
+    }
+
+    return null;
+  }
+
+
+  public List<SwitchTarget> getTargets(boolean onlyVisible) {
+    Collection<GridCellImpl> cells = myPlaceInGrid2Cell.values();
+    ArrayList<SwitchTarget> result = new ArrayList<SwitchTarget>();
+    for (GridCellImpl each : cells) {
+      if (!each.isDetached()) {
+        result.addAll(each.getTargets(onlyVisible));
+      }
+    }
+    return result;
+  }
 }
index 28ea488ec65ebd4a49a20bd4dce783b5b57a6aed..604d6cc96c19f1c310f69a4ded4dbf7338b3b9ee 100644 (file)
@@ -29,9 +29,12 @@ import com.intellij.openapi.util.Disposer;
 import com.intellij.openapi.util.Ref;
 import com.intellij.openapi.wm.IdeFocusManager;
 import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.ui.awt.RelativeRectangle;
 import com.intellij.ui.components.panels.NonOpaquePanel;
 import com.intellij.ui.components.panels.Wrapper;
 import com.intellij.ui.content.*;
+import com.intellij.ui.switcher.SwitchProvider;
+import com.intellij.ui.switcher.SwitchTarget;
 import com.intellij.ui.tabs.JBTabs;
 import com.intellij.ui.tabs.TabInfo;
 import com.intellij.ui.tabs.TabsListener;
@@ -50,7 +53,7 @@ import java.beans.PropertyChangeListener;
 import java.util.*;
 import java.util.List;
 
-public class RunnerContentUi implements ContentUI, Disposable, CellTransform.Facade, ViewContextEx, PropertyChangeListener {
+public class RunnerContentUi implements ContentUI, Disposable, CellTransform.Facade, ViewContextEx, PropertyChangeListener, SwitchProvider {
 
   @NonNls public static final String LAYOUT = "Runner.Layout";
   @NonNls public static final String VIEW_POPUP = "Runner.View.Popup";
@@ -1103,5 +1106,38 @@ public class RunnerContentUi implements ContentUI, Disposable, CellTransform.Fac
     return myRunnerUi;
   }
 
+  public SwitchTarget getCurrentTarget() {
+    Component owner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
+    if (owner == null) return myTabs.getCurrentTarget();
 
+    GridImpl grid = getSelectedGrid();
+    if (grid.getContents().size() <= 1) return myTabs.getCurrentTarget();
+
+    SwitchTarget cell = grid.getCellFor(owner);
+
+    return cell != null ? cell : myTabs.getCurrentTarget();
+  }
+
+  public List<SwitchTarget> getTargets(boolean onlyVisible) {
+    List<SwitchTarget> result = new ArrayList<SwitchTarget>();
+
+    result.addAll(myTabs.getTargets(true));
+    result.addAll(getSelectedGrid().getTargets(onlyVisible));
+
+    return result;
+  }
+
+  private class LayoutTarget implements SwitchTarget {
+    public ActionCallback switchTo(boolean requestFocus) {
+      return null;
+    }
+
+    public boolean isVisible() {
+      return false;
+    }
+
+    public RelativeRectangle getRectangle() {
+      return null;
+    }
+  }
 }
index 0c35126212ddb3098479c1e65dcfb364c44ecd00..51cf2bc42371385fd903f1c0bf7f98acd55523ce 100644 (file)
@@ -25,6 +25,7 @@ import com.intellij.openapi.Disposable;
 import com.intellij.openapi.actionSystem.ActionGroup;
 import com.intellij.openapi.actionSystem.ActionManager;
 import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.DataProvider;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.ComponentWithActions;
 import com.intellij.openapi.util.ActionCallback;
@@ -35,6 +36,9 @@ import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
 import com.intellij.ui.content.ContentManager;
 import com.intellij.ui.content.ContentManagerListener;
+import com.intellij.ui.switcher.SwitchProvider;
+import com.intellij.ui.switcher.SwitchTarget;
+import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -56,7 +60,7 @@ public class RunnerLayoutUiImpl implements Disposable, RunnerLayoutUi, LayoutSta
     myContentUI = new RunnerContentUi(project, this, ActionManager.getInstance(), IdeFocusManager.getInstance(project), myLayout,
                                            runnerTitle + " - " + sessionName);
 
-    myContentPanel = new JPanel(new BorderLayout());
+    myContentPanel = new MyContent();
 
     myViewsContentManager = getContentFactory().createContentManager(myContentUI.getContentUI(), false, project);
     Disposer.register(this, myViewsContentManager);
@@ -296,4 +300,19 @@ public class RunnerLayoutUiImpl implements Disposable, RunnerLayoutUi, LayoutSta
     }
     return contents;
   }
+
+  private class MyContent extends JPanel implements DataProvider {
+    public MyContent() {
+      super(new BorderLayout());
+    }
+
+    public Object getData(@NonNls String dataId) {
+      if (SwitchProvider.KEY.getName().equals(dataId)) {
+        return myContentUI;
+      }
+
+      return null;
+    }
+  }
+
 }
index 0f29fdb31ee54dcb16ef617caa08e43ae2867a3e..2ac1da060fa4b3e73fa7109e6692558805ce1244 100644 (file)
@@ -40,12 +40,7 @@ public abstract class SwitchAction extends AnAction implements DumbAware {
   }
 
   private SwitchProvider getProvider(AnActionEvent e) {
-    return new SwitchProvider() {
-      public SwitchTarget[] getTargets() {
-        return new SwitchTarget[0];
-      }
-    };
-    //return e.getData(SwitchProvider.KEY);
+    return e.getData(SwitchProvider.KEY);
   }
 
   protected abstract void move(SwitchingSession session);
index 04273cb5e680aed9a1500a80ade25e0f1ecf64a4..ac68d7108a81c33af0671907c8d7b9a8a4fc4cda 100644 (file)
@@ -17,10 +17,16 @@ package com.intellij.ui.switcher;
 
 import com.intellij.openapi.actionSystem.DataKey;
 
+import javax.swing.*;
+import java.util.List;
+
 public interface SwitchProvider {
 
   DataKey<SwitchProvider> KEY = DataKey.create("SwitchProvider");
 
-  SwitchTarget[] getTargets();
+  List<SwitchTarget> getTargets(boolean onlyVisible);
+  SwitchTarget getCurrentTarget();
+
+  JComponent getComponent();
 
 }
index b5928fc5ef9132e05aed6d67b18c6912927a7c7e..876cd3193f1934fd961ec005a0d56cb11b6549f1 100644 (file)
  */
 package com.intellij.ui.switcher;
 
-public class SwitchTarget {
+import com.intellij.openapi.util.ActionCallback;
+import com.intellij.ui.awt.RelativeRectangle;
+
+public interface SwitchTarget {
+
+  ActionCallback switchTo(boolean requestFocus);
+
+  boolean isVisible();
+  RelativeRectangle getRectangle();
+
 }
index 4e23d6bbacf22517fcfdeddc5f69f7ec6e4211ae..4b9a39b19670a5ee30fa497804fb1c01dd43e587 100644 (file)
  */
 package com.intellij.ui.switcher;
 
+import com.intellij.openapi.Disposable;
 import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.ui.AbstractPainter;
+import com.intellij.openapi.ui.Painter;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.wm.IdeGlassPane;
+import com.intellij.openapi.wm.IdeGlassPaneUtil;
+import com.intellij.openapi.wm.impl.content.GraphicsConfig;
 
+import javax.swing.*;
 import java.awt.*;
 import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
+import java.util.*;
+import java.util.List;
 
-public class SwitchingSession implements KeyEventDispatcher {
+import static java.lang.Math.abs;
+import static java.lang.Math.sqrt;
+
+public class SwitchingSession implements KeyEventDispatcher, Disposable {
 
   private SwitchProvider myProvider;
   private AnActionEvent myInitialEvent;
   private boolean myFinished;
+  private java.util.List<SwitchTarget> myTargets;
+  private IdeGlassPane myGlassPane;
+
+  private Map<SwitchTarget, TargetPainer> myPainters = new Hashtable<SwitchTarget, TargetPainer>();
+  private JComponent myRootComponent;
+  private SwitchTarget mySelection;
 
   public SwitchingSession(SwitchProvider provider, AnActionEvent e) {
     myProvider = provider;
@@ -33,6 +52,26 @@ public class SwitchingSession implements KeyEventDispatcher {
 
     KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
 
+
+    myTargets = myProvider.getTargets(true);
+    if (myTargets.size() == 0) {
+      Disposer.dispose(this);
+      return;
+    }
+
+
+    mySelection = myProvider.getCurrentTarget();
+
+    myGlassPane = IdeGlassPaneUtil.find(myProvider.getComponent());
+    for (SwitchTarget each : myTargets) {
+      TargetPainer eachPainter = new TargetPainer(each);
+      Disposer.register(this, eachPainter);
+
+      myRootComponent = myProvider.getComponent();
+      myGlassPane.addPainter(myRootComponent, eachPainter, this);
+      myPainters.put(each, eachPainter);
+    }
+
   }
 
   public boolean dispatchKeyEvent(KeyEvent e) {
@@ -45,28 +84,182 @@ public class SwitchingSession implements KeyEventDispatcher {
     return false;
   }
 
+  private SwitchTarget getSelection() {
+    return mySelection;
+  }
+
+  private class TargetPainer extends AbstractPainter implements Disposable {
+
+    private SwitchTarget myTarget;
+
+    private TargetPainer(SwitchTarget target) {
+      myTarget = target;
+    }
+
+    @Override
+    public void executePaint(Component component, Graphics2D g) {
+      GraphicsConfig cfg = new GraphicsConfig(g);
+      cfg.setAntialiasing(true);
+
+      g.setColor(Color.red);
+      Rectangle paintRect = myTarget.getRectangle().getRectangleOn(component);
+
+      boolean selected = myTarget.equals(getSelection());
+      if (selected) {
+        g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[] {2, 4}, 0));
+        g.draw(paintRect);
+      } else {
+        g.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[] {2, 4}, 0));
+        g.draw(paintRect);
+      }
+
+      cfg.restore();
+    }
+
+    @Override
+    public boolean needsRepaint() {
+      return true;
+    }
+
+    public void dispose() {
+      myGlassPane.removePainter(this);
+    }
+  }
+
+  private enum Direction {
+    up, down, left, right
+  }
 
   public void up() {
-    System.out.println("SwitchingSession.up");
+    setSelection(getNextTarget(Direction.up));
   }
 
   public void down() {
-    System.out.println("SwitchingSession.down");
+    setSelection(getNextTarget(Direction.down));
   }
 
   public void left() {
-    System.out.println("SwitchingSession.left");
+    setSelection(getNextTarget(Direction.left));
   }
 
   public void right() {
-    System.out.println("SwitchingSession.right");
+    setSelection(getNextTarget(Direction.right));
   }
 
-  private void finish() {
+  private void setSelection(SwitchTarget target) {
+    mySelection = target;
+
+    for (TargetPainer each : myPainters.values()) {
+      each.setNeedsRepaint(true);
+    }
+  }
+
+  private SwitchTarget getNextTarget(Direction direction) {
+    List<Point> points = new ArrayList<Point>();
+    Point selected = null;
+    Map<SwitchTarget, Point> target2Point = new HashMap<SwitchTarget, Point>();
+    for (SwitchTarget each : myTargets) {
+      Rectangle eachRec = each.getRectangle().getRectangleOn(myRootComponent);
+      Point eachPoint = null;
+      switch (direction) {
+        case up:
+          eachPoint = new Point(eachRec.x + eachRec.width / 2, eachRec.y + eachRec.height);
+          break;
+        case down:
+          eachPoint = new Point(eachRec.x + eachRec.width, eachRec.y);
+          break;
+        case left:
+          eachPoint = new Point(eachRec.x + eachRec.width, eachRec.y + eachRec.height / 2);
+          break;
+        case right:
+          eachPoint = new Point(eachRec.x, eachRec.y + eachRec.height / 2);
+          break;
+      }
+
+      if (each.equals(mySelection)) {
+        switch (direction) {
+          case up:
+            selected = new Point(eachRec.x + eachRec.width / 2, eachRec.y);
+            break;
+          case down:
+            selected = new Point(eachRec.x + eachRec.width / 2, eachRec.y + eachRec.height);
+            break;
+          case left:
+            selected = new Point(eachRec.x, eachRec.y + eachRec.height / 2);
+            break;
+          case right:
+            selected = new Point(eachRec.x + eachRec.width, eachRec.y + eachRec.y / 2);
+            break;
+        }
+      }
+      points.add(eachPoint);
+      target2Point.put(each, eachPoint);
+    }
+
+    TreeMap<Integer, SwitchTarget> distance = new TreeMap<Integer, SwitchTarget>();
+    for (SwitchTarget eachTarget : myTargets) {
+      Point eachPoint = target2Point.get(eachTarget);
+      if (selected == eachPoint) continue;
+
+      double eachDistance = sqrt(abs(eachPoint.getX() - selected.getX())) + sqrt(abs(eachPoint.getY() - selected.getY()));
+      distance.put((int)eachDistance, eachTarget);
+    }
+
+
+    for (Integer eachDistance : distance.keySet()) {
+      SwitchTarget eachTarget = distance.get(eachDistance);
+      Point eachPoint = target2Point.get(eachTarget);
+      switch (direction) {
+        case up:
+          if (eachPoint.y < selected.y) {
+            return eachTarget;
+          }
+          break;
+        case down:
+          if (eachPoint.y > selected.y) {
+            return eachTarget;
+          }
+          break;
+        case left:
+          if (eachPoint.x < selected.x) {
+            return eachTarget;
+          }
+          break;
+        case right:
+          if (eachPoint.x > selected.x) {
+            return eachTarget;
+          }
+          break;
+      }
+    }
+
+    return distance.values().iterator().next();
+  }
+
+  private void selectNext() {
+    int index = myTargets.indexOf(getSelection());
+    if (index + 1 < myTargets.size()) {
+      mySelection = myTargets.get(index + 1);
+    } else {
+      mySelection = myTargets.get(0);
+    }
+
+    for (TargetPainer each : myPainters.values()) {
+      each.setNeedsRepaint(true);
+    }
+  }
+
+  public void dispose() {
     KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(this);
     myFinished = true;
+  }
 
-    System.out.println("SwitchingSession.finish");
+  private void finish() {
+    SwitchTarget selection = getSelection();
+    if (selection != null) {
+      selection.switchTo(true);
+    }
+    Disposer.dispose(this);
   }
 
   public boolean isFinished() {
index 849ea9ece83f441f750cbf08e1218a5d60652ebb..3a17bfd35aa8629617094f848a2977a3007ffe95 100644 (file)
@@ -19,6 +19,8 @@ import com.intellij.openapi.actionSystem.ActionGroup;
 import com.intellij.openapi.actionSystem.DataProvider;
 import com.intellij.openapi.util.ActionCallback;
 import com.intellij.openapi.util.Getter;
+import com.intellij.ui.switcher.SwitchProvider;
+import com.intellij.ui.switcher.SwitchTarget;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -29,7 +31,7 @@ import java.awt.event.MouseListener;
 import java.util.List;
 import java.util.Comparator;
 
-public interface JBTabs {
+public interface JBTabs extends SwitchProvider {
 
   @NotNull
   TabInfo addTab(TabInfo info, int index);
index c5ab2124993296528fb3f267f8410c6bc728ec51..78111f515eb3155d377ee21aea055d8bf3cbb220 100644 (file)
@@ -29,6 +29,8 @@ import com.intellij.openapi.wm.IdeGlassPane;
 import com.intellij.openapi.wm.IdeGlassPaneUtil;
 import com.intellij.openapi.wm.impl.content.GraphicsConfig;
 import com.intellij.ui.CaptionPanel;
+import com.intellij.ui.awt.RelativeRectangle;
+import com.intellij.ui.switcher.SwitchTarget;
 import com.intellij.ui.tabs.*;
 import com.intellij.ui.tabs.impl.singleRow.SingleRowLayout;
 import com.intellij.ui.tabs.impl.singleRow.SingleRowPassInfo;
@@ -37,6 +39,7 @@ import com.intellij.ui.tabs.impl.table.TablePassInfo;
 import com.intellij.util.ui.Animator;
 import com.intellij.util.ui.TimedDeadzone;
 import com.intellij.util.ui.UIUtil;
+import com.intellij.util.ui.update.ComparableObject;
 import com.intellij.util.ui.update.LazyUiDisposable;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
@@ -2723,4 +2726,50 @@ public class JBTabsImpl extends JComponent
     revalidate();
     repaint();
   }
+
+  public List<SwitchTarget> getTargets(boolean onlyVisible) {
+    ArrayList<SwitchTarget> result = new ArrayList<SwitchTarget>();
+    for (TabInfo each : myVisibleInfos) {
+      result.add(new TabTarget(each));
+    }
+    return result;
+  }
+
+  public SwitchTarget getCurrentTarget() {
+    return new TabTarget(getSelectedInfo());
+  }
+
+  private class TabTarget extends ComparableObject.Impl implements SwitchTarget {
+
+    private TabInfo myInfo;
+
+    private TabTarget(TabInfo info) {
+      myInfo = info;
+    }
+
+    public ActionCallback switchTo(boolean requestFocus) {
+      return select(myInfo, requestFocus);
+    }
+
+    public boolean isVisible() {
+      return getRectangle() != null;
+    }
+
+    public RelativeRectangle getRectangle() {
+      TabLabel label = myInfo2Label.get(myInfo);
+      if (label.getRootPane() == null) return null;
+
+      Rectangle b = label.getBounds();
+      b.x += 2;
+      b.width -= 4;
+      b.y += 2;
+      b.height -= 4;
+      return new RelativeRectangle(label.getParent(), b);
+    }
+
+    @Override
+    public Object[] getEqualityObjects() {
+      return new Object[] {myInfo};
+    }
+  }
 }