Difference between revisions of "macOS Drawers"

From Lazarus wiki
Jump to navigationJump to search
(→‎See also: New section)
m (→‎External Links: Add link)
Line 341: Line 341:
 
* [https://developer.apple.com/documentation/appkit/nstextfield Apple: NSTextField].
 
* [https://developer.apple.com/documentation/appkit/nstextfield Apple: NSTextField].
 
* [https://developer.apple.com/documentation/appkit/nsbutton Apple: NSButton].
 
* [https://developer.apple.com/documentation/appkit/nsbutton Apple: NSButton].
 +
* [https://developer.apple.com/documentation/appkit/nscontrol Apple NSControl].

Revision as of 12:15, 16 March 2021

macOSlogo.png

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:

macOS NSDrawer.jpg

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.

NSMinXEdge etc - to do.

The above example code 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 at 20 along the X (horizontal) axis and 100 along the Y (vertical) axis. Note that the Cocoa coordinate system has its origin (0,0) at the bottom, left corner of the content view.

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 at 20 along the X (horizontal) axis and 10 along the Y (vertical) axis. Remember, the X,Y 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.

See also

External links