diff --git a/platform/platform-api/src/com/intellij/ui/components/AbstractScrollBarUI.java b/platform/platform-api/src/com/intellij/ui/components/AbstractScrollBarUI.java new file mode 100644 index 000000000000..10ce0f93ccf9 --- /dev/null +++ b/platform/platform-api/src/com/intellij/ui/components/AbstractScrollBarUI.java @@ -0,0 +1,509 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.components; + +import com.intellij.openapi.util.Key; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBScrollBar.Alignment; +import com.intellij.util.ui.JBInsets; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.RegionPainter; +import com.intellij.util.ui.UIUtil; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.ScrollBarUI; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import static java.awt.Adjustable.VERTICAL; + +/** + * @author Sergey.Malenkov + */ +abstract class AbstractScrollBarUI extends ScrollBarUI { + static final Key> LEADING_AREA = Key.create("PLAIN_SCROLL_BAR_UI_LEADING_AREA");//TODO:support + + private static final JBColor TRACK_BACKGROUND = new JBColor(0xF5F5F5, 0x3c3f41); + private static final JBColor TRACK_FOREGROUND = new JBColor(0xE6E6E6, 0x3C3F41); + + private final Listener myListener = new Listener(); + private final Timer myScrollTimer = new Timer(60, myListener); + + private final Rectangle myThumbBounds = new Rectangle(); + private final Rectangle myTrackBounds = new Rectangle(); + private final Rectangle myLeadingBounds = new Rectangle(); + + private JScrollBar myScrollBar; + + private boolean isTrackVisible; + private boolean isValueCached; + private int myCachedValue; + + abstract int getThickness(); + + abstract int getMinimalThickness(); + + abstract void onTrackHover(boolean hover); + + abstract void onThumbHover(boolean hover); + + abstract void paintTrack(Graphics2D g, int x, int y, int width, int height, JComponent c); + + abstract void paintThumb(Graphics2D g, int x, int y, int width, int height, JComponent c); + + void setTrackVisible(boolean trackVisible) { + if (isTrackVisible != trackVisible) { + isTrackVisible = trackVisible; + repaint(); + } + } + + void repaint() { + if (myScrollBar != null) myScrollBar.repaint(); + } + + void repaint(int x, int y, int width, int height) { + if (myScrollBar != null) myScrollBar.repaint(x, y, width, height); + } + + int scale(int value) { + value = JBUI.scale(value); + //noinspection EnumSwitchStatementWhichMissesCases + switch (UIUtil.getComponentStyle(myScrollBar)) { + case LARGE: + return (int)(value * 1.15); + case SMALL: + return (int)(value * 0.857); + case MINI: + return (int)(value * 0.714); + } + return value; + } + + @Override + public void installUI(JComponent c) { + myScrollBar = (JScrollBar)c; + myScrollBar.setBackground(TRACK_BACKGROUND); + myScrollBar.setForeground(TRACK_FOREGROUND); + myScrollBar.setFocusable(false); + myScrollBar.addMouseListener(myListener); + myScrollBar.addMouseMotionListener(myListener); + myScrollBar.getModel().addChangeListener(myListener); + myScrollBar.addPropertyChangeListener(myListener); + myScrollBar.addFocusListener(myListener); + myScrollTimer.setInitialDelay(300); + } + + @Override + public void uninstallUI(JComponent c) { + myScrollTimer.stop(); + myScrollBar.removeFocusListener(myListener); + myScrollBar.removePropertyChangeListener(myListener); + myScrollBar.getModel().removeChangeListener(myListener); + myScrollBar.removeMouseMotionListener(myListener); + myScrollBar.removeMouseListener(myListener); + myScrollBar = null; + } + + @Override + public Dimension getPreferredSize(JComponent c) { + int thickness = getThickness(); + Alignment alignment = Alignment.get(c); + return alignment == Alignment.LEFT || alignment == Alignment.RIGHT + ? new Dimension(thickness, thickness * 3) + : new Dimension(thickness * 3, thickness); + } + + @Override + public void paint(Graphics g, JComponent c) { + Alignment alignment = Alignment.get(c); + if (alignment != null && g instanceof Graphics2D) { + Rectangle bounds = new Rectangle(c.getSize()); + JBInsets.removeFrom(bounds, c.getInsets()); + if (c.isOpaque() && c.getParent() instanceof JScrollPane) { + g.setColor(c.getForeground()); + switch (alignment) { + case TOP: + bounds.height--; + g.drawLine(bounds.x, bounds.y + bounds.height, bounds.x + bounds.width, bounds.y + bounds.height); + break; + case LEFT: + bounds.width--; + g.drawLine(bounds.x + bounds.width, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height); + break; + case RIGHT: + g.drawLine(bounds.x, bounds.y, bounds.x, bounds.y + bounds.height); + bounds.width--; + bounds.x++; + break; + case BOTTOM: + g.drawLine(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y); + bounds.height--; + bounds.y++; + break; + } + } + else if (isTrackVisible) { + paintTrack((Graphics2D)g, bounds.x, bounds.y, bounds.width, bounds.height, c); + } + // process a square area before the track + RegionPainter leading = UIUtil.getClientProperty(c, LEADING_AREA); + if (leading == null) { + myLeadingBounds.setSize(0, 0); + } + else if (alignment == Alignment.LEFT || alignment == Alignment.RIGHT) { + int size = bounds.width; + myLeadingBounds.setBounds(bounds.x, bounds.y, size, size); + leading.paint((Graphics2D)g, bounds.x, bounds.y, size, size, null); + bounds.height -= size; + bounds.y += size; + } + else { + int size = bounds.height; + myLeadingBounds.setBounds(bounds.x, bounds.y, size, size); + leading.paint((Graphics2D)g, bounds.x, bounds.y, size, size, null); + bounds.width -= size; + bounds.x += size; + } + // process additional drawing on the track + myTrackBounds.setBounds(bounds); + RegionPainter track = UIUtil.getClientProperty(c, JBScrollBar.TRACK); + if (track != null && myTrackBounds.width > 0 && myTrackBounds.height > 0) { + track.paint((Graphics2D)g, myTrackBounds.x, myTrackBounds.y, myTrackBounds.width, myTrackBounds.height, null); + } + // process drawing the thumb + updateThumbBounds(); + if (myThumbBounds.width > 0 && myThumbBounds.height > 0) { + paintThumb((Graphics2D)g, myThumbBounds.x, myThumbBounds.y, myThumbBounds.width, myThumbBounds.height, c); + } + } + } + + private void updateThumbBounds() { + int min = myScrollBar.getMinimum(); + int max = myScrollBar.getMaximum(); + int range = max - min; + if (range <= 0) { + myThumbBounds.setBounds(0, 0, 0, 0); + } + else if (VERTICAL == myScrollBar.getOrientation()) { + int extent = myScrollBar.getVisibleAmount(); + int height = Math.max(myTrackBounds.height * extent / range, 2 * getThickness()); + if (myTrackBounds.height <= height) { + myThumbBounds.setBounds(0, 0, 0, 0); + } + else { + int value = getValue(); + int maxY = myTrackBounds.y + myTrackBounds.height - height; + int y = (value < max - extent) ? (myTrackBounds.height - height) * (value - min) / (range - extent) : maxY; + myThumbBounds.setBounds(myTrackBounds.x, adjust(y, myTrackBounds.y, maxY), myTrackBounds.width, height); + } + } + else { + int extent = myScrollBar.getVisibleAmount(); + int width = Math.max(myTrackBounds.width * extent / range, 2 * getThickness()); + if (myTrackBounds.width <= width) { + myThumbBounds.setBounds(0, 0, 0, 0); + } + else { + int value = getValue(); + int maxX = myTrackBounds.x + myTrackBounds.width - width; + int x = (value < max - extent) ? (myTrackBounds.width - width) * (value - min) / (range - extent) : maxX; + myThumbBounds.setBounds(adjust(x, myTrackBounds.x, maxX), myTrackBounds.y, width, myTrackBounds.height); + } + } + } + + private int getValue() { + return isValueCached ? myCachedValue : myScrollBar.getValue(); + } + + private static int adjust(int value, int min, int max) { + return value < min ? min : value > max ? max : value; + } + + private final class Listener extends MouseAdapter implements ActionListener, FocusListener, ChangeListener, PropertyChangeListener { + private int myOffset; + private int myMouseX, myMouseY; + private boolean isReversed; + private boolean isDragging; + private boolean isOverTrack; + private boolean isOverThumb; + + private void updateMouse(int x, int y) { + if (myTrackBounds.contains(x, y)) { + if (!isOverTrack) onTrackHover(isOverTrack = true); + boolean hover = myThumbBounds.contains(x, y); + if (isOverThumb != hover) onThumbHover(isOverThumb = hover); + } + else { + updateMouseExit(); + } + } + + private void updateMouseExit() { + if (isOverThumb) onThumbHover(isOverThumb = false); + if (isOverTrack) onTrackHover(isOverTrack = false); + } + + @Override + public void mousePressed(MouseEvent event) { + if (myScrollBar == null || !myScrollBar.isEnabled()) return; + if (SwingUtilities.isRightMouseButton(event)) return; + + isValueCached = true; + myCachedValue = myScrollBar.getValue(); + myScrollBar.setValueIsAdjusting(true); + + myMouseX = event.getX(); + myMouseY = event.getY(); + + boolean vertical = VERTICAL == myScrollBar.getOrientation(); + if (myThumbBounds.contains(myMouseX, myMouseY)) { + // pressed on the thumb + myOffset = vertical ? (myMouseY - myThumbBounds.y) : (myMouseX - myThumbBounds.x); + isDragging = true; + } + else if (isTrackVisible && myTrackBounds.contains(myMouseX, myMouseY)) { + // pressed on the track + if (SwingUtilities.isMiddleMouseButton(event)) { + myOffset = (vertical ? myThumbBounds.height : myThumbBounds.width) / 2; + isDragging = true; + setValueFrom(event); + } + else { + myScrollTimer.stop(); + isDragging = false; + if (VERTICAL == myScrollBar.getOrientation()) { + int y = myThumbBounds.isEmpty() ? myScrollBar.getHeight() / 2 : myThumbBounds.y; + isReversed = myMouseY < y; + } + else { + int x = myThumbBounds.isEmpty() ? myScrollBar.getWidth() / 2 : myThumbBounds.x; + isReversed = myMouseX < x; + if (!myScrollBar.getComponentOrientation().isLeftToRight()) { + isReversed = !isReversed; + } + } + scroll(isReversed); + startScrollTimerIfNecessary(); + } + } + } + + @Override + public void mouseReleased(MouseEvent event) { + if (isDragging) updateMouse(event.getX(), event.getY()); + if (myScrollBar == null || !myScrollBar.isEnabled()) return; + if (SwingUtilities.isRightMouseButton(event)) return; + isDragging = false; + myOffset = 0; + myScrollTimer.stop(); + isValueCached = true; + myCachedValue = myScrollBar.getValue(); + myScrollBar.setValueIsAdjusting(false); + repaint(); + } + + @Override + public void mouseDragged(MouseEvent event) { + if (myScrollBar == null || !myScrollBar.isEnabled()) return; + if (myThumbBounds.isEmpty() || SwingUtilities.isRightMouseButton(event)) return; + if (isDragging) { + setValueFrom(event); + } + else { + myMouseX = event.getX(); + myMouseY = event.getY(); + updateMouse(myMouseX, myMouseY); + startScrollTimerIfNecessary(); + } + } + + @Override + public void mouseMoved(MouseEvent event) { + if (myScrollBar == null || !myScrollBar.isEnabled()) return; + if (!isDragging) updateMouse(event.getX(), event.getY()); + } + + @Override + public void mouseExited(MouseEvent event) { + if (myScrollBar == null || !myScrollBar.isEnabled()) return; + if (!isDragging) updateMouseExit(); + } + + @Override + public void actionPerformed(ActionEvent event) { + if (myScrollBar == null) { + myScrollTimer.stop(); + } + else { + scroll(isReversed); + if (!myThumbBounds.isEmpty()) { + if (isReversed ? !isMouseBeforeThumb() : !isMouseAfterThumb()) { + myScrollTimer.stop(); + } + } + int value = myScrollBar.getValue(); + if (isReversed ? value <= myScrollBar.getMinimum() : value >= myScrollBar.getMaximum() - myScrollBar.getVisibleAmount()) { + myScrollTimer.stop(); + } + } + } + + @Override + public void focusGained(FocusEvent event) { + repaint(); + } + + @Override + public void focusLost(FocusEvent event) { + repaint(); + } + + @Override + public void stateChanged(ChangeEvent event) { + updateThumbBounds(); + // TODO: update mouse + isValueCached = false; + repaint(); + } + + @Override + public void propertyChange(PropertyChangeEvent event) { + String name = event.getPropertyName(); + if ("model" == name) { + BoundedRangeModel oldModel = (BoundedRangeModel)event.getOldValue(); + BoundedRangeModel newModel = (BoundedRangeModel)event.getNewValue(); + oldModel.removeChangeListener(this); + newModel.addChangeListener(this); + } + if ("model" == name || "orientation" == name || "componentOrientation" == name) { + repaint(); + } + } + + private void setValueFrom(MouseEvent event) { + int x = event.getX(); + int y = event.getY(); + + int thumbMin, thumbMax, thumbPos; + if (VERTICAL == myScrollBar.getOrientation()) { + thumbMin = myTrackBounds.y; + thumbMax = myTrackBounds.y + myTrackBounds.height - myThumbBounds.height; + thumbPos = Math.min(thumbMax, Math.max(thumbMin, (y - myOffset))); + if (myThumbBounds.y != thumbPos) { + int minY = Math.min(myThumbBounds.y, thumbPos); + int maxY = Math.max(myThumbBounds.y, thumbPos) + myThumbBounds.height; + myThumbBounds.y = thumbPos; + repaint(myThumbBounds.x, minY, myThumbBounds.width, maxY - minY); + } + } + else { + thumbMin = myTrackBounds.x; + thumbMax = myTrackBounds.x + myTrackBounds.width - myThumbBounds.width; + thumbPos = Math.min(thumbMax, Math.max(thumbMin, (x - myOffset))); + if (myThumbBounds.x != thumbPos) { + int minX = Math.min(myThumbBounds.x, thumbPos); + int maxX = Math.max(myThumbBounds.x, thumbPos) + myThumbBounds.width; + myThumbBounds.x = thumbPos; + repaint(minX, myThumbBounds.y, maxX - minX, myThumbBounds.height); + } + } + int valueMin = myScrollBar.getMinimum(); + int valueMax = myScrollBar.getMaximum() - myScrollBar.getVisibleAmount(); + // If the thumb has reached the end of the scrollbar, then just set the value to its maximum. + // Otherwise compute the value as accurately as possible. + boolean isDefaultOrientation = VERTICAL == myScrollBar.getOrientation() || myScrollBar.getComponentOrientation().isLeftToRight(); + if (thumbPos == thumbMax) { + myScrollBar.setValue(isDefaultOrientation ? valueMax : valueMin); + } + else { + int valueRange = valueMax - valueMin; + int thumbRange = thumbMax - thumbMin; + int thumbValue = isDefaultOrientation + ? thumbPos - thumbMin + : thumbMax - thumbPos; + isValueCached = true; + myCachedValue = valueMin + valueRange * thumbValue / thumbRange; + myScrollBar.setValue(myCachedValue); + } + if (!isDragging) updateMouse(x, y); + } + + private void startScrollTimerIfNecessary() { + if (!myScrollTimer.isRunning()) { + if (isReversed ? isMouseBeforeThumb() : isMouseAfterThumb()) { + myScrollTimer.start(); + } + } + } + + private boolean isMouseBeforeThumb() { + return VERTICAL == myScrollBar.getOrientation() + ? isMouseOnTop() + : myScrollBar.getComponentOrientation().isLeftToRight() + ? isMouseOnLeft() + : isMouseOnRight(); + } + + private boolean isMouseAfterThumb() { + return VERTICAL == myScrollBar.getOrientation() + ? isMouseOnBottom() + : myScrollBar.getComponentOrientation().isLeftToRight() + ? isMouseOnRight() + : isMouseOnLeft(); + } + + private boolean isMouseOnTop() { + return myMouseY < myThumbBounds.y; + } + + private boolean isMouseOnLeft() { + return myMouseX < myThumbBounds.x; + } + + private boolean isMouseOnRight() { + return myMouseX > myThumbBounds.x + myThumbBounds.width; + } + + private boolean isMouseOnBottom() { + return myMouseY > myThumbBounds.y + myThumbBounds.height; + } + + private void scroll(boolean reversed) { + int delta = myScrollBar.getBlockIncrement(reversed ? -1 : 1); + if (reversed) delta = -delta; + + int oldValue = myScrollBar.getValue(); + int newValue = oldValue + delta; + + if (delta > 0 && newValue < oldValue) { + newValue = myScrollBar.getMaximum(); + } + else if (delta < 0 && newValue > oldValue) { + newValue = myScrollBar.getMinimum(); + } + if (oldValue != newValue) { + myScrollBar.setValue(newValue); + } + } + } +} \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java b/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java new file mode 100644 index 000000000000..029c3b707d84 --- /dev/null +++ b/platform/platform-api/src/com/intellij/ui/components/DefaultScrollBarUI.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.components; + +import com.intellij.ui.Gray; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBScrollBar.Alignment; + +import java.awt.AlphaComposite; +import java.awt.Composite; +import java.awt.Graphics2D; +import javax.swing.JComponent; + +/** + * @author Sergey.Malenkov + */ +final class DefaultScrollBarUI extends AbstractScrollBarUI { + private static final JBColor THUMB_BACKGROUND = new JBColor(0x808080, 0x808080); + private static final JBColor THUMB_FOREGROUND = new JBColor(0x6E6E6E, 0x949494); + + private float myTrackValue; + private float myThumbValue; + + @Override + int getThickness() { + return scale(14); + } + + @Override + int getMinimalThickness() { + return scale(10); + } + + @Override + void onTrackHover(boolean hover) { + if (hover) { + myTrackAnimator.startForward(); + } + else { + myTrackAnimator.startBackward(); + } + } + + @Override + void onThumbHover(boolean hover) { + if (hover) { + myThumbAnimator.startForward(); + } + else { + myThumbAnimator.startBackward(); + } + } + + @Override + void paintTrack(Graphics2D g, int x, int y, int width, int height, JComponent c) { + Composite old = g.getComposite(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .1f * myTrackValue)); + g.setColor(Gray.x80); + g.fillRect(x, y, width, height); + g.setComposite(old); + } + + @Override + void paintThumb(Graphics2D g, int x, int y, int width, int height, JComponent c) { + Composite old = g.getComposite(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .4f + .1f * myThumbValue)); + if (!c.isOpaque()) { + Alignment alignment = Alignment.get(c); + if (alignment == Alignment.LEFT || alignment == Alignment.RIGHT) { + int offset = width - getMinimalThickness(); + if (offset > 0) { + offset *= 1 - myThumbValue; + width -= offset; + if (alignment == Alignment.RIGHT) x += offset; + } + } + else { + int offset = height - getMinimalThickness(); + if (offset > 0) { + offset *= 1 - myThumbValue; + height -= offset; + if (alignment == Alignment.BOTTOM) y += offset; + } + } + } + g.setColor(THUMB_BACKGROUND); + g.fillRect(x + 1, y + 1, width - 2, height - 2); + g.setColor(THUMB_FOREGROUND); + g.drawRect(x, y, width - 1, height - 1); + g.setComposite(old); + } + + private TwoWayAnimator myTrackAnimator = new TwoWayAnimator("ScrollBarTrack", 6, 300, 300, 1000) { + @Override + void onFrame(int frame, int maxFrame) { + myTrackValue = (float)frame / maxFrame; + setTrackVisible(frame > 0); + repaint(); + } + }; + + private TwoWayAnimator myThumbAnimator = new TwoWayAnimator("ScrollBarThumb", 5, 125, 300, 1000) { + @Override + void onFrame(int frame, int maxFrame) { + myThumbValue = (float)frame / maxFrame; + repaint(); + } + }; +} diff --git a/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java b/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java index 0ae88e5efe9c..e9a4b1bd4861 100644 --- a/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java +++ b/platform/platform-api/src/com/intellij/ui/components/JBScrollPane.java @@ -15,15 +15,20 @@ */ package com.intellij.ui.components; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.wm.IdeGlassPane; import com.intellij.ui.IdeBorderFactory; +import com.intellij.ui.components.JBScrollBar.Alignment; import com.intellij.util.ArrayUtil; import com.intellij.util.ReflectionUtil; import com.intellij.util.ui.ButtonlessScrollBarUI; +import com.intellij.util.ui.JBInsets; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.border.LineBorder; import javax.swing.plaf.ScrollBarUI; import javax.swing.plaf.ScrollPaneUI; @@ -67,7 +72,7 @@ public class JBScrollPane extends JScrollPane { Container vp = c.getParent(); if (vp instanceof JViewport) c = vp; } - + c = c.getParent(); if (!(c instanceof JScrollPane)) return null; @@ -77,10 +82,10 @@ public class JBScrollPane extends JScrollPane { private void init() { init(true); } - + private void init(boolean setupCorners) { - setLayout(new ScrollPaneLayout()); - + setLayout(Registry.is("ide.scroll.new.layout") ? new Layout() : new ScrollPaneLayout()); + if (setupCorners) { setupCorners(); } @@ -102,9 +107,21 @@ public class JBScrollPane extends JScrollPane { @Override public boolean isOptimizedDrawingEnabled() { + if (getLayout() instanceof Layout) { + return isOptimizedDrawingEnabledFor(getVerticalScrollBar()) && + isOptimizedDrawingEnabledFor(getHorizontalScrollBar()); + } return !myHasOverlayScrollbars; } + /** + * Returns {@code false} for visible translucent scroll bars, or {@code true} otherwise. + * It is needed to repaint translucent scroll bars on viewport repainting. + */ + private static boolean isOptimizedDrawingEnabledFor(JScrollBar bar) { + return bar == null || bar.isOpaque() || !bar.isVisible(); + } + private void updateViewportBorder() { setViewportBorder(new ViewportBorder(myViewportBorderWidth >= 0 ? myViewportBorderWidth : 1)); } @@ -138,8 +155,10 @@ public class JBScrollPane extends JScrollPane { // Now we let JScrollPane layout everything as necessary super.layout(); + if (layout instanceof Layout) return; + if (scrollLayout != null) { - // Now it's time to jump in and expand the viewport so it fits the whole area + // Now it's time to jump in and expand the viewport so it fits the whole area // (taking into consideration corners, headers and other stuff). myHasOverlayScrollbars = relayoutScrollbars( this, scrollLayout, @@ -154,7 +173,7 @@ public class JBScrollPane extends JScrollPane { private boolean relayoutScrollbars(@NotNull JComponent container, @NotNull ScrollPaneLayout layout, boolean forceRelayout) { JViewport viewport = layout.getViewport(); if (viewport == null) return false; - + JScrollBar vsb = layout.getVerticalScrollBar(); JScrollBar hsb = layout.getHorizontalScrollBar(); JViewport colHead = layout.getColumnHeader(); @@ -225,7 +244,7 @@ public class JBScrollPane extends JScrollPane { protected boolean isOverlaidScrollbar(@Nullable JScrollBar scrollbar) { if (!ButtonlessScrollBarUI.isMacOverlayScrollbarSupported()) return false; - + ScrollBarUI vsbUI = scrollbar == null ? null : scrollbar.getUI(); return vsbUI instanceof ButtonlessScrollBarUI && !((ButtonlessScrollBarUI)vsbUI).alwaysShowTrack(); } @@ -242,7 +261,9 @@ public class JBScrollPane extends JScrollPane { @Override public void updateUI() { - setUI(ButtonlessScrollBarUI.createNormal()); + setUI(!SystemInfo.isMac && Registry.is("ide.scroll.new.layout") + ? new DefaultScrollBarUI() + : ButtonlessScrollBarUI.createNormal()); } @Override @@ -327,4 +348,303 @@ public class JBScrollPane extends JScrollPane { lineColor = view.getBackground(); } } + + /** + * These client properties modify a scroll pane layout. + * Use the class object as a property key. + * + * @see #putClientProperty(Object, Object) + */ + public enum Flip { + NONE, VERTICAL, HORIZONTAL, BOTH + } + + /** + * ScrollPaneLayout implementation that supports + * ScrollBar flipping and non-opaque ScrollBars. + */ + private static class Layout extends ScrollPaneLayout { + private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0); + + @Override + public void layoutContainer(Container parent) { + JScrollPane pane = (JScrollPane)parent; + // Calculate inner bounds of the scroll pane + Rectangle bounds = new Rectangle(pane.getWidth(), pane.getHeight()); + JBInsets.removeFrom(bounds, pane.getInsets()); + // Determine positions of scroll bars on the scroll pane + Object property = pane.getClientProperty(Flip.class); + Flip flip = property instanceof Flip ? (Flip)property : Flip.NONE; + boolean hsbOnTop = flip == Flip.BOTH || flip == Flip.VERTICAL; + boolean vsbOnLeft = pane.getComponentOrientation().isLeftToRight() + ? flip == Flip.BOTH || flip == Flip.HORIZONTAL + : flip == Flip.NONE || flip == Flip.VERTICAL; + // If there's a visible row header remove the space it needs. + // The row header is treated as if it were fixed width, arbitrary height. + Rectangle rowHeadBounds = new Rectangle(bounds.x, 0, 0, 0); + if (rowHead != null && rowHead.isVisible()) { + rowHeadBounds.width = min(bounds.width, rowHead.getPreferredSize().width); + bounds.width -= rowHeadBounds.width; + if (vsbOnLeft) { + rowHeadBounds.x += bounds.width; + } + else { + bounds.x += rowHeadBounds.width; + } + } + // If there's a visible column header remove the space it needs. + // The column header is treated as if it were fixed height, arbitrary width. + Rectangle colHeadBounds = new Rectangle(0, bounds.y, 0, 0); + if (colHead != null && colHead.isVisible()) { + colHeadBounds.height = min(bounds.height, colHead.getPreferredSize().height); + bounds.height -= colHeadBounds.height; + if (hsbOnTop) { + colHeadBounds.y += bounds.height; + } + else { + bounds.y += colHeadBounds.height; + } + } + // If there's a JScrollPane.viewportBorder, remove the space it occupies + Border border = pane.getViewportBorder(); + Insets insets = border == null ? null : border.getBorderInsets(parent); + JBInsets.removeFrom(bounds, insets); + if (insets == null) insets = EMPTY_INSETS; + // At this point: + // colHeadBounds is correct except for its width and x + // rowHeadBounds is correct except for its height and y + // bounds - the space available for the viewport and scroll bars + // Once we're through computing the dimensions of these three parts + // we can go back and set the bounds for the corners and the dimensions of + // colHeadBounds.x, colHeadBounds.width, rowHeadBounds.y, rowHeadBounds.height. + boolean isEmpty = bounds.width < 0 || bounds.height < 0; + Component view = viewport == null ? null : viewport.getView(); + Dimension viewPreferredSize = view == null ? new Dimension() : view.getPreferredSize(); + Dimension viewportExtentSize = viewport == null ? new Dimension() : viewport.toViewCoordinates(bounds.getSize()); + // If the view is tracking the viewports width we don't bother with a horizontal scrollbar. + // If the view is tracking the viewports height we don't bother with a vertical scrollbar. + Scrollable scrollable = null; + boolean viewTracksViewportWidth = false; + boolean viewTracksViewportHeight = false; + // Don't bother checking the Scrollable methods if there is no room for the viewport, + // we aren't going to show any scroll bars in this case anyway. + if (!isEmpty && view instanceof Scrollable) { + scrollable = (Scrollable)view; + viewTracksViewportWidth = scrollable.getScrollableTracksViewportWidth(); + viewTracksViewportHeight = scrollable.getScrollableTracksViewportHeight(); + } + // If there's a vertical scroll bar and we need one, allocate space for it. + // A vertical scroll bar is considered to be fixed width, arbitrary height. + boolean vsbOpaque = false; + boolean vsbNeeded = false; + int vsbPolicy = pane.getVerticalScrollBarPolicy(); + if (!isEmpty && vsbPolicy != VERTICAL_SCROLLBAR_NEVER) { + vsbNeeded = vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS + || !viewTracksViewportHeight && viewPreferredSize.height > viewportExtentSize.height; + } + Rectangle vsbBounds = new Rectangle(0, bounds.y - insets.top, 0, 0); + if (vsb != null) { + vsbOpaque = vsb.isOpaque(); + if (vsbNeeded) { + adjustForVSB(bounds, insets, vsbBounds, vsbOpaque, vsbOnLeft); + if (vsbOpaque && viewport != null) { + viewportExtentSize = viewport.toViewCoordinates(bounds.getSize()); + } + } + } + // If there's a horizontal scroll bar and we need one, allocate space for it. + // A horizontal scroll bar is considered to be fixed height, arbitrary width. + boolean hsbOpaque = false; + boolean hsbNeeded = false; + int hsbPolicy = pane.getHorizontalScrollBarPolicy(); + if (!isEmpty && hsbPolicy != HORIZONTAL_SCROLLBAR_NEVER) { + hsbNeeded = hsbPolicy == HORIZONTAL_SCROLLBAR_ALWAYS + || !viewTracksViewportWidth && viewPreferredSize.width > viewportExtentSize.width; + } + Rectangle hsbBounds = new Rectangle(bounds.x - insets.left, 0, 0, 0); + if (hsb != null) { + hsbOpaque = hsb.isOpaque(); + if (hsbNeeded) { + adjustForHSB(bounds, insets, hsbBounds, hsbOpaque, hsbOnTop); + if (hsbOpaque && viewport != null) { + // If we added the horizontal scrollbar and reduced the vertical space + // we may have to add the vertical scrollbar, if that hasn't been done so already. + if (vsb != null && !vsbNeeded && vsbPolicy != VERTICAL_SCROLLBAR_NEVER) { + viewportExtentSize = viewport.toViewCoordinates(bounds.getSize()); + vsbNeeded = viewPreferredSize.height > viewportExtentSize.height; + if (vsbNeeded) adjustForVSB(bounds, insets, vsbBounds, vsbOpaque, vsbOnLeft); + } + } + } + } + // Set the size of the viewport first, and then recheck the Scrollable methods. + // Some components base their return values for the Scrollable methods on the size of the viewport, + // so that if we don't ask after resetting the bounds we may have gotten the wrong answer. + if (viewport != null) { + viewport.setBounds(bounds); + pane.setComponentZOrder(viewport, pane.getComponentCount() - 1); + if (scrollable != null && hsbOpaque && vsbOpaque) { + viewTracksViewportWidth = scrollable.getScrollableTracksViewportWidth(); + viewTracksViewportHeight = scrollable.getScrollableTracksViewportHeight(); + viewportExtentSize = viewport.toViewCoordinates(bounds.getSize()); + + boolean vsbNeededOld = vsbNeeded; + if (vsb != null && vsbPolicy == VERTICAL_SCROLLBAR_AS_NEEDED) { + boolean vsbNeededNew = !viewTracksViewportHeight && viewPreferredSize.height > viewportExtentSize.height; + if (vsbNeeded != vsbNeededNew) { + vsbNeeded = vsbNeededNew; + if (vsbNeeded) { + adjustForVSB(bounds, insets, vsbBounds, vsbOpaque, vsbOnLeft); + } + else if (vsbOpaque) { + bounds.width += vsbBounds.width; + } + if (vsbOpaque) viewportExtentSize = viewport.toViewCoordinates(bounds.getSize()); + } + } + boolean hsbNeededOld = hsbNeeded; + if (hsb != null && hsbPolicy == HORIZONTAL_SCROLLBAR_AS_NEEDED) { + boolean hsbNeededNew = !viewTracksViewportWidth && viewPreferredSize.width > viewportExtentSize.width; + if (hsbNeeded != hsbNeededNew) { + hsbNeeded = hsbNeededNew; + if (hsbNeeded) { + adjustForHSB(bounds, insets, hsbBounds, hsbOpaque, hsbOnTop); + } + else if (hsbOpaque) { + bounds.height += hsbBounds.height; + } + if (hsbOpaque && vsb != null && !vsbNeeded && vsbPolicy != VERTICAL_SCROLLBAR_NEVER) { + viewportExtentSize = viewport.toViewCoordinates(bounds.getSize()); + vsbNeeded = viewPreferredSize.height > viewportExtentSize.height; + if (vsbNeeded) adjustForVSB(bounds, insets, vsbBounds, vsbOpaque, vsbOnLeft); + } + } + } + if (hsbNeededOld != hsbNeeded || vsbNeededOld != vsbNeeded) { + viewport.setBounds(bounds); + // You could argue that we should recheck the Scrollable methods again until they stop changing, + // but they might never stop changing, so we stop here and don't do any additional checks. + } + } + } + // Set the bounds of the row header. + rowHeadBounds.y = bounds.y - insets.top; + rowHeadBounds.height = bounds.height + insets.top + insets.bottom; + if (rowHead != null) { + rowHead.setBounds(rowHeadBounds); + rowHead.putClientProperty(Alignment.class, vsbOnLeft ? Alignment.RIGHT : Alignment.LEFT); + } + // Set the bounds of the column header. + colHeadBounds.x = bounds.x - insets.left; + colHeadBounds.width = bounds.width + insets.left + insets.right; + if (colHead != null) { + colHead.setBounds(colHeadBounds); + colHead.putClientProperty(Alignment.class, hsbOnTop ? Alignment.BOTTOM : Alignment.TOP); + } + // Set the bounds of the vertical scroll bar. + vsbBounds.y = bounds.y - insets.top; + vsbBounds.height = bounds.height + insets.top + insets.bottom; + if (vsb != null) { + vsb.setVisible(vsbNeeded); + if (vsbNeeded) { + if (vsbOpaque && colHead != null && UIManager.getBoolean("ScrollPane.fillUpperCorner")) { + if ((vsbOnLeft ? upperLeft : upperRight) == null) { + // This is used primarily for GTK L&F, which needs to extend + // the vertical scrollbar to fill the upper corner near the column header. + // Note that we skip this step (and use the default behavior) + // if the user has set a custom corner component. + if (!hsbOnTop) vsbBounds.y -= colHeadBounds.height; + vsbBounds.height += colHeadBounds.height; + } + } + vsb.setBounds(vsbBounds); + vsb.putClientProperty(Alignment.class, vsbOnLeft ? Alignment.LEFT : Alignment.RIGHT); + pane.setComponentZOrder(vsb, 0); + } + // Modify the bounds of the translucent scroll bar. + if (!vsbOpaque) { + if (!vsbOnLeft) vsbBounds.x += vsbBounds.width; + vsbBounds.width = 0; + } + } + // Set the bounds of the horizontal scroll bar. + hsbBounds.x = bounds.x - insets.left; + hsbBounds.width = bounds.width + insets.left + insets.right; + if (hsb != null) { + hsb.setVisible(hsbNeeded); + if (hsbNeeded) { + if (hsbOpaque && rowHead != null && UIManager.getBoolean("ScrollPane.fillLowerCorner")) { + if ((vsbOnLeft ? lowerRight : lowerLeft) == null) { + // This is used primarily for GTK L&F, which needs to extend + // the horizontal scrollbar to fill the lower corner near the row header. + // Note that we skip this step (and use the default behavior) + // if the user has set a custom corner component. + if (!vsbOnLeft) hsbBounds.x -= rowHeadBounds.width; + hsbBounds.width += rowHeadBounds.width; + } + } + hsb.setBounds(hsbBounds); + hsb.putClientProperty(Alignment.class, hsbOnTop ? Alignment.TOP : Alignment.BOTTOM); + } + // Modify the bounds of the translucent scroll bar. + if (!hsbOpaque) { + if (!hsbOnTop) hsbBounds.y += hsbBounds.height; + hsbBounds.height = 0; + } + } + // Set the bounds of the corners. + if (lowerLeft != null) { + lowerLeft.setBounds(vsbOnLeft ? vsbBounds.x : rowHeadBounds.x, + hsbOnTop ? colHeadBounds.y : hsbBounds.y, + vsbOnLeft ? vsbBounds.width : rowHeadBounds.width, + hsbOnTop ? colHeadBounds.height : hsbBounds.height); + } + if (lowerRight != null) { + lowerRight.setBounds(vsbOnLeft ? rowHeadBounds.x : vsbBounds.x, + hsbOnTop ? colHeadBounds.y : hsbBounds.y, + vsbOnLeft ? rowHeadBounds.width : vsbBounds.width, + hsbOnTop ? colHeadBounds.height : hsbBounds.height); + } + if (upperLeft != null) { + upperLeft.setBounds(vsbOnLeft ? vsbBounds.x : rowHeadBounds.x, + hsbOnTop ? hsbBounds.y : colHeadBounds.y, + vsbOnLeft ? vsbBounds.width : rowHeadBounds.width, + hsbOnTop ? hsbBounds.height : colHeadBounds.height); + } + if (upperRight != null) { + upperRight.setBounds(vsbOnLeft ? rowHeadBounds.x : vsbBounds.x, + hsbOnTop ? hsbBounds.y : colHeadBounds.y, + vsbOnLeft ? rowHeadBounds.width : vsbBounds.width, + hsbOnTop ? hsbBounds.height : colHeadBounds.height); + } + } + + private void adjustForVSB(Rectangle bounds, Insets insets, Rectangle vsbBounds, boolean vsbOpaque, boolean vsbOnLeft) { + vsbBounds.width = min(bounds.width, vsb.getPreferredSize().width); + if (vsbOnLeft) { + vsbBounds.x = bounds.x - insets.left/* + vsbBounds.width*/; + if (vsbOpaque) bounds.x += vsbBounds.width; + } + else { + vsbBounds.x = bounds.x + bounds.width + insets.right - vsbBounds.width; + } + if (vsbOpaque) bounds.width -= vsbBounds.width; + } + + private void adjustForHSB(Rectangle bounds, Insets insets, Rectangle hsbBounds, boolean hsbOpaque, boolean hsbOnTop) { + hsbBounds.height = min(bounds.height, hsb.getPreferredSize().height); + if (hsbOnTop) { + hsbBounds.y = bounds.y - insets.top/* + hsbBounds.height*/; + if (hsbOpaque) bounds.y += hsbBounds.height; + } + else { + hsbBounds.y = bounds.y + bounds.height + insets.bottom - hsbBounds.height; + } + if (hsbOpaque) bounds.height -= hsbBounds.height; + } + + private static int min(int one, int two) { + return Math.max(0, Math.min(one, two)); + } + } } diff --git a/platform/platform-api/src/com/intellij/ui/components/TwoWayAnimator.java b/platform/platform-api/src/com/intellij/ui/components/TwoWayAnimator.java new file mode 100644 index 000000000000..03b5172a80e2 --- /dev/null +++ b/platform/platform-api/src/com/intellij/ui/components/TwoWayAnimator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.components; + +import com.intellij.util.Alarm; +import com.intellij.util.ui.Animator; + +/** + * @author Sergey.Malenkov + */ +abstract class TwoWayAnimator { + private final Alarm myAlarm = new Alarm(); + private final MyAnimator myForwardAnimator; + private final MyAnimator myBackwardAnimator; + + private final int myMaxFrame; + private int myFrame; + + abstract void onFrame(int frame, int maxFrame); + + TwoWayAnimator(String name, int totalFrames, int cycleDuration, int pauseForward, int pauseBackward) { + myMaxFrame = totalFrames - 1; + myForwardAnimator = new MyAnimator(name + "ForwardAnimator", totalFrames, cycleDuration, pauseForward, true); + myBackwardAnimator = new MyAnimator(name + "BackwardAnimator", totalFrames, cycleDuration, pauseBackward, false); + } + + void startForward() { + stop(); + myForwardAnimator.start(); + } + + void startBackward() { + stop(); + myBackwardAnimator.start(); + } + + private void stop() { + myAlarm.cancelAllRequests(); + myForwardAnimator.suspend(); + myBackwardAnimator.suspend(); + } + + private final class MyAnimator extends Animator implements Runnable { + private final int myPause; + + private MyAnimator(String name, int totalFrames, int cycleDuration, int pause, boolean forward) { + super(name, totalFrames, cycleDuration, false, forward); + myPause = pause; + } + + private void start() { + if (isForward() ? myFrame > 0 : myFrame < myMaxFrame) { + run(); + } + else { + myAlarm.addRequest(this, myPause); + } + } + + @Override + public void run() { + reset(); + resume(); + } + + @Override + public void paintNow(int frame, int totalFrames, int cycle) { + if (isForward() ? (frame > myFrame) : (frame < myFrame)) { + myFrame = frame; + onFrame(myFrame, myMaxFrame); + } + } + } +} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 47ced80e09f2..2272a8c1df7e 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -110,6 +110,7 @@ ide.tree.deferred.icon.invalidates.cache=true ide.splitter.mouseZone=6 ide.smart.horizontal.scrolling=true +ide.scroll.new.layout=false ide.tooltip.callout=true ide.tooltip.animationCycle=150