1/*
2 * tkMacOSXMenubutton.c --
3 *
4 *	This file implements the Macintosh specific portion of the
5 *	menubutton 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 "tkMenubutton.h"
19#include "tkMacOSXFont.h"
20#include "tkMacOSXDebug.h"
21
22/*
23#ifdef TK_MAC_DEBUG
24#define TK_MAC_DEBUG_MENUBUTTON
25#endif
26*/
27
28typedef struct MacMenuButton {
29    TkMenuButton info;
30    NSPopUpButton *button;
31#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
32    int fix;
33#endif
34} MacMenuButton;
35
36#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
37
38/*
39 * Use the following heuristic conversion constants to make NSButton-based
40 * widget metrics match up with the old Carbon control buttons (for the
41 * default Lucida Grande 13 font).
42 * TODO: provide a scriptable way to turn this off and use the raw NSButton
43 *       metrics (will also need dynamic adjustment of the default padding,
44 *       c.f. tkMacOSXDefault.h).
45 */
46
47typedef struct {
48    int trimW, trimH, inset, shrinkW, offsetX, offsetY;
49} BoundsFix;
50
51#define fixForStyle(style) ( \
52	style == NSRoundedBezelStyle ? 1 : \
53	style == NSRegularSquareBezelStyle ? 2 : \
54	style == NSShadowlessSquareBezelStyle ? 3 : \
55	INT_MIN)
56
57static const BoundsFix boundsFixes[] = {
58    [fixForStyle(NSRoundedBezelStyle)] =	    { 14, 10, -2, -1},
59    [fixForStyle(NSRegularSquareBezelStyle)] =	    {  6, 13, -2,  1, 1},
60    [fixForStyle(NSShadowlessSquareBezelStyle)] =   { 15,  0, 2 },
61};
62
63#endif
64
65/*
66 * Forward declarations for procedures defined later in this file:
67 */
68
69static void MenuButtonEventProc(ClientData clientData, XEvent *eventPtr);
70
71/*
72 * The structure below defines menubutton class behavior by means of functions
73 * that can be invoked from generic window code.
74 */
75
76Tk_ClassProcs tkpMenubuttonClass = {
77    sizeof(Tk_ClassProcs),	/* size */
78    TkMenuButtonWorldChanged,	/* worldChangedProc */
79};
80
81/*
82 *----------------------------------------------------------------------
83 *
84 * TkpCreateMenuButton --
85 *
86 *	Allocate a new TkMenuButton structure.
87 *
88 * Results:
89 *	Returns a newly allocated TkMenuButton structure.
90 *
91 * Side effects:
92 *	Registers an event handler for the widget.
93 *
94 *----------------------------------------------------------------------
95 */
96
97TkMenuButton *
98TkpCreateMenuButton(
99    Tk_Window tkwin)
100{
101    MacMenuButton *macButtonPtr =
102	    (MacMenuButton *) ckalloc(sizeof(MacMenuButton));
103
104    macButtonPtr->button = nil;
105
106    Tk_CreateEventHandler(tkwin, ActivateMask,
107	    MenuButtonEventProc, (ClientData) macButtonPtr);
108    return (TkMenuButton *) macButtonPtr;
109}
110
111/*
112 *----------------------------------------------------------------------
113 *
114 * TkpDestroyMenuButton --
115 *
116 *	Free data structures associated with the menubutton control.
117 *
118 * Results:
119 *	None.
120 *
121 * Side effects:
122 *	Restores the default control state.
123 *
124 *----------------------------------------------------------------------
125 */
126
127void
128TkpDestroyMenuButton(
129    TkMenuButton *mbPtr)
130{
131    MacMenuButton *macButtonPtr = (MacMenuButton *) mbPtr;
132
133    TkMacOSXMakeCollectableAndRelease(macButtonPtr->button);
134}
135
136/*
137 *----------------------------------------------------------------------
138 *
139 * TkpDisplayMenuButton --
140 *
141 *	This function is invoked to display a menubutton widget.
142 *
143 * Results:
144 *	None.
145 *
146 * Side effects:
147 *	Commands are output to X to display the menubutton in its current
148 *	mode.
149 *
150 *----------------------------------------------------------------------
151 */
152
153void
154TkpDisplayMenuButton(
155    ClientData clientData)	/* Information about widget. */
156{
157    TkMenuButton *mbPtr = (TkMenuButton *) clientData;
158    MacMenuButton *macButtonPtr = (MacMenuButton *) mbPtr;
159    NSPopUpButton *button = macButtonPtr->button;
160    Tk_Window tkwin = mbPtr->tkwin;
161    TkWindow *winPtr = (TkWindow *) tkwin;
162    MacDrawable *macWin =  (MacDrawable *) winPtr->window;
163    TkMacOSXDrawingContext dc;
164    NSView *view = TkMacOSXDrawableView(macWin);
165    CGFloat viewHeight = [view bounds].size.height;
166    CGAffineTransform t = { .a = 1, .b = 0, .c = 0, .d = -1, .tx = 0,
167	    .ty = viewHeight};
168    NSRect frame;
169    int enabled;
170
171    mbPtr->flags &= ~REDRAW_PENDING;
172    if (!tkwin || !Tk_IsMapped(tkwin) || !view ||
173	    !TkMacOSXSetupDrawingContext((Drawable) macWin, NULL, 1, &dc)) {
174	return;
175    }
176    CGContextConcatCTM(dc.context, t);
177    Tk_Fill3DRectangle(tkwin, (Pixmap) macWin, mbPtr->normalBorder, 0, 0,
178	    Tk_Width(tkwin), Tk_Height(tkwin), 0, TK_RELIEF_FLAT);
179    if ([button superview] != view) {
180	[view addSubview:button];
181    }
182    enabled = !(mbPtr->state == STATE_DISABLED);
183    [button setEnabled:enabled];
184    if (enabled) {
185	[[button cell] setHighlighted:(mbPtr->state == STATE_ACTIVE)];
186    }
187    frame = NSMakeRect(macWin->xOff, macWin->yOff, Tk_Width(tkwin),
188	    Tk_Height(tkwin));
189    frame = NSInsetRect(frame, mbPtr->inset, mbPtr->inset);
190#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
191    if (tkMacOSXUseCompatibilityMetrics) {
192	BoundsFix boundsFix = boundsFixes[macButtonPtr->fix];
193	frame = NSOffsetRect(frame, boundsFix.offsetX, boundsFix.offsetY);
194	frame.size.width -= boundsFix.shrinkW;
195	frame = NSInsetRect(frame, boundsFix.inset, boundsFix.inset);
196    }
197#endif
198    frame.origin.y = viewHeight - (frame.origin.y + frame.size.height);
199    if (!NSEqualRects(frame, [button frame])) {
200	[button setFrame:frame];
201    }
202    [button displayRectIgnoringOpacity:[button bounds]];
203    TkMacOSXRestoreDrawingContext(&dc);
204#ifdef TK_MAC_DEBUG_MENUBUTTON
205    TKLog(@"menubutton %s frame %@ width %d height %d",
206	    ((TkWindow *)mbPtr->tkwin)->pathName, NSStringFromRect(frame),
207	    Tk_Width(tkwin), Tk_Height(tkwin));
208#endif
209}
210
211/*
212 *----------------------------------------------------------------------
213 *
214 * TkpComputeMenuButtonGeometry --
215 *
216 *	After changes in a menu button's text or bitmap, this function
217 *	recomputes the menu button's geometry and passes this information
218 *	along to the geometry manager for the window.
219 *
220 * Results:
221 *	None.
222 *
223 * Side effects:
224 *	The menu button's window may change size.
225 *
226 *----------------------------------------------------------------------
227 */
228
229void
230TkpComputeMenuButtonGeometry(
231    TkMenuButton *mbPtr)	/* Widget record for menu button. */
232{
233    MacMenuButton *macButtonPtr = (MacMenuButton *) mbPtr;
234    NSPopUpButton *button = macButtonPtr->button;
235    NSPopUpButtonCell *cell;
236    NSMenuItem *menuItem;
237    NSBezelStyle style = NSRoundedBezelStyle;
238    NSFont *font;
239    NSRect bounds = NSZeroRect, titleRect = NSZeroRect;
240    int haveImage = (mbPtr->image || mbPtr->bitmap != None), haveText = 0;
241    int haveCompound = (mbPtr->compound != COMPOUND_NONE);
242    int width, height;
243
244    if (!button) {
245	button = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:YES];
246	macButtonPtr->button = TkMacOSXMakeUncollectable(button);
247	cell = [button cell];
248	[cell setUsesItemFromMenu:NO];
249	menuItem = [[[NSMenuItem alloc] initWithTitle:@""
250		action:NULL keyEquivalent:@""] autorelease];
251	[cell setMenuItem:menuItem];
252    } else {
253	cell = [button cell];
254	menuItem = [cell menuItem];
255    }
256    if (haveImage) {
257	style = NSShadowlessSquareBezelStyle;
258    } else if (!mbPtr->indicatorOn) {
259	style = NSRegularSquareBezelStyle;
260    }
261    [button setBezelStyle:style];
262    [cell setArrowPosition:(mbPtr->indicatorOn ? NSPopUpArrowAtBottom :
263	    NSPopUpNoArrow)];
264#if 0
265    NSControlSize controlSize = NSRegularControlSize;
266
267    if (mbPtr->borderWidth <= 2) {
268	controlSize = NSMiniControlSize;
269    } else if (mbPtr->borderWidth == 3) {
270	controlSize = NSSmallControlSize;
271    }
272    [cell setControlSize:controlSize];
273#endif
274
275    if (mbPtr->text && *(mbPtr->text) && (!haveImage || haveCompound)) {
276	NSString *title = [[NSString alloc] initWithUTF8String:mbPtr->text];
277	[button setTitle:title];
278	[title release];
279	haveText = 1;
280    }
281    haveCompound = (haveCompound && haveImage && haveText);
282    if (haveText) {
283	NSTextAlignment alignment = NSNaturalTextAlignment;
284
285	switch (mbPtr->justify) {
286	case TK_JUSTIFY_LEFT:
287	    alignment = NSLeftTextAlignment;
288	    break;
289	case TK_JUSTIFY_RIGHT:
290	    alignment = NSRightTextAlignment;
291	    break;
292	case TK_JUSTIFY_CENTER:
293	    alignment = NSCenterTextAlignment;
294	    break;
295	}
296	[button setAlignment:alignment];
297    } else {
298	[button setTitle:@""];
299    }
300    font = TkMacOSXNSFontForFont(mbPtr->tkfont);
301    if (font) {
302	[button setFont:font];
303    }
304    if (haveImage) {
305	int width, height;
306	NSImage *image;
307	NSCellImagePosition pos = NSImageOnly;
308
309	if (mbPtr->image) {
310	    Tk_SizeOfImage(mbPtr->image, &width, &height);
311	    image = TkMacOSXGetNSImageWithTkImage(mbPtr->display,
312		    mbPtr->image, width, height);
313	} else {
314	    Tk_SizeOfBitmap(mbPtr->display, mbPtr->bitmap, &width, &height);
315	    image = TkMacOSXGetNSImageWithBitmap(mbPtr->display,
316		    mbPtr->bitmap, mbPtr->normalTextGC, width, height);
317	}
318	if (haveCompound) {
319	    switch ((enum compound) mbPtr->compound) {
320		case COMPOUND_TOP:
321		    pos = NSImageAbove;
322		    break;
323		case COMPOUND_BOTTOM:
324		    pos = NSImageBelow;
325		    break;
326		case COMPOUND_LEFT:
327		    pos = NSImageLeft;
328		    break;
329		case COMPOUND_RIGHT:
330		    pos = NSImageRight;
331		    break;
332		case COMPOUND_CENTER:
333		    pos = NSImageOverlaps;
334		    break;
335		case COMPOUND_NONE:
336		    pos = NSImageOnly;
337		    break;
338	    }
339	}
340	[button setImagePosition:pos];
341	[menuItem setImage:image];
342	bounds.size = cell ? [cell cellSize] : NSZeroSize;
343	if (bounds.size.height < height + 8) { /* workaround AppKit sizing bug */
344	    bounds.size.height = height + 8;
345	}
346#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
347	if (!mbPtr->indicatorOn && tkMacOSXUseCompatibilityMetrics) {
348	    bounds.size.width -= 16;
349	}
350#endif
351    } else {
352	bounds.size = cell ? [cell cellSize] : NSZeroSize;
353    }
354    if (haveText) {
355	titleRect =  cell ? [cell titleRectForBounds:bounds] : NSZeroRect;
356	if (mbPtr->wrapLength > 0 &&
357		titleRect.size.width > mbPtr->wrapLength) {
358	    if (style == NSRoundedBezelStyle) {
359		[button setBezelStyle:(style = NSRegularSquareBezelStyle)];
360		bounds.size = cell ? [cell cellSize] : NSZeroSize;
361		titleRect = cell ? [cell titleRectForBounds:bounds] : NSZeroRect;
362	    }
363	    bounds.size.width -= titleRect.size.width - mbPtr->wrapLength;
364	    bounds.size.height = 40000.0;
365	    [cell setWraps:YES];
366	    bounds.size = cell ? [cell cellSizeForBounds:bounds] : NSZeroSize;
367#ifdef TK_MAC_DEBUG_MENUBUTTON
368	    titleRect = cell ? [cell titleRectForBounds:bounds] : NSZeroRect;
369#endif
370#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
371	    if (tkMacOSXUseCompatibilityMetrics) {
372		bounds.size.height += 3;
373	    }
374#endif
375	}
376    }
377    width = lround(bounds.size.width);
378    height = lround(bounds.size.height);
379#if TK_MAC_BUTTON_USE_COMPATIBILITY_METRICS
380    if (tkMacOSXUseCompatibilityMetrics) {
381	macButtonPtr->fix = fixForStyle(style);
382	width -= boundsFixes[macButtonPtr->fix].trimW;
383	height -= boundsFixes[macButtonPtr->fix].trimH;
384    }
385#endif
386
387    if (haveImage || haveCompound) {
388	if (mbPtr->width > 0) {
389	    width = mbPtr->width;
390	}
391	if (mbPtr->height > 0) {
392	    height = mbPtr->height;
393	}
394    } else {
395	if (mbPtr->width > 0) {
396	    int avgWidth = Tk_TextWidth(mbPtr->tkfont, "0", 1);
397	    width = mbPtr->width * avgWidth;
398	}
399	if (mbPtr->height > 0) {
400	    Tk_FontMetrics fm;
401
402	    Tk_GetFontMetrics(mbPtr->tkfont, &fm);
403	    height = mbPtr->height * fm.linespace;
404	}
405    }
406    if (!haveImage || haveCompound) {
407	width += 2*mbPtr->padX;
408	height += 2*mbPtr->padY;
409    }
410    if (mbPtr->highlightWidth < 0) {
411	mbPtr->highlightWidth = 0;
412    }
413    if (haveImage) {
414	mbPtr->inset = mbPtr->highlightWidth;
415	width += 2*mbPtr->borderWidth;
416	height += 2*mbPtr->borderWidth;
417    } else {
418	mbPtr->inset = mbPtr->highlightWidth + mbPtr->borderWidth;
419    }
420    Tk_GeometryRequest(mbPtr->tkwin, width + 2 * mbPtr->inset,
421	    height + 2 * mbPtr->inset);
422    Tk_SetInternalBorder(mbPtr->tkwin, mbPtr->inset);
423#ifdef TK_MAC_DEBUG_MENUBUTTON
424    TKLog(@"menubutton %s bounds %@ titleRect %@ width %d height %d inset %d borderWidth %d",
425	    ((TkWindow *)mbPtr->tkwin)->pathName, NSStringFromRect(bounds),
426	    NSStringFromRect(titleRect), width, height, mbPtr->inset,
427	    mbPtr->borderWidth);
428#endif
429}
430
431/*
432 *--------------------------------------------------------------
433 *
434 * MenuButtonEventProc --
435 *
436 *	This procedure is invoked by the Tk dispatcher for various
437 *	events on buttons.
438 *
439 * Results:
440 *	None.
441 *
442 * Side effects:
443 *	When activation state changes, it is redisplayed.
444 *
445 *--------------------------------------------------------------
446 */
447
448static void
449MenuButtonEventProc(
450    ClientData clientData,	/* Information about window. */
451    XEvent *eventPtr)		/* Information about event. */
452{
453    TkMenuButton *mbPtr = (TkMenuButton *) clientData;
454
455    if (!mbPtr->tkwin || !Tk_IsMapped(mbPtr->tkwin)) {
456	return;
457    }
458    switch (eventPtr->type) {
459    case ActivateNotify:
460    case DeactivateNotify:
461	if (!(mbPtr->flags & REDRAW_PENDING)) {
462	    Tcl_DoWhenIdle(TkpDisplayMenuButton, (ClientData) mbPtr);
463	    mbPtr->flags |= REDRAW_PENDING;
464	}
465	break;
466    }
467}
468
469/*
470 * Local Variables:
471 * mode: c
472 * c-basic-offset: 4
473 * fill-column: 79
474 * coding: utf-8
475 * End:
476 */
477