Skip to content

Commit

Permalink
Highlighting parsed date when typing new reminder - Related to #136
Browse files Browse the repository at this point in the history
  • Loading branch information
DamascenoRafael committed May 9, 2023
1 parent 9f47b9a commit 4510abf
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 104 deletions.
8 changes: 8 additions & 0 deletions reminders-menubar.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
714598D329295330008CAA43 /* RmbDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714598D229295330008CAA43 /* RmbDatePicker.swift */; };
714C28A22A07573A00734DAF /* FocusOnAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C28A12A07573A00734DAF /* FocusOnAppear.swift */; };
714C28A42A082E0000734DAF /* OnKeyboardShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C28A32A082E0000734DAF /* OnKeyboardShortcut.swift */; };
714C28A82A0A056600734DAF /* RmbHighlightedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C28A72A0A056600734DAF /* RmbHighlightedTextField.swift */; };
714C28AC2A0AE37600734DAF /* FocusOnReceive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C28AB2A0AE37600734DAF /* FocusOnReceive.swift */; };
714C4980267FA56F00721516 /* remindersLocalized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714C497F267FA56F00721516 /* remindersLocalized.swift */; };
714D03B226042416003063F7 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714D03B126042416003063F7 /* Calendar+Extensions.swift */; };
714E6F6125CCF66000BA0099 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714E6F6025CCF66000BA0099 /* Constants.swift */; };
Expand Down Expand Up @@ -122,6 +124,8 @@
714598D229295330008CAA43 /* RmbDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RmbDatePicker.swift; sourceTree = "<group>"; };
714C28A12A07573A00734DAF /* FocusOnAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusOnAppear.swift; sourceTree = "<group>"; };
714C28A32A082E0000734DAF /* OnKeyboardShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnKeyboardShortcut.swift; sourceTree = "<group>"; };
714C28A72A0A056600734DAF /* RmbHighlightedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RmbHighlightedTextField.swift; sourceTree = "<group>"; };
714C28AB2A0AE37600734DAF /* FocusOnReceive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusOnReceive.swift; sourceTree = "<group>"; };
714C497F267FA56F00721516 /* remindersLocalized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = remindersLocalized.swift; sourceTree = "<group>"; };
714D03B126042416003063F7 /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
714E6F6025CCF66000BA0099 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -235,8 +239,10 @@
isa = PBXGroup;
children = (
7119C2EF25D85B75002C8013 /* SelectableView.swift */,
714C28A72A0A056600734DAF /* RmbHighlightedTextField.swift */,
714598D229295330008CAA43 /* RmbDatePicker.swift */,
714C28A12A07573A00734DAF /* FocusOnAppear.swift */,
714C28AB2A0AE37600734DAF /* FocusOnReceive.swift */,
714C28A32A082E0000734DAF /* OnKeyboardShortcut.swift */,
);
path = Helpers;
Expand Down Expand Up @@ -531,7 +537,9 @@
7141456325CF9693006695B2 /* Release.swift in Sources */,
71F36A9829C16B7A0099D337 /* RmbColorKey.swift in Sources */,
71E7126A2908C6D100DA97BD /* NilCoalescingBindingOverload.swift in Sources */,
714C28AC2A0AE37600734DAF /* FocusOnReceive.swift in Sources */,
714C28A42A082E0000734DAF /* OnKeyboardShortcut.swift in Sources */,
714C28A82A0A056600734DAF /* RmbHighlightedTextField.swift in Sources */,
71E51D5825BFB8CF009A4B56 /* AboutView.swift in Sources */,
714C28A22A07573A00734DAF /* FocusOnAppear.swift in Sources */,
7115461924C0C280007781E2 /* RemindersService.swift in Sources */,
Expand Down
6 changes: 5 additions & 1 deletion reminders-menubar/Extensions/String+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Foundation

extension String {
func substringRange(_ range: NSRange) -> String {
func substring(in range: NSRange) -> String {
let start = self.index(self.startIndex, offsetBy: range.lowerBound)
let end = self.index(self.startIndex, offsetBy: range.upperBound)
let subString = self[start..<end]
return String(subString)
}

var fullRange: NSRange {
return NSRange(location: 0, length: self.count)
}
}
23 changes: 14 additions & 9 deletions reminders-menubar/Models/RmbReminder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ struct RmbReminder {
return hasChanges
}

var title: String
var title: String {
willSet {
updateTextDateResult(with: newValue)
}
}

var notes: String?
var date: Date {
didSet {
// NOTE: When the date is changed, we assume that it was done by the user.
// If it was changed by DateParser it is necessary to change dateRelatedString after changing the date.
dateRelatedString = ""
// If it was changed by DateParser it is necessary to add textDateResult after changing the date.
textDateResult = DateParser.TextDateResult()
}
}
var hasDueDate: Bool {
Expand All @@ -42,7 +47,7 @@ struct RmbReminder {
}
var priority: EKReminderPriority

var dateRelatedString = ""
var textDateResult = DateParser.TextDateResult()

init() {
title = ""
Expand All @@ -67,23 +72,23 @@ struct RmbReminder {
priority = reminder.ekPriority
}

mutating func updateWithDateParser() {
private mutating func updateTextDateResult(with newTitle: String) {
// NOTE: If a date was defined by the user then the DateParser should not be applied.
if hasDueDate && dateRelatedString.isEmpty {
if hasDueDate && textDateResult.string.isEmpty {
return
}

guard let dateResult = DateParser.shared.getDate(from: title) else {
guard let dateResult = DateParser.shared.getDate(from: newTitle) else {
hasDueDate = false
hasTime = false
date = .nextHour()
dateRelatedString = ""
textDateResult = DateParser.TextDateResult()
return
}

hasDueDate = true
hasTime = dateResult.hasTime
date = dateResult.date
dateRelatedString = dateResult.dateRelatedString
textDateResult = dateResult.textDateResult
}
}
42 changes: 30 additions & 12 deletions reminders-menubar/Services/DateParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ class DateParser {

private let detector: NSDataDetector?

struct DateParseResult {
struct TextDateResult {
let range: NSRange
let string: String

init() {
self.range = NSRange()
self.string = ""
}

init(range: NSRange, string: String) {
self.range = range
self.string = string
}
}

struct DateParserResult {
let date: Date
let hasTime: Bool
let dateRelatedString: String
let textDateResult: TextDateResult
}

private init() {
Expand All @@ -17,7 +32,7 @@ class DateParser {
detector = try? NSDataDetector(types: types.rawValue)
}

private func adjustDateAccordingToNow(_ dateResult: DateParseResult) -> DateParseResult? {
private func adjustDateAccordingToNow(_ dateResult: DateParserResult) -> DateParserResult? {
// NOTE: Date will be adjusted only if it is in the past.
let dateIsPastAndHasTime = dateResult.hasTime && dateResult.date.isPast
let dateIsPastAndHasNoTime = !dateResult.hasTime && dateResult.date.isPast && !dateResult.date.isToday
Expand All @@ -28,24 +43,24 @@ class DateParser {
// NOTE: If the time is set for today, but it's past time today, then we assume it's next day.
// "Do something at 9am" - when it's already 2pm.
if dateResult.hasTime && dateResult.date.isToday {
return DateParseResult(date: .nextDay(of: dateResult.date),
hasTime: dateResult.hasTime,
dateRelatedString: dateResult.dateRelatedString)
return DateParserResult(date: .nextDay(of: dateResult.date),
hasTime: dateResult.hasTime,
textDateResult: dateResult.textDateResult)
}

// NOTE: If the date is set to a day in the current year, but it's past that day, then we assume it's next year.
// "Do something on February 2nd" - when it's already March.
if dateResult.date.isThisYear {
return DateParseResult(date: .nextYear(of: dateResult.date),
hasTime: dateResult.hasTime,
dateRelatedString: dateResult.dateRelatedString)
return DateParserResult(date: .nextYear(of: dateResult.date),
hasTime: dateResult.hasTime,
textDateResult: dateResult.textDateResult)
}

// NOTE: If the date is not adjusted we prefer not to suggest a date that is in the past.
return nil
}

func getDate(from textString: String) -> DateParseResult? {
func getDate(from textString: String) -> DateParserResult? {
let range = NSRange(textString.startIndex..., in: textString)

let matches = detector?.matches(in: textString, options: [], range: range)
Expand All @@ -59,8 +74,11 @@ class DateParser {
hasTime = match.value(forKey: timeIsSignificantKey) as? Bool ?? false
}

let dateRelatedString = textString.substringRange(match.range)
let dateResult = DateParseResult(date: date, hasTime: hasTime, dateRelatedString: dateRelatedString)
let textDateResult = TextDateResult(range: match.range,
string: textString.substring(in: match.range))
let dateResult = DateParserResult(date: date,
hasTime: hasTime,
textDateResult: textDateResult)

return adjustDateAccordingToNow(dateResult)
}
Expand Down
89 changes: 7 additions & 82 deletions reminders-menubar/Views/FormNewReminderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ struct FormNewReminderView: View {
withAnimation(.easeOut(duration: 0.3)) {
isShowingDueDateOptions = !newValue.isEmpty
}
rmbReminder.updateWithDateParser()
if newValue.isEmpty {
rmbReminder = newRmbReminder()
}
Expand All @@ -85,11 +84,12 @@ struct FormNewReminderView: View {
@ViewBuilder
func newReminderTextFieldView(placeholder: String) -> some View {
VStack(alignment: .leading) {
if #available(macOS 12.0, *) {
ReminderTextField(placeholder: placeholder, text: $rmbReminder.title, onSubmit: createNewReminder)
} else {
LegacyReminderTextField(placeholder: placeholder, text: $rmbReminder.title, onSubmit: createNewReminder)
}
RmbHighlightedTextField(placeholder: placeholder,
text: $rmbReminder.title,
highlightedTextRange: rmbReminder.textDateResult.range,
onSubmit: createNewReminder)
.modifier(FocusOnReceive(userPreferences.$remindersMenuBarOpeningEvent))

if isShowingDueDateOptions {
reminderDueDateOptionsView(date: $rmbReminder.date,
hasDueDate: $rmbReminder.hasDueDate,
Expand All @@ -109,89 +109,14 @@ struct FormNewReminderView: View {
}

if userPreferences.removeParsedDateFromTitle {
rmbReminder.title = rmbReminder.title.replacingOccurrences(of: rmbReminder.dateRelatedString, with: "")
rmbReminder.title = rmbReminder.title.replacingOccurrences(of: rmbReminder.textDateResult.string, with: "")
}

RemindersService.shared.createNew(with: rmbReminder, in: calendarForSaving)
rmbReminder = newRmbReminder()
}
}

@available(macOS 12.0, *)
struct ReminderTextField: View {
@FocusState private var newReminderTextFieldInFocus: Bool
@ObservedObject var userPreferences = UserPreferences.shared

var placeholder: String
var text: Binding<String>
var onSubmit: () -> Void

var body: some View {
let placeholdderText = Text(placeholder)
TextField("", text: text, prompt: placeholdderText)
.onSubmit {
onSubmit()
}
.focused($newReminderTextFieldInFocus)
.onReceive(userPreferences.$remindersMenuBarOpeningEvent) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.newReminderTextFieldInFocus = true
}
}
}
}

struct LegacyReminderTextField: NSViewRepresentable {
let placeholder: String
var text: Binding<String>
var onSubmit: () -> Void

func makeCoordinator() -> Coordinator {
return Coordinator(self)
}

func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.placeholderString = placeholder
textField.isBordered = false
textField.font = .systemFont(ofSize: NSFont.systemFontSize)

textField.backgroundColor = NSColor.clear

return textField
}

func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = self.text.wrappedValue
}

class Coordinator: NSObject, NSTextFieldDelegate {
var parent: LegacyReminderTextField

init(_ parent: LegacyReminderTextField) {
self.parent = parent
}

func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
guard !textView.string.isEmpty else {
return false
}
self.parent.onSubmit()
return true
}
return false
}

func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.parent.text.wrappedValue = textField.stringValue
}
}
}
}

@ViewBuilder
func reminderDueDateOptionsView(date: Binding<Date>, hasDueDate: Binding<Bool>, hasTime: Binding<Bool>) -> some View {
HStack {
Expand Down
35 changes: 35 additions & 0 deletions reminders-menubar/Views/Helpers/FocusOnReceive.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SwiftUI

struct FocusOnReceive: ViewModifier {
let publisher: Published<Bool>.Publisher

init(_ publisher: Published<Bool>.Publisher) {
self.publisher = publisher
}

@ViewBuilder
func body(content: Content) -> some View {
if #available(macOS 12.0, *) {
content
.modifier(FocusOnReceiveWhenAvailable(publisher: publisher))
} else {
content
}
}
}

@available(macOS 12.0, *)
private struct FocusOnReceiveWhenAvailable: ViewModifier {
let publisher: Published<Bool>.Publisher
@FocusState private var textFieldInFocus: Bool

func body(content: Content) -> some View {
content
.focused($textFieldInFocus)
.onReceive(publisher) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.textFieldInFocus = true
}
}
}
}
Loading

0 comments on commit 4510abf

Please sign in to comment.