1/*
2 * tkMacOSXScrollbar.c --
3 *
4 *	This file implements the Macintosh specific portion of the scrollbar
5 *	widget.
6 *
7 * Copyright (c) 1996 by Sun Microsystems, Inc.
8 * Copyright 2001-2009, Apple Inc.
9 * Copyright (c) 2006-2009 Daniel A. Steffen <das@users.sourceforge.net>
10 *
11 * See the file "license.terms" for information on usage and redistribution
12 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
13 *
14 * RCS: @(#) $Id$
15 */
16
17#include "tkMacOSXPrivate.h"
18#include "tkScrollbar.h"
19
20/*
21#ifdef TK_MAC_DEBUG
22#define TK_MAC_DEBUG_SCROLLBAR
23#endif
24*/
25
26/*
27 * Declaration of Mac specific scrollbar structure.
28 */
29
30typedef struct MacScrollbar {
31    TkScrollbar info;
32    NSScroller	*scroller;
33    int variant;
34} MacScrollbar;
35
36typedef struct ScrollbarMetrics {
37    SInt32 width, minThumbHeight;
38    int minHeight, topArrowHeight, bottomArrowHeight;
39    NSControlSize controlSize;
40} ScrollbarMetrics;
41
42static ScrollbarMetrics metrics[2] = {
43    {15, 54, 26, 14, 14, NSRegularControlSize}, /* kThemeScrollBarMedium */
44    {11, 40, 20, 10, 10, NSSmallControlSize},  /* kThemeScrollBarSmall  */
45};
46
47/*
48 * This variable holds the default width for a scrollbar in string form for
49 * use in a Tk_ConfigSpec.
50 */
51
52static char defWidth[TCL_INTEGER_SPACE];
53
54/*
55 * Declarations for functions defined in this file.
56 */
57
58static void		UpdateScrollbarMetrics(void);
59static void		ScrollbarEventProc(ClientData clientData,
60			    XEvent *eventPtr);
61
62/*
63 * The class procedure table for the scrollbar widget.
64 */
65
66Tk_ClassProcs tkpScrollbarProcs = {
67    sizeof(Tk_ClassProcs)	/* size */
68};
69
70#pragma mark TKApplication(TKScrlbr)
71
72#define NSAppleAquaScrollBarVariantChanged @"AppleAquaScrollBarVariantChanged"
73
74@implementation TKApplication(TKScrlbr)
75- (void)tkScroller:(NSScroller *)scroller {
76    NSScrollerPart hitPart = [scroller hitPart];
77    TkScrollbar *scrollPtr = (TkScrollbar *)[scroller tag];
78    Tcl_DString cmdString;
79    Tcl_Interp *interp;
80    int result;
81
82    if (!scrollPtr || !scrollPtr->command || !scrollPtr->commandSize ||
83	    hitPart == NSScrollerNoPart) {
84	return;
85    }
86
87    Tcl_DStringInit(&cmdString);
88    Tcl_DStringAppend(&cmdString, scrollPtr->command,
89	    scrollPtr->commandSize);
90    switch (hitPart) {
91    case NSScrollerKnob:
92    case NSScrollerKnobSlot: {
93	char valueString[TCL_DOUBLE_SPACE];
94
95	Tcl_PrintDouble(NULL, [scroller doubleValue] *
96		(1.0 - [scroller knobProportion]), valueString);
97	Tcl_DStringAppendElement(&cmdString, "moveto");
98	Tcl_DStringAppendElement(&cmdString, valueString);
99	break;
100    }
101    case NSScrollerDecrementLine:
102    case NSScrollerIncrementLine:
103	Tcl_DStringAppendElement(&cmdString, "scroll");
104	Tcl_DStringAppendElement(&cmdString,
105		(hitPart == NSScrollerDecrementLine) ? "-1" : "1");
106	Tcl_DStringAppendElement(&cmdString, "unit");
107	break;
108    case NSScrollerDecrementPage:
109    case NSScrollerIncrementPage:
110	Tcl_DStringAppendElement(&cmdString, "scroll");
111	Tcl_DStringAppendElement(&cmdString,
112		(hitPart == NSScrollerDecrementPage) ? "-1" : "1");
113	Tcl_DStringAppendElement(&cmdString, "page");
114	break;
115    }
116    interp = scrollPtr->interp;
117    Tcl_Preserve(interp);
118    Tcl_Preserve(scrollPtr);
119    result = Tcl_EvalEx(interp, Tcl_DStringValue(&cmdString),
120	    Tcl_DStringLength(&cmdString), TCL_EVAL_GLOBAL);
121    if (result != TCL_OK && result != TCL_CONTINUE && result != TCL_BREAK) {
122	Tcl_AddErrorInfo(interp, "\n    (scrollbar command)");
123	Tcl_BackgroundError(interp);
124    }
125    Tcl_Release(scrollPtr);
126    Tcl_Release(interp);
127    Tcl_DStringFree(&cmdString);
128#ifdef TK_MAC_DEBUG_SCROLLBAR
129    TKLog(@"scroller %s value %f knobProportion %f",
130	    ((TkWindow *)scrollPtr->tkwin)->pathName, [scroller doubleValue],
131	    [scroller knobProportion]);
132#endif
133}
134- (void)scrollBarVariantChanged:(NSNotification *)notification {
135#ifdef TK_MAC_DEBUG_NOTIFICATIONS
136    TKLog(@"-[%@(%p) %s] %@", [self class], self, _cmd, notification);
137#endif
138    UpdateScrollbarMetrics();
139}
140- (void)_setupScrollBarNotifications {
141    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
142#define observe(n, s) [nc addObserver:self selector:@selector(s) name:(n) object:nil]
143    observe(NSAppleAquaScrollBarVariantChanged, scrollBarVariantChanged:);
144#undef observe
145
146    UpdateScrollbarMetrics();
147}
148@end
149
150#pragma mark -
151
152/*
153 *----------------------------------------------------------------------
154 *
155 * UpdateScrollbarMetrics --
156 *
157 *	This function retrieves the current system metrics for a scrollbar.
158 *
159 * Results:
160 *	None.
161 *
162 * Side effects:
163 *	Updates the geometry cache info for all scrollbars.
164 *
165 *----------------------------------------------------------------------
166 */
167
168static void
169UpdateScrollbarMetrics(void)
170{
171    const short height = 100, width = 50;
172    HIThemeTrackDrawInfo info = {
173	.version = 0,
174	.bounds = {{0, 0}, {width, height}},
175	.min = 0,
176	.max = 1,
177	.value = 0,
178	.attributes = kThemeTrackShowThumb,
179	.enableState = kThemeTrackActive,
180	.trackInfo.scrollbar = {.viewsize = 1, .pressState = 0},
181    };
182    CGRect bounds;
183    Tk_ConfigSpec *specPtr;
184
185    ChkErr(GetThemeMetric, kThemeMetricScrollBarWidth, &metrics[0].width);
186    ChkErr(GetThemeMetric, kThemeMetricScrollBarMinThumbHeight,
187	    &metrics[0].minThumbHeight);
188    info.kind = kThemeScrollBarMedium;
189    ChkErr(HIThemeGetTrackDragRect, &info, &bounds);
190    metrics[0].topArrowHeight = bounds.origin.y;
191    metrics[0].bottomArrowHeight = height - (bounds.origin.y +
192	    bounds.size.height);
193    metrics[0].minHeight = metrics[0].minThumbHeight +
194	    metrics[0].topArrowHeight + metrics[0].bottomArrowHeight;
195    ChkErr(GetThemeMetric, kThemeMetricSmallScrollBarWidth, &metrics[1].width);
196    ChkErr(GetThemeMetric, kThemeMetricSmallScrollBarMinThumbHeight,
197	    &metrics[1].minThumbHeight);
198    info.kind = kThemeScrollBarSmall;
199    ChkErr(HIThemeGetTrackDragRect, &info, &bounds);
200    metrics[1].topArrowHeight = bounds.origin.y;
201    metrics[1].bottomArrowHeight = height - (bounds.origin.y +
202	    bounds.size.height);
203    metrics[1].minHeight = metrics[1].minThumbHeight +
204	    metrics[1].topArrowHeight + metrics[1].bottomArrowHeight;
205
206    sprintf(defWidth, "%d", (int)(metrics[0].width));
207    for (specPtr = tkpScrollbarConfigSpecs; specPtr->type != TK_CONFIG_END;
208	    specPtr++) {
209	if (specPtr->offset == Tk_Offset(TkScrollbar, width)) {
210	    specPtr->defValue = defWidth;
211	}
212    }
213}
214
215/*
216 *----------------------------------------------------------------------
217 *
218 * TkpCreateScrollbar --
219 *
220 *	Allocate a new TkScrollbar structure.
221 *
222 * Results:
223 *	Returns a newly allocated TkScrollbar structure.
224 *
225 * Side effects:
226 *	Registers an event handler for the widget.
227 *
228 *----------------------------------------------------------------------
229 */
230
231TkScrollbar *
232TkpCreateScrollbar(
233    Tk_Window tkwin)
234{
235    MacScrollbar *scrollPtr = (MacScrollbar *) ckalloc(sizeof(MacScrollbar));
236
237    scrollPtr->scroller = nil;
238
239    Tk_CreateEventHandler(tkwin, ActivateMask|ExposureMask|
240	    StructureNotifyMask|FocusChangeMask,
241	    ScrollbarEventProc, (ClientData) scrollPtr);
242
243    return (TkScrollbar *) scrollPtr;
244}
245
246/*
247 *----------------------------------------------------------------------
248 *
249 * TkpDestroyScrollbar --
250 *
251 *	Free data structures associated with the scrollbar control.
252 *
253 * Results:
254 *	None.
255 *
256 * Side effects:
257 *	None.
258 *
259 *----------------------------------------------------------------------
260 */
261
262void
263TkpDestroyScrollbar(
264    TkScrollbar *scrollPtr)
265{
266    MacScrollbar *macScrollPtr = (MacScrollbar *) scrollPtr;
267
268    TkMacOSXMakeCollectableAndRelease(macScrollPtr->scroller);
269}
270
271/*
272 *--------------------------------------------------------------
273 *
274 * TkpDisplayScrollbar --
275 *
276 *	This procedure redraws the contents of a scrollbar window. It is
277 *	invoked as a do-when-idle handler, so it only runs when there's
278 *	nothing else for the application to do.
279 *
280 * Results:
281 *	None.
282 *
283 * Side effects:
284 *	Information appears on the screen.
285 *
286 *--------------------------------------------------------------
287 */
288
289void
290TkpDisplayScrollbar(
291    ClientData clientData)	/* Information about window. */
292{
293    TkScrollbar *scrollPtr = (TkScrollbar *) clientData;
294    MacScrollbar *macScrollPtr = (MacScrollbar *) clientData;
295    NSScroller *scroller = macScrollPtr->scroller;
296    Tk_Window tkwin = scrollPtr->tkwin;
297    TkWindow *winPtr = (TkWindow *) tkwin;
298    MacDrawable *macWin =  (MacDrawable *) winPtr->window;
299    TkMacOSXDrawingContext dc;
300    NSView *view = TkMacOSXDrawableView(macWin);
301    CGFloat viewHeight = [view bounds].size.height;
302    CGAffineTransform t = { .a = 1, .b = 0, .c = 0, .d = -1, .tx = 0,
303	    .ty = viewHeight};
304    NSRect frame;
305    double knobProportion = scrollPtr->lastFraction - scrollPtr->firstFraction;
306
307    scrollPtr->flags &= ~REDRAW_PENDING;
308    if (!scrollPtr->tkwin || !Tk_IsMapped(tkwin) || !view ||
309	    !TkMacOSXSetupDrawingContext((Drawable) macWin, NULL, 1, &dc)) {
310	return;
311    }
312    CGContextConcatCTM(dc.context, t);
313    if (scrollPtr->highlightWidth != 0) {
314	GC fgGC, bgGC;
315
316	bgGC = Tk_GCForColor(scrollPtr->highlightBgColorPtr, (Pixmap) macWin);
317	if (scrollPtr->flags & GOT_FOCUS) {
318	    fgGC = Tk_GCForColor(scrollPtr->highlightColorPtr, (Pixmap) macWin);
319	} else {
320	    fgGC = bgGC;
321	}
322	TkpDrawHighlightBorder(tkwin, fgGC, bgGC, scrollPtr->highlightWidth,
323		(Pixmap) macWin);
324    }
325    Tk_Draw3DRectangle(tkwin, (Pixmap) macWin, scrollPtr->bgBorder,
326	    scrollPtr->highlightWidth, scrollPtr->highlightWidth,
327	    Tk_Width(tkwin) - 2*scrollPtr->highlightWidth,
328	    Tk_Height(tkwin) - 2*scrollPtr->highlightWidth,
329	    scrollPtr->borderWidth, scrollPtr->relief);
330    Tk_Fill3DRectangle(tkwin, (Pixmap) macWin, scrollPtr->bgBorder,
331	    scrollPtr->inset, scrollPtr->inset,
332	    Tk_Width(tkwin) - 2*scrollPtr->inset,
333	    Tk_Height(tkwin) - 2*scrollPtr->inset, 0, TK_RELIEF_FLAT);
334    if ([scroller superview] != view) {
335	[view addSubview:scroller];
336    }
337    frame = NSMakeRect(macWin->xOff, macWin->yOff, Tk_Width(tkwin),
338	    Tk_Height(tkwin));
339    frame = NSInsetRect(frame, scrollPtr->inset, scrollPtr->inset);
340    frame.origin.y = viewHeight - (frame.origin.y + frame.size.height);
341    NSWindow *w = [view window];
342    if ([w showsResizeIndicator]) {
343	NSRect growBox = [view convertRect:[w _growBoxRect] fromView:nil];
344	if (NSIntersectsRect(growBox, frame)) {
345	    if (scrollPtr->vertical) {
346		CGFloat y = frame.origin.y;
347		frame.origin.y = growBox.origin.y + growBox.size.height;
348		frame.size.height -= frame.origin.y - y;
349 	    } else {
350		frame.size.width = growBox.origin.x - frame.origin.x;
351	    }
352	    TkMacOSXSetScrollbarGrow(winPtr, true);
353	}
354    }
355    if (!NSEqualRects(frame, [scroller frame])) {
356	[scroller setFrame:frame];
357    }
358    [scroller setEnabled:(knobProportion < 1.0 &&
359	    (scrollPtr->vertical ? frame.size.height : frame.size.width) >
360	    metrics[macScrollPtr->variant].minHeight)];
361    [scroller setDoubleValue:scrollPtr->firstFraction / (1.0 - knobProportion)];
362    [scroller setKnobProportion:knobProportion];
363    [scroller displayRectIgnoringOpacity:[scroller bounds]];
364    TkMacOSXRestoreDrawingContext(&dc);
365#ifdef TK_MAC_DEBUG_SCROLLBAR
366    TKLog(@"scroller %s frame %@ width %d height %d",
367	    ((TkWindow *)scrollPtr->tkwin)->pathName, NSStringFromRect(frame),
368	    Tk_Width(tkwin), Tk_Height(tkwin));
369#endif
370}
371
372/*
373 *----------------------------------------------------------------------
374 *
375 * TkpComputeScrollbarGeometry --
376 *
377 *	After changes in a scrollbar's size or configuration, this procedure
378 *	recomputes various geometry information used in displaying the
379 *	scrollbar.
380 *
381 * Results:
382 *	None.
383 *
384 * Side effects:
385 *	The scrollbar will be displayed differently.
386 *
387 *----------------------------------------------------------------------
388 */
389
390void
391TkpComputeScrollbarGeometry(
392    register TkScrollbar *scrollPtr)
393				/* Scrollbar whose geometry may have
394				 * changed. */
395{
396    MacScrollbar *macScrollPtr = (MacScrollbar *) scrollPtr;
397    NSScroller *scroller = macScrollPtr->scroller;
398    int width, height, variant, fieldLength;
399
400    if (scrollPtr->highlightWidth < 0) {
401	scrollPtr->highlightWidth = 0;
402    }
403    scrollPtr->inset = scrollPtr->highlightWidth + scrollPtr->borderWidth;
404    width = Tk_Width(scrollPtr->tkwin) - 2 * scrollPtr->inset;
405    height = Tk_Height(scrollPtr->tkwin) - 2 * scrollPtr->inset;
406    variant = ((scrollPtr->vertical ?  width : height) < metrics[0].width) ?
407	    1 : 0;
408    macScrollPtr->variant = variant;
409    if (scroller) {
410	NSSize size = [scroller frame].size;
411	if ((size.width > size.height) ^ !scrollPtr->vertical) {
412	    /* Orientation changed, need new scroller */
413	    if ([scroller superview]) {
414		[scroller removeFromSuperviewWithoutNeedingDisplay];
415	    }
416	    TkMacOSXMakeCollectableAndRelease(scroller);
417	}
418    }
419    if (!scroller) {
420	if ((width > height) ^ !scrollPtr->vertical) {
421	    /* -[NSScroller initWithFrame:] determines horizonalness for the
422	     * lifetime of the scroller via isHoriz = (width > height) */
423	    if (scrollPtr->vertical) {
424		width = height;
425	    } else if (width > 1) {
426		height = width - 1;
427	    } else {
428		height = 1;
429		width = 2;
430	    }
431	}
432	scroller = [[NSScroller alloc] initWithFrame:
433		NSMakeRect(0, 0, width, height)];
434	macScrollPtr->scroller = TkMacOSXMakeUncollectable(scroller);
435	[scroller setAction:@selector(tkScroller:)];
436	[scroller setTarget:NSApp];
437	[scroller setTag:(NSInteger)scrollPtr];
438    }
439    [[scroller cell] setControlSize:metrics[variant].controlSize];
440
441    scrollPtr->arrowLength = (metrics[variant].topArrowHeight +
442	    metrics[variant].bottomArrowHeight) / 2;
443    fieldLength = (scrollPtr->vertical ? Tk_Height(scrollPtr->tkwin)
444	    : Tk_Width(scrollPtr->tkwin))
445	    - 2 * (scrollPtr->arrowLength + scrollPtr->inset);
446    if (fieldLength < 0) {
447	fieldLength = 0;
448    }
449    scrollPtr->sliderFirst = fieldLength * scrollPtr->firstFraction;
450    scrollPtr->sliderLast = fieldLength * scrollPtr->lastFraction;
451
452    /*
453     * Adjust the slider so that some piece of it is always displayed in the
454     * scrollbar and so that it has at least a minimal width (so it can be
455     * grabbed with the mouse).
456     */
457
458    if (scrollPtr->sliderFirst > (fieldLength - 2*scrollPtr->borderWidth)) {
459	scrollPtr->sliderFirst = fieldLength - 2*scrollPtr->borderWidth;
460    }
461    if (scrollPtr->sliderFirst < 0) {
462	scrollPtr->sliderFirst = 0;
463    }
464    if (scrollPtr->sliderLast < (scrollPtr->sliderFirst +
465	    metrics[variant].minThumbHeight)) {
466	scrollPtr->sliderLast = scrollPtr->sliderFirst +
467		metrics[variant].minThumbHeight;
468    }
469    if (scrollPtr->sliderLast > fieldLength) {
470	scrollPtr->sliderLast = fieldLength;
471    }
472    scrollPtr->sliderFirst += scrollPtr->inset +
473	    metrics[variant].topArrowHeight;
474    scrollPtr->sliderLast += scrollPtr->inset +
475	    metrics[variant].bottomArrowHeight;
476
477    /*
478     * Register the desired geometry for the window (leave enough space for
479     * the two arrows plus a minimum-size slider, plus border around the whole
480     * window, if any). Then arrange for the window to be redisplayed.
481     */
482
483    if (scrollPtr->vertical) {
484	Tk_GeometryRequest(scrollPtr->tkwin, scrollPtr->width +
485		2 * scrollPtr->inset, 2 * (scrollPtr->arrowLength +
486		scrollPtr->borderWidth + scrollPtr->inset) +
487		metrics[variant].minThumbHeight);
488    } else {
489	Tk_GeometryRequest(scrollPtr->tkwin, 2 * (scrollPtr->arrowLength +
490		scrollPtr->borderWidth + scrollPtr->inset) +
491		metrics[variant].minThumbHeight, scrollPtr->width +
492		2 * scrollPtr->inset);
493    }
494    Tk_SetInternalBorder(scrollPtr->tkwin, scrollPtr->inset);
495#ifdef TK_MAC_DEBUG_SCROLLBAR
496    TKLog(@"scroller %s bounds %@ width %d height %d inset %d borderWidth %d",
497	    ((TkWindow *)scrollPtr->tkwin)->pathName,
498	    NSStringFromRect([scroller bounds]),
499	    width, height, scrollPtr->inset, scrollPtr->borderWidth);
500#endif
501}
502
503/*
504 *----------------------------------------------------------------------
505 *
506 * TkpConfigureScrollbar --
507 *
508 *	This procedure is called after the generic code has finished
509 *	processing configuration options, in order to configure platform
510 *	specific options.
511 *
512 * Results:
513 *	None.
514 *
515 * Side effects:
516 *	None.
517 *
518 *----------------------------------------------------------------------
519 */
520
521void
522TkpConfigureScrollbar(
523    register TkScrollbar *scrollPtr)
524				/* Information about widget; may or may not
525				 * already have values for some fields. */
526{
527}
528
529/*
530 *--------------------------------------------------------------
531 *
532 * TkpScrollbarPosition --
533 *
534 *	Determine the scrollbar element corresponding to a given position.
535 *
536 * Results:
537 *	One of TOP_ARROW, TOP_GAP, etc., indicating which element of the
538 *	scrollbar covers the position given by (x, y). If (x,y) is outside the
539 *	scrollbar entirely, then OUTSIDE is returned.
540 *
541 * Side effects:
542 *	None.
543 *
544 *--------------------------------------------------------------
545 */
546
547int
548TkpScrollbarPosition(
549    register TkScrollbar *scrollPtr,
550				/* Scrollbar widget record. */
551    int x, int y)		/* Coordinates within scrollPtr's window. */
552{
553    NSScroller *scroller = ((MacScrollbar *) scrollPtr)->scroller;
554    MacDrawable *macWin =  (MacDrawable *)
555	    ((TkWindow *) scrollPtr->tkwin)->window;
556    NSView *view = TkMacOSXDrawableView(macWin);
557
558    switch ([scroller testPart:NSMakePoint(macWin->xOff + x,
559	    [view bounds].size.height - (macWin->yOff + y))]) {
560    case NSScrollerDecrementLine:
561	return TOP_ARROW;
562    case NSScrollerDecrementPage:
563	return TOP_GAP;
564    case NSScrollerKnob:
565	return SLIDER;
566    case NSScrollerIncrementPage:
567	return BOTTOM_GAP;
568    case NSScrollerIncrementLine:
569	return BOTTOM_ARROW;
570    case NSScrollerKnobSlot:
571    case NSScrollerNoPart:
572    default:
573	return OUTSIDE;
574    }
575}
576
577/*
578 *--------------------------------------------------------------
579 *
580 * ScrollbarEventProc --
581 *
582 *	This procedure is invoked by the Tk dispatcher for various events on
583 *	scrollbars.
584 *
585 * Results:
586 *	None.
587 *
588 * Side effects:
589 *	When the window gets deleted, internal structures get cleaned up. When
590 *	it gets exposed, it is redisplayed.
591 *
592 *--------------------------------------------------------------
593 */
594
595static void
596ScrollbarEventProc(
597    ClientData clientData,	/* Information about window. */
598    XEvent *eventPtr)		/* Information about event. */
599{
600    TkScrollbar *scrollPtr = (TkScrollbar *) clientData;
601
602    switch (eventPtr->type) {
603    case UnmapNotify:
604	TkMacOSXSetScrollbarGrow((TkWindow *) scrollPtr->tkwin, false);
605	break;
606    case ActivateNotify:
607    case DeactivateNotify:
608	TkScrollbarEventuallyRedraw((ClientData) scrollPtr);
609	break;
610    default:
611	TkScrollbarEventProc(clientData, eventPtr);
612    }
613}
614
615/*
616 * Local Variables:
617 * mode: c
618 * c-basic-offset: 4
619 * fill-column: 79
620 * coding: utf-8
621 * End:
622 */
623