|
|
@@ -4857,7 +4857,7 @@ private final class CalendarDayActionMenuViewController: NSViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
+private final class CreateMeetingPopoverViewController: NSViewController, NSTextViewDelegate {
|
|
|
struct Draft {
|
|
|
let title: String
|
|
|
let notes: String?
|
|
|
@@ -4875,7 +4875,13 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
private var timePicker: NSDatePicker?
|
|
|
private var durationField: NSTextField?
|
|
|
private var notesView: NSTextView?
|
|
|
+ private var notesScrollView: NSScrollView?
|
|
|
private var errorLabel: NSTextField?
|
|
|
+ private var notesBorderIdle = NSColor.clear
|
|
|
+ private var notesBorderHover = NSColor.clear
|
|
|
+ private var notesBorderFocused = NSColor.clear
|
|
|
+ private var notesIsHovered = false
|
|
|
+ private var notesIsFocused = false
|
|
|
|
|
|
init(palette: Palette,
|
|
|
typography: Typography,
|
|
|
@@ -4896,16 +4902,28 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
let root = NSView()
|
|
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
root.userInterfaceLayoutDirection = .leftToRight
|
|
|
+ root.wantsLayer = true
|
|
|
+ root.layer?.cornerRadius = 14
|
|
|
+ root.layer?.masksToBounds = true
|
|
|
+ root.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
+ root.layer?.borderWidth = 1
|
|
|
+ root.layer?.borderColor = palette.inputBorder.withAlphaComponent(0.9).cgColor
|
|
|
|
|
|
let stack = NSStackView()
|
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
stack.orientation = .vertical
|
|
|
stack.alignment = .leading
|
|
|
- stack.spacing = 12
|
|
|
+ stack.spacing = 14
|
|
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
|
|
|
|
+ let inputSurface = palette.inputBackground.blended(withFraction: 0.18, of: palette.sectionCard) ?? palette.inputBackground
|
|
|
+ let fieldBorder = palette.textSecondary.withAlphaComponent(0.4)
|
|
|
+ notesBorderIdle = fieldBorder
|
|
|
+ notesBorderHover = palette.textSecondary.withAlphaComponent(0.72)
|
|
|
+ notesBorderFocused = palette.primaryBlueBorder
|
|
|
+
|
|
|
let header = NSTextField(labelWithString: "Schedule meeting")
|
|
|
- header.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
|
+ header.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
|
header.textColor = palette.textPrimary
|
|
|
header.alignment = .left
|
|
|
header.userInterfaceLayoutDirection = .leftToRight
|
|
|
@@ -4922,9 +4940,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
titleShell.translatesAutoresizingMaskIntoConstraints = false
|
|
|
titleShell.wantsLayer = true
|
|
|
titleShell.layer?.cornerRadius = 8
|
|
|
- titleShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
- titleShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
- titleShell.layer?.borderWidth = 1
|
|
|
+ titleShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
+ titleShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
+ titleShell.layer?.borderWidth = 1.2
|
|
|
titleShell.heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
|
|
|
let titleField = NSTextField(string: "")
|
|
|
@@ -4961,9 +4979,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
pickerShell.translatesAutoresizingMaskIntoConstraints = false
|
|
|
pickerShell.wantsLayer = true
|
|
|
pickerShell.layer?.cornerRadius = 8
|
|
|
- pickerShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
- pickerShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
- pickerShell.layer?.borderWidth = 1
|
|
|
+ pickerShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
+ pickerShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
+ pickerShell.layer?.borderWidth = 1.2
|
|
|
pickerShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
|
|
|
let timePicker = NSDatePicker()
|
|
|
@@ -4995,9 +5013,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
durationShell.translatesAutoresizingMaskIntoConstraints = false
|
|
|
durationShell.wantsLayer = true
|
|
|
durationShell.layer?.cornerRadius = 8
|
|
|
- durationShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
- durationShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
- durationShell.layer?.borderWidth = 1
|
|
|
+ durationShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
+ durationShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
+ durationShell.layer?.borderWidth = 1.2
|
|
|
durationShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
|
|
|
let durationField = NSTextField(string: "30")
|
|
|
@@ -5039,25 +5057,54 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
notesLabel.userInterfaceLayoutDirection = .leftToRight
|
|
|
notesLabel.baseWritingDirection = .leftToRight
|
|
|
|
|
|
- let notesScroll = NSScrollView()
|
|
|
+ let notesScroll = HoverFocusScrollView()
|
|
|
notesScroll.translatesAutoresizingMaskIntoConstraints = false
|
|
|
notesScroll.drawsBackground = true
|
|
|
- notesScroll.backgroundColor = palette.inputBackground
|
|
|
+ notesScroll.backgroundColor = inputSurface
|
|
|
notesScroll.hasVerticalScroller = true
|
|
|
+ notesScroll.hasHorizontalScroller = false
|
|
|
notesScroll.borderType = .noBorder
|
|
|
notesScroll.wantsLayer = true
|
|
|
notesScroll.layer?.cornerRadius = 8
|
|
|
- notesScroll.layer?.borderWidth = 1
|
|
|
- notesScroll.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
- notesScroll.heightAnchor.constraint(equalToConstant: 90).isActive = true
|
|
|
+ notesScroll.layer?.masksToBounds = true
|
|
|
+ notesScroll.layer?.borderWidth = 1.2
|
|
|
+ notesScroll.layer?.borderColor = notesBorderIdle.cgColor
|
|
|
+ notesScroll.heightAnchor.constraint(equalToConstant: 100).isActive = true
|
|
|
+ notesScroll.onHoverChanged = { [weak self] hovering in
|
|
|
+ guard let self else { return }
|
|
|
+ self.notesIsHovered = hovering
|
|
|
+ self.updateNotesBorderAppearance()
|
|
|
+ }
|
|
|
+ notesScroll.onMouseDown = { [weak self] in
|
|
|
+ guard let self, let notesView = self.notesView as? ImmediateFocusTextView else { return }
|
|
|
+ self.view.window?.makeFirstResponder(notesView)
|
|
|
+ notesView.ensureCaretVisibleImmediately()
|
|
|
+ self.notesIsFocused = true
|
|
|
+ self.updateNotesBorderAppearance()
|
|
|
+ }
|
|
|
|
|
|
- let notesView = NSTextView()
|
|
|
+ let notesView = ImmediateFocusTextView(frame: .zero)
|
|
|
notesView.drawsBackground = false
|
|
|
notesView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
notesView.textColor = palette.textPrimary
|
|
|
notesView.insertionPointColor = palette.textPrimary
|
|
|
+ notesView.isEditable = true
|
|
|
+ notesView.isSelectable = true
|
|
|
+ notesView.isRichText = false
|
|
|
+ notesView.importsGraphics = false
|
|
|
+ notesView.isHorizontallyResizable = false
|
|
|
+ notesView.isVerticallyResizable = true
|
|
|
+ notesView.autoresizingMask = [.width]
|
|
|
+ notesView.textContainerInset = NSSize(width: 6, height: 6)
|
|
|
+ notesView.textContainer?.widthTracksTextView = true
|
|
|
+ notesView.textContainer?.containerSize = NSSize(
|
|
|
+ width: notesScroll.contentSize.width,
|
|
|
+ height: CGFloat.greatestFiniteMagnitude
|
|
|
+ )
|
|
|
+ notesView.delegate = self
|
|
|
notesScroll.documentView = notesView
|
|
|
self.notesView = notesView
|
|
|
+ self.notesScrollView = notesScroll
|
|
|
|
|
|
let error = NSTextField(labelWithString: "")
|
|
|
error.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -5098,11 +5145,11 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
|
|
|
root.addSubview(stack)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- root.widthAnchor.constraint(equalToConstant: 360),
|
|
|
- stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 14),
|
|
|
- stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -14),
|
|
|
- stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 12),
|
|
|
- stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -12),
|
|
|
+ root.widthAnchor.constraint(equalToConstant: 372),
|
|
|
+ stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
|
|
|
+ stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
|
|
|
+ stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 16),
|
|
|
+ stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16),
|
|
|
titleShell.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
timeRow.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
notesScroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
@@ -5154,11 +5201,124 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
onSave(Draft(title: title, notes: cleanedNotes, startDate: start, endDate: end))
|
|
|
}
|
|
|
|
|
|
+ private func updateNotesBorderAppearance() {
|
|
|
+ let color: NSColor
|
|
|
+ let width: CGFloat
|
|
|
+ if notesIsFocused {
|
|
|
+ color = notesBorderFocused
|
|
|
+ width = 1.6
|
|
|
+ } else if notesIsHovered {
|
|
|
+ color = notesBorderHover
|
|
|
+ width = 1.4
|
|
|
+ } else {
|
|
|
+ color = notesBorderIdle
|
|
|
+ width = 1.2
|
|
|
+ }
|
|
|
+ notesScrollView?.layer?.borderColor = color.cgColor
|
|
|
+ notesScrollView?.layer?.borderWidth = width
|
|
|
+ }
|
|
|
+
|
|
|
private func setError(_ message: String?) {
|
|
|
guard let errorLabel else { return }
|
|
|
errorLabel.stringValue = message ?? ""
|
|
|
errorLabel.isHidden = message == nil
|
|
|
}
|
|
|
+
|
|
|
+ func textDidBeginEditing(_ notification: Notification) {
|
|
|
+ guard let current = notification.object as? NSTextView, current === notesView else { return }
|
|
|
+ notesIsFocused = true
|
|
|
+ updateNotesBorderAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ func textDidEndEditing(_ notification: Notification) {
|
|
|
+ guard let current = notification.object as? NSTextView, current === notesView else { return }
|
|
|
+ notesIsFocused = false
|
|
|
+ updateNotesBorderAppearance()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private final class HoverFocusScrollView: NSScrollView {
|
|
|
+ var onHoverChanged: ((Bool) -> Void)?
|
|
|
+ var onMouseDown: (() -> Void)?
|
|
|
+ private var hoverTrackingArea: NSTrackingArea?
|
|
|
+
|
|
|
+ override func updateTrackingAreas() {
|
|
|
+ super.updateTrackingAreas()
|
|
|
+ if let hoverTrackingArea {
|
|
|
+ removeTrackingArea(hoverTrackingArea)
|
|
|
+ }
|
|
|
+ let area = NSTrackingArea(
|
|
|
+ rect: bounds,
|
|
|
+ options: [.activeInActiveApp, .inVisibleRect, .mouseEnteredAndExited],
|
|
|
+ owner: self,
|
|
|
+ userInfo: nil
|
|
|
+ )
|
|
|
+ addTrackingArea(area)
|
|
|
+ hoverTrackingArea = area
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
+ super.mouseEntered(with: event)
|
|
|
+ onHoverChanged?(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
+ super.mouseExited(with: event)
|
|
|
+ onHoverChanged?(false)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
|
|
+ true
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
+ onMouseDown?()
|
|
|
+ if let window, !window.isKeyWindow {
|
|
|
+ window.makeKeyAndOrderFront(nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // Forward the click straight to the text view so caret placement/blink starts immediately.
|
|
|
+ if let textView = documentView as? NSTextView {
|
|
|
+ window?.makeFirstResponder(textView)
|
|
|
+ textView.mouseDown(with: event)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ super.mouseDown(with: event)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private final class ImmediateFocusTextView: NSTextView {
|
|
|
+ override var acceptsFirstResponder: Bool { true }
|
|
|
+
|
|
|
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
|
|
+ true
|
|
|
+ }
|
|
|
+
|
|
|
+ @discardableResult
|
|
|
+ override func becomeFirstResponder() -> Bool {
|
|
|
+ let accepted = super.becomeFirstResponder()
|
|
|
+ if accepted {
|
|
|
+ ensureCaretVisibleImmediately()
|
|
|
+ }
|
|
|
+ return accepted
|
|
|
+ }
|
|
|
+
|
|
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
+ window?.makeFirstResponder(self)
|
|
|
+ super.mouseDown(with: event)
|
|
|
+ ensureCaretVisibleImmediately()
|
|
|
+ }
|
|
|
+
|
|
|
+ func ensureCaretVisibleImmediately() {
|
|
|
+ var range = selectedRange()
|
|
|
+ if range.location == NSNotFound {
|
|
|
+ range = NSRange(location: string.utf16.count, length: 0)
|
|
|
+ }
|
|
|
+ setSelectedRange(range)
|
|
|
+ scrollRangeToVisible(range)
|
|
|
+ needsDisplay = true
|
|
|
+ displayIfNeeded()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// MARK: - Schedule actions (OAuth entry)
|