| 1 | import 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. |
| 5 | final 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 | } |