ui: avoid unnecessary copy-paste of super method
[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       // We want to repaint whole scrollbar even if thumb wasn't moved (on small scroll of a big panel)
315       // Even if scrollbar wasn't changed itself, myRepaintCallback could need repaint
316       scrollbar.repaint(trackRect);
317     }
318
319     super.setThumbBounds(x, y, width, height);
320   }
321
322   @Override
323   protected ModelListener createModelListener() {
324     return new ModelListener() {
325       @Override
326       public void stateChanged(ChangeEvent e) {
327         if (scrollbar != null) {
328           super.stateChanged(e);
329         }
330       }
331     };
332   }
333
334   public int getDecrementButtonHeight() {
335     return Math.max(0, decrButton.getHeight());
336   }
337   public int getIncrementButtonHeight() {
338     return Math.max(0, incrButton.getHeight());
339   }
340
341   private void startRegularThumbAnimator() {
342     if (isMacOverlayScrollbar()) return;
343     
344     myThumbFadeAnimator.reset();
345     if (scrollbar != null && scrollbar.getValueIsAdjusting() || myMouseIsOverThumb || Registry.is("ui.no.bangs.and.whistles")) {
346       myThumbFadeAnimator.suspend();
347       myThumbFadeColorShift = getAnimationColorShift();
348     }
349     else {
350       myThumbFadeAnimator.resume();
351     }
352   }
353
354   private void startMacScrollbarExpandAnimator() {
355     if (!isMacOverlayScrollbar()) return;
356     
357     if (myMouseOverScrollbarExpandLevel == 0) {
358       myMouseOverScrollbarExpandAnimator.reset();
359       myMouseOverScrollbarExpandAnimator.suspend();
360       if (myMouseOverScrollbar) {
361         myMouseOverScrollbarExpandAnimator.resume();
362       }
363     }
364   }
365
366   private void startMacScrollbarFadeout() {
367     startMacScrollbarFadeout(false);
368   }
369
370   private void startMacScrollbarFadeout(boolean now) {
371     if (!isMacOverlayScrollbar()) return;
372
373     myMacScrollbarFadeTimer.cancelAllRequests();
374
375     if (now) {
376       if (!myMacScrollbarHidden && !myMacScrollbarFadeAnimator.isRunning()) {
377         myMacScrollbarFadeAnimator.resume();
378       }
379       return;
380     }
381
382     myMacScrollbarFadeAnimator.suspend();
383     myMacScrollbarFadeAnimator.reset();
384     myMacScrollbarHidden = false;
385     myMacScrollbarFadeLevel = 0;
386
387     JScrollBar sb = scrollbar; // concurrency in background editors initialization
388     if (sb != null) {
389       sb.repaint();
390
391       if (!myMouseOverScrollbar && !sb.getValueIsAdjusting()) {
392         myMacScrollbarFadeTimer.addRequest(new Runnable() {
393           @Override
394           public void run() {
395             myMacScrollbarFadeAnimator.resume();
396           }
397         }, 700, null);
398       }
399     }
400   }
401
402   public static BasicScrollBarUI createNormal() {
403     return new ButtonlessScrollBarUI();
404   }
405
406   public static BasicScrollBarUI createTransparent() {
407     return new ButtonlessScrollBarUI() {
408       @Override
409       public boolean alwaysShowTrack() {
410         return false;
411       }
412     };
413   }
414
415   @Override
416   protected void installDefaults() {
417     final int incGap = UIManager.getInt("ScrollBar.incrementButtonGap");
418     final int decGap = UIManager.getInt("ScrollBar.decrementButtonGap");
419     try {
420       UIManager.put("ScrollBar.incrementButtonGap", 0);
421       UIManager.put("ScrollBar.decrementButtonGap", 0);
422       super.installDefaults();
423     }
424     finally {
425       UIManager.put("ScrollBar.incrementButtonGap", incGap);
426       UIManager.put("ScrollBar.decrementButtonGap", decGap);
427     }
428
429     myMacScrollerStyle = NSScrollerHelper.getScrollerStyle();
430     scrollbar.setFocusable(false);
431     updateStyleDefaults();
432   }
433
434   private void updateStyleDefaults() {
435     scrollbar.setOpaque(alwaysShowTrack());
436   }
437
438   @Override
439   protected void installListeners() {
440     initRegularThumbAnimator();
441     initMacScrollbarAnimators();
442
443     super.installListeners();
444     scrollbar.addAdjustmentListener(myAdjustmentListener);
445     scrollbar.addMouseListener(myMouseListener);
446     scrollbar.addMouseMotionListener(myMouseMotionListener);
447  
448     scrollbar.addHierarchyListener(myHierarchyListener);
449     updateGlobalListeners(false);
450
451     restart();
452   }
453
454   private void restart() {
455     startRegularThumbAnimator();
456     startMacScrollbarFadeout();
457   }
458
459   private static final Method setValueFrom = ReflectionUtil.getDeclaredMethod(TrackListener.class, "setValueFrom", MouseEvent.class);
460   static {
461     LOG.assertTrue(setValueFrom != null, "Cannot get TrackListener.setValueFrom method");
462   }
463
464   @Override
465   protected TrackListener createTrackListener() {
466     return new TrackListener() {
467       @Override
468       public void mousePressed(MouseEvent e) {
469         if (scrollbar.isEnabled()
470             && SwingUtilities.isLeftMouseButton(e)
471             && !getThumbBounds().contains(e.getPoint())
472             && NSScrollerHelper.getClickBehavior() == NSScrollerHelper.ClickBehavior.JumpToSpot
473             && setValueFrom != null) {
474
475           switch (scrollbar.getOrientation()) {
476             case Adjustable.VERTICAL:
477               offset = getThumbBounds().height / 2;
478               break;
479             case Adjustable.HORIZONTAL:
480               offset = getThumbBounds().width / 2;
481               break;
482           }
483           isDragging = true;
484           try {
485             setValueFrom.invoke(this, e);
486           }
487           catch (Exception ex) {
488             LOG.error(ex);
489           }
490
491           return;
492         }
493
494         super.mousePressed(e);
495       }
496     };
497   }
498   
499   private void updateGlobalListeners(boolean forceRemove) {
500     boolean shouldAdd = scrollbar.isDisplayable();
501
502     if (myGlobalListenersAdded && (!shouldAdd || forceRemove)) {
503       Toolkit.getDefaultToolkit().removeAWTEventListener(myAWTMouseListener);
504       NSScrollerHelper.removeScrollbarStyleListener(myNSScrollerListener);
505       myGlobalListenersAdded = false;
506     }
507
508     if (!myGlobalListenersAdded && shouldAdd && !forceRemove) {
509       Toolkit.getDefaultToolkit().addAWTEventListener(myAWTMouseListener, AWTEvent.MOUSE_MOTION_EVENT_MASK);
510       NSScrollerHelper.addScrollbarStyleListener(myNSScrollerListener);
511       myGlobalListenersAdded = true;
512     }
513   }
514   
515   private void initRegularThumbAnimator() {
516     myThumbFadeAnimator = new Animator("Regular scrollbar thumb animator", FRAMES_COUNT, FRAMES_COUNT * 50, false) {
517       @Override
518       public void paintNow(int frame, int totalFrames, int cycle) {
519         myThumbFadeColorShift = getAnimationColorShift();
520         if (frame > DELAY_FRAMES) {
521           myThumbFadeColorShift *= 1 - (double)(frame - DELAY_FRAMES) / (double)(totalFrames - DELAY_FRAMES);
522         }
523
524         if (scrollbar != null) {
525           scrollbar.repaint(((ButtonlessScrollBarUI)scrollbar.getUI()).getThumbBounds());
526         }
527       }
528     };
529   }
530
531   private void initMacScrollbarAnimators() {
532     myMouseOverScrollbarExpandAnimator = new Animator("Mac scrollbar mouse over animator", 10, 200, false) {
533       @Override
534       protected void paintCycleEnd() {
535         myMouseOverScrollbarExpandLevel = 1;
536         if (scrollbar != null) scrollbar.repaint();
537       }
538
539       @Override
540       public void paintNow(int frame, int totalFrames, int cycle) {
541         int delay = totalFrames / 2;
542         int frameAfterDelay = frame - delay;
543
544         if (frameAfterDelay > 0) {
545           myMouseOverScrollbarExpandLevel = frameAfterDelay / (float)(totalFrames - delay);
546           if (scrollbar != null) scrollbar.repaint();
547         }
548       }
549     };
550
551     myMacScrollbarFadeTimer = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
552     myMacScrollbarFadeAnimator = new Animator("Mac scrollbar fade animator", 30, 300, false) {
553       @Override
554       protected void paintCycleEnd() {
555         myMacScrollbarHidden = true;
556         myMouseOverScrollbar = false;
557         myMouseOverScrollbarExpandLevel = 0;
558
559         if (scrollbar != null) scrollbar.repaint();
560       }
561
562       @Override
563       public void paintNow(int frame, int totalFrames, int cycle) {
564         myMacScrollbarFadeLevel = frame / (float)totalFrames;
565         if (scrollbar != null) scrollbar.repaint();
566       }
567     };
568   }
569
570   private boolean isOverThumb(Point p) {
571     final Rectangle bounds = getThumbBounds();
572     return bounds != null && bounds.contains(p);
573   }
574
575   @Override
576   public Rectangle getThumbBounds() {
577     return super.getThumbBounds();
578   }
579
580   @Override
581   protected void uninstallListeners() {
582     if (scrollTimer != null) {
583       // it is already called otherwise
584       super.uninstallListeners();
585     }
586
587     scrollbar.removeAdjustmentListener(myAdjustmentListener);
588     scrollbar.removeMouseListener(myMouseListener);
589     scrollbar.removeMouseMotionListener(myMouseMotionListener);
590
591     scrollbar.removeHierarchyListener(myHierarchyListener);
592     updateGlobalListeners(true);
593
594     Disposer.dispose(myThumbFadeAnimator);
595     Disposer.dispose(myMouseOverScrollbarExpandAnimator);
596     Disposer.dispose(myMacScrollbarFadeTimer);
597     Disposer.dispose(myMacScrollbarFadeAnimator);
598   }
599
600   @Override
601   protected Dimension getMinimumThumbSize() {
602     final int thickness = getThickness();
603     return isVertical() ? new Dimension(thickness, thickness * 2) : new Dimension(thickness * 2, thickness);
604   }
605
606   protected int getThickness() {
607     return isMacOverlayScrollbar() ? JBUI.scale(15) : JBUI.scale(13);
608   }
609
610   @Override
611   public Dimension getMaximumSize(JComponent c) {
612     int thickness = getThickness();
613     return new Dimension(thickness, thickness);
614   }
615
616   @Override
617   public Dimension getMinimumSize(JComponent c) {
618     return getMaximumSize(c);
619   }
620
621   @Override
622   public Dimension getPreferredSize(JComponent c) {
623     return getMaximumSize(c);
624   }
625   
626   @Override
627   public boolean contains(JComponent c, int x, int y) {
628     if (isMacOverlayScrollbar() && !alwaysShowTrack() && !alwaysPaintThumb() && myMacScrollbarHidden) return false;  
629     return super.contains(c, x, y);
630   }
631
632   @Override
633   protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
634     if (alwaysShowTrack() || myMouseOverScrollbarExpandLevel > 0) {
635       doPaintTrack(g, c, trackBounds);
636     }
637   }
638
639   protected void doPaintTrack(Graphics g, JComponent c, Rectangle bounds) {
640     if (isMacOverlayScrollbar() && !alwaysShowTrack()) {
641       bounds = getMacScrollBarBounds(bounds, false);
642       boolean vertical = isVertical();
643
644       final Paint paint;
645       final Color start = adjustColor(UIUtil.getSlightlyDarkerColor(getTrackBackground()));
646       final Color end = adjustColor(getTrackBackground().brighter());
647
648       if (vertical) {
649         paint = UIUtil.getGradientPaint(bounds.x + 1, bounds.y, start, bounds.width + 1, bounds.y, end);
650       }
651       else {
652         paint = UIUtil.getGradientPaint(bounds.x, bounds.y + 1, start, bounds.x, bounds.height + 1, end);
653       }
654
655       Graphics2D g2d = (Graphics2D)g;
656       g2d.setPaint(paint);
657       g2d.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
658
659       g.setColor(adjustColor(start.darker()));
660     }
661     else {
662       g.setColor(getTrackBackground());
663       g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
664
665       g.setColor(getTrackBorderColor());
666     }
667
668     if (isVertical()) {
669       g.drawLine(bounds.x, bounds.y, bounds.x, bounds.y + bounds.height);
670     }
671     else {
672       g.drawLine(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y);
673     }
674
675     if (myRepaintCallback != null) {
676       myRepaintCallback.call(g);
677     }
678   }
679
680   @Override
681   protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
682     doPaintThumb(g, thumbBounds);
683   }
684
685   private void doPaintThumb(Graphics g, Rectangle thumbBounds) {
686     if (thumbBounds.isEmpty() || !scrollbar.isEnabled()) {
687       return;
688     }
689
690     if (isMacOverlayScrollbar()) {
691       paintMacThumb(g, thumbBounds);
692     }
693     else {
694       g.translate(thumbBounds.x, thumbBounds.y);
695       paintMaxiThumb((Graphics2D)g, thumbBounds);
696       g.translate(-thumbBounds.x, -thumbBounds.y);
697     }
698   }
699
700   private void paintMacThumb(Graphics g, Rectangle thumbBounds) {
701     if (isMacScrollbarHiddenAndXcodeLikeScrollbar()) return;
702
703     thumbBounds = getMacScrollBarBounds(thumbBounds, true);
704     Graphics2D g2d = (Graphics2D)g;
705     RenderingHints oldHints = g2d.getRenderingHints();
706     g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
707
708     JBColor baseColor = new JBColor(new NotNullProducer<Color>() {
709       @NotNull
710       @Override
711       public Color produce() {
712         return !isDark() ? Gray._0 : Gray._128;
713       }
714     });
715     
716     int arc = Math.min(thumbBounds.width, thumbBounds.height);
717
718     if (alwaysPaintThumb()) {
719       //noinspection UseJBColor
720       g2d.setColor(new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), isDark() ? 100 : 40));
721       g2d.fillRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
722       //g2d.drawRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
723     }
724
725     if (!myMacScrollbarHidden) {
726       g2d.setColor(adjustColor(baseColor));
727       g2d.fillRoundRect(thumbBounds.x, thumbBounds.y, thumbBounds.width, thumbBounds.height, arc, arc);
728     }
729     g2d.setRenderingHints(oldHints);
730   }
731   
732   protected boolean isDark() {
733     return UIUtil.isUnderDarcula();
734   }
735
736   protected boolean alwaysPaintThumb() {
737     return alwaysShowTrack();
738   }
739
740   protected Rectangle getMacScrollBarBounds(Rectangle baseBounds, boolean thumb) {
741     boolean vertical = isVertical();
742
743     int borderSize = 2;
744     int baseSize = vertical ? baseBounds.width : baseBounds.height;
745
746     int maxSize = baseSize - (thumb ? borderSize * 2 : 0);
747     int minSize = Math.min(baseSize / 2, 7) + (thumb ? 0 : borderSize * 2);
748
749     int currentSize = minSize + (int)(myMouseOverScrollbarExpandLevel * (maxSize - minSize));
750
751     int currentBolderSize = thumb ? borderSize : 0;
752
753     int x = baseBounds.x;
754     int y = baseBounds.y;
755     int width;
756     int height;
757
758     if (vertical) {
759       x += baseBounds.width - currentSize - currentBolderSize;
760       y += currentBolderSize;
761       width = currentSize;
762       height = baseBounds.height - currentBolderSize * 2;
763     }
764     else {
765       x += currentBolderSize;
766       y += baseBounds.height - currentSize - currentBolderSize;
767       width = baseBounds.width - currentBolderSize * 2;
768       height = currentSize;
769     }
770
771     width = Math.max(width, currentSize);
772     height = Math.max(height, currentSize);
773
774     return new Rectangle(x, y, width, height);
775   }
776
777   protected void paintMaxiThumb(Graphics2D g, Rectangle thumbBounds) {
778     final boolean vertical = isVertical();
779     int hGap = vertical ? 2 : 1;
780     int vGap = vertical ? 1 : 2;
781
782     int w = thumbBounds.width - hGap * 2;
783     int h = thumbBounds.height - vGap * 2;
784
785     // leave one pixel between thumb and right or bottom edge
786     if (vertical) {
787       h -= 1;
788     }
789     else {
790       w -= 1;
791     }
792
793     final Paint paint;
794     final Color start = adjustColor(getGradientLightColor());
795     final Color end = adjustColor(getGradientDarkColor());
796
797     if (vertical) {
798       paint = UIUtil.getGradientPaint(1, 0, start, w + 1, 0, end);
799     }
800     else {
801       paint = UIUtil.getGradientPaint(0, 1, start, 0, h + 1, end);
802     }
803
804     g.setPaint(paint);
805     g.fillRect(hGap + 1, vGap + 1, w - 1, h - 1);
806
807     final Stroke stroke = g.getStroke();
808     g.setStroke(BORDER_STROKE);
809     g.setColor(getGradientThumbBorderColor());
810     final int R = JBUI.scale(3);
811     g.drawRoundRect(hGap, vGap, w, h, R, R);
812     g.setStroke(stroke);
813   }
814
815   @Override
816   public boolean getSupportsAbsolutePositioning() {
817     return true;
818   }
819
820   protected Color adjustColor(Color c) {
821     if (isMacOverlayScrollbar()) {
822       int alpha = (int)((120 + myMouseOverScrollbarExpandLevel * 20) * (1 - myMacScrollbarFadeLevel));
823       //noinspection UseJBColor
824       return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
825     }
826     else {
827       if (myThumbFadeColorShift == 0) return c;
828       final int sign = isDark() ? -1 : 1;
829       return Gray.get(Math.max(0, Math.min(255, c.getRed() - sign * myThumbFadeColorShift)));
830     }
831   }
832
833   private boolean isVertical() {
834     return scrollbar.getOrientation() == Adjustable.VERTICAL;
835   }
836
837   @Override
838   protected JButton createIncreaseButton(int orientation) {
839     return new EmptyButton();
840   }
841
842   @Override
843   protected JButton createDecreaseButton(int orientation) {
844     return new EmptyButton();
845   }
846
847   protected boolean isMacScrollbarHiddenAndXcodeLikeScrollbar() {
848     return myMacScrollbarHidden && isMacOverlayScrollbarSupported() && xcodeLikeScrollbar();
849   }
850
851   protected static boolean xcodeLikeScrollbar() {
852     return Registry.is("editor.xcode.like.scrollbar");
853   }
854
855   public void registerRepaintCallback(ScrollbarRepaintCallback callback) {
856     myRepaintCallback = callback;
857   }
858
859   private static class EmptyButton extends JButton {
860     private EmptyButton() {
861       setFocusable(false);
862       setRequestFocusEnabled(false);
863     }
864
865     @Override
866     public Dimension getMaximumSize() {
867       return JBUI.emptySize();
868     }
869
870     @Override
871     public Dimension getPreferredSize() {
872       return getMaximumSize();
873     }
874
875     @Override
876     public Dimension getMinimumSize() {
877       return getMaximumSize();
878     }
879   }
880
881   public interface ScrollbarRepaintCallback {
882     void call(Graphics g);
883   }
884 }