Add support for menus

This commit is contained in:
david-swift 2023-10-22 16:39:46 +02:00
parent af59675089
commit ac0c775985
33 changed files with 593 additions and 10 deletions

View File

@ -3,6 +3,8 @@
## Protocols ## Protocols
- [App](protocols/App.md) - [App](protocols/App.md)
- [MenuItem](protocols/MenuItem.md)
- [MenuItemGroup](protocols/MenuItemGroup.md)
- [StateProtocol](protocols/StateProtocol.md) - [StateProtocol](protocols/StateProtocol.md)
- [View](protocols/View.md) - [View](protocols/View.md)
- [Widget](protocols/Widget.md) - [Widget](protocols/Widget.md)
@ -11,7 +13,6 @@
## Structs ## Structs
- [ApplicationWindow](structs/ApplicationWindow.md)
- [Binding](structs/Binding.md) - [Binding](structs/Binding.md)
- [Button](structs/Button.md) - [Button](structs/Button.md)
- [Clamp](structs/Clamp.md) - [Clamp](structs/Clamp.md)
@ -20,11 +21,15 @@
- [HeaderBar](structs/HeaderBar.md) - [HeaderBar](structs/HeaderBar.md)
- [InspectorWrapper](structs/InspectorWrapper.md) - [InspectorWrapper](structs/InspectorWrapper.md)
- [List](structs/List.md) - [List](structs/List.md)
- [Menu](structs/Menu.md)
- [MenuButton](structs/MenuButton.md)
- [MenuSection](structs/MenuSection.md)
- [NavigationSplitView](structs/NavigationSplitView.md) - [NavigationSplitView](structs/NavigationSplitView.md)
- [ScrollView](structs/ScrollView.md) - [ScrollView](structs/ScrollView.md)
- [State](structs/State.md) - [State](structs/State.md)
- [StateWrapper](structs/StateWrapper.md) - [StateWrapper](structs/StateWrapper.md)
- [StatusPage](structs/StatusPage.md) - [StatusPage](structs/StatusPage.md)
- [Submenu](structs/Submenu.md)
- [Text](structs/Text.md) - [Text](structs/Text.md)
- [ToolbarView](structs/ToolbarView.md) - [ToolbarView](structs/ToolbarView.md)
- [UpdateObserver](structs/UpdateObserver.md) - [UpdateObserver](structs/UpdateObserver.md)
@ -49,6 +54,8 @@
- [App](extensions/App.md) - [App](extensions/App.md)
- [Array](extensions/Array.md) - [Array](extensions/Array.md)
- [MenuItem](extensions/MenuItem.md)
- [MenuItemGroup](extensions/MenuItemGroup.md)
- [NativeWidgetPeer](extensions/NativeWidgetPeer.md) - [NativeWidgetPeer](extensions/NativeWidgetPeer.md)
- [String](extensions/String.md) - [String](extensions/String.md)
- [View](extensions/View.md) - [View](extensions/View.md)
@ -61,6 +68,8 @@
- [Body](typealiases/Body.md) - [Body](typealiases/Body.md)
- [GTUIApplicationWindow](typealiases/GTUIApplicationWindow.md) - [GTUIApplicationWindow](typealiases/GTUIApplicationWindow.md)
- [GTUIWindow](typealiases/GTUIWindow.md) - [GTUIWindow](typealiases/GTUIWindow.md)
- [MenuBuilder](typealiases/MenuBuilder.md)
- [MenuContent](typealiases/MenuContent.md)
- [Scene](typealiases/Scene.md) - [Scene](typealiases/Scene.md)
- [SceneBuilder](typealiases/SceneBuilder.md) - [SceneBuilder](typealiases/SceneBuilder.md)

View File

@ -0,0 +1,8 @@
**EXTENSION**
# `MenuItem`
## Properties
### `content`
The menu item's content is itself.

View File

@ -0,0 +1,9 @@
**EXTENSION**
# `MenuItemGroup`
## Methods
### `addMenuItems(menu:app:window:)`
Add the menu items described by the group to a menu.
- Parameter menu: The menu.

View File

@ -0,0 +1,14 @@
**PROTOCOL**
# `MenuItem`
A structure representing the content for a certain menu item type.
## Methods
### `addMenuItem(menu:app:window:)`
Add the menu item to a certain menu.
- Parameters:
- menu: The menu.
- app: The application containing the menu.
- window: The application window containing the menu.

View File

@ -0,0 +1,10 @@
**PROTOCOL**
# `MenuItemGroup`
A structure conforming to `MenuItemGroup` can be added to the content accepting a menu.
## Properties
### `content`
The menu's content.

View File

@ -0,0 +1,56 @@
**STRUCT**
# `Menu`
A menu button widget.
## Properties
### `label`
The button's label.
### `icon`
The button's icon.
### `content`
The menu's content.
### `app`
The application.
### `window`
The window.
## Methods
### `init(_:icon:app:window:content:)`
Initialize a menu button.
- Parameters:
- label: The button's label.
- icon: The button's icon.
- app: The application.
- window: The application window.
- content: The menu's content.
### `init(_:app:window:content:)`
Initialize a menu button.
- Parameters:
- label: The buttons label.
- app: The application.
- window: The application window.
- content: The menu's content.
### `update(_:)`
Update a button's view storage.
- Parameter storage: The view storage.
### `container()`
Get a button's view storage.
- Returns: The button's view storage.

View File

@ -0,0 +1,48 @@
**STRUCT**
# `MenuButton`
A button widget for menus.
## Properties
### `label`
The button's label.
### `handler`
The button's action handler.
### `shortcut`
The keyboard shortcut.
### `preferApplicationWindow`
Whether to prefer adding the action to the application window.
## Methods
### `init(_:window:handler:)`
Initialize a menu button.
- Parameters:
- label: The buttons label.
- window: Whether to prefer adding the action to the application window.
- handler: The button's action handler.
### `addMenuItem(menu:app:window:)`
Add the button to a menu.
- Parameters:
- menu: The menu.
- app: The application containing the menu.
- window: The application window containing the menu.
### `keyboardShortcut(_:)`
Create a keyboard shortcut for an application from a button.
Note that the keyboard shortcut is available after the view has been visible for the first time.
- Parameters:
- shortcut: The keyboard shortcut.
- Returns: The button.

View File

@ -0,0 +1,24 @@
**STRUCT**
# `MenuSection`
A section for menus.
## Properties
### `sectionContent`
The content of the section.
## Methods
### `init(content:)`
Initialize a section for menus.
- Parameter content: The content of the section.
### `addMenuItem(menu:app:window:)`
Add the section to a menu.
- Parameters:
- menu: The menu.
- app: The application containing the menu.
- window: The application window containing the menu.

View File

@ -0,0 +1,30 @@
**STRUCT**
# `Submenu`
A submenu widget.
## Properties
### `label`
The submenu's label.
### `submenuContent`
The content of the submenu.
## Methods
### `init(_:content:)`
Initialize a submenu.
- Parameters:
- label: The submenu's label.
- content: The content of the submenu.
### `addMenuItem(menu:app:window:)`
Add the submenu to a menu.
- Parameters:
- menu: The menu.
- app: The application containing the menu.
- window: The application window containing the menu.

View File

@ -2,7 +2,7 @@
# `Window` # `Window`
A structure representing a simple window type. A structure representing an application window type.
Note that multiple instances of a window can be opened at the same time. Note that multiple instances of a window can be opened at the same time.
@ -19,6 +19,10 @@ The window's content.
Whether an instance of the window type should be opened when the app is starting up. Whether an instance of the window type should be opened when the app is starting up.
### `shortcuts`
The keyboard shortcuts.
### `appShortcuts` ### `appShortcuts`
The keyboard shortcuts on the app level. The keyboard shortcuts on the app level.
@ -53,6 +57,22 @@ Get the storage of the content view.
### `update(_:app:)` ### `update(_:app:)`
Update a window storage's content. Update a window storage's content.
- Parameter storage: The storage to update.
### `keyboardShortcut(_:action:)`
Add a keyboard shortcut.
- Parameters: - Parameters:
- storage: The storage to update. - shortcut: The keyboard shortcut.
- app: The application. - action: The closure to execute when the keyboard shortcut is pressed.
- Returns: The window.
### `updateShortcuts(window:)`
Update the keyboard shortcuts.
- Parameter window: The application window.
### `closeShortcut()`
Add the shortcut "<Ctrl>w" which closes the window.
- Returns: The window.

View File

@ -0,0 +1,5 @@
**TYPEALIAS**
# `MenuBuilder`
A builder for the `MenuContent`

View File

@ -0,0 +1,5 @@
**TYPEALIAS**
# `MenuContent`
`MenuContent` is an array of menu item groups.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -78,6 +78,7 @@ If you want to use _Adwaita_ in a project, but there are widgets missing, open a
| VStack | A widget which arranges child widgets into a single column. | GtkBox | | VStack | A widget which arranges child widgets into a single column. | GtkBox |
| HStack | A widget which arranges child widgets into a single row. | GtkBox | | HStack | A widget which arranges child widgets into a single row. | GtkBox |
| List | A widget which arranges child widgets vertically into rows. | GtkListBox | | List | A widget which arranges child widgets vertically into rows. | GtkListBox |
| Menu | A widget showing a button that toggles the appearance of a menu. | GtkMenuButton |
| NavigationSplitView | A widget presenting sidebar and content side by side. | AdwNavigationSplitView | | NavigationSplitView | A widget presenting sidebar and content side by side. | AdwNavigationSplitView |
| ScrollView | A container that makes its child scrollable. | GtkScrolledWindow | | ScrollView | A container that makes its child scrollable. | GtkScrolledWindow |
| StatusPage | A page with an icon, title, and optionally description and widget.| AdwStatusPage | | StatusPage | A page with an icon, title, and optionally description and widget.| AdwStatusPage |
@ -134,7 +135,19 @@ If you want to use _Adwaita_ in a project, but there are widgets missing, open a
| Syntax | Description | | Syntax | Description |
| ------------------------------- | --------------------------------------------------------------------------------------- | | ------------------------------- | --------------------------------------------------------------------------------------- |
| `keyboardShortcut(_:action:)` | Create a keyboard shortcut available in one window. | | `keyboardShortcut(_:action:)` | Create a keyboard shortcut available in one window. |
| `closeShortcut()` | Create a keyboard shortcut for closing the window with "Ctrl + w". | | `closeShortcut()` | Create a keyboard shortcut for closing the window with "Ctrl + w".
### Menu Widgets
| Name | Description | Widget |
| -------------------- | ----------------------------------------------------------------- | ---------------------- |
| MenuButton | A button in a menu. | GMenuItem |
| MenuSection | A collection of menu widgets grouped with lines. | GMenuItem |
| Submenu | A collection of menu widgets grouped by navigation. | GMenuItem |
### `MenuButton` Modifiers
| Syntax | Description |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| `keyboardShortcut(_:)` | Assign a keyboard shortcut to the button's action. |
## Installation ## Installation
### Dependencies ### Dependencies

View File

@ -0,0 +1,58 @@
//
// MenuButton.swift
// Adwaita
//
// Created by david-swift on 22.10.23.
//
import GTUI
/// A button widget for menus.
public struct MenuButton: MenuItem {
/// The button's label.
var label: String
/// The button's action handler.
var handler: () -> Void
/// The keyboard shortcut.
var shortcut = ""
/// Whether to prefer adding the action to the application window.
var preferApplicationWindow: Bool
/// Initialize a menu button.
/// - Parameters:
/// - label: The buttons label.
/// - window: Whether to prefer adding the action to the application window.
/// - handler: The button's action handler.
public init(_ label: String, window: Bool = true, handler: @escaping () -> Void) {
self.label = label
preferApplicationWindow = window
self.handler = handler
}
/// Add the button to a menu.
/// - Parameters:
/// - menu: The menu.
/// - app: The application containing the menu.
/// - window: The application window containing the menu.
public func addMenuItem(menu: GTUI.Menu, app: GTUIApp, window: GTUIApplicationWindow?) {
if let window, preferApplicationWindow {
_ = menu.append(label, window: window, shortcut: shortcut, handler: handler)
} else {
_ = menu.append(label, app: app, shortcut: shortcut, handler: handler)
}
}
/// Create a keyboard shortcut for an application from a button.
///
/// Note that the keyboard shortcut is available after the view has been visible for the first time.
/// - Parameters:
/// - shortcut: The keyboard shortcut.
/// - Returns: The button.
public func keyboardShortcut(_ shortcut: String) -> Self {
var newSelf = self
newSelf.shortcut = shortcut
return newSelf
}
}

View File

@ -0,0 +1,35 @@
//
// Submenu.swift
// Adwaita
//
// Created by david-swift on 22.10.23.
//
import GTUI
/// A section for menus.
public struct MenuSection: MenuItem {
/// The content of the section.
var sectionContent: MenuContent
/// Initialize a section for menus.
/// - Parameter content: The content of the section.
public init(@MenuBuilder content: () -> MenuContent) {
self.sectionContent = content()
}
/// Add the section to a menu.
/// - Parameters:
/// - menu: The menu.
/// - app: The application containing the menu.
/// - window: The application window containing the menu.
public func addMenuItem(menu: GTUI.Menu, app: GTUIApp, window: GTUIApplicationWindow?) {
let section = GTUI.Menu()
_ = menu.append("", section: section)
for element in sectionContent {
element.addMenuItems(menu: section, app: app, window: window)
}
}
}

View File

@ -0,0 +1,40 @@
//
// Submenu.swift
// Adwaita
//
// Created by david-swift on 22.10.23.
//
import GTUI
/// A submenu widget.
public struct Submenu: MenuItem {
/// The submenu's label.
var label: String
/// The content of the submenu.
var submenuContent: MenuContent
/// Initialize a submenu.
/// - Parameters:
/// - label: The submenu's label.
/// - content: The content of the submenu.
public init(_ label: String, @MenuBuilder content: () -> MenuContent) {
self.label = label
self.submenuContent = content()
}
/// Add the submenu to a menu.
/// - Parameters:
/// - menu: The menu.
/// - app: The application containing the menu.
/// - window: The application window containing the menu.
public func addMenuItem(menu: GTUI.Menu, app: GTUIApp, window: GTUIApplicationWindow?) {
let submenu = GTUI.Menu()
_ = menu.append(label, submenu: submenu)
for element in submenuContent {
element.addMenuItems(menu: submenu, app: app, window: window)
}
}
}

View File

@ -0,0 +1,27 @@
//
// MenuItem.swift
// Adwaita
//
// Created by david-swift on 22.10.23.
//
import GTUI
/// A structure representing the content for a certain menu item type.
public protocol MenuItem: MenuItemGroup {
/// Add the menu item to a certain menu.
/// - Parameters:
/// - menu: The menu.
/// - app: The application containing the menu.
/// - window: The application window containing the menu.
func addMenuItem(menu: GTUI.Menu, app: GTUIApp, window: GTUIApplicationWindow?)
}
extension MenuItem {
/// The menu item's content is itself.
@MenuBuilder public var content: MenuContent { self }
}

View File

@ -0,0 +1,37 @@
//
// MenuItemGroup.swift
// Adwaita
//
// Created by david-swift on 22.10.23.
//
import GTUI
/// A structure conforming to `MenuItemGroup` can be added to the content accepting a menu.
public protocol MenuItemGroup {
/// The menu's content.
@MenuBuilder var content: MenuContent { get }
}
extension MenuItemGroup {
/// Add the menu items described by the group to a menu.
/// - Parameter menu: The menu.
func addMenuItems(menu: GTUI.Menu, app: GTUIApp, window: GTUIApplicationWindow?) {
for element in content {
if let item = element as? MenuItem {
item.addMenuItem(menu: menu, app: app, window: window)
} else {
element.addMenuItems(menu: menu, app: app, window: window)
}
}
}
}
/// `MenuContent` is an array of menu item groups.
public typealias MenuContent = [MenuItemGroup]
/// A builder for the `MenuContent`
public typealias MenuBuilder = ArrayBuilder<MenuItemGroup>

View File

@ -0,0 +1,98 @@
//
// Menu.swift
// Adwaita
//
// Created by david-swift on 21.10.23.
//
import GTUI
/// A menu button widget.
public struct Menu: Widget {
/// The button's label.
var label: String?
/// The button's icon.
var icon: Icon?
/// The menu's content.
var content: MenuContent
/// The application.
var app: GTUIApp
/// The window.
var window: GTUIApplicationWindow?
// swiftlint:disable function_default_parameter_at_end
/// Initialize a menu button.
/// - Parameters:
/// - label: The button's label.
/// - icon: The button's icon.
/// - app: The application.
/// - window: The application window.
/// - content: The menu's content.
public init(
_ label: String? = nil,
icon: Icon,
app: GTUIApp,
window: GTUIApplicationWindow?,
@MenuBuilder content: () -> MenuContent
) {
self.label = label
self.icon = icon
self.app = app
self.window = window
self.content = content()
}
// swiftlint:enable function_default_parameter_at_end
/// Initialize a menu button.
/// - Parameters:
/// - label: The buttons label.
/// - app: The application.
/// - window: The application window.
/// - content: The menu's content.
public init(
_ label: String,
app: GTUIApp,
window: GTUIApplicationWindow?,
@MenuBuilder content: () -> MenuContent
) {
self.label = label
self.app = app
self.window = window
self.content = content()
}
/// Update a button's view storage.
/// - Parameter storage: The view storage.
public func update(_ storage: ViewStorage) {
if let button = storage.view as? GTUI.MenuButton {
let content = button.getContent()
if let label {
if icon == nil {
button.setLabel(label)
} else {
content?.setLabel(label)
}
}
if let icon {
content?.setIcon(icon)
}
}
}
/// Get a button's view storage.
/// - Returns: The button's view storage.
public func container() -> ViewStorage {
let button: GTUI.MenuButton
if let icon {
button = .init(label, icon: icon)
} else {
button = .init(label ?? .init())
}
for element in content {
element.addMenuItems(menu: button.getMenu(), app: app, window: window)
}
return .init(button)
}
}

View File

@ -20,9 +20,6 @@ struct Demo: App {
Window(id: "main") { window in Window(id: "main") { window in
DemoContent(window: window, app: app) DemoContent(window: window, app: app)
} }
.appKeyboardShortcut("n".ctrl()) { $0.addWindow("main") }
.closeShortcut()
.quitShortcut()
HelperWindows() HelperWindows()
} }
@ -44,7 +41,7 @@ struct Demo: App {
struct DemoContent: View { struct DemoContent: View {
@State private var selection: Page = .welcome @State private var selection: Page = .welcome
var window: GTUIWindow var window: GTUIApplicationWindow
var app: GTUIApp! var app: GTUIApp!
var view: Body { var view: Body {
@ -58,7 +55,24 @@ struct Demo: App {
.sidebarStyle() .sidebarStyle()
} }
.topToolbar { .topToolbar {
HeaderBar.empty() HeaderBar.end {
Menu(icon: .default(icon: .openMenu), app: app, window: window) {
MenuButton("New Window", window: false) {
app.addWindow("main")
}
.keyboardShortcut("n".ctrl())
MenuButton("Close Window") {
window.close()
}
.keyboardShortcut("w".ctrl())
MenuSection {
MenuButton("Quit", window: false) {
app.quit()
}
.keyboardShortcut("q".ctrl())
}
}
}
} }
.navigationTitle("Demo") .navigationTitle("Demo")
} content: { } content: {

View File

@ -70,6 +70,29 @@ struct HelloWorld: App {
} }
``` ```
## Create Shortcuts from a Menu
The most elegant way for adding keyboard shortcuts is in many cases adding them via menus.
Here is an example using a menu button:
```swift
struct TestView: View {
var app: GTUIApp
var view: Body {
Menu(icon: .default(icon: .openMenu), app: app) {
MenuButton("New Window", window: false) {
app.addWindow("main")
}
// Add a keyboard shortcut to the app.
.keyboardShortcut("n".ctrl())
}
}
}
```
Add the keyboard shortcut to a single window by specifying the `window` parameter in the initializer of `Menu`,
and removing `window: false` in the initializer of `MenuButton`.
## Create Shortcuts from a Button ## Create Shortcuts from a Button
It's possible to easily create a keyboard shortcut from a button. It's possible to easily create a keyboard shortcut from a button.
Use `appKeyboardShortcut` instead of `keyboardShortcut` for shortcuts on an application level. Use `appKeyboardShortcut` instead of `keyboardShortcut` for shortcuts on an application level.