|
|
@@ -4857,7 +4857,7 @@ private final class CalendarDayActionMenuViewController: NSViewController {
|
|
4857
|
4857
|
}
|
|
4858
|
4858
|
}
|
|
4859
|
4859
|
|
|
4860
|
|
-private final class CreateMeetingPopoverViewController: NSViewController {
|
|
|
4860
|
+private final class CreateMeetingPopoverViewController: NSViewController, NSTextViewDelegate {
|
|
4861
|
4861
|
struct Draft {
|
|
4862
|
4862
|
let title: String
|
|
4863
|
4863
|
let notes: String?
|
|
|
@@ -4875,7 +4875,13 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
4875
|
4875
|
private var timePicker: NSDatePicker?
|
|
4876
|
4876
|
private var durationField: NSTextField?
|
|
4877
|
4877
|
private var notesView: NSTextView?
|
|
|
4878
|
+ private var notesScrollView: NSScrollView?
|
|
4878
|
4879
|
private var errorLabel: NSTextField?
|
|
|
4880
|
+ private var notesBorderIdle = NSColor.clear
|
|
|
4881
|
+ private var notesBorderHover = NSColor.clear
|
|
|
4882
|
+ private var notesBorderFocused = NSColor.clear
|
|
|
4883
|
+ private var notesIsHovered = false
|
|
|
4884
|
+ private var notesIsFocused = false
|
|
4879
|
4885
|
|
|
4880
|
4886
|
init(palette: Palette,
|
|
4881
|
4887
|
typography: Typography,
|
|
|
@@ -4896,16 +4902,28 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
4896
|
4902
|
let root = NSView()
|
|
4897
|
4903
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
4898
|
4904
|
root.userInterfaceLayoutDirection = .leftToRight
|
|
|
4905
|
+ root.wantsLayer = true
|
|
|
4906
|
+ root.layer?.cornerRadius = 14
|
|
|
4907
|
+ root.layer?.masksToBounds = true
|
|
|
4908
|
+ root.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
4909
|
+ root.layer?.borderWidth = 1
|
|
|
4910
|
+ root.layer?.borderColor = palette.inputBorder.withAlphaComponent(0.9).cgColor
|
|
4899
|
4911
|
|
|
4900
|
4912
|
let stack = NSStackView()
|
|
4901
|
4913
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
4902
|
4914
|
stack.orientation = .vertical
|
|
4903
|
4915
|
stack.alignment = .leading
|
|
4904
|
|
- stack.spacing = 12
|
|
|
4916
|
+ stack.spacing = 14
|
|
4905
|
4917
|
stack.userInterfaceLayoutDirection = .leftToRight
|
|
4906
|
4918
|
|
|
|
4919
|
+ let inputSurface = palette.inputBackground.blended(withFraction: 0.18, of: palette.sectionCard) ?? palette.inputBackground
|
|
|
4920
|
+ let fieldBorder = palette.textSecondary.withAlphaComponent(0.4)
|
|
|
4921
|
+ notesBorderIdle = fieldBorder
|
|
|
4922
|
+ notesBorderHover = palette.textSecondary.withAlphaComponent(0.72)
|
|
|
4923
|
+ notesBorderFocused = palette.primaryBlueBorder
|
|
|
4924
|
+
|
|
4907
|
4925
|
let header = NSTextField(labelWithString: "Schedule meeting")
|
|
4908
|
|
- header.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
|
4926
|
+ header.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
4909
|
4927
|
header.textColor = palette.textPrimary
|
|
4910
|
4928
|
header.alignment = .left
|
|
4911
|
4929
|
header.userInterfaceLayoutDirection = .leftToRight
|
|
|
@@ -4922,9 +4940,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
4922
|
4940
|
titleShell.translatesAutoresizingMaskIntoConstraints = false
|
|
4923
|
4941
|
titleShell.wantsLayer = true
|
|
4924
|
4942
|
titleShell.layer?.cornerRadius = 8
|
|
4925
|
|
- titleShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
4926
|
|
- titleShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
4927
|
|
- titleShell.layer?.borderWidth = 1
|
|
|
4943
|
+ titleShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
4944
|
+ titleShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
4945
|
+ titleShell.layer?.borderWidth = 1.2
|
|
4928
|
4946
|
titleShell.heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
4929
|
4947
|
|
|
4930
|
4948
|
let titleField = NSTextField(string: "")
|
|
|
@@ -4961,9 +4979,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
4961
|
4979
|
pickerShell.translatesAutoresizingMaskIntoConstraints = false
|
|
4962
|
4980
|
pickerShell.wantsLayer = true
|
|
4963
|
4981
|
pickerShell.layer?.cornerRadius = 8
|
|
4964
|
|
- pickerShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
4965
|
|
- pickerShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
4966
|
|
- pickerShell.layer?.borderWidth = 1
|
|
|
4982
|
+ pickerShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
4983
|
+ pickerShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
4984
|
+ pickerShell.layer?.borderWidth = 1.2
|
|
4967
|
4985
|
pickerShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
4968
|
4986
|
|
|
4969
|
4987
|
let timePicker = NSDatePicker()
|
|
|
@@ -4995,9 +5013,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
4995
|
5013
|
durationShell.translatesAutoresizingMaskIntoConstraints = false
|
|
4996
|
5014
|
durationShell.wantsLayer = true
|
|
4997
|
5015
|
durationShell.layer?.cornerRadius = 8
|
|
4998
|
|
- durationShell.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
4999
|
|
- durationShell.layer?.borderColor = palette.inputBorder.cgColor
|
|
5000
|
|
- durationShell.layer?.borderWidth = 1
|
|
|
5016
|
+ durationShell.layer?.backgroundColor = inputSurface.cgColor
|
|
|
5017
|
+ durationShell.layer?.borderColor = fieldBorder.cgColor
|
|
|
5018
|
+ durationShell.layer?.borderWidth = 1.2
|
|
5001
|
5019
|
durationShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
5002
|
5020
|
|
|
5003
|
5021
|
let durationField = NSTextField(string: "30")
|
|
|
@@ -5039,25 +5057,54 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
5039
|
5057
|
notesLabel.userInterfaceLayoutDirection = .leftToRight
|
|
5040
|
5058
|
notesLabel.baseWritingDirection = .leftToRight
|
|
5041
|
5059
|
|
|
5042
|
|
- let notesScroll = NSScrollView()
|
|
|
5060
|
+ let notesScroll = HoverFocusScrollView()
|
|
5043
|
5061
|
notesScroll.translatesAutoresizingMaskIntoConstraints = false
|
|
5044
|
5062
|
notesScroll.drawsBackground = true
|
|
5045
|
|
- notesScroll.backgroundColor = palette.inputBackground
|
|
|
5063
|
+ notesScroll.backgroundColor = inputSurface
|
|
5046
|
5064
|
notesScroll.hasVerticalScroller = true
|
|
|
5065
|
+ notesScroll.hasHorizontalScroller = false
|
|
5047
|
5066
|
notesScroll.borderType = .noBorder
|
|
5048
|
5067
|
notesScroll.wantsLayer = true
|
|
5049
|
5068
|
notesScroll.layer?.cornerRadius = 8
|
|
5050
|
|
- notesScroll.layer?.borderWidth = 1
|
|
5051
|
|
- notesScroll.layer?.borderColor = palette.inputBorder.cgColor
|
|
5052
|
|
- notesScroll.heightAnchor.constraint(equalToConstant: 90).isActive = true
|
|
|
5069
|
+ notesScroll.layer?.masksToBounds = true
|
|
|
5070
|
+ notesScroll.layer?.borderWidth = 1.2
|
|
|
5071
|
+ notesScroll.layer?.borderColor = notesBorderIdle.cgColor
|
|
|
5072
|
+ notesScroll.heightAnchor.constraint(equalToConstant: 100).isActive = true
|
|
|
5073
|
+ notesScroll.onHoverChanged = { [weak self] hovering in
|
|
|
5074
|
+ guard let self else { return }
|
|
|
5075
|
+ self.notesIsHovered = hovering
|
|
|
5076
|
+ self.updateNotesBorderAppearance()
|
|
|
5077
|
+ }
|
|
|
5078
|
+ notesScroll.onMouseDown = { [weak self] in
|
|
|
5079
|
+ guard let self, let notesView = self.notesView as? ImmediateFocusTextView else { return }
|
|
|
5080
|
+ self.view.window?.makeFirstResponder(notesView)
|
|
|
5081
|
+ notesView.ensureCaretVisibleImmediately()
|
|
|
5082
|
+ self.notesIsFocused = true
|
|
|
5083
|
+ self.updateNotesBorderAppearance()
|
|
|
5084
|
+ }
|
|
5053
|
5085
|
|
|
5054
|
|
- let notesView = NSTextView()
|
|
|
5086
|
+ let notesView = ImmediateFocusTextView(frame: .zero)
|
|
5055
|
5087
|
notesView.drawsBackground = false
|
|
5056
|
5088
|
notesView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
5057
|
5089
|
notesView.textColor = palette.textPrimary
|
|
5058
|
5090
|
notesView.insertionPointColor = palette.textPrimary
|
|
|
5091
|
+ notesView.isEditable = true
|
|
|
5092
|
+ notesView.isSelectable = true
|
|
|
5093
|
+ notesView.isRichText = false
|
|
|
5094
|
+ notesView.importsGraphics = false
|
|
|
5095
|
+ notesView.isHorizontallyResizable = false
|
|
|
5096
|
+ notesView.isVerticallyResizable = true
|
|
|
5097
|
+ notesView.autoresizingMask = [.width]
|
|
|
5098
|
+ notesView.textContainerInset = NSSize(width: 6, height: 6)
|
|
|
5099
|
+ notesView.textContainer?.widthTracksTextView = true
|
|
|
5100
|
+ notesView.textContainer?.containerSize = NSSize(
|
|
|
5101
|
+ width: notesScroll.contentSize.width,
|
|
|
5102
|
+ height: CGFloat.greatestFiniteMagnitude
|
|
|
5103
|
+ )
|
|
|
5104
|
+ notesView.delegate = self
|
|
5059
|
5105
|
notesScroll.documentView = notesView
|
|
5060
|
5106
|
self.notesView = notesView
|
|
|
5107
|
+ self.notesScrollView = notesScroll
|
|
5061
|
5108
|
|
|
5062
|
5109
|
let error = NSTextField(labelWithString: "")
|
|
5063
|
5110
|
error.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -5098,11 +5145,11 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
5098
|
5145
|
|
|
5099
|
5146
|
root.addSubview(stack)
|
|
5100
|
5147
|
NSLayoutConstraint.activate([
|
|
5101
|
|
- root.widthAnchor.constraint(equalToConstant: 360),
|
|
5102
|
|
- stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 14),
|
|
5103
|
|
- stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -14),
|
|
5104
|
|
- stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 12),
|
|
5105
|
|
- stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -12),
|
|
|
5148
|
+ root.widthAnchor.constraint(equalToConstant: 372),
|
|
|
5149
|
+ stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
|
|
|
5150
|
+ stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
|
|
|
5151
|
+ stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 16),
|
|
|
5152
|
+ stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16),
|
|
5106
|
5153
|
titleShell.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
5107
|
5154
|
timeRow.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
5108
|
5155
|
notesScroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
|
@@ -5154,11 +5201,124 @@ private final class CreateMeetingPopoverViewController: NSViewController {
|
|
5154
|
5201
|
onSave(Draft(title: title, notes: cleanedNotes, startDate: start, endDate: end))
|
|
5155
|
5202
|
}
|
|
5156
|
5203
|
|
|
|
5204
|
+ private func updateNotesBorderAppearance() {
|
|
|
5205
|
+ let color: NSColor
|
|
|
5206
|
+ let width: CGFloat
|
|
|
5207
|
+ if notesIsFocused {
|
|
|
5208
|
+ color = notesBorderFocused
|
|
|
5209
|
+ width = 1.6
|
|
|
5210
|
+ } else if notesIsHovered {
|
|
|
5211
|
+ color = notesBorderHover
|
|
|
5212
|
+ width = 1.4
|
|
|
5213
|
+ } else {
|
|
|
5214
|
+ color = notesBorderIdle
|
|
|
5215
|
+ width = 1.2
|
|
|
5216
|
+ }
|
|
|
5217
|
+ notesScrollView?.layer?.borderColor = color.cgColor
|
|
|
5218
|
+ notesScrollView?.layer?.borderWidth = width
|
|
|
5219
|
+ }
|
|
|
5220
|
+
|
|
5157
|
5221
|
private func setError(_ message: String?) {
|
|
5158
|
5222
|
guard let errorLabel else { return }
|
|
5159
|
5223
|
errorLabel.stringValue = message ?? ""
|
|
5160
|
5224
|
errorLabel.isHidden = message == nil
|
|
5161
|
5225
|
}
|
|
|
5226
|
+
|
|
|
5227
|
+ func textDidBeginEditing(_ notification: Notification) {
|
|
|
5228
|
+ guard let current = notification.object as? NSTextView, current === notesView else { return }
|
|
|
5229
|
+ notesIsFocused = true
|
|
|
5230
|
+ updateNotesBorderAppearance()
|
|
|
5231
|
+ }
|
|
|
5232
|
+
|
|
|
5233
|
+ func textDidEndEditing(_ notification: Notification) {
|
|
|
5234
|
+ guard let current = notification.object as? NSTextView, current === notesView else { return }
|
|
|
5235
|
+ notesIsFocused = false
|
|
|
5236
|
+ updateNotesBorderAppearance()
|
|
|
5237
|
+ }
|
|
|
5238
|
+}
|
|
|
5239
|
+
|
|
|
5240
|
+private final class HoverFocusScrollView: NSScrollView {
|
|
|
5241
|
+ var onHoverChanged: ((Bool) -> Void)?
|
|
|
5242
|
+ var onMouseDown: (() -> Void)?
|
|
|
5243
|
+ private var hoverTrackingArea: NSTrackingArea?
|
|
|
5244
|
+
|
|
|
5245
|
+ override func updateTrackingAreas() {
|
|
|
5246
|
+ super.updateTrackingAreas()
|
|
|
5247
|
+ if let hoverTrackingArea {
|
|
|
5248
|
+ removeTrackingArea(hoverTrackingArea)
|
|
|
5249
|
+ }
|
|
|
5250
|
+ let area = NSTrackingArea(
|
|
|
5251
|
+ rect: bounds,
|
|
|
5252
|
+ options: [.activeInActiveApp, .inVisibleRect, .mouseEnteredAndExited],
|
|
|
5253
|
+ owner: self,
|
|
|
5254
|
+ userInfo: nil
|
|
|
5255
|
+ )
|
|
|
5256
|
+ addTrackingArea(area)
|
|
|
5257
|
+ hoverTrackingArea = area
|
|
|
5258
|
+ }
|
|
|
5259
|
+
|
|
|
5260
|
+ override func mouseEntered(with event: NSEvent) {
|
|
|
5261
|
+ super.mouseEntered(with: event)
|
|
|
5262
|
+ onHoverChanged?(true)
|
|
|
5263
|
+ }
|
|
|
5264
|
+
|
|
|
5265
|
+ override func mouseExited(with event: NSEvent) {
|
|
|
5266
|
+ super.mouseExited(with: event)
|
|
|
5267
|
+ onHoverChanged?(false)
|
|
|
5268
|
+ }
|
|
|
5269
|
+
|
|
|
5270
|
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
|
|
5271
|
+ true
|
|
|
5272
|
+ }
|
|
|
5273
|
+
|
|
|
5274
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
5275
|
+ onMouseDown?()
|
|
|
5276
|
+ if let window, !window.isKeyWindow {
|
|
|
5277
|
+ window.makeKeyAndOrderFront(nil)
|
|
|
5278
|
+ return
|
|
|
5279
|
+ }
|
|
|
5280
|
+ // Forward the click straight to the text view so caret placement/blink starts immediately.
|
|
|
5281
|
+ if let textView = documentView as? NSTextView {
|
|
|
5282
|
+ window?.makeFirstResponder(textView)
|
|
|
5283
|
+ textView.mouseDown(with: event)
|
|
|
5284
|
+ return
|
|
|
5285
|
+ }
|
|
|
5286
|
+ super.mouseDown(with: event)
|
|
|
5287
|
+ }
|
|
|
5288
|
+}
|
|
|
5289
|
+
|
|
|
5290
|
+private final class ImmediateFocusTextView: NSTextView {
|
|
|
5291
|
+ override var acceptsFirstResponder: Bool { true }
|
|
|
5292
|
+
|
|
|
5293
|
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
|
|
5294
|
+ true
|
|
|
5295
|
+ }
|
|
|
5296
|
+
|
|
|
5297
|
+ @discardableResult
|
|
|
5298
|
+ override func becomeFirstResponder() -> Bool {
|
|
|
5299
|
+ let accepted = super.becomeFirstResponder()
|
|
|
5300
|
+ if accepted {
|
|
|
5301
|
+ ensureCaretVisibleImmediately()
|
|
|
5302
|
+ }
|
|
|
5303
|
+ return accepted
|
|
|
5304
|
+ }
|
|
|
5305
|
+
|
|
|
5306
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
5307
|
+ window?.makeFirstResponder(self)
|
|
|
5308
|
+ super.mouseDown(with: event)
|
|
|
5309
|
+ ensureCaretVisibleImmediately()
|
|
|
5310
|
+ }
|
|
|
5311
|
+
|
|
|
5312
|
+ func ensureCaretVisibleImmediately() {
|
|
|
5313
|
+ var range = selectedRange()
|
|
|
5314
|
+ if range.location == NSNotFound {
|
|
|
5315
|
+ range = NSRange(location: string.utf16.count, length: 0)
|
|
|
5316
|
+ }
|
|
|
5317
|
+ setSelectedRange(range)
|
|
|
5318
|
+ scrollRangeToVisible(range)
|
|
|
5319
|
+ needsDisplay = true
|
|
|
5320
|
+ displayIfNeeded()
|
|
|
5321
|
+ }
|
|
5162
|
5322
|
}
|
|
5163
|
5323
|
|
|
5164
|
5324
|
// MARK: - Schedule actions (OAuth entry)
|