00892e4c57e8a57c22428a09b0310fa2876b448d
[idea/community.git] / platform / platform-api / src / com / intellij / util / ui / ButtonlessScrollBarUI.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.util.ui;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.Disposer;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.intellij.openapi.util.registry.Registry;
22 import com.intellij.ui.Gray;
23 import com.intellij.ui.JBColor;
24 import com.intellij.ui.LightColors;
25 import com.intellij.ui.components.JBScrollPane;
26 import com.intellij.util.Alarm;
27 import com.intellij.util.NotNullProducer;
28 import com.intellij.util.ReflectionUtil;
29 import org.jetbrains.annotations.NotNull;
30
31 import javax.swing.*;
32 import javax.swing.event.ChangeEvent;
33 import javax.swing.plaf.ScrollBarUI;
34 import javax.swing.plaf.basic.BasicScrollBarUI;
35 import java.awt.*;
36 import java.awt.event.*;
37 import java.lang.reflect.Method;
38
39 /**
40  * @author max
41  * @author Konstantin Bulenkov
42  */
43 public class ButtonlessScrollBarUI extends BasicScrollBarUI {
44   private static final Logger LOG = Logger.getInstance("#" + ButtonlessScrollBarUI.class.getName());
45
46   protected JBColor getGradientLightColor() {
47     return jbColor(Gray._251, Gray._95);
48   }
49
50   protected JBColor getGradientDarkColor() {
51     return jbColor(Gray._215, Gray._80);
52   }
53
54   private JBColor getGradientThumbBorderColor() {
55     return jbColor(Gray._201, Gray._85);
56   }
57
58   public static JBColor getTrackBackgroundDefault() {
59     return new JBColor(LightColors.SLIGHTLY_GRAY, UIUtil.getListBackground());
60   }
61
62   public static JBColor getTrackBorderColorDefault() {
63     return new JBColor(Gray._230, UIUtil.getListBackground());
64   }
65
66   private JBColor getTrackBackground() {
67     return jbColor(LightColors.SLIGHTLY_GRAY, UIUtil.getListBackground());
68   }
69   
70   private JBColor getTrackBorderColor() {
71     return jbColor(Gray._230, UIUtil.getListBackground());
72   }
73
74   private static final BasicStroke BORDER_STROKE = new BasicStroke();
75   
76   private JBColor jbColor(final Color regular, final Color dark) {
77     return new JBColor(new NotNullProducer<Color>() {
78       @NotNull
79       @Override
80       public Color produce() {
81         return isDark() ? dark : regular;
82       }
83     });
84   }
85
86   private int getAnimationColorShift() {
87     return isDark() ? 20 : 40;
88   }
89
90   private final AdjustmentListener myAdjustmentListener;
91   private final MouseMotionAdapter myMouseMotionListener;
92   private final MouseAdapter myMouseListener;
93   private final HierarchyListener myHierarchyListener;
94   private final AWTEventListener myAWTMouseListener;
95   private final NSScrollerHelper.ScrollbarStyleListener myNSScrollerListener;
96   private boolean myGlobalListenersAdded;
97
98   public static final int DELAY_FRAMES = 4;
99   public static final int FRAMES_COUNT = 10 + DELAY_FRAMES;
100
101   private Animator myThumbFadeAnimator;
102   private int myThumbFadeColorShift = 0;
103  
104   private boolean myMouseIsOverThumb = false;
105   private boolean myMouseOverScrollbar;
106   private double myMouseOverScrollbarExpandLevel = 0;
107
108   private NSScrollerHelper.Style myMacScrollerStyle;
109   private Animator myMouseOverScrollbarExpandAnimator;
110   private Alarm myMacScrollbarFadeTimer;
111   private Animator myMacScrollbarFadeAnimator;
112   private double myMacScrollbarFadeLevel = 0;
113   private boolean myMacScrollbarHidden;
114
115   private ScrollbarRepaintCallback myRepaintCallback;
116
117   protected ButtonlessScrollBarUI() {
118     myAdjustmentListener = new AdjustmentListener() {
119       Point oldViewportPosition = null;
120       Dimension oldViewportDimension = null;
121
122       @Override
123       public void adjustmentValueChanged(AdjustmentEvent e) {
124         JScrollPane scrollpane = (JScrollPane)SwingUtilities.getAncestorOfClass(JScrollPane.class, scrollbar);
125         JViewport viewport = scrollpane == null ? null : scrollpane.getViewport();
126
127         if (viewport == null) {
128           oldViewportPosition = null;
129           return;
130         }
131
132         boolean vertical = isVertical();
133         Point position = viewport.getViewPosition();
134         // we don't take viewport's size here since it often changes on scrollbar appearance.
135         // instead, we want to only react on visible area resizes
136         Dimension dimension = scrollpane.getSize();
137
138         boolean scrolled = false;
139         if (oldViewportPosition != null) {
140           int scrollH = position.x - oldViewportPosition.x;
141           int scrollV = position.y - oldViewportPosition.y;
142           scrolled = vertical && scrollH == 0 && scrollV != 0 ||
143                      !vertical && scrollV == 0 && scrollH != 0;
144         }
145         oldViewportPosition = position;
146
147         boolean resized = false;
148         if (oldViewportDimension != null) {
149           int resizedH = dimension.width - oldViewportDimension.width;
150           int resizedV = dimension.height - oldViewportDimension.height;
151           resized = vertical && resizedV != 0 || !vertical && resizedH != 0;
152         }
153         oldViewportDimension = dimension;
154         
155         if (scrolled) {
156           // hide the opposite scrollbar when user scrolls 
157           JScrollBar other = vertical ? scrollpane.getHorizontalScrollBar()
158                                       : scrollpane.getVerticalScrollBar();
159           ScrollBarUI otherUI = other == null ? null : other.getUI();
160           if (otherUI instanceof ButtonlessScrollBarUI) {
161             ((ButtonlessScrollBarUI)otherUI).startMacScrollbarFadeout(true);
162           }
163
164           restart();
165         }
166         else if (resized) {
167           startMacScrollbarFadeout();
168         }
169       }
170     };
171
172     myMouseMotionListener = new MouseMotionAdapter() {
173       @Override
174       public void mouseMoved(MouseEvent e) {
175         boolean inside = isOverThumb(e.getPoint());
176         if (inside != myMouseIsOverThumb) {
177           myMouseIsOverThumb = inside;
178           startRegularThumbAnimator();
179         }
180       }
181     };
182
183     myMouseListener = new MouseAdapter() {
184       @Override
185       public void mouseEntered(MouseEvent e) {
186         // only restart animations when fading hasn't started yet
187         if (myMacScrollbarFadeLevel == 0) {
188           myMouseOverScrollbar = true;
189           startMacScrollbarExpandAnimator();
190           startMacScrollbarFadeout();
191         }
192       }
193       
194       @Override
195       public void mouseExited(MouseEvent e) {
196         if (myMouseIsOverThumb) {
197           myMouseIsOverThumb = false;
198           startRegularThumbAnimator();
199         }
200
201         if (myMouseOverScrollbar) {
202           myMouseOverScrollbar = false;
203           startMacScrollbarExpandAnimator();
204           startMacScrollbarFadeout();
205         }
206       }
207     };
208
209     myHierarchyListener = new HierarchyListener() {
210       @Override
211       public void hierarchyChanged(HierarchyEvent e) {
212         if (e.getChanged() == scrollbar) {
213           // scrollbar is added to the screen resources
214           if ((HierarchyEvent.DISPLAYABILITY_CHANGED & e.getChangeFlags()) != 0) {
215             updateGlobalListeners(false);
216           }
217         }
218
219         if (e.getChanged() == scrollbar.getParent()) {
220           // when scrollpane is shown first time, we 'blink' the scrollbars
221           if ((HierarchyEvent.SHOWING_CHANGED & e.getChangeFlags()) != 0) {
222             restart();
223           }
224         }
225       }
226     };
227     myAWTMouseListener = new AWTEventListener() {
228       public void eventDispatched(AWTEvent event) {
229         if (event.getID() == MouseEvent.MOUSE_MOVED) {
230           
231             // user is moving inside the scrollpane of the scrollbar and fade-out hasn't started yet 
232           Container scrollpane = SwingUtilities.getAncestorOfClass(JScrollPane.class, scrollbar);
233           if (scrollpane != null) {
234             Point loc = ((MouseEvent)event).getLocationOnScreen();
235             SwingUtilities.convertPointFromScreen(loc, scrollpane);
236             if (scrollpane.contains(loc) && !myMacScrollbarHidden && myMacScrollbarFadeLevel == 0) {
237               startMacScrollbarFadeout();
238             }
239           }
240         }
241       }
242     };
243     myNSScrollerListener = new NSScrollerHelper.ScrollbarStyleListener() {
244       @Override
245       public void styleChanged() {
246         updateMacScrollbarStyle();
247       }
248     };
249   }
250
251   @Override
252   protected ArrowButtonListener createArrowButtonListener() {
253     return new ArrowButtonListener() {
254       @Override
255       public void mousePressed(MouseEvent event) {
256       }
257
258       @Override
259       public void mouseReleased(MouseEvent event) {
260       }
261     };
262   }
263
264   protected boolean isMacOverlayScrollbar() {
265     return myMacScrollerStyle == NSScrollerHelper.Style.Overlay && isMacOverlayScrollbarSupported();
266   }
267   
268   public static boolean isMacOverlayScrollbarSupported() {
269     return SystemInfo.isMac && !Registry.is("ide.mac.disableMacScrollbars");
270   }
271
272   private void updateMacScrollbarStyle() {
273     NSScrollerHelper.Style style = NSScrollerHelper.getScrollerStyle();
274
275     if (style != myMacScrollerStyle && scrollbar != null) {
276       myMacScrollerStyle = style;
277
278       updateStyleDefaults();
279       restart();
280       
281       JScrollPane pane = JBScrollPane.findScrollPane(scrollbar);
282       if (pane != null) pane.revalidate();
283     }
284   }
285
286   public boolean alwaysShowTrack() {
287     return !isMacOverlayScrollbar();
288   }
289
290   @Override
291   public void layoutContainer(Container scrollbarContainer) {
292     try {
293       super.layoutContainer(scrollbarContainer);
294     }
295     catch (NullPointerException ignore) {
296       //installUI is not performed yet or uninstallUI has set almost every field to null. Just ignore it //IDEA-89674
297     }
298   }
299
300   /**
301    * This is overridden only to increase the invalid area.
302    * This ensures that whole track will be repainted in case of installed callback
303    */
304   @Override
305   protected void setThumbBounds(int x, int y, int width, int height) {
306     if (width > 0 && height > 0 && UIManager.getBoolean("ScrollBar.alwaysShowThumb") && !alwaysShowTrack()) {
307       int w = scrollbar.getWidth(), h = scrollbar.getHeight();
308       if (w > h && w == width || w < h && h == height) {
309         x = y = width = height = 0;
310       }
311     }
312
313     if (myRepaintCallback == null) {
314       super.setThumbBounds(x, y, width, height);
315     }
316     else {
317       // We want to repaint whole scrollbar even if thumb wasn't moved (on small scroll of a big panel)
318       // Even if scrollbar wasn't changed itself, myRepaintCallback could need repaint
319
320       // Update thumbRect, and repaint the union of x,y,w,h and the old thumbRect.
321       int minX = Math.min(x, trackRect.x);
322       int minY = Math.min(y, trackRect.y);
323       int maxX = Math.max(x + width, trackRect.x + trackRect.width);
324       int maxY = Math.max(y + height, trackRect.y + trackRect.height);
325
326       thumbRect.setBounds(x, y, width, height);
327       scrollbar.repaint(minX, minY, maxX - minX, maxY - minY);
328
329       // Once there is API to determine the mouse location this will need to be changed.
330       setThumbRollover(false);
331     }
332   }
333
334   @Override
335   protected ModelListener createModelListener() {
336     return new ModelListener() {
337       @Override
338       public void stateChanged(ChangeEvent e) {
339         if (scrollbar != null) {
340           super.stateChanged(e);
341         }
342       }
343     };
344   }
345
346   public int getDecrementButtonHeight() {
347     return Math.max(0, decrButton.getHeight());
348   }
349   public int getIncrementButtonHeight() {
350     return Math.max(0, incrButton.getHeight());
351   }
352
353   private void startRegularThumbAnimator() {
354     if (isMacOverlayScrollbar()) return;
355     
356     myThumbFadeAnimator.reset();
357     if (scrollbar != null && scrollbar.getValueIsAdjusting() || myMouseIsOverThumb || Registry.is("ui.no.bangs.and.whistles")) {
358       myThumbFadeAnimator.suspend();
359       myThumbFadeColorShift = getAnimationColorShift();
360     }
361     else {
362       myThumbFadeAnimator.resume();
363     }
364   }
365
366   private void startMacScrollbarExpandAnimator() {
367     if (!isMacOverlayScrollbar()) return;
368     
369     if (myMouseOverScrollbarExpandLevel == 0) {
370       myMouseOverScrollbarExpandAnimator.reset();
371       myMouseOverScrollbarExpandAnimator.suspend();
372       if (myMouseOverScrollbar) {
373         myMouseOverScrollbarExpandAnimator.resume();
374       }
375     }
376   }
377
378   private void startMacScrollbarFadeout() {
379     startMacScrollbarFadeout(false);
380   }
381
382   private void startMacScrollbarFadeout(boolean now) {
383     if (!isMacOverlayScrollbar()) return;
384
385     myMacScrollbarFadeTimer.cancelAllRequests();
386
387     if (now) {
388       if (!myMacScrollbarHidden && !myMacScrollbarFadeAnimator.isRunning()) {
389         myMacScrollbarFadeAnimator.resume();
390       }
391       return;
392     }
393
394     myMacScrollbarFadeAnimator.suspend();
395     myMacScrollbarFadeAnimator.reset();
396     myMacScrollbarHidden = false;
397     myMacScrollbarFadeLevel = 0;
398
399     JScrollBar sb = scrollbar; // concurrency in background editors initialization
400     if (sb != null) {
401       sb.repaint();
402
403       if (!myMouseOverScrollbar && !sb.getValueIsAdjusting()) {
404         myMacScrollbarFadeTimer.addRequest(new Runnable() {
405           @Override
406           public void run() {
407             myMacScrollbarFadeAnimator.resume();
408           }
409         }, 700, null);
410       }
411     }
412   }
413
414   public static BasicScrollBarUI createNormal() {
415     return new ButtonlessScrollBarUI();
416   }
417
418   public static BasicScrollBarUI createTransparent() {
419     return new ButtonlessScrollBarUI() {
420       @Override
421       public boolean alwaysShowTrack() {
422         return false;
423       }
424     };
425   }
426
427   @Override
428   protected void installDefaults() {
429     final int incGap = UIManager.getInt("ScrollBar.incrementButtonGap");
430     final int decGap = UIManager.getInt("ScrollBar.decrementButtonGap");
431     try {
432       UIManager.put("ScrollBar.incrementButtonGap", 0);
433       UIManager.put("ScrollBar.decrementButtonGap", 0);
434       super.installDefaults();
435     }
436     finally {
437       UIManager.put("ScrollBar.incrementButtonGap", incGap);
438       UIManager.put("ScrollBar.decrementButtonGap", decGap);
439     }
440
441     myMacScrollerStyle = NSScrollerHelper.getScrollerStyle();
442     scrollbar.setFocusable(false);
443     updateStyleDefaults();
444   }
445
446   private void updateStyleDefaults() {
447     scrollbar.setOpaque(alwaysShowTrack());
448   }
449
450   @Override
451   protected void installListeners() {
452     initRegularThumbAnimator();
453     initMacScrollbarAnimators();
454
455     super.installListeners();
456     scrollbar.addAdjustmentListener(myAdjustmentListener);
457     scrollbar.addMouseListener(myMouseListener);
458     scrollbar.addMouseMotionListener(myMouseMotionListener);
459  
460     scrollbar.addHierarchyListener(myHierarchyListener);
461     updateGlobalListeners(false);
462
463     restart();
464   }
465
466   private void restart() {
467     startRegularThumbAnimator();
468     startMacScrollbarFadeout();
469   }
470
471   private static final Method setValueFrom = ReflectionUtil.getDeclaredMethod(TrackListener.class, "setValueFrom", MouseEvent.class);
472   static {
473     LOG.assertTrue(setValueFrom != null, "Cannot get TrackListener.setValueFrom method");
474   }
475
476   @Override
477   protected TrackListener createTrackListener() {
478     return new TrackListener() {
479       @Override
480       public void mousePressed(MouseEvent e) {
481         if (scrollbar.isEnabled()
482             && SwingUtilities.isLeftMouseButton(e)
483             && !getThumbBounds().contains(e.getPoint())
484             && NSScrollerHelper.getClickBehavior() == NSScrollerHelper.ClickBehavior.JumpToSpot
485             && setValueFrom != null) {
486
487           switch (scrollbar.getOrientation()) {
488             case Adjustable.VERTICAL:
489               offset = getThumbBounds().height / 2;
490               break;
491             case Adjustable.HORIZONTAL:
492               offset = getThumbBounds().width / 2;
493               break;
494           }
495           isDragging = true;
496           try {
497             setValueFrom.invoke(this, e);
498           }
499           catch (Exception ex) {
500             LOG.error(ex);
501           }
502
503           return;
504         }
505
506         super.mousePressed(e);
507       }
508     };
509   }
510   
511   private void updateGlobalListeners(boolean forceRemove) {
512     boolean shouldAdd = scrollbar.isDisplayable();
513
514     if (myGlobalListenersAdded && (!shouldAdd || forceRemove)) {
515       Toolkit.getDefaultToolkit().removeAWTEventListener(myAWTMouseListener);
516       NSScrollerHelper.removeScrollbarStyleListener(myNSScrollerListener);
517       myGlobalListenersAdded = false;
518     }
519
520     if (!myGlobalListenersAdded && shouldAdd && !forceRemove) {
521       Toolkit.getDefaultToolkit().addAWTEventListener(myAWTMouseListener, AWTEvent.MOUSE_MOTION_EVENT_MASK);
522       NSScrollerHelper.addScrollbarStyleListener(myNSScrollerListener);
523       myGlobalListenersAdded = true;
524     }
525   }
526   
527   private void initRegularThumbAnimator() {
528     myThumbFadeAnimator = new Animator("Regular scrollbar thumb animator", FRAMES_COUNT, FRAMES_COUNT * 50, false) {
529       @Override
530       public void paintNow(int frame, int totalFrames, int cycle) {
531         myThumbFadeColorShift = getAnimationColorShift();
532         if (frame > DELAY_FRAMES) {
533           myThumbFadeColorShift *= 1 - (double)(frame - DELAY_FRAMES) / (double)(totalFrames - DELAY_FRAMES);
534         }
535
536         if (scrollbar != null) {
537           scrollbar.repaint(((ButtonlessScrollBarUI)scrollbar.getUI()).getThumbBounds());
538         }
539       }
540     };
541   }
542
543   private void initMacScrollbarAnimators() {
544     myMouseOverScrollbarExpandAnimator = new Animator("Mac scrollbar mouse over animator", 10, 200, false) {
545       @Override
546       protected void paintCycleEnd() {
547         myMouseOverScrollbarExpandLevel = 1;
548         if (scrollbar != null) scrollbar.repaint();
549       }
550
551       @Override
552       public void paintNow(int frame, int totalFrames, int cycle) {
553         int delay = totalFrames / 2;
554         int frameAfterDelay = frame - delay;
555
556         if (frameAfterDelay > 0) {
557           myMouseOverScrollbarExpandLevel = frameAfterDelay / (float)(totalFrames - delay);
558           if (scrollbar != null) scrollbar.repaint();
559         }
560       }
561     };
562
563     myMacScrollbarFadeTimer = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
564     myMacScrollbarFadeAnimator = new Animator("Mac scrollbar fade animator", 30, 300, false) {
565       @Override
566       protected void paintCycleEnd() {
567         myMacScrollbarHidden = true;
568         myMouseOverScrollbar = false;
569         myMouseOverScrollbarExpandLevel = 0;
570
571         if (scrollbar != null) scrollbar.repaint();
572       }
573
574       @Override
575       public void paintNow(int frame, int totalFrames, int cycle) {
576         myMacScrollbarFadeLevel = frame / (float)totalFrames;
577         if (scrollbar != null) scrollbar.repaint();
578       }
579     };
580   }
581
582   private boolean isOverThumb(Point p) {
583     final Rectangle bounds = getThumbBounds();
584     return bounds != null && bounds.contains(p);
585   }
586
587   @Override
588   public Rectangle getThumbBounds() {
589     return super.getThumbBounds();
590   }
591
592   @Override
593   protected void uninstallListeners() {
594     if (scrollTimer != null) {
595       // it is already called otherwise
596       super.uninstallListeners();
597     }
598
599     scrollbar.removeAdjustmentListener(myAdjustmentListener);
600     scrollbar.removeMouseListener(myMouseListener);
601     scrollbar.removeMouseMotionListener(myMouseMotionListener);
602
603     scrollbar.removeHierarchyListener(myHierarchyListener);
604     updateGlobalListeners(true);
605
606     Disposer.dispose(myThumbFadeAnimator);
607     Disposer.dispose(myMouseOverScrollbarExpandAnimator);
608     Disposer.dispose(myMacScrollbarFadeTimer);
609     Disposer.dispose(myMacScrollbarFadeAnimator);
610   }
611
612   @Override
613   protected Dimension getMinimumThumbSize() {
614     final int thickness = getThickness();
615     return isVertical() ? new Dimension(thickness, thickness * 2) : new Dimension(thickness * 2, thickness);
616   }
617
618   protected int getThickness() {
619     return isMacOverlayScrollbar() ? JBUI.scale(15) : JBUI.scale(13);
620   }
621
622   @Override
623   public Dimension getMaximumSize(JComponent c) {
624     int thickness = getThickness();
625     return new Dimension(thickness, thickness);
626   }
627
628   @Override
629   public Dimension getMinimumSize(JComponent c) {
630     return getMaximumSize(c);
631   }
632
633   @Override
634   public Dimension getPreferredSize(JComponent c) {
635     return getMaximumSize(c);
636   }
637   
638   @Override
639   public boolean contains(JComponent c, int x, int y) {
640     if (isMacOverlayScrollbar() && !alwaysShowTrack() && !alwaysPaintThumb() && myMacScrollbarHidden) return false;  
641     return super.contains(c, x, y);
642   }
643
644   @Override
645   protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
646     if (alwaysShowTrack() || myMouseOverScrollbarExpandLevel > 0) {
647       doPaintTrack(g, c, trackBounds);
648     }
649   }
650
651   protected void doPaintTrack(Graphics g, JComponent c, Rectangle bounds) {
652     if (isMacOverlayScrollbar() && !alwaysShowTrack()) {
653       bounds = getMacScrollBarBounds(bounds, false);
654       boolean vertical = isVertical();
655
656       final Paint paint;
657       final Color start = adjustColor(UIUtil.getSlightlyDarkerColor(getTrackBackground()));
658       final Color end = adjustColor(getTrackBackground().brighter());
659
660       if (vertical) {
661         paint = UIUtil.getGradientPaint(bounds.x + 1, bounds.y, start, bounds.width + 1, bounds.y, end);
662       }
663       else {
664         paint = UIUtil.getGradientPaint(bounds.x, bounds.y + 1, start, bounds.x, bounds.height + 1, end);
665       }
666
667       Graphics2D g2d = (Graphics2D)g;
668       g2d.setPaint(paint);
669       g2d.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
670
671       g.setColor(adjustColor(start.darker()));
672     }
673     else {
674       g.setColor(getTrackBackground());
675       g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
676
677       g.setColor(getTrackBorderColor());
678     }
679
680     if (isVertical()) {
681       g.drawLine(bounds.x, bounds.y, bounds.x, bounds.y + bounds.height);
682     }
683     else {
684       g.drawLine(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y);
685     }
686
687     if (myRepaintCallback != null) {
688       myRepaintCallback.call(g);
689     }
690   }
691
692   @Override
693   protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
694     doPaintThumb(g, thumbBounds);
695   }
696
697   private void doPaintThumb(Graphics g, Rectangle thumbBounds) {
698     if (thumbBounds.isEmpty() || !scrollbar.isEnabled()) {
699       return;
700     }
701
702     if (isMacOverlayScrollbar()) {
703       paintMacThumb(g, thumbBounds);
704     }
705     else {
706       g.translate(thumbBounds.x, thumbBounds.y);
707       paintMaxiThumb((Graphics2D)g, thumbBounds);
708       g.translate(-thumbBounds.x, -thumbBounds.y);
709     }
710   }
711
712   private void paintMacThumb(Graphics g, Rectangle thumbBounds) {
713     if (isMacScrollbarHiddenAndXcodeLikeScrollbar()) return;
714
715     thumbBounds = getMacScrollBarBounds(thumbBounds, true);
716     Graphics2D g2d = (Graphics2D)g;
717     RenderingHints oldHints = g2d.getRenderingHints();
718     g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
719
720     JBColor baseColor = new JBColor(new NotNullProducer<Color>() {
721       @NotNull
722       @Override
723       public Color produce() {
724         return !isDark() ? Gray._0 : Gray._128;
725       }
726     });
727     
728     int arc = Math.min(thumbBounds.width, thumbBounds.height);
729
730     if (alwaysPaintThumb()) {
731       //noinspection UseJBColor
732       g2d.setColor(new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), isDark() ? 100 : 40));
733       g2d.fillRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
734       //g2d.drawRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
735     }
736
737     if (!myMacScrollbarHidden) {
738       g2d.setColor(adjustColor(baseColor));
739       g2d.fillRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
740     }
741     g2d.setRenderingHints(oldHints);
742   }
743   
744   protected boolean isDark() {
745     return UIUtil.isUnderDarcula();
746   }
747
748   protected boolean alwaysPaintThumb() {
749     return alwaysShowTrack();
750   }
751
752   protected Rectangle getMacScrollBarBounds(Rectangle baseBounds, boolean thumb) {
753     boolean vertical = isVertical();
754
755     int borderSize = 2;
756     int baseSize = vertical ? baseBounds.width : baseBounds.height;
757
758     int maxSize = baseSize - (thumb ? borderSize * 2 : 0);
759     int minSize = Math.min(baseSize / 2, 7) + (thumb ? 0 : borderSize * 2);
760
761     int currentSize = minSize + (int)(myMouseOverScrollbarExpandLevel * (maxSize - minSize));
762
763     int currentBolderSize = thumb ? borderSize : 0;
764
765     int x = baseBounds.x;
766     int y = baseBounds.y;
767     int width;
768     int height;
769
770     if (vertical) {
771       x += baseBounds.width - currentSize - currentBolderSize;
772       y += currentBolderSize;
773       width = currentSize;
774       height = baseBounds.height - currentBolderSize * 2;
775     }
776     else {
777       x += currentBolderSize;
778       y += baseBounds.height - currentSize - currentBolderSize;
779       width = baseBounds.width - currentBolderSize * 2;
780       height = currentSize;
781     }
782
783     width = Math.max(width, currentSize);
784     height = Math.max(height, currentSize);
785
786     return new Rectangle(x, y, width, height);
787   }
788
789   protected void paintMaxiThumb(Graphics2D g, Rectangle thumbBounds) {
790     final boolean vertical = isVertical();
791     int hGap = vertical ? 2 : 1;
792     int vGap = vertical ? 1 : 2;
793
794     int w = thumbBounds.width - hGap * 2;
795     int h = thumbBounds.height - vGap * 2;
796
797     // leave one pixel between thumb and right or bottom edge
798     if (vertical) {
799       h -= 1;
800     }
801     else {
802       w -= 1;
803     }
804
805     final Paint paint;
806     final Color start = adjustColor(getGradientLightColor());
807     final Color end = adjustColor(getGradientDarkColor());
808
809     if (vertical) {
810       paint = UIUtil.getGradientPaint(1, 0, start, w + 1, 0, end);
811     }
812     else {
813       paint = UIUtil.getGradientPaint(0, 1, start, 0, h + 1, end);
814     }
815
816     g.setPaint(paint);
817     g.fillRect(hGap + 1, vGap + 1, w - 1, h - 1);
818
819     final Stroke stroke = g.getStroke();
820     g.setStroke(BORDER_STROKE);
821     g.setColor(getGradientThumbBorderColor());
822     final int R = JBUI.scale(3);
823     g.drawRoundRect(hGap, vGap, w, h, R, R);
824     g.setStroke(stroke);
825   }
826
827   @Override
828   public boolean getSupportsAbsolutePositioning() {
829     return true;
830   }
831
832   protected Color adjustColor(Color c) {
833     if (isMacOverlayScrollbar()) {
834       int alpha = (int)((120 + myMouseOverScrollbarExpandLevel * 20) * (1 - myMacScrollbarFadeLevel));
835       //noinspection UseJBColor
836       return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
837     }
838     else {
839       if (myThumbFadeColorShift == 0) return c;
840       final int sign = isDark() ? -1 : 1;
841       return Gray.get(Math.max(0, Math.min(255, c.getRed() - sign * myThumbFadeColorShift)));
842     }
843   }
844
845   private boolean isVertical() {
846     return scrollbar.getOrientation() == Adjustable.VERTICAL;
847   }
848
849   @Override
850   protected JButton createIncreaseButton(int orientation) {
851     return new EmptyButton();
852   }
853
854   @Override
855   protected JButton createDecreaseButton(int orientation) {
856     return new EmptyButton();
857   }
858
859   protected boolean isMacScrollbarHiddenAndXcodeLikeScrollbar() {
860     return myMacScrollbarHidden && isMacOverlayScrollbarSupported() && xcodeLikeScrollbar();
861   }
862
863   protected static boolean xcodeLikeScrollbar() {
864     return Registry.is("editor.xcode.like.scrollbar");
865   }
866
867   public void registerRepaintCallback(ScrollbarRepaintCallback callback) {
868     myRepaintCallback = callback;
869   }
870
871   private static class EmptyButton extends JButton {
872     private EmptyButton() {
873       setFocusable(false);
874       setRequestFocusEnabled(false);
875     }
876
877     @Override
878     public Dimension getMaximumSize() {
879       return JBUI.emptySize();
880     }
881
882     @Override
883     public Dimension getPreferredSize() {
884       return getMaximumSize();
885     }
886
887     @Override
888     public Dimension getMinimumSize() {
889       return getMaximumSize();
890     }
891   }
892
893   public interface ScrollbarRepaintCallback {
894     void call(Graphics g);
895   }
896 }