diff options
-rw-r--r-- | src/plugins/styles/mac/qmacstyle_mac.mm | 857 |
1 files changed, 385 insertions, 472 deletions
diff --git a/src/plugins/styles/mac/qmacstyle_mac.mm b/src/plugins/styles/mac/qmacstyle_mac.mm index 8d991cc721..a5e3dd1c4a 100644 --- a/src/plugins/styles/mac/qmacstyle_mac.mm +++ b/src/plugins/styles/mac/qmacstyle_mac.mm @@ -288,6 +288,22 @@ static bool isVerticalTabs(const QTabBar::Shape shape) { } #endif +static bool setupScroller(NSScroller *scroller, const QStyleOptionSlider *sb) +{ + const qreal length = sb->maximum - sb->minimum + sb->pageStep; + if (qFuzzyIsNull(length)) + return false; + const qreal proportion = sb->pageStep / length; + qreal value = qreal(sb->sliderValue - sb->minimum) / qreal(sb->maximum - sb->minimum); + if (sb->orientation == Qt::Horizontal && sb->direction == Qt::RightToLeft) + value = 1.0 - value; + + scroller.frame = sb->rect.toCGRect(); + scroller.floatValue = value; + scroller.knobProportion = proportion; + return true; +} + static bool isInMacUnifiedToolbarArea(QWindow *window, int windowY) { QPlatformNativeInterface *nativeInterface = QGuiApplication::platformNativeInterface(); @@ -1717,50 +1733,24 @@ void QMacStylePrivate::drawTableHeader(const CGRect &outerBounds, p->translate(-outerBounds.origin.x, -outerBounds.origin.y); } -/* - Returns cutoff sizes for scroll bars. - thumbIndicatorCutoff is the smallest size where the thumb indicator is drawn. - scrollButtonsCutoff is the smallest size where the up/down buttons is drawn. -*/ -enum ScrollBarCutoffType { thumbIndicatorCutoff = 0, scrollButtonsCutoff = 1 }; -static int scrollButtonsCutoffSize(ScrollBarCutoffType cutoffType, QStyleHelper::WidgetSizePolicy widgetSize) -{ - // Mini scroll bars do not exist as of version 10.4. - if (widgetSize == QStyleHelper::SizeMini) - return 0; - - const int sizeIndex = (widgetSize == QStyleHelper::SizeSmall) ? 1 : 0; - static const int sizeTable[2][2] = { { 61, 56 }, { 49, 44 } }; - return sizeTable[sizeIndex][cutoffType]; -} - void QMacStylePrivate::getSliderInfo(QStyle::ComplexControl cc, const QStyleOptionSlider *slider, HIThemeTrackDrawInfo *tdi, const QWidget *needToRemoveMe) const { + Q_UNUSED(cc); memset(tdi, 0, sizeof(HIThemeTrackDrawInfo)); // We don't get it all for some reason or another... tdi->version = qt_mac_hitheme_version; tdi->reserved = 0; tdi->filler1 = 0; - bool isScrollbar = (cc == QStyle::CC_ScrollBar); switch (aquaSizeConstrain(slider, needToRemoveMe)) { case QStyleHelper::SizeDefault: case QStyleHelper::SizeLarge: - if (isScrollbar) - tdi->kind = kThemeMediumScrollBar; - else - tdi->kind = kThemeMediumSlider; + tdi->kind = kThemeMediumSlider; break; case QStyleHelper::SizeMini: - if (isScrollbar) - tdi->kind = kThemeSmallScrollBar; // should be kThemeMiniScrollBar, but not implemented - else - tdi->kind = kThemeMiniSlider; + tdi->kind = kThemeMiniSlider; break; case QStyleHelper::SizeSmall: - if (isScrollbar) - tdi->kind = kThemeSmallScrollBar; - else - tdi->kind = kThemeSmallSlider; + tdi->kind = kThemeSmallSlider; break; } @@ -1768,63 +1758,42 @@ void QMacStylePrivate::getSliderInfo(QStyle::ComplexControl cc, const QStyleOpti || slider->tickPosition == QSlider::TicksBothSides; tdi->bounds = slider->rect.toCGRect(); - if (isScrollbar) { - tdi->min = slider->minimum; - tdi->max = slider->maximum; - tdi->value = slider->sliderPosition; + // Fix min and max positions. HITheme seems confused when it comes to rendering + // a slider at those positions. We give it a hand by extending and offsetting + // the slider range accordingly. See also comment for CC_Slider in drawComplexControl() + tdi->min = 0; + if (slider->orientation == Qt::Horizontal) + tdi->max = 10 * slider->rect.width(); + else + tdi->max = 10 * slider->rect.height(); + + int range = slider->maximum - slider->minimum; + if (range == 0) { + tdi->value = 0; + } else if (usePlainKnob || slider->orientation == Qt::Horizontal) { + int endsCorrection = usePlainKnob ? 25 : 10; + tdi->value = (tdi->max + 2 * endsCorrection) * (slider->sliderPosition - slider->minimum) / range - endsCorrection; } else { - // Fix min and max positions. HITheme seems confused when it comes to rendering - // a slider at those positions. We give it a hand by extending and offsetting - // the slider range accordingly. See also comment for CC_Slider in drawComplexControl() - tdi->min = 0; - if (slider->orientation == Qt::Horizontal) - tdi->max = 10 * slider->rect.width(); - else - tdi->max = 10 * slider->rect.height(); - - int range = slider->maximum - slider->minimum; - if (range == 0) { - tdi->value = 0; - } else if (usePlainKnob || slider->orientation == Qt::Horizontal) { - int endsCorrection = usePlainKnob ? 25 : 10; - tdi->value = (tdi->max + 2 * endsCorrection) * (slider->sliderPosition - slider->minimum) / range - endsCorrection; - } else { - tdi->value = (tdi->max + 30) * (slider->sliderPosition - slider->minimum) / range - 20; - } + tdi->value = (tdi->max + 30) * (slider->sliderPosition - slider->minimum) / range - 20; } + tdi->attributes = kThemeTrackShowThumb; if (slider->upsideDown) tdi->attributes |= kThemeTrackRightToLeft; if (slider->orientation == Qt::Horizontal) { tdi->attributes |= kThemeTrackHorizontal; - if (isScrollbar && slider->direction == Qt::RightToLeft) { - if (!slider->upsideDown) - tdi->attributes |= kThemeTrackRightToLeft; - else - tdi->attributes &= ~kThemeTrackRightToLeft; - } - } - - // Tiger broke reverse scroll bars so put them back and "fake it" - if (isScrollbar && (tdi->attributes & kThemeTrackRightToLeft)) { - tdi->attributes &= ~kThemeTrackRightToLeft; - tdi->value = tdi->max - tdi->value; } tdi->enableState = (slider->state & QStyle::State_Enabled) ? kThemeTrackActive : kThemeTrackDisabled; - if (!isScrollbar) { - if (slider->state & QStyle::QStyle::State_HasFocus) - tdi->attributes |= kThemeTrackHasFocus; - if (usePlainKnob) - tdi->trackInfo.slider.thumbDir = kThemeThumbPlain; - else if (slider->tickPosition == QSlider::TicksAbove) - tdi->trackInfo.slider.thumbDir = kThemeThumbUpward; - else - tdi->trackInfo.slider.thumbDir = kThemeThumbDownward; - } else { - tdi->trackInfo.scrollbar.viewsize = slider->pageStep; - } + if (slider->state & QStyle::QStyle::State_HasFocus) + tdi->attributes |= kThemeTrackHasFocus; + if (usePlainKnob) + tdi->trackInfo.slider.thumbDir = kThemeThumbPlain; + else if (slider->tickPosition == QSlider::TicksAbove) + tdi->trackInfo.slider.thumbDir = kThemeThumbUpward; + else + tdi->trackInfo.slider.thumbDir = kThemeThumbDownward; } QMacStylePrivate::QMacStylePrivate() @@ -5309,372 +5278,320 @@ void QMacStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex QStyleHelper::styleObjectWindow(opt->styleObject); const_cast<QMacStylePrivate *>(d)->resolveCurrentNSView(window); switch (cc) { - case CC_Slider: case CC_ScrollBar: - if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { - HIThemeTrackDrawInfo tdi; - d->getSliderInfo(cc, slider, &tdi, widget); - if (slider->state & State_Sunken) { - if (cc == CC_Slider) { - if (slider->activeSubControls == SC_SliderHandle) - tdi.trackInfo.slider.pressState = kThemeThumbPressed; - else if (slider->activeSubControls == SC_SliderGroove) - tdi.trackInfo.slider.pressState = kThemeLeftTrackPressed; - } else { - if (slider->activeSubControls == SC_ScrollBarSubLine - || slider->activeSubControls == SC_ScrollBarAddLine) { - // This test looks complex but it basically boils down - // to the following: The "RTL look" on the mac also - // changed the directions of the controls, that's not - // what people expect (an arrow is an arrow), so we - // kind of fake and say the opposite button is hit. - // This works great, up until 10.4 which broke the - // scroll bars, so I also have actually do something - // similar when I have an upside down scroll bar - // because on Tiger I only "fake" the reverse stuff. - bool reverseHorizontal = (slider->direction == Qt::RightToLeft - && slider->orientation == Qt::Horizontal); - - if ((reverseHorizontal - && slider->activeSubControls == SC_ScrollBarAddLine) - || (!reverseHorizontal - && slider->activeSubControls == SC_ScrollBarSubLine)) { - tdi.trackInfo.scrollbar.pressState = kThemeRightInsideArrowPressed - | kThemeLeftOutsideArrowPressed; - } else { - tdi.trackInfo.scrollbar.pressState = kThemeLeftInsideArrowPressed - | kThemeRightOutsideArrowPressed; - } - } else if (slider->activeSubControls == SC_ScrollBarAddPage) { - tdi.trackInfo.scrollbar.pressState = kThemeRightTrackPressed; - } else if (slider->activeSubControls == SC_ScrollBarSubPage) { - tdi.trackInfo.scrollbar.pressState = kThemeLeftTrackPressed; - } else if (slider->activeSubControls == SC_ScrollBarSlider) { - tdi.trackInfo.scrollbar.pressState = kThemeThumbPressed; - } - } - } - CGRect macRect; - bool tracking = slider->sliderPosition == slider->sliderValue; - if (!tracking) { - // Small optimization, the same as q->subControlRect - QCFType<HIShapeRef> shape; - HIThemeGetTrackThumbShape(&tdi, &shape); - HIShapeGetBounds(shape, &macRect); - tdi.value = slider->sliderValue; - } + if (const QStyleOptionSlider *sb = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { - // Remove controls from the scroll bar if it is to short to draw them correctly. - // This is done in two stages: first the thumb indicator is removed when it is - // no longer possible to move it, second the up/down buttons are removed when - // there is not enough space for them. - if (cc == CC_ScrollBar) { - if (opt && opt->styleObject && !QMacStylePrivate::scrollBars.contains(opt->styleObject)) - QMacStylePrivate::scrollBars.append(QPointer<QObject>(opt->styleObject)); - const int scrollBarLength = (slider->orientation == Qt::Horizontal) - ? slider->rect.width() : slider->rect.height(); - const QStyleHelper::WidgetSizePolicy sizePolicy = QStyleHelper::widgetSizePolicy(widget, opt); - if (scrollBarLength < scrollButtonsCutoffSize(thumbIndicatorCutoff, sizePolicy)) - tdi.attributes &= ~kThemeTrackShowThumb; - if (scrollBarLength < scrollButtonsCutoffSize(scrollButtonsCutoff, sizePolicy)) - tdi.enableState = kThemeTrackNothingToScroll; - } else { - if (!(slider->subControls & SC_SliderHandle)) - tdi.attributes &= ~kThemeTrackShowThumb; - if (!(slider->subControls & SC_SliderGroove)) - tdi.attributes |= kThemeTrackHideTrack; - } + const bool isHorizontal = sb->orientation == Qt::Horizontal; - const bool isHorizontal = slider->orientation == Qt::Horizontal; + if (opt && opt->styleObject && !QMacStylePrivate::scrollBars.contains(opt->styleObject)) + QMacStylePrivate::scrollBars.append(QPointer<QObject>(opt->styleObject)); - if (cc == CC_ScrollBar && proxy()->styleHint(SH_ScrollBar_Transient, opt, widget)) { - bool wasActive = false; - CGFloat opacity = 0.0; - CGFloat expandScale = 1.0; - CGFloat expandOffset = -1.0; - bool shouldExpand = false; - const CGFloat maxExpandScale = tdi.kind == kThemeSmallScrollBar ? 11.0 / 7.0 : 13.0 / 9.0; - - if (QObject *styleObject = opt->styleObject) { - int oldPos = styleObject->property("_q_stylepos").toInt(); - int oldMin = styleObject->property("_q_stylemin").toInt(); - int oldMax = styleObject->property("_q_stylemax").toInt(); - QRect oldRect = styleObject->property("_q_stylerect").toRect(); - QStyle::State oldState = static_cast<QStyle::State>(styleObject->property("_q_stylestate").value<QStyle::State::Int>()); - uint oldActiveControls = styleObject->property("_q_stylecontrols").toUInt(); - - // a scrollbar is transient when the scrollbar itself and - // its sibling are both inactive (ie. not pressed/hovered/moved) - bool transient = !opt->activeSubControls && !(slider->state & State_On); - - if (!transient || - oldPos != slider->sliderPosition || - oldMin != slider->minimum || - oldMax != slider->maximum || - oldRect != slider->rect || - oldState != slider->state || - oldActiveControls != slider->activeSubControls) { - - // if the scrollbar is transient or its attributes, geometry or - // state has changed, the opacity is reset back to 100% opaque - opacity = 1.0; - - styleObject->setProperty("_q_stylepos", slider->sliderPosition); - styleObject->setProperty("_q_stylemin", slider->minimum); - styleObject->setProperty("_q_stylemax", slider->maximum); - styleObject->setProperty("_q_stylerect", slider->rect); - styleObject->setProperty("_q_stylestate", static_cast<QStyle::State::Int>(slider->state)); - styleObject->setProperty("_q_stylecontrols", static_cast<uint>(slider->activeSubControls)); - - QScrollbarStyleAnimation *anim = qobject_cast<QScrollbarStyleAnimation *>(d->animation(styleObject)); - if (transient) { - if (!anim) { - anim = new QScrollbarStyleAnimation(QScrollbarStyleAnimation::Deactivating, styleObject); - d->startAnimation(anim); - } else if (anim->mode() == QScrollbarStyleAnimation::Deactivating) { - // the scrollbar was already fading out while the - // state changed -> restart the fade out animation - anim->setCurrentTime(0); - } - } else if (anim && anim->mode() == QScrollbarStyleAnimation::Deactivating) { - d->stopAnimation(styleObject); + static const CGFloat knobWidths[] = { 7.0, 5.0, 5.0 }; + static const CGFloat expandedKnobWidths[] = { 11.0, 9.0, 9.0 }; + const auto cocoaSize = d->effectiveAquaSizeConstrain(opt, widget); + const CGFloat maxExpandScale = expandedKnobWidths[cocoaSize] / knobWidths[cocoaSize]; + + const bool isTransient = proxy()->styleHint(SH_ScrollBar_Transient, opt, widget); + if (!isTransient) + d->stopAnimation(opt->styleObject); + bool wasActive = false; + CGFloat opacity = 0.0; + CGFloat expandScale = 1.0; + CGFloat expandOffset = 0.0; + bool shouldExpand = false; + + if (QObject *styleObject = opt->styleObject) { + const int oldPos = styleObject->property("_q_stylepos").toInt(); + const int oldMin = styleObject->property("_q_stylemin").toInt(); + const int oldMax = styleObject->property("_q_stylemax").toInt(); + const QRect oldRect = styleObject->property("_q_stylerect").toRect(); + const QStyle::State oldState = static_cast<QStyle::State>(styleObject->property("_q_stylestate").value<QStyle::State::Int>()); + const uint oldActiveControls = styleObject->property("_q_stylecontrols").toUInt(); + + // a scrollbar is transient when the scrollbar itself and + // its sibling are both inactive (ie. not pressed/hovered/moved) + const bool transient = isTransient && !opt->activeSubControls && !(sb->state & State_On); + + if (!transient || + oldPos != sb->sliderPosition || + oldMin != sb->minimum || + oldMax != sb->maximum || + oldRect != sb->rect || + oldState != sb->state || + oldActiveControls != sb->activeSubControls) { + + // if the scrollbar is transient or its attributes, geometry or + // state has changed, the opacity is reset back to 100% opaque + opacity = 1.0; + + styleObject->setProperty("_q_stylepos", sb->sliderPosition); + styleObject->setProperty("_q_stylemin", sb->minimum); + styleObject->setProperty("_q_stylemax", sb->maximum); + styleObject->setProperty("_q_stylerect", sb->rect); + styleObject->setProperty("_q_stylestate", static_cast<QStyle::State::Int>(sb->state)); + styleObject->setProperty("_q_stylecontrols", static_cast<uint>(sb->activeSubControls)); + + QScrollbarStyleAnimation *anim = qobject_cast<QScrollbarStyleAnimation *>(d->animation(styleObject)); + if (transient) { + if (!anim) { + anim = new QScrollbarStyleAnimation(QScrollbarStyleAnimation::Deactivating, styleObject); + d->startAnimation(anim); + } else if (anim->mode() == QScrollbarStyleAnimation::Deactivating) { + // the scrollbar was already fading out while the + // state changed -> restart the fade out animation + anim->setCurrentTime(0); } + } else if (anim && anim->mode() == QScrollbarStyleAnimation::Deactivating) { + d->stopAnimation(styleObject); } + } - QScrollbarStyleAnimation *anim = qobject_cast<QScrollbarStyleAnimation *>(d->animation(styleObject)); - if (anim && anim->mode() == QScrollbarStyleAnimation::Deactivating) { - // once a scrollbar was active (hovered/pressed), it retains - // the active look even if it's no longer active while fading out - if (oldActiveControls) - anim->setActive(true); + QScrollbarStyleAnimation *anim = qobject_cast<QScrollbarStyleAnimation *>(d->animation(styleObject)); + if (anim && anim->mode() == QScrollbarStyleAnimation::Deactivating) { + // once a scrollbar was active (hovered/pressed), it retains + // the active look even if it's no longer active while fading out + if (oldActiveControls) + anim->setActive(true); - wasActive = anim->wasActive(); - opacity = anim->currentValue(); - } + wasActive = anim->wasActive(); + opacity = anim->currentValue(); + } - shouldExpand = (opt->activeSubControls || wasActive); - if (shouldExpand) { - if (!anim && !oldActiveControls) { - // Start expand animation only once and when entering - anim = new QScrollbarStyleAnimation(QScrollbarStyleAnimation::Activating, styleObject); - d->startAnimation(anim); - } - if (anim && anim->mode() == QScrollbarStyleAnimation::Activating) { - expandScale = 1.0 + (maxExpandScale - 1.0) * anim->currentValue(); - expandOffset = 5.5 * anim->currentValue() - 1; - } else { - // Keep expanded state after the animation ends, and when fading out - expandScale = maxExpandScale; - expandOffset = 4.5; - } + shouldExpand = isTransient && (opt->activeSubControls || wasActive); + if (shouldExpand) { + if (!anim && !oldActiveControls) { + // Start expand animation only once and when entering + anim = new QScrollbarStyleAnimation(QScrollbarStyleAnimation::Activating, styleObject); + d->startAnimation(anim); + } + if (anim && anim->mode() == QScrollbarStyleAnimation::Activating) { + expandScale = 1.0 + (maxExpandScale - 1.0) * anim->currentValue(); + expandOffset = 5.5 * (1.0 - anim->currentValue()); + } else { + // Keep expanded state after the animation ends, and when fading out + expandScale = maxExpandScale; + expandOffset = 0.0; } } + } - d->setupNSGraphicsContext(cg, NO); + d->setupNSGraphicsContext(cg, NO /* flipped */); - const QCocoaWidget cw(isHorizontal ? QCocoaHorizontalScroller : QCocoaVerticalScroller, - tdi.kind == kThemeSmallScrollBar ? QStyleHelper::SizeMini : QStyleHelper::SizeLarge); - NSScroller *scroller = static_cast<NSScroller *>(d->cocoaControl(cw)); - // mac os behaviour: as soon as one color channel is >= 128, - // the bg is considered bright, scroller is dark + const QCocoaWidget cw(isHorizontal ? QCocoaHorizontalScroller : QCocoaVerticalScroller, cocoaSize); + NSScroller *scroller = static_cast<NSScroller *>(d->cocoaControl(cw)); + + if (isTransient) { + // macOS behavior: as soon as one color channel is >= 128, + // the background is considered bright, scroller is dark. const QColor bgColor = QStyleHelper::backgroundColor(opt->palette, widget); - const bool isDarkBg = bgColor.red() < 128 && bgColor.green() < 128 && - bgColor.blue() < 128; - if (isDarkBg) - [scroller setKnobStyle:NSScrollerKnobStyleLight]; - else - [scroller setKnobStyle:NSScrollerKnobStyleDefault]; + const bool hasDarkBg = bgColor.red() < 128 && bgColor.green() < 128 && bgColor.blue() < 128; + scroller.knobStyle = hasDarkBg? NSScrollerKnobStyleLight : NSScrollerKnobStyleDark; + } else { + scroller.knobStyle = NSScrollerKnobStyleDefault; + } - [scroller setBounds:NSMakeRect(0, 0, slider->rect.width(), slider->rect.height())]; - [scroller setScrollerStyle:NSScrollerStyleOverlay]; + scroller.scrollerStyle = isTransient ? NSScrollerStyleOverlay : NSScrollerStyleLegacy; + if (!setupScroller(scroller, sb)) + break; + + if (isTransient) { CGContextBeginTransparencyLayer(cg, NULL); CGContextSetAlpha(cg, opacity); + } - // Draw the track when hovering - if (opt->activeSubControls || wasActive) { - NSRect rect = [scroller bounds]; - if (shouldExpand) { - if (isHorizontal) - rect.origin.y += 4.5 - expandOffset; - else - rect.origin.x += 4.5 - expandOffset; - } - [scroller drawKnobSlotInRect:rect highlight:YES]; - } - - const qreal length = slider->maximum - slider->minimum + slider->pageStep; - const qreal proportion = slider->pageStep / length; - qreal value = (slider->sliderValue - slider->minimum) / length; - if (isHorizontal && slider->direction == Qt::RightToLeft) - value = 1.0 - value - proportion; - - [scroller setKnobProportion:1.0]; - - const int minKnobWidth = 26; + // Draw the track when hovering. Expand by shifting the track rect. + if (!isTransient || opt->activeSubControls || wasActive) { + CGRect trackRect = scroller.bounds; + if (isHorizontal) + trackRect.origin.y += expandOffset; + else + trackRect.origin.x += expandOffset; + [scroller drawKnobSlotInRect:trackRect highlight:NO]; + } - if (isHorizontal) { - const qreal plannedWidth = proportion * slider->rect.width(); - const qreal width = qMax<qreal>(minKnobWidth, plannedWidth); - const qreal totalWidth = slider->rect.width() + plannedWidth - width; - [scroller setFrame:NSMakeRect(0, 0, width, slider->rect.height())]; - if (shouldExpand) { - CGContextScaleCTM(cg, 1, expandScale); - CGContextTranslateCTM(cg, value * totalWidth, -expandOffset); - } else { - CGContextTranslateCTM(cg, value * totalWidth, 1); - } - } else { - const qreal plannedHeight = proportion * slider->rect.height(); - const qreal height = qMax<qreal>(minKnobWidth, plannedHeight); - const qreal totalHeight = slider->rect.height() + plannedHeight - height; - [scroller setFrame:NSMakeRect(0, 0, slider->rect.width(), height)]; - if (shouldExpand) { - CGContextScaleCTM(cg, expandScale, 1); - CGContextTranslateCTM(cg, -expandOffset, value * totalHeight); - } else { - CGContextTranslateCTM(cg, 1, value * totalHeight); - } - } - if (length > 0.0) { - [scroller layout]; + if (shouldExpand) { + // -[NSScroller drawKnob] is not useful here because any scaling applied + // will only be used to draw the hi-DPI artwork. And even if did scale, + // the stretched knob would look wrong, actually. So we need to draw the + // scroller manually when it's being hovered. + const CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:scroller.controlSize scrollerStyle:scroller.scrollerStyle]; + const CGFloat knobWidth = knobWidths[cocoaSize] * expandScale; + // Cocoa can help get the exact knob length in the current orientation + const CGRect scrollerKnobRect = CGRectInset([scroller rectForPart:NSScrollerKnob], 1, 1); + const CGFloat knobLength = isHorizontal ? scrollerKnobRect.size.width : scrollerKnobRect.size.height; + const CGFloat knobPos = isHorizontal ? scrollerKnobRect.origin.x : scrollerKnobRect.origin.y; + const CGFloat knobOffset = qRound((scrollerWidth + expandOffset - knobWidth) / 2.0); + const CGFloat knobRadius = knobWidth / 2.0; + CGRect knobRect; + if (isHorizontal) + knobRect = CGRectMake(knobPos, knobOffset, knobLength, knobWidth); + else + knobRect = CGRectMake(knobOffset, knobPos, knobWidth, knobLength); + QCFType<CGPathRef> knobPath = CGPathCreateWithRoundedRect(knobRect, knobRadius, knobRadius, nullptr); + CGContextAddPath(cg, knobPath); + CGContextSetAlpha(cg, 0.5); + CGContextSetFillColorWithColor(cg, NSColor.blackColor.CGColor); + CGContextFillPath(cg); + } else { + [scroller drawKnob]; + + if (!isTransient && opt->activeSubControls) { + // The knob should appear darker (going from 0.76 down to 0.49). + // But no blending mode can help darken enough in a single pass, + // so we resort to drawing the knob twice with a small help from + // blending. This brings the gray level to a close enough 0.53. + CGContextSetBlendMode(cg, kCGBlendModePlusDarker); [scroller drawKnob]; } + } + if (isTransient) CGContextEndTransparencyLayer(cg); - d->restoreNSGraphicsContext(cg); - } else { - d->stopAnimation(opt->styleObject); - - if (cc == CC_Slider) { - // Fix min and max positions. (See also getSliderInfo() - // for the slider values adjustments.) - // HITheme seems to have forgotten how to render - // a slide at those positions, leaving a gap between - // the knob and the ends of the track. - // We fix this by rendering the track first, and then - // the knob on top. However, in order to not clip the - // knob, we reduce the the drawing rect for the track. - CGRect bounds = tdi.bounds; - if (isHorizontal) { - tdi.bounds.size.width -= 2; - tdi.bounds.origin.x += 1; - if (tdi.trackInfo.slider.thumbDir == kThemeThumbDownward) - tdi.bounds.origin.y -= 2; - else if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) - tdi.bounds.origin.y += 3; - } else { - tdi.bounds.size.height -= 2; - tdi.bounds.origin.y += 1; - if (tdi.trackInfo.slider.thumbDir == kThemeThumbDownward) // pointing right - tdi.bounds.origin.x -= 4; - else if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) // pointing left - tdi.bounds.origin.x += 2; - } + d->restoreNSGraphicsContext(cg); + } + break; + case CC_Slider: + if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { + const bool isHorizontal = slider->orientation == Qt::Horizontal; - // Yosemite demands its blue progress track when no tickmarks are present - if (!(slider->subControls & SC_SliderTickmarks)) { - QCocoaWidgetKind sliderKind = slider->orientation == Qt::Horizontal ? QCocoaHorizontalSlider : QCocoaVerticalSlider; - QCocoaWidget cw = QCocoaWidget(sliderKind, QStyleHelper::SizeLarge); - NSSlider *sl = (NSSlider *)d->cocoaControl(cw); - sl.minValue = slider->minimum; - sl.maxValue = slider->maximum; - sl.intValue = slider->sliderValue; - sl.enabled = slider->state & QStyle::State_Enabled; - d->drawNSViewInRect(cw, sl, opt->rect, p, widget != 0, ^(CGContextRef ctx, const CGRect &rect) { - const bool isSierraOrLater = QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSSierra; - if (slider->upsideDown) { - if (isHorizontal) { - CGContextTranslateCTM(ctx, rect.size.width, 0); - CGContextScaleCTM(ctx, -1, 1); - } - } else if (!isHorizontal && !isSierraOrLater) { - CGContextTranslateCTM(ctx, 0, rect.size.height); - CGContextScaleCTM(ctx, 1, -1); - } - const bool shouldFlip = isHorizontal || (slider->upsideDown && isSierraOrLater); - [sl.cell drawBarInside:NSRectFromCGRect(tdi.bounds) flipped:shouldFlip]; - // No need to restore the CTM later, the context has been saved - // and will be restored at the end of drawNSViewInRect() - }); - tdi.attributes |= kThemeTrackHideTrack; - } else { - tdi.attributes &= ~(kThemeTrackShowThumb | kThemeTrackHasFocus); - HIThemeDrawTrack(&tdi, tracking ? 0 : &macRect, cg, - kHIThemeOrientationNormal); - tdi.attributes |= kThemeTrackHideTrack | kThemeTrackShowThumb; - } - tdi.bounds = bounds; + HIThemeTrackDrawInfo tdi; + d->getSliderInfo(cc, slider, &tdi, widget); + if (slider->state & State_Sunken) { + if (cc == CC_Slider) { + if (slider->activeSubControls == SC_SliderHandle) + tdi.trackInfo.slider.pressState = kThemeThumbPressed; + else if (slider->activeSubControls == SC_SliderGroove) + tdi.trackInfo.slider.pressState = kThemeLeftTrackPressed; } + } + CGRect macRect; + bool tracking = slider->sliderPosition == slider->sliderValue; + if (!tracking) { + // Small optimization, the same as q->subControlRect + QCFType<HIShapeRef> shape; + HIThemeGetTrackThumbShape(&tdi, &shape); + HIShapeGetBounds(shape, &macRect); + tdi.value = slider->sliderValue; + } - if (cc == CC_Slider && slider->subControls & SC_SliderTickmarks) { - - CGRect bounds; - // As part of fixing the min and max positions, - // we need to adjust the tickmarks as well - bounds = tdi.bounds; - if (slider->orientation == Qt::Horizontal) { - tdi.bounds.size.width += 2; - tdi.bounds.origin.x -= 1; - if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) - tdi.bounds.origin.y -= 2; - } else { - tdi.bounds.size.height += 3; - tdi.bounds.origin.y -= 3; - tdi.bounds.origin.y += 1; - if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) // pointing left - tdi.bounds.origin.x -= 2; - } + if (!(slider->subControls & SC_SliderHandle)) + tdi.attributes &= ~kThemeTrackShowThumb; + if (!(slider->subControls & SC_SliderGroove)) + tdi.attributes |= kThemeTrackHideTrack; + + // Fix min and max positions. (See also getSliderInfo() + // for the slider values adjustments.) + // HITheme seems to have forgotten how to render + // a slide at those positions, leaving a gap between + // the knob and the ends of the track. + // We fix this by rendering the track first, and then + // the knob on top. However, in order to not clip the + // knob, we reduce the the drawing rect for the track. + CGRect bounds = tdi.bounds; + if (isHorizontal) { + tdi.bounds.size.width -= 2; + tdi.bounds.origin.x += 1; + if (tdi.trackInfo.slider.thumbDir == kThemeThumbDownward) + tdi.bounds.origin.y -= 2; + else if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) + tdi.bounds.origin.y += 3; + } else { + tdi.bounds.size.height -= 2; + tdi.bounds.origin.y += 1; + if (tdi.trackInfo.slider.thumbDir == kThemeThumbDownward) // pointing right + tdi.bounds.origin.x -= 4; + else if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) // pointing left + tdi.bounds.origin.x += 2; + } - int interval = slider->tickInterval; - if (interval == 0) { - interval = slider->pageStep; - if (interval == 0) - interval = slider->singleStep; - if (interval == 0) - interval = 1; - } - int numMarks = 1 + ((slider->maximum - slider->minimum) / interval); - - if (tdi.trackInfo.slider.thumbDir == kThemeThumbPlain) { - // They asked for both, so we'll give it to them. - tdi.trackInfo.slider.thumbDir = kThemeThumbDownward; - HIThemeDrawTrackTickMarks(&tdi, numMarks, - cg, - kHIThemeOrientationNormal); - tdi.trackInfo.slider.thumbDir = kThemeThumbUpward; - // 10.10 and above need a slight shift - if (slider->orientation == Qt::Vertical) - tdi.bounds.origin.x -= 2; - HIThemeDrawTrackTickMarks(&tdi, numMarks, - cg, - kHIThemeOrientationNormal); - // Reset to plain thumb to be drawn further down - tdi.trackInfo.slider.thumbDir = kThemeThumbPlain; - } else { - HIThemeDrawTrackTickMarks(&tdi, numMarks, - cg, - kHIThemeOrientationNormal); - } + // Yosemite demands its blue progress track when no tickmarks are present + if (!(slider->subControls & SC_SliderTickmarks)) { + QCocoaWidgetKind sliderKind = slider->orientation == Qt::Horizontal ? QCocoaHorizontalSlider : QCocoaVerticalSlider; + QCocoaWidget cw = QCocoaWidget(sliderKind, QStyleHelper::SizeLarge); + NSSlider *sl = (NSSlider *)d->cocoaControl(cw); + sl.minValue = slider->minimum; + sl.maxValue = slider->maximum; + sl.intValue = slider->sliderValue; + sl.enabled = slider->state & QStyle::State_Enabled; + d->drawNSViewInRect(cw, sl, opt->rect, p, widget != 0, ^(CGContextRef ctx, const CGRect &rect) { + const bool isSierraOrLater = QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSSierra; + if (slider->upsideDown) { + if (isHorizontal) { + CGContextTranslateCTM(ctx, rect.size.width, 0); + CGContextScaleCTM(ctx, -1, 1); + } + } else if (!isHorizontal && !isSierraOrLater) { + CGContextTranslateCTM(ctx, 0, rect.size.height); + CGContextScaleCTM(ctx, 1, -1); + } + const bool shouldFlip = isHorizontal || (slider->upsideDown && isSierraOrLater); + [sl.cell drawBarInside:NSRectFromCGRect(tdi.bounds) flipped:shouldFlip]; + // No need to restore the CTM later, the context has been saved + // and will be restored at the end of drawNSViewInRect() + }); + tdi.attributes |= kThemeTrackHideTrack; + + tdi.bounds = bounds; + } - tdi.bounds = bounds; + if (slider->subControls & SC_SliderTickmarks) { + + CGRect bounds; + // As part of fixing the min and max positions, + // we need to adjust the tickmarks as well + bounds = tdi.bounds; + if (slider->orientation == Qt::Horizontal) { + tdi.bounds.size.width += 2; + tdi.bounds.origin.x -= 1; + if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) + tdi.bounds.origin.y -= 2; + } else { + tdi.bounds.size.height += 3; + tdi.bounds.origin.y -= 3; + tdi.bounds.origin.y += 1; + if (tdi.trackInfo.slider.thumbDir == kThemeThumbUpward) // pointing left + tdi.bounds.origin.x -= 2; } - if (cc == CC_Slider) { - // Still as part of fixing the min and max positions, - // we also adjust the knob position. We can do this - // because it's rendered separately from the track. - if (slider->orientation == Qt::Vertical) { - if (tdi.trackInfo.slider.thumbDir == kThemeThumbDownward) // pointing right - tdi.bounds.origin.x -= 2; - } + int interval = slider->tickInterval; + if (interval == 0) { + interval = slider->pageStep; + if (interval == 0) + interval = slider->singleStep; + if (interval == 0) + interval = 1; + } + int numMarks = 1 + ((slider->maximum - slider->minimum) / interval); + + if (tdi.trackInfo.slider.thumbDir == kThemeThumbPlain) { + // They asked for both, so we'll give it to them. + tdi.trackInfo.slider.thumbDir = kThemeThumbDownward; + HIThemeDrawTrackTickMarks(&tdi, numMarks, + cg, + kHIThemeOrientationNormal); + tdi.trackInfo.slider.thumbDir = kThemeThumbUpward; + // 10.10 and above need a slight shift + if (slider->orientation == Qt::Vertical) + tdi.bounds.origin.x -= 2; + HIThemeDrawTrackTickMarks(&tdi, numMarks, + cg, + kHIThemeOrientationNormal); + // Reset to plain thumb to be drawn further down + tdi.trackInfo.slider.thumbDir = kThemeThumbPlain; + } else { + HIThemeDrawTrackTickMarks(&tdi, numMarks, + cg, + kHIThemeOrientationNormal); } - HIThemeDrawTrack(&tdi, tracking ? 0 : &macRect, cg, - kHIThemeOrientationNormal); + tdi.bounds = bounds; } + + HIThemeDrawTrack(&tdi, tracking ? 0 : &macRect, cg, + kHIThemeOrientationNormal); } break; #ifndef QT_NO_SPINBOX @@ -6064,50 +5981,39 @@ QStyle::SubControl QMacStyle::hitTestComplexControl(ComplexControl cc, break; case CC_ScrollBar: if (const QStyleOptionSlider *sb = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { - HIScrollBarTrackInfo sbi; - sbi.version = qt_mac_hitheme_version; - if (!(sb->state & State_Active)) - sbi.enableState = kThemeTrackInactive; - else if (sb->state & State_Enabled) - sbi.enableState = kThemeTrackActive; - else - sbi.enableState = kThemeTrackDisabled; - - // The arrow buttons are not drawn if the scroll bar is to short, - // exclude them from the hit test. - const int scrollBarLength = (sb->orientation == Qt::Horizontal) - ? sb->rect.width() : sb->rect.height(); - if (scrollBarLength < scrollButtonsCutoffSize(scrollButtonsCutoff, QStyleHelper::widgetSizePolicy(widget, opt))) - sbi.enableState = kThemeTrackNothingToScroll; + if (!sb->rect.contains(pt)) { + sc = SC_None; + break; + } - sbi.viewsize = sb->pageStep; - CGPoint pos = CGPointMake(pt.x(), pt.y()); + const auto controlSize = d->effectiveAquaSizeConstrain(opt, widget); + const bool isHorizontal = sb->orientation == Qt::Horizontal; + const auto cw = QCocoaWidget(isHorizontal ? QCocoaHorizontalScroller : QCocoaVerticalScroller, controlSize); + auto *scroller = static_cast<NSScroller *>(d->cocoaControl(cw)); + if (!setupScroller(scroller, sb)) { + sc = SC_None; + break; + } - CGRect macSBRect = sb->rect.toCGRect(); - ControlPartCode part; - bool reverseHorizontal = (sb->direction == Qt::RightToLeft - && sb->orientation == Qt::Horizontal); - if (HIThemeHitTestScrollBarArrows(&macSBRect, &sbi, sb->orientation == Qt::Horizontal, - &pos, 0, &part)) { - if (part == kControlUpButtonPart) - sc = reverseHorizontal ? SC_ScrollBarAddLine : SC_ScrollBarSubLine; - else if (part == kControlDownButtonPart) - sc = reverseHorizontal ? SC_ScrollBarSubLine : SC_ScrollBarAddLine; + // Since -[NSScroller testPart:] doesn't want to cooperate, we do it the + // straightforward way. In any case, macOS doesn't return line-sized changes + // with NSScroller since 10.7, according to the aforementioned method's doc. + const auto knobRect = QRectF::fromCGRect([scroller rectForPart:NSScrollerKnob]); + if (isHorizontal) { + const bool isReverse = sb->direction == Qt::RightToLeft; + if (pt.x() < knobRect.left()) + sc = isReverse ? SC_ScrollBarAddPage : SC_ScrollBarSubPage; + else if (pt.x() > knobRect.right()) + sc = isReverse ? SC_ScrollBarSubPage : SC_ScrollBarAddPage; + else + sc = SC_ScrollBarSlider; } else { - HIThemeTrackDrawInfo tdi; - d->getSliderInfo(cc, sb, &tdi, widget); - if(tdi.enableState == kThemeTrackInactive) - tdi.enableState = kThemeTrackActive; - if (HIThemeHitTestTrack(&tdi, &pos, &part)) { - if (part == kControlPageUpPart) - sc = reverseHorizontal ? SC_ScrollBarAddPage - : SC_ScrollBarSubPage; - else if (part == kControlPageDownPart) - sc = reverseHorizontal ? SC_ScrollBarSubPage - : SC_ScrollBarAddPage; - else - sc = SC_ScrollBarSlider; - } + if (pt.y() < knobRect.top()) + sc = SC_ScrollBarSubPage; + else if (pt.y() > knobRect.bottom()) + sc = SC_ScrollBarAddPage; + else + sc = SC_ScrollBarSlider; } } break; @@ -6168,40 +6074,47 @@ QRect QMacStyle::subControlRect(ComplexControl cc, const QStyleOptionComplex *op Q_D(const QMacStyle); QRect ret; switch (cc) { - case CC_Slider: case CC_ScrollBar: + if (const QStyleOptionSlider *sb = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { + const bool isHorizontal = sb->orientation == Qt::Horizontal; + const bool isReverseHorizontal = isHorizontal && (sb->direction == Qt::RightToLeft); + + NSScrollerPart part = NSScrollerNoPart; + if (sc == SC_ScrollBarSlider) { + part = NSScrollerKnob; + } else if (sc == SC_ScrollBarGroove) { + part = NSScrollerKnobSlot; + } else if (sc == SC_ScrollBarSubPage || sc == SC_ScrollBarAddPage) { + if ((!isReverseHorizontal && sc == SC_ScrollBarSubPage) + || (isReverseHorizontal && sc == SC_ScrollBarAddPage)) + part = NSScrollerDecrementPage; + else + part = NSScrollerIncrementPage; + } + // And nothing else since 10.7 + + if (part != NSScrollerNoPart) { + const auto controlSize = d->effectiveAquaSizeConstrain(opt, widget); + const auto cw = QCocoaWidget(isHorizontal ? QCocoaHorizontalScroller : QCocoaVerticalScroller, controlSize); + auto *scroller = static_cast<NSScroller *>(d->cocoaControl(cw)); + if (setupScroller(scroller, sb)) + ret = QRectF::fromCGRect([scroller rectForPart:part]).toRect(); + } + } + break; + case CC_Slider: if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { HIThemeTrackDrawInfo tdi; d->getSliderInfo(cc, slider, &tdi, widget); CGRect macRect; QCFType<HIShapeRef> shape; - bool scrollBar = cc == CC_ScrollBar; - if ((scrollBar && sc == SC_ScrollBarSlider) - || (!scrollBar && sc == SC_SliderHandle)) { + if (sc == SC_SliderHandle) { HIThemeGetTrackThumbShape(&tdi, &shape); HIShapeGetBounds(shape, &macRect); - } else if (!scrollBar && sc == SC_SliderGroove) { + } else if (sc == SC_SliderGroove) { HIThemeGetTrackBounds(&tdi, &macRect); - } else if (sc == SC_ScrollBarGroove) { // Only scroll bar parts available... - HIThemeGetTrackDragRect(&tdi, &macRect); - } else { - ControlPartCode cpc; - if (sc == SC_ScrollBarSubPage || sc == SC_ScrollBarAddPage) { - cpc = sc == SC_ScrollBarSubPage ? kControlPageDownPart - : kControlPageUpPart; - } else { - cpc = sc == SC_ScrollBarSubLine ? kControlUpButtonPart - : kControlDownButtonPart; - if (slider->direction == Qt::RightToLeft - && slider->orientation == Qt::Horizontal) { - if (cpc == kControlDownButtonPart) - cpc = kControlUpButtonPart; - else if (cpc == kControlUpButtonPart) - cpc = kControlDownButtonPart; - } - } - HIThemeGetTrackPartBounds(&tdi, cpc, &macRect); } + // FIXME No SC_SliderTickmarks? ret = QRectF::fromCGRect(macRect).toRect(); // Tweak: the dark line between the sub/add line buttons belong to only one of the buttons @@ -6745,7 +6658,7 @@ QSize QMacStyle::sizeFromContents(ContentsType ct, const QStyleOption *opt, case CT_ScrollBar : // Make sure that the scroll bar is large enough to display the thumb indicator. if (const QStyleOptionSlider *slider = qstyleoption_cast<const QStyleOptionSlider *>(opt)) { - const int minimumSize = scrollButtonsCutoffSize(thumbIndicatorCutoff, QStyleHelper::widgetSizePolicy(widget, opt)); + const int minimumSize = 24; // Smallest knob size, but Cocoa doesn't seem to care if (slider->orientation == Qt::Horizontal) sz = sz.expandedTo(QSize(minimumSize, sz.height())); else |