mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
Merge pull request #7906 from utmapp/settings/notes-field
settings: use a bezeled multi-line text field for notes on macOS
This commit is contained in:
@@ -89,11 +89,12 @@ struct VMConfigInfoView: View {
|
||||
}
|
||||
|
||||
private var notesField: some View {
|
||||
#if os(macOS)
|
||||
MultilineTextField(text: $config.notes.bound, minHeight: 200)
|
||||
#else
|
||||
TextEditor(text: $config.notes.bound)
|
||||
#if os(macOS)
|
||||
.border(Color.primary, width: 0.5)
|
||||
#endif
|
||||
.frame(minHeight: 200)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// Copyright © 2026 Turing Software, LLC. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A bezeled, multi-line text field that grows with its contents.
|
||||
///
|
||||
/// `TextEditor` has no bezel and `TextField(axis: .vertical)` submits on Return,
|
||||
/// so this wraps an `NSTextField` and lets AppKit draw the same bezel and focus
|
||||
/// ring as a single-line `TextField`.
|
||||
struct MultilineTextField: NSViewRepresentable {
|
||||
@Binding var text: String
|
||||
var minHeight: CGFloat = 0
|
||||
|
||||
func makeNSView(context: Context) -> WrappingTextField {
|
||||
let field = WrappingTextField()
|
||||
field.minHeight = minHeight
|
||||
field.lineBreakMode = .byWordWrapping
|
||||
field.maximumNumberOfLines = 0
|
||||
field.cell?.wraps = true
|
||||
field.cell?.isScrollable = false
|
||||
field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
field.delegate = context.coordinator
|
||||
return field
|
||||
}
|
||||
|
||||
func updateNSView(_ field: WrappingTextField, context: Context) {
|
||||
context.coordinator.parent = self
|
||||
field.disableAutocorrection = context.environment.disableAutocorrection
|
||||
if field.minHeight != minHeight {
|
||||
field.minHeight = minHeight
|
||||
field.invalidateIntrinsicContentSize()
|
||||
}
|
||||
if field.stringValue != text {
|
||||
field.stringValue = text
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 13, *)
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, nsView field: WrappingTextField, context: Context) -> CGSize? {
|
||||
guard let width = proposal.width, width.isFinite, let cell = field.cell else {
|
||||
return nil
|
||||
}
|
||||
guard width > 0 else {
|
||||
return CGSize(width: 0, height: minHeight)
|
||||
}
|
||||
let bounds = NSRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)
|
||||
return CGSize(width: width, height: max(cell.cellSize(forBounds: bounds).height, minHeight))
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, NSTextFieldDelegate {
|
||||
var parent: MultilineTextField
|
||||
|
||||
init(_ parent: MultilineTextField) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func controlTextDidChange(_ notification: Notification) {
|
||||
guard let field = notification.object as? NSTextField else {
|
||||
return
|
||||
}
|
||||
parent.text = field.stringValue
|
||||
}
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||
textView.insertNewlineIgnoringFieldEditor(nil)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps at the current width so the intrinsic height follows the text on macOS 12,
|
||||
/// where SwiftUI sizes the view from `intrinsicContentSize`.
|
||||
class WrappingTextField: NSTextField {
|
||||
var minHeight: CGFloat = 0
|
||||
/// SwiftUI's `disableAutocorrection` does not reach the field editor, so it is applied by hand.
|
||||
var disableAutocorrection: Bool?
|
||||
|
||||
override var intrinsicContentSize: NSSize {
|
||||
var size = super.intrinsicContentSize
|
||||
size.height = max(size.height, minHeight)
|
||||
return size
|
||||
}
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
let result = super.becomeFirstResponder()
|
||||
if result, let textView = currentEditor() as? NSTextView {
|
||||
// the field editor is shared by the window, so always set it
|
||||
textView.isAutomaticSpellingCorrectionEnabled = disableAutocorrection != true && NSSpellChecker.isAutomaticSpellingCorrectionEnabled
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
if preferredMaxLayoutWidth != bounds.width {
|
||||
preferredMaxLayoutWidth = bounds.width
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
super.layout()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,6 +268,7 @@
|
||||
84C584E1268E95B3000FCABF /* UTMLegacyAppleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C584DE268E95B3000FCABF /* UTMLegacyAppleConfiguration.swift */; };
|
||||
84C584E3268F8AE7000FCABF /* VMQEMUSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C584E2268F8AE7000FCABF /* VMQEMUSettingsView.swift */; };
|
||||
84C584E5268F8C65000FCABF /* VMAppleSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C584E4268F8C65000FCABF /* VMAppleSettingsView.swift */; };
|
||||
CE7B3A112EA1B00000C0FFEE /* MultilineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7B3A102EA1B00000C0FFEE /* MultilineTextField.swift */; };
|
||||
84C584EB268FA6D1000FCABF /* VMConfigAppleSystemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C584EA268FA6D1000FCABF /* VMConfigAppleSystemView.swift */; };
|
||||
84C60FB72681A41B00B58C00 /* VMToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB62681A41B00B58C00 /* VMToolbarView.swift */; platformFilter = ios; };
|
||||
84C60FB82681A41B00B58C00 /* VMToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB62681A41B00B58C00 /* VMToolbarView.swift */; platformFilter = ios; };
|
||||
@@ -1873,6 +1874,7 @@
|
||||
84C584DE268E95B3000FCABF /* UTMLegacyAppleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLegacyAppleConfiguration.swift; sourceTree = "<group>"; };
|
||||
84C584E2268F8AE7000FCABF /* VMQEMUSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMQEMUSettingsView.swift; sourceTree = "<group>"; };
|
||||
84C584E4268F8C65000FCABF /* VMAppleSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMAppleSettingsView.swift; sourceTree = "<group>"; };
|
||||
CE7B3A102EA1B00000C0FFEE /* MultilineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextField.swift; sourceTree = "<group>"; };
|
||||
84C584EA268FA6D1000FCABF /* VMConfigAppleSystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigAppleSystemView.swift; sourceTree = "<group>"; };
|
||||
84C60FB62681A41B00B58C00 /* VMToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMToolbarView.swift; sourceTree = "<group>"; };
|
||||
84C60FB9268269D700B58C00 /* VMDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -2855,6 +2857,7 @@
|
||||
84A381A9268CB30C0048EE4D /* VMDrivesSettingsView.swift */,
|
||||
CE928C3026ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift */,
|
||||
84C584E4268F8C65000FCABF /* VMAppleSettingsView.swift */,
|
||||
CE7B3A102EA1B00000C0FFEE /* MultilineTextField.swift */,
|
||||
845F1706289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift */,
|
||||
846F8D572E3850620037162B /* VMKeyboardShortcutsView.swift */,
|
||||
84C584E2268F8AE7000FCABF /* VMQEMUSettingsView.swift */,
|
||||
@@ -4150,6 +4153,7 @@
|
||||
CE25125129C806AF000790AB /* UTMScriptingDeleteCommand.swift in Sources */,
|
||||
CE0B6CFB24AD568400FE012D /* UTMLegacyQemuConfiguration+Networking.m in Sources */,
|
||||
84C584E5268F8C65000FCABF /* VMAppleSettingsView.swift in Sources */,
|
||||
CE7B3A112EA1B00000C0FFEE /* MultilineTextField.swift in Sources */,
|
||||
CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */,
|
||||
84F746BB276FF70700A20C87 /* VMDisplayQemuDisplayController.swift in Sources */,
|
||||
CE772AB425C8B7B500E4E379 /* VMCommands.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user