IDEA-149395 New look for scrollbars on Windows and Linux

This commit is contained in:
Sergey Malenkov
2015-12-30 20:12:31 +03:00
parent 7e469f595a
commit c3c762a5f3
5 changed files with 1047 additions and 8 deletions
@@ -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<RegionPainter<Object>> 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<Object> 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<Object> 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);
}
}
}
}
@@ -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();
}
};
}
@@ -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));
}
}
}
@@ -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);
}
}
}
}
@@ -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