macOS Drawers
This article applies to macOS only.
See also: Multiplatform Programming Guide
│
English (en) │
Overview
A drawer is a child window that slides out from a parent window and that the user can open or close (show or hide) while the parent window is open. A drawer should contain frequently accessed controls that do not need to be visible at all times. A drawer’s contents should be closely related to the contents of its parent window.
Here is an example of the open drawer which is generated by the example 1 code below:
When to use drawers
The suggested use of drawers is only for controls that need to be accessed fairly frequently but that do not need to be visible all the time. Contrast this criterion with a utility window, which should be visible and available whenever its main window is in the top layer. Some, now dated, examples of uses of drawers include access to favourites lists, the Mailbox drawer (in the Mail application) or browser bookmarks.
It should be noted that drawers have been deprecated by Apple with the recommendation that they not be used in "modern" applications. The Cocoa NSDrawer component does, however, remain available in the latest release of macOS (Big Sur) at the time of writing.
Drawer behaviour
The user shows or hides a drawer, typically by clicking a button or choosing a command. If a drawer contains a valid drop target, you may also want to open the drawer when the user drags an appropriate object to where the drawer appears.
When a drawer opens, it appears to be sliding from behind its parent window, to the left, right, or down. If a user moves a parent window to the edge of the screen and then opens a drawer, it opens on the side of the window that has room. If the user makes a window so big that there’s no room on either side, the drawer opens off the screen.
To support the illusion that a closed drawer is hidden behind its parent window, an open drawer should be smaller than its parent window. When the parent window is resized vertically, an open drawer resizes, if necessary, to ensure that it does not exceed the height of the parent window. A drawer can be shorter than its parent window. The illusion is further reinforced by the fact that the inner border of a drawer is hidden by the parent window and that the parent window’s shadow is seen on the drawer when appropriate.
The user can resize an open drawer by dragging its outside border. The degree to which a drawer can be resized is determined by the content of the drawer. If the user resizes a drawer significantly — to the point where content is mostly obscured — the drawer should simply close. For example, if a drawer contains a scrolling list, the user should be able to resize the drawer to cover up the edge of the list. But if the user makes the drawer so small that the items in the list are difficult to identify, the drawer should close. If the user sets a new size (if that is possible) for a drawer, the new size should be used the next time the drawer is opened.
A drawer should maintain its state (open or closed) when its parent window becomes inactive or when the window is closed and then reopened. When a parent window with an open drawer is minimized, the drawer should close; the drawer should reopen when the window is made active again.
A drawer can contain any control that is appropriate to its intended use. Consider a drawer part of the parent window; do not dim a drawer’s controls when the parent window has focus, and vice versa. When full keyboard access is on, a drawer’s contents should be included in the window components that the user can select by pressing Tab ⇆.
Example 1 code
unit Unit1;
{$mode objfpc}{$H+}
{$modeswitch objectivec1}
interface
uses
Forms, // for LCL Form
StdCtrls, // for LCL buttons
CocoaAll; // for Cocoa controls (NSDrawer, NSWindow etc)
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure OpenButtonClick(Sender: TObject);
procedure CloseButtonClick(Sender: TObject);
procedure ToggleButtonClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
public
end;
var
Form1: TForm1;
myDrawer: NSDrawer;
myWindow: NSWindow;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormActivate(Sender: TObject);
var
mySize: NSsize;
//myLeadingOffset: Double; // Attribute implementation bug in FPC
myTrailingOffset: Double;
begin
// Assign the Form1 window which contains the view (object) to myWindow.
// To display itself, a view must be placed in a window (represented by an
// NSWindow object). myWindow is later made the parent window for the drawer.
myWindow := NSView(Form1.Handle).window;
// Drawer construction
mySize.width := 240; // set width of drawer
//myLeadingOffset := 25; // distance from the top/left edge of the parent window to drawer
myTrailingOffset := 25; // distance to the right/bottom edge of drawer from right/bottom edge of the parent window
myDrawer := NSDrawer.alloc.initWithContentSize_preferredEdge (mySize, NSMinXEdge);
myDrawer.setParentWindow(myWindow);
myDrawer.setMaxContentSize(mySize);
//myDrawer.setLeadingOffset(myLeadingOffset);
myDrawer.setTrailingOffset(myTrailingOffset);
end;
procedure TForm1.OpenButtonClick(Sender: TObject);
begin
myDrawer.open; // Open drawer
end;
procedure TForm1.CloseButtonClick(Sender: TObject);
begin
myDrawer.close; // Close drawer
end;
procedure TForm1.ToggleButtonClick(Sender: TObject);
begin
myDrawer.toggle(myWindow); // Toggle drawer state open/closed
end;
end.
When initialising our drawer's content view we set the preferred edge for it to open from to NSMinXEdge, but what is it and what does that mean? This table will explain the opaqueness:
Constant | Value | Meaning |
---|---|---|
NSMinXEdge | 0 | The left side of a rectangle (window) |
NSMinYEdge | 1 | The bottom edge of a rectangle (window) |
NSMaxXEdge | 2 | The right side of a rectangle (window) |
NSMaxYEdge | 3 | The top edge of a rectangle (window) |
When initialising our drawer's content view we also only set the pixel width of the mySize variable. Why not the height too? The height of the content view is a calculated value based on the leading and trailing pixel offsets of the drawer and the height of the parent window, and is not affected by the height specified in the content size. Unless you specify otherwise, the height of the drawer is the same as that of the parent window.
In the case of a drawer on the top or bottom edge of the parent window, the height of the content view is set according to the height specified in the content size. The width of the content view is a calculated value based on the leading and trailing offsets for the drawer and the width of the parent window, and is not affected by the height specified in the content size.
Note: the leading offset is unfortunately currently faulty in FPC (up to and including trunk) and does not do what it is supposed to do (try it and see what happens).
After creating the drawer, you must attach it to a parent window by calling the setParentWindow(NSWindow) method.
The example code above creates a drawer for our application and allows us to open/close/toggle it open/closed, but we seem to be missing something... the drawer has no content. In the next code example, we will remedy this oversight and add some content.
Example 2 code
unit Unit1;
{$mode objfpc}{$H+}
{$modeswitch objectivec1}
interface
uses
Forms, // for LCL Form
StdCtrls, // for LCL buttons
CocoaAll; // for Cocoa controls (NSDrawer, NSWindow etc)
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure OpenButtonClick(Sender: TObject);
procedure CloseButtonClick(Sender: TObject);
procedure ToggleButtonClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
public
end;
var
Form1: TForm1;
myDrawer: NSDrawer;
myWindow: NSWindow;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormActivate(Sender: TObject);
var
mySize: NSsize;
//myLeadingOffset: Double; // Attribute implementation bug in FPC
myTrailingOffset: Double;
myText: NSTextField;
begin
// Assign the Form1 window which contains the view (object) to myWindow.
// To display itself, a view must be placed in a window (represented by an
// NSWindow object). myWindow is later made the parent window for the drawer.
myWindow := NSView(Form1.Handle).window;
// Drawer construction
mySize.width := 240; // set width of drawer
//myLeadingOffset := 25; // distance from the top/left edge of the parent window to drawer
myTrailingOffset := 25; // distance to the right/bottom edge of drawer from right/bottom edge of the parent window
myDrawer := NSDrawer.alloc.initWithContentSize_preferredEdge (mySize, NSMinXEdge);
myDrawer.setParentWindow(myWindow);
myDrawer.setMaxContentSize(mySize);
//myDrawer.setLeadingOffset(myLeadingOffset);
myDrawer.setTrailingOffset(myTrailingOffset);
// textfield construction (NSMakeRect: origin of x, y, and size of width, height)
myText := NSTextField.alloc.initWithFrame(NSMakeRect(20, 100, 120, 20)).autorelease;
myText.setBezeled(False);
myText.setDrawsBackground(False);
myText.setEditable(False);
myText.setSelectable(False);
myText.setStringValue(NSSTR('Test text'));
myDrawer.contentView.addSubview(myText);
end;
procedure TForm1.OpenButtonClick(Sender: TObject);
begin
myDrawer.open; // Open drawer
end;
procedure TForm1.CloseButtonClick(Sender: TObject);
begin
myDrawer.close; // Close drawer
end;
procedure TForm1.ToggleButtonClick(Sender: TObject);
begin
myDrawer.toggle(myWindow); // Toggle drawer state open/closed
end;
end.
The highlighted lines above show the code which we have added to display our NSTextField containing "Test text". The text was positioned 20 pixels along the X (horizontal) axis and 100 pixels along the Y (vertical) axis. Note that the Cocoa pixel coordinate system has its origin (0,0) at the bottom, left corner of the content view or window as appropriate.
Text fields display text either as a static label (as here because we specified myText.setEditable(False);
and myText.setSelectable(False);
) or as an editable input field. You can experiment with an editable input field by changing those two text field attributes to True.
The content of a text field is either plain text or a rich-text attributed string. Text fields also support line wrapping to display multiline text, and a variety of truncation styles if the content does not fit the available space. For more details on text fields, see the External links below.
Although our application already has Close and Toggle buttons, this is a rather contrived example for demonstration purposes. In a real application, you might wish to locate a Close button in the drawer itself. For how to do this, see the next code example.
Example 3 code
unit Unit1;
{$mode objfpc}{$H+}
{$modeswitch objectivec1}
interface
uses
Forms, // for LCL Form
StdCtrls, // for LCL buttons
CocoaAll; // for Cocoa controls (NSDrawer, NSWindow etc)
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure OpenButtonClick(Sender: TObject);
procedure CloseButtonClick(Sender: TObject);
procedure ToggleButtonClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
public
end;
var
Form1: TForm1;
myDrawer: NSDrawer;
myWindow: NSWindow;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.FormActivate(Sender: TObject);
var
mySize: NSsize;
//myLeadingOffset: Double; // Attribute implementation bug in FPC
myTrailingOffset: Double;
myText: NSTextField;
myButton: NSButton;
begin
// Assign the Form1 window which contains the view (object) to myWindow.
// To display itself, a view must be placed in a window (represented by an
// NSWindow object). myWindow is later made the parent window for the drawer.
myWindow := NSView(Form1.Handle).window;
// Drawer construction
mySize.width := 240; // set width of drawer
//myLeadingOffset := 25; // distance from the top/left edge of the parent window to drawer
myTrailingOffset := 25; // distance to the right/bottom edge of drawer from right/bottom edge of the parent window
myDrawer := NSDrawer.alloc.initWithContentSize_preferredEdge (mySize, NSMinXEdge);
myDrawer.setParentWindow(myWindow);
myDrawer.setMaxContentSize(mySize);
//myDrawer.setLeadingOffset(myLeadingOffset);
myDrawer.setTrailingOffset(myTrailingOffset);
// textfield construction (NSMakeRect: origin of x, y, and size of width, height)
myText := NSTextField.alloc.initWithFrame(NSMakeRect(10, 100, 120, 20)).autorelease;
myText.setBezeled(False);
myText.setDrawsBackground(False);
myText.setEditable(False);
myText.setSelectable(False);
myText.setStringValue(NSSTR('Test text'));
myDrawer.contentView.addSubview(myText);
// button construction (NSMakeRect: origin of x, y, and size of width, height)
myButton := NSButton.alloc.initWithFrame(NSMakeRect(20, 10, 80, 50)).autorelease;
myButton.setTitle(NSSTR('Close'));
myButton.setButtonType(NSMomentaryLightButton);
myButton.setBezelStyle(NSRoundedBezelStyle);
myDrawer.contentView.addSubview(myButton);
end;
procedure TForm1.OpenButtonClick(Sender: TObject);
begin
myDrawer.open; // Open drawer
end;
procedure TForm1.CloseButtonClick(Sender: TObject);
begin
myDrawer.close; // Close drawer
end;
procedure TForm1.ToggleButtonClick(Sender: TObject);
begin
myDrawer.toggle(myWindow); // Toggle drawer state open/closed
end;
end.
The highlighted lines above show the code which we have added to display our NSButton titled "Close". The text was positioned 20 pixels along the X (horizontal) axis and 10 pixels along the Y (vertical) axis. Remember, the X,Y coordinate origin is the bottom left corner of the content view.
What? The button does not do anything when clicked? We will fix that oversight in the next code example.
Example 4 code
unit Unit1;
{$mode objfpc}{$H+}
{$modeswitch objectivec1}
interface
uses
Forms, // for LCL Form
StdCtrls, // for LCL buttons
CocoaAll; // for Cocoa controls (NSDrawer, NSWindow etc)
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
procedure OpenButtonClick(Sender: TObject);
procedure CloseButtonClick(Sender: TObject);
procedure ToggleButtonClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
public
end;
{ TMyDelegate }
TMyDelegate = objcclass(NSObject)
public
procedure HandleButtonClick; message 'HandleButtonClick';
end;
var
Form1: TForm1;
myDrawer: NSDrawer;
myWindow: NSWindow;
myDelegate: TMyDelegate;
implementation
{$R *.lfm}
{ TForm1 }
procedure TMyDelegate.HandleButtonClick;
begin
myDrawer.Close;
end;
procedure TForm1.FormActivate(Sender: TObject);
var
mySize: NSsize;
//myLeadingOffset: Double; // Attribute implementation bug in FPC
myTrailingOffset: Double;
myText: NSTextField;
myButton: NSButton;
begin
// Create delegate
myDelegate := TMyDelegate.alloc.init;
// Assign the Form1 window which contains the view (object) to myWindow.
// To display itself, a view must be placed in a window (represented by an
// NSWindow object). myWindow is later made the parent window for the drawer.
myWindow := NSView(Form1.Handle).window;
// Drawer construction
mySize.width := 240; // set width of drawer
//myLeadingOffset := 25; // distance from the top/left edge of the parent window to drawer
myTrailingOffset := 25; // distance to the right/bottom edge of drawer from right/bottom edge of the parent window
myDrawer := NSDrawer.alloc.initWithContentSize_preferredEdge (mySize, NSMinXEdge);
myDrawer.setParentWindow(myWindow);
myDrawer.setMaxContentSize(mySize);
//myDrawer.setLeadingOffset(myLeadingOffset);
myDrawer.setTrailingOffset(myTrailingOffset);
// textfield construction (NSMakeRect: origin of x, y, and size of width, height)
myText := NSTextField.alloc.initWithFrame(NSMakeRect(10, 100, 120, 20)).autorelease;
myText.setBezeled(False);
myText.setDrawsBackground(False);
myText.setEditable(False);
myText.setSelectable(False);
myText.setStringValue(NSSTR('Test text'));
myDrawer.contentView.addSubview(myText);
// button construction (NSMakeRect: origin of x, y, and size of width, height)
myButton := NSButton.alloc.initWithFrame(NSMakeRect(20, 10, 80, 50)).autorelease;
myButton.setTitle(NSSTR('Close'));
myButton.setButtonType(NSMomentaryLightButton);
myButton.setBezelStyle(NSRoundedBezelStyle);
myDrawer.contentView.addSubview(myButton);
// button event handler
myButton.setTarget(myDelegate);
myButton.setAction(ObjCSelector(myDelegate.HandleButtonClick));
end;
procedure TForm1.OpenButtonClick(Sender: TObject);
begin
myDrawer.open; // Open drawer
end;
procedure TForm1.CloseButtonClick(Sender: TObject);
begin
myDrawer.close; // Close drawer
end;
procedure TForm1.ToggleButtonClick(Sender: TObject);
begin
myDrawer.toggle(myWindow); // Toggle drawer state open/closed
end;
end.
Cocoa makes much use of a common design pattern called delegation. The idea behind the delegation pattern is similar to that behind inheritance and prototypes: You allow one object to define some subset of the behaviour of another object. This is very common in the AppKit framework where each user interface object (windows, panels, buttons, menus, scrollers, and text fields) uses delegation to allow you to define what happens in response to user interface events. This helper object is known as the delegate of the first object (in this case our NSButton).
To handle a button click, we need a method in the delegate object that the contains code we want to be executed when the button is clicked. In this case the corresponding method is our delegate's HandleButtonClick procedure which we implemented. Then we need to connect this method to the button click event using the setTarget
and setAction
methods. The target is the delegate object that receives the action messages from our control and the action is the default action-message selector associated with our control.