1import UIKit
2
3/// Formatting above the keyboard, as Notes offers it: To Do first, then text styles, lists,
4/// indentation, tags and pictures, the keyboard's dismissal at the end.
5final class FormatBar: UIInputView {
6 /// `sb_view_apply` commands and `sb_view_format` bits.
7 private static let buttons: [(symbol: String, label: String, command: UInt8)] = [
8 ("checklist", "To Do", 16),
9 ("bold", "Bold", 0),
10 ("italic", "Italic", 1),
11 ("underline", "Underline", 2),
12 ("strikethrough", "Strikethrough", 3),
13 ("highlighter", "Highlight", 10),
14 ("list.bullet", "Bullets", 4),
15 ("list.number", "Numbering", 5),
16 ("decrease.indent", "Decrease Indent", 7),
17 ("increase.indent", "Increase Indent", 6),
18 ]
19 private var toggles: [(UIButton, UInt8)] = []
20 private var bits: UInt64 = 0
21 var onApply: ((UInt8) -> Void)?
22 /// Applies a style of OneNote's gallery, by place; `stylePlace` names the selection's.
23 var onStyle: ((UInt8) -> Void)?
24 var stylePlace: (() -> Int32)?
25 /// OneNote 2010's Styles gallery, in its order.
26 private static let styles = [
27 "Heading 1", "Heading 2", "Heading 3", "Heading 4", "Heading 5", "Heading 6",
28 "Page Title", "Citation", "Quote", "Code", "Normal",
29 ]
30 /// Asks for a picture from the camera (true) or the photo library.
31 var onPicture: ((Bool) -> Void)?
32 var onDismiss: (() -> Void)?
33
34 init() {
35 super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 48), inputViewStyle: .keyboard)
36 allowsSelfSizing = true
37 // Over the page when a hardware keyboard hides the software one.
38 let backdrop = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
39 backdrop.frame = bounds
40 backdrop.autoresizingMask = [.flexibleWidth, .flexibleHeight]
41 addSubview(backdrop)
42 let row = UIStackView()
43 row.spacing = 2
44 let styles = Self.button("textformat", "Styles")
45 styles.menu = UIMenu(children: [
46 UIDeferredMenuElement.uncached { [weak self] done in
47 let current = self?.stylePlace?() ?? -1
48 done(Self.styles.enumerated().map { place, name in
49 UIAction(title: name, state: Int32(place) == current ? .on : .off) { _ in
50 self?.onStyle?(UInt8(place))
51 }
52 })
53 }
54 ])
55 styles.showsMenuAsPrimaryAction = true
56 row.addArrangedSubview(styles)
57 for (symbol, label, command) in Self.buttons {
58 let button = Self.button(symbol, label)
59 button.addAction(UIAction { [weak self] _ in self?.onApply?(command) }, for: .primaryActionTriggered)
60 toggles.append((button, command))
61 row.addArrangedSubview(button)
62 }
63 let tag = Self.button("tag", "Tags")
64 tag.menu = UIMenu(children: [
65 UIDeferredMenuElement.uncached { [weak self] done in
66 done(Tags.menu(bits: self?.bits ?? 0) { self?.onApply?($0) })
67 }
68 ])
69 tag.showsMenuAsPrimaryAction = true
70 row.addArrangedSubview(tag)
71 let picture = Self.button("photo", "Insert Picture")
72 picture.menu = UIMenu(children: [
73 UIAction(title: "Photo Library", image: UIImage(systemName: "photo.on.rectangle")) { [weak self] _ in
74 self?.onPicture?(false)
75 },
76 UIAction(
77 title: "Take Photo", image: UIImage(systemName: "camera"),
78 attributes: UIImagePickerController.isSourceTypeAvailable(.camera) ? [] : .disabled
79 ) { [weak self] _ in self?.onPicture?(true) },
80 ])
81 picture.showsMenuAsPrimaryAction = true
82 row.addArrangedSubview(picture)
83 let scroll = UIScrollView()
84 scroll.showsHorizontalScrollIndicator = false
85 scroll.addSubview(row)
86 let dismiss = Self.button("keyboard.chevron.compact.down", "Hide Keyboard")
87 dismiss.addAction(UIAction { [weak self] _ in self?.onDismiss?() }, for: .primaryActionTriggered)
88 for view in [scroll, dismiss, row] as [UIView] { view.translatesAutoresizingMaskIntoConstraints = false }
89 addSubview(scroll)
90 addSubview(dismiss)
91 NSLayoutConstraint.activate([
92 heightAnchor.constraint(equalToConstant: 48),
93 scroll.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4),
94 scroll.topAnchor.constraint(equalTo: topAnchor),
95 scroll.bottomAnchor.constraint(equalTo: bottomAnchor),
96 scroll.trailingAnchor.constraint(equalTo: dismiss.leadingAnchor, constant: -4),
97 dismiss.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4),
98 dismiss.centerYAnchor.constraint(equalTo: centerYAnchor),
99 row.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor),
100 row.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor),
101 row.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
102 row.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
103 row.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
104 ])
105 }
106
107 required init?(coder: NSCoder) { fatalError() }
108
109 private static func button(_ symbol: String, _ label: String) -> UIButton {
110 var configuration = UIButton.Configuration.plain()
111 configuration.image = UIImage(systemName: symbol)
112 configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 17)
113 configuration.baseForegroundColor = .label
114 configuration.cornerStyle = .medium
115 let button = UIButton(configuration: configuration)
116 button.accessibilityLabel = label
117 button.widthAnchor.constraint(equalToConstant: 42).isActive = true
118 button.configurationUpdateHandler = { button in
119 button.configuration?.background.backgroundColor =
120 button.isSelected ? UIColor.tintColor.withAlphaComponent(0.18) : .clear
121 button.configuration?.baseForegroundColor = button.isSelected ? .tintColor : .label
122 }
123 return button
124 }
125
126 /// Marks what the selection has, from `sb_view_format`.
127 func show(_ bits: UInt64) {
128 self.bits = bits
129 for (button, command) in toggles {
130 button.isSelected = bits & (1 << UInt64(command)) != 0
131 }
132 }
133}