Compare commits

...

5 Commits

Author SHA1 Message Date
Levitating Pineapple 08db2852f3 cleanup 2024-11-21 15:50:18 +02:00
Levitating Pineapple 7d23ed3086 implement format menu 2024-11-18 22:10:52 +02:00
Levitating Pineapple 9e1c543c51 Merge branch 'master' into lp/markdown-compose 2024-11-15 13:57:32 +02:00
Levitating Pineapple c52065b5ef add encode stub 2024-11-15 13:57:04 +02:00
Levitating Pineapple c0e673d63f ios: add markdown format menu to compose 2024-11-01 13:55:46 +02:00
2 changed files with 153 additions and 46 deletions
@@ -126,7 +126,7 @@ struct ChatItemForwardingView: View {
ChatItemForwardingView( ChatItemForwardingView(
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")], chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")],
fromChatInfo: .direct(contact: Contact.sampleData), fromChatInfo: .direct(contact: Contact.sampleData),
composeState: Binding.constant(ComposeState(message: "hello")) composeState: Binding.constant(ComposeState())
).environmentObject(CurrentColors.toAppTheme()) ).environmentObject(CurrentColors.toAppTheme())
} }
@@ -29,24 +29,28 @@ struct NativeTextEditor: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView { func makeUIView(context: Context) -> UITextView {
let field = CustomUITextField(height: _height) let field = CustomUITextField(height: _height)
field.backgroundColor = .clear field.backgroundColor = .clear
field.text = text field.attributedText = NSAttributedString(
string: "",
attributes: CustomUITextField.defaultAttributes
)
field.textAlignment = alignment(text) field.textAlignment = alignment(text)
field.autocapitalizationType = .sentences field.autocapitalizationType = .sentences
field.setOnTextChangedListener { newText, images in // TODO: Reintegrate with attributed string
if !disableEditing { // field.setOnTextChangedListener { newText, images in
text = newText // if !disableEditing {
field.textAlignment = alignment(text) // text = newText
updateFont(field) // field.textAlignment = alignment(text)
// Speed up the process of updating layout, reduce jumping content on screen // updateFont(field)
updateHeight(field) // // Speed up the process of updating layout, reduce jumping content on screen
self.height = field.frame.size.height // updateHeight(field)
} else { // self.height = field.frame.size.height
field.text = text // } else {
} // field.text = text
if !images.isEmpty { // }
onImagesAdded(images) // if !images.isEmpty {
} // onImagesAdded(images)
} // }
// }
field.setOnFocusChangedListener { focused = $0 } field.setOnFocusChangedListener { focused = $0 }
field.delegate = field field.delegate = field
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
@@ -57,7 +61,8 @@ struct NativeTextEditor: UIViewRepresentable {
func updateUIView(_ field: UITextView, context: Context) { func updateUIView(_ field: UITextView, context: Context) {
if field.markedTextRange == nil && field.text != text { if field.markedTextRange == nil && field.text != text {
field.text = text // TODO: Reintegrate with attributed string
// field.text = text
field.textAlignment = alignment(text) field.textAlignment = alignment(text)
updateFont(field) updateFont(field)
updateHeight(field) updateHeight(field)
@@ -91,7 +96,7 @@ private func alignment(_ text: String) -> NSTextAlignment {
isRightToLeft(text) ? .right : .left isRightToLeft(text) ? .right : .left
} }
private class CustomUITextField: UITextView, UITextViewDelegate { class CustomUITextField: UITextView, UITextViewDelegate {
var height: Binding<CGFloat> var height: Binding<CGFloat>
var newHeight: CGFloat = 0 var newHeight: CGFloat = 0
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in } var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
@@ -130,6 +135,7 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
} }
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
let suggestedActions = [formatMenu] + suggestedActions
if !UIPasteboard.general.hasImages { return UIMenu(children: suggestedActions)} if !UIPasteboard.general.hasImages { return UIMenu(children: suggestedActions)}
return UIMenu(children: suggestedActions.map { elem in return UIMenu(children: suggestedActions.map { elem in
if let elem = elem as? UIMenu { if let elem = elem as? UIMenu {
@@ -172,33 +178,34 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
} }
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
if textView.markedTextRange == nil { // TODO: Reintegrate with attributed string
var images: [UploadContent] = [] // if textView.markedTextRange == nil {
var rangeDiff = 0 // var images: [UploadContent] = []
let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText) // var rangeDiff = 0
textView.attributedText.enumerateAttribute( // let newAttributedText = NSMutableAttributedString(attributedString: textView.attributedText)
NSAttributedString.Key.attachment, // textView.attributedText.enumerateAttribute(
in: NSRange(location: 0, length: textView.attributedText.length), // NSAttributedString.Key.attachment,
options: [], // in: NSRange(location: 0, length: textView.attributedText.length),
using: { value, range, _ in // options: [],
if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents { // using: { value, range, _ in
do { // if let attachment = (value as? NSTextAttachment)?.fileWrapper?.regularFileContents {
images.append(.animatedImage(image: try UIImage(gifData: attachment))) // do {
} catch { // images.append(.animatedImage(image: try UIImage(gifData: attachment)))
if let img = (value as? NSTextAttachment)?.image { // } catch {
images.append(.simpleImage(image: img)) // if let img = (value as? NSTextAttachment)?.image {
} // images.append(.simpleImage(image: img))
} // }
newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "") // }
rangeDiff += range.length // newAttributedText.replaceCharacters(in: NSMakeRange(range.location - rangeDiff, range.length), with: "")
} // rangeDiff += range.length
} // }
) // }
if textView.attributedText != newAttributedText { // )
textView.attributedText = newAttributedText // if textView.attributedText != newAttributedText {
} // textView.attributedText = newAttributedText
onTextChanged(textView.text, images) // }
} // onTextChanged(textView.text, images)
// }
} }
func textViewDidBeginEditing(_ textView: UITextView) { func textViewDidBeginEditing(_ textView: UITextView) {
@@ -208,6 +215,106 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
func textViewDidEndEditing(_ textView: UITextView) { func textViewDidEndEditing(_ textView: UITextView) {
onFocusChanged(false) onFocusChanged(false)
} }
// MARK: Formatting
private var formatMenu: UIMenu {
UIMenu(
title: "Format",
children: [
UIAction(image: UIImage(systemName: "bold")) { _ in self.toggleBold() },
UIAction(image: UIImage(systemName: "italic")) { _ in self.toggleItalic() },
UIAction(image: UIImage(systemName: "strikethrough")) { _ in self.toggleStriketrough() },
UIAction(title: "Mono") { _ in self.toggleMono() },
UIMenu(
title: "Color",
children: [UIColor]([.red, .green, .blue, .yellow, .cyan, .magenta])
.map { color in
UIAction(
image: UIImage(systemName: "circle.fill")?
.withTintColor(color, renderingMode: .alwaysOriginal),
handler: { _ in self.toggle(color: color) }
)
}
),
UIAction(title: "Secret") { _ in self.toggleSecret() }
]
)
}
func toggleBold() {
toggle(UIFont.boldSystemFont(ofSize: Self.bodySize), for: .font) {
($0 as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitBold)
}
}
func toggleItalic() {
toggle(UIFont.italicSystemFont(ofSize: Self.bodySize), for: .font) {
($0 as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitItalic)
}
}
func toggleStriketrough() {
toggle(NSUnderlineStyle.single.rawValue, for: .strikethroughStyle) {
($0 as? Int).map { $0 == NSUnderlineStyle.single.rawValue }
}
}
func toggleMono() {
toggle(UIFont.monospacedSystemFont(ofSize: Self.bodySize, weight: .regular), for: .font) {
($0 as? UIFont)?.fontDescriptor.symbolicTraits.contains(.traitMonoSpace)
}
}
func toggle(color: UIColor) {
toggle(color, for: .foregroundColor) {
($0 as? UIColor).map { $0 == color }
}
}
func toggleSecret() {
toggle(UIColor.clear, for: .foregroundColor) { value in
(value as? UIColor).map { $0 == .clear }
}
toggle(UIColor.secondarySystemFill, for: .backgroundColor) { value in
(value as? UIColor).map { $0 == .secondarySystemFill }
}
}
/// Toggles an attribute in the currently selected text range
/// - Parameters:
/// - attribute: Value of the attribute to be enabled or disabled
/// - key: Key for which to apply the attribute
/// - detect: Block which detects, if the attribute is already present within the selection
private func toggle(
_ attribute: Any,
for key: NSAttributedString.Key,
detect: (Any) -> Bool?
) {
var detected = false
textStorage.enumerateAttribute(key, in: selectedRange) { value, _, stop in
if let value, detect(value) == true {
detected = true
stop.pointee = true
}
}
if key == .backgroundColor {
textStorage.removeAttribute(.backgroundColor, range: selectedRange)
} else {
textStorage.setAttributes(Self.defaultAttributes, range: selectedRange)
}
if !detected {
textStorage.addAttribute(key, value: attribute, range: selectedRange)
}
}
static var bodySize: CGFloat {
UIFont.preferredFont(forTextStyle: .body).pointSize
}
static var defaultAttributes: [NSAttributedString.Key : Any] = [
.font: UIFont.preferredFont(forTextStyle: .body),
.foregroundColor: UIColor.label
]
} }
struct NativeTextEditor_Previews: PreviewProvider{ struct NativeTextEditor_Previews: PreviewProvider{