Unverified Commit 47c33a3e authored by dima.avdeev's avatar dima.avdeev Committed by GitHub

Text input multistage for China and Japan languages (#583)

parent 52640c31
package org.jetbrains.skiko
actual interface SkikoInput {
actual object Empty : SkikoInput
}
package org.jetbrains.skiko
actual interface SkikoInput {
actual object Empty : SkikoInput
}
package org.jetbrains.skiko
expect interface SkikoInput {
object Empty : SkikoInput
}
......@@ -6,7 +6,10 @@ interface SkikoView {
// Input
fun onKeyboardEvent(event: SkikoKeyboardEvent) = Unit
fun onPointerEvent(event: SkikoPointerEvent) = Unit
@Deprecated("This method will be removed. Use override val input: SkikoInput")
fun onInputEvent(event: SkikoInputEvent) = Unit
val input: SkikoInput get() = SkikoInput.Empty
fun onTouchEvent(events: Array<SkikoTouchEvent>) = Unit
fun onGestureEvent(event: SkikoGestureEvent) = Unit
......@@ -31,6 +34,8 @@ open class GenericSkikoView(
app.onInputEvent(event)
}
override val input: SkikoInput get() = app.input
override fun onKeyboardEvent(event: SkikoKeyboardEvent) {
app.onKeyboardEvent(event)
}
......
package org.jetbrains.skiko
actual interface SkikoInput {
/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
*/
fun hasText(): Boolean
/**
* Inserts a character into the displayed text.
* Add the character text to your class’s backing store at the index corresponding to the cursor and redisplay the text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614543-inserttext
* @param text A string object representing the character typed on the system keyboard.
*/
fun insertText(text: String)
/**
* Deletes a character from the displayed text.
* Remove the character just before the cursor from your class’s backing store and redisplay the text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614572-deletebackward
*/
fun deleteBackward()
/**
* The text position for the end of a document.
* https://developer.apple.com/documentation/uikit/uitextinput/1614555-endofdocument
*/
fun endOfDocument(): Long
/**
* The range of selected text in a document.
* If the text range has a length, it indicates the currently selected text.
* If it has zero length, it indicates the caret (insertion point).
* If the text-range object is nil, it indicates that there is no current selection.
* https://developer.apple.com/documentation/uikit/uitextinput/1614541-selectedtextrange
*/
fun getSelectedTextRange(): IntRange?
fun setSelectedTextRange(range: IntRange?)
fun selectAll()
/**
* Returns the text in the specified range.
* https://developer.apple.com/documentation/uikit/uitextinput/1614527-text
* @param range A range of text in a document.
* @return A substring of a document that falls within the specified range.
*/
fun textInRange(range: IntRange): String
/**
* Replaces the text in a document that is in the specified range.
* https://developer.apple.com/documentation/uikit/uitextinput/1614558-replace
* @param range A range of text in a document.
* @param text A string to replace the text in range.
*/
fun replaceRange(range: IntRange, text: String)
/**
* Inserts the provided text and marks it to indicate that it is part of an active input session.
* Setting marked text either replaces the existing marked text or,
* if none is present, inserts it in place of the current selection.
* https://developer.apple.com/documentation/uikit/uitextinput/1614465-setmarkedtext
* @param markedText The text to be marked.
* @param selectedRange A range within markedText that indicates the current selection.
* This range is always relative to markedText.
*/
fun setMarkedText(markedText: String?, selectedRange: IntRange)
/**
* The range of currently marked text in a document.
* If there is no marked text, the value of the property is nil.
* Marked text is provisionally inserted text that requires user confirmation;
* it occurs in multistage text input.
* The current selection, which can be a caret or an extended range, always occurs within the marked text.
* https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange
*/
fun markedTextRange(): IntRange?
/**
* Unmarks the currently marked text.
* After this method is called, the value of markedTextRange is nil.
* https://developer.apple.com/documentation/uikit/uitextinput/1614512-unmarktext
*/
fun unmarkText()
actual object Empty : SkikoInput {
override fun hasText(): Boolean = false
override fun insertText(text: String) = Unit
override fun deleteBackward() = Unit
override fun endOfDocument(): Long = 0L
override fun getSelectedTextRange(): IntRange? = null
override fun setSelectedTextRange(range: IntRange?) = Unit
override fun selectAll() = Unit
override fun textInRange(range: IntRange): String = ""
override fun replaceRange(range: IntRange, text: String) = Unit
override fun setMarkedText(markedText: String?, selectedRange: IntRange) = Unit
override fun markedTextRange(): IntRange? = null
override fun unmarkText() = Unit
}
}
package org.jetbrains.skiko
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExportObjCClass
import kotlinx.cinterop.useContents
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectMake
import platform.Foundation.NSCoder
import platform.UIKit.UIEvent
import platform.UIKit.UITouch
import platform.UIKit.UIScreen
import platform.UIKit.UIView
import platform.UIKit.setFrame
import platform.UIKit.contentScaleFactor
import platform.UIKit.UIKeyInputProtocol
import platform.UIKit.UIPress
import platform.UIKit.UIPressesEvent
import kotlinx.cinterop.*
import platform.CoreGraphics.*
import platform.Foundation.*
import platform.UIKit.*
import platform.darwin.NSInteger
import kotlin.math.max
import kotlin.math.min
@Suppress("CONFLICTING_OVERLOADS")
@ExportObjCClass
class SkikoUIView : UIView, UIKeyInputProtocol {
class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, UITextPasteConfigurationSupportingProtocol {
@OverrideInit
constructor(frame: CValue<CGRect>) : super(frame)
......@@ -25,11 +18,18 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
constructor(coder: NSCoder) : super(coder)
private var skiaLayer: SkiaLayer? = null
private var _inputDelegate: UITextInputDelegateProtocol? = null
private var _pasteConfiguration: UIPasteConfiguration? = null
private var _pasteDelegate: UITextPasteDelegateProtocol? = null
constructor(skiaLayer: SkiaLayer, frame: CValue<CGRect> = CGRectMake(0.0, 0.0, 1.0, 1.0)) : super(frame) {
constructor(skiaLayer: SkiaLayer, frame: CValue<CGRect> = CGRectNull.readValue()) : super(frame) {
this.skiaLayer = skiaLayer
}
override fun selectAll(sender: Any?) {
skiaLayer?.skikoView?.input?.selectAll()
}
fun detach() = skiaLayer?.detach()
fun load(): SkikoUIView {
......@@ -50,33 +50,31 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
fun hideScreenKeyboard() = resignFirstResponder()
fun isScreenKeyboardOpen() = isFirstResponder
private val pressedKeycodes: MutableSet<Long> = mutableSetOf()
@Deprecated("need be deleted")
private var inputText: String = ""//todo delete, because it's redundant and may leaks memory
/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
*/
override fun hasText(): Boolean {
return inputText.length > 0
return skiaLayer?.skikoView?.input?.hasText() ?: false
}
override fun insertText(theText: String) {
inputText += theText
skiaLayer?.skikoView?.onInputEvent(toSkikoTypeEvent(theText, null))
/**
* Inserts a character into the displayed text.
* Add the character text to your class’s backing store at the index corresponding to the cursor and redisplay the text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614543-inserttext
* @param text A string object representing the character typed on the system keyboard.
*/
override fun insertText(text: String) {
skiaLayer?.skikoView?.input?.insertText(text)
}
/**
* Deletes a character from the displayed text.
* Remove the character just before the cursor from your class’s backing store and redisplay the text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614572-deletebackward
*/
override fun deleteBackward() {
inputText = inputText.dropLast(1)
if (!pressedKeycodes.contains(SkikoKey.KEY_BACKSPACE.value)) {
val downEvent = SkikoKeyboardEvent(
key = SkikoKey.KEY_BACKSPACE,
kind = SkikoKeyboardEventKind.DOWN,
platform = null
)
val upEvent = downEvent.copy(
kind = SkikoKeyboardEventKind.UP
)
skiaLayer?.skikoView?.onKeyboardEvent(downEvent)
skiaLayer?.skikoView?.onKeyboardEvent(upEvent)
}
skiaLayer?.skikoView?.input?.deleteBackward()
}
override fun canBecomeFirstResponder() = true
......@@ -86,9 +84,6 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
for (press in withEvent.allPresses) {
val uiPress = press as? UIPress
if (uiPress != null) {
uiPress.key?.let {
pressedKeycodes.add(it.keyCode)
}
skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.DOWN)
)
......@@ -103,9 +98,6 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
for (press in withEvent.allPresses) {
val uiPress = press as? UIPress
if (uiPress != null) {
uiPress.key?.let {
pressedKeycodes.remove(it.keyCode)
}
skiaLayer?.skikoView?.onKeyboardEvent(
toSkikoKeyboardEvent(press, SkikoKeyboardEventKind.UP)
)
......@@ -117,57 +109,358 @@ class SkikoUIView : UIView, UIKeyInputProtocol {
override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) {
super.touchesBegan(touches, withEvent)
val events: MutableList<SkikoTouchEvent> = mutableListOf()
for (touch in touches) {
val event = touch as UITouch
val (x, y) = event.locationInView(null).useContents { x to y }
val timestamp = (event.timestamp * 1_000).toLong()
events.add(
SkikoTouchEvent(x, y, SkikoTouchEventKind.STARTED, timestamp, event)
)
}
skiaLayer?.skikoView?.onTouchEvent(events.toTypedArray())
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.STARTED)
}
override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) {
super.touchesEnded(touches, withEvent)
val events: MutableList<SkikoTouchEvent> = mutableListOf()
for (touch in touches) {
val event = touch as UITouch
val (x, y) = event.locationInView(null).useContents { x to y }
val timestamp = (event.timestamp * 1_000).toLong()
events.add(
SkikoTouchEvent(x, y, SkikoTouchEventKind.ENDED, timestamp, event)
)
}
skiaLayer?.skikoView?.onTouchEvent(events.toTypedArray())
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.ENDED)
}
override fun touchesMoved(touches: Set<*>, withEvent: UIEvent?) {
super.touchesMoved(touches, withEvent)
val events: MutableList<SkikoTouchEvent> = mutableListOf()
for (touch in touches) {
val event = touch as UITouch
val (x, y) = event.locationInView(null).useContents { x to y }
val timestamp = (event.timestamp * 1_000).toLong()
events.add(
SkikoTouchEvent(x, y, SkikoTouchEventKind.MOVED, timestamp, event)
)
}
skiaLayer?.skikoView?.onTouchEvent(events.toTypedArray())
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.MOVED)
}
override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) {
super.touchesCancelled(touches, withEvent)
val events: MutableList<SkikoTouchEvent> = mutableListOf()
for (touch in touches) {
val event = touch as UITouch
sendTouchEventToSkikoView(touches, SkikoTouchEventKind.CANCELLED)
}
private fun sendTouchEventToSkikoView(touches: Set<*>, kind: SkikoTouchEventKind) {
val events = touches.map {
val event = it as UITouch
val (x, y) = event.locationInView(null).useContents { x to y }
val timestamp = (event.timestamp * 1_000).toLong()
events.add(
SkikoTouchEvent(x, y, SkikoTouchEventKind.CANCELLED, timestamp, event)
SkikoTouchEvent(x, y, kind, timestamp, event)
}.toTypedArray()
skiaLayer?.skikoView?.onTouchEvent(events)
}
override fun inputDelegate(): UITextInputDelegateProtocol? {
return _inputDelegate
}
override fun setInputDelegate(inputDelegate: UITextInputDelegateProtocol?) {
_inputDelegate = inputDelegate
}
/**
* Returns the text in the specified range.
* https://developer.apple.com/documentation/uikit/uitextinput/1614527-text
* @param range A range of text in a document.
* @return A substring of a document that falls within the specified range.
*/
override fun textInRange(range: UITextRange): String? {
return skiaLayer?.skikoView?.input?.textInRange(range.toIntRange())
}
/**
* Replaces the text in a document that is in the specified range.
* https://developer.apple.com/documentation/uikit/uitextinput/1614558-replace
* @param range A range of text in a document.
* @param text A string to replace the text in range.
*/
override fun replaceRange(range: UITextRange, withText: String) {
skiaLayer?.skikoView?.input?.replaceRange(range.toIntRange(), withText)
}
override fun setSelectedTextRange(selectedTextRange: UITextRange?) {
skiaLayer?.skikoView?.input?.setSelectedTextRange(selectedTextRange?.toIntRange())
}
/**
* The range of selected text in a document.
* If the text range has a length, it indicates the currently selected text.
* If it has zero length, it indicates the caret (insertion point).
* If the text-range object is nil, it indicates that there is no current selection.
* https://developer.apple.com/documentation/uikit/uitextinput/1614541-selectedtextrange
*/
override fun selectedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.getSelectedTextRange()?.toUITextRange()
}
/**
* The range of currently marked text in a document.
* If there is no marked text, the value of the property is nil.
* Marked text is provisionally inserted text that requires user confirmation;
* it occurs in multistage text input.
* The current selection, which can be a caret or an extended range, always occurs within the marked text.
* https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange
*/
override fun markedTextRange(): UITextRange? {
return skiaLayer?.skikoView?.input?.markedTextRange()?.toUITextRange()
}
override fun setMarkedTextStyle(markedTextStyle: Map<Any?, *>?) {
// do nothing
}
override fun markedTextStyle(): Map<Any?, *>? {
return null
}
/**
* Inserts the provided text and marks it to indicate that it is part of an active input session.
* Setting marked text either replaces the existing marked text or,
* if none is present, inserts it in place of the current selection.
* https://developer.apple.com/documentation/uikit/uitextinput/1614465-setmarkedtext
* @param markedText The text to be marked.
* @param selectedRange A range within markedText that indicates the current selection.
* This range is always relative to markedText.
*/
override fun setMarkedText(markedText: String?, selectedRange: CValue<NSRange>) {
val (locationRelative, lengthRelative) = selectedRange.useContents {
location.toInt() to length.toInt()
}
val relativeTextRange = locationRelative until locationRelative + lengthRelative
skiaLayer?.skikoView?.input?.setMarkedText(markedText, relativeTextRange)
}
/**
* Unmarks the currently marked text.
* After this method is called, the value of markedTextRange is nil.
* https://developer.apple.com/documentation/uikit/uitextinput/1614512-unmarktext
*/
override fun unmarkText() {
skiaLayer?.skikoView?.input?.unmarkText()
}
override fun beginningOfDocument(): UITextPosition {
return IntermediateTextPosition(0)
}
/**
* The text position for the end of a document.
* https://developer.apple.com/documentation/uikit/uitextinput/1614555-endofdocument
*/
override fun endOfDocument(): UITextPosition {
return IntermediateTextPosition(skiaLayer?.skikoView?.input?.endOfDocument() ?: 0)
}
/**
* Attention! fromPosition and toPosition may be null
*/
override fun textRangeFromPosition(fromPosition: UITextPosition, toPosition: UITextPosition): UITextRange? {
val from = (fromPosition as? IntermediateTextPosition)?.position ?: 0
val to = (toPosition as? IntermediateTextPosition)?.position ?: 0
return IntermediateTextRange(
IntermediateTextPosition(minOf(from, to)),
IntermediateTextPosition(maxOf(from, to))
)
}
skiaLayer?.skikoView?.onTouchEvent(events.toTypedArray())
override fun positionFromPosition(position: UITextPosition, offset: NSInteger): UITextPosition? {
val p = (position as IntermediateTextPosition).position
val endOfDocument = skiaLayer?.skikoView?.input?.endOfDocument()
return if (endOfDocument != null) {
IntermediateTextPosition(max(min(p + offset, endOfDocument), 0))
} else {
null
}
}
override fun positionFromPosition(
position: UITextPosition,
inDirection: UITextLayoutDirection,
offset: NSInteger
): UITextPosition? {
return when (inDirection) {
UITextLayoutDirectionLeft, UITextLayoutDirectionUp -> {
positionFromPosition(position, -offset)
}
else -> positionFromPosition(position, offset)
}
}
/**
* Attention! position and toPosition may be null
*/
override fun comparePosition(position: UITextPosition, toPosition: UITextPosition): NSComparisonResult {
val from = (position as? IntermediateTextPosition)?.position ?: 0
val to = (toPosition as? IntermediateTextPosition)?.position ?: 0
val result = if (from < to) {
NSOrderedAscending
} else if (from > to) {
NSOrderedDescending
} else {
NSOrderedSame
}
return result
}
override fun offsetFromPosition(from: UITextPosition, toPosition: UITextPosition): NSInteger {
val fromPosition = from as IntermediateTextPosition
val to = toPosition as IntermediateTextPosition
return to.position - fromPosition.position
}
override fun tokenizer(): UITextInputTokenizerProtocol {
return UITextInputStringTokenizer()
}
override fun positionWithinRange(range: UITextRange, atCharacterOffset: NSInteger): UITextPosition? =
TODO("positionWithinRange range: $range, atCharacterOffset: $atCharacterOffset")
override fun positionWithinRange(range: UITextRange, farthestInDirection: UITextLayoutDirection): UITextPosition? =
TODO("positionWithinRange, farthestInDirection: ${farthestInDirection.directionToStr()}")
override fun characterRangeByExtendingPosition(
position: UITextPosition,
inDirection: UITextLayoutDirection
): UITextRange? {
TODO("characterRangeByExtendingPosition, inDirection: ${inDirection.directionToStr()}")
}
override fun baseWritingDirectionForPosition(
position: UITextPosition,
inDirection: UITextStorageDirection
): NSWritingDirection {
return NSWritingDirectionLeftToRight // TODO support RTL text direction
}
override fun setBaseWritingDirection(writingDirection: NSWritingDirection, forRange: UITextRange) {
// TODO support RTL text direction
}
//Working with Geometry and Hit-Testing. All methods return stubs for now.
override fun firstRectForRange(range: UITextRange): CValue<CGRect> = CGRectNull.readValue()
override fun caretRectForPosition(position: UITextPosition): CValue<CGRect> = CGRectNull.readValue()
override fun selectionRectsForRange(range: UITextRange): List<*> = listOf<UITextSelectionRect>()
override fun closestPositionToPoint(point: CValue<CGPoint>): UITextPosition? = null
override fun closestPositionToPoint(point: CValue<CGPoint>, withinRange: UITextRange): UITextPosition? = null
override fun characterRangeAtPoint(point: CValue<CGPoint>): UITextRange? = null
override fun textStylingAtPosition(position: UITextPosition, inDirection: UITextStorageDirection): Map<Any?, *>? {
return NSDictionary.dictionary()
}
override fun characterOffsetOfPosition(position: UITextPosition, withinRange: UITextRange): NSInteger {
TODO("characterOffsetOfPosition")
}
override fun shouldChangeTextInRange(range: UITextRange, replacementText: String): Boolean {
// Here we should decide to replace text in range or not.
// By default, this method returns true.
return true
}
override fun textInputView(): UIView {
return this
}
override fun canPerformAction(action: COpaquePointer?, withSender: Any?): Boolean {
//todo context menu with actions
return true
}
override fun pasteConfiguration(): UIPasteConfiguration? {
//https://developer.apple.com/documentation/uikit/uitextpasteconfigurationsupporting
//todo uikit copy/paste
return _pasteConfiguration
}
override fun setPasteConfiguration(pasteConfiguration: UIPasteConfiguration?) {
//todo uikit copy/paste
_pasteConfiguration = pasteConfiguration
}
override fun pasteDelegate(): UITextPasteDelegateProtocol? {
//todo uikit copy/paste
return _pasteDelegate
}
override fun setPasteDelegate(pasteDelegate: UITextPasteDelegateProtocol?) {
//todo uikit copy/paste
_pasteDelegate = pasteDelegate
}
override fun keyboardType(): UIKeyboardType {
return UIKeyboardTypeDefault //todo keyboardType
}
override fun isSecureTextEntry(): Boolean {
return false //todo secure text to prevent copy
}
override fun autocapitalizationType(): UITextAutocapitalizationType {
return UITextAutocapitalizationType.UITextAutocapitalizationTypeSentences
// return UITextAutocapitalizationType.UITextAutocapitalizationTypeAllCharacters
}
override fun autocorrectionType(): UITextAutocorrectionType {
return UITextAutocorrectionType.UITextAutocorrectionTypeYes
}
override fun dictationRecognitionFailed() {
//todo may be useful
}
override fun dictationRecordingDidEnd() {
//todo may be useful
}
/**
* Call when something changes in text data
*/
fun textWillChange() {
_inputDelegate?.textWillChange(this)
}
/**
* Call when something changes in text data
*/
fun textDidChange() {
_inputDelegate?.textDidChange(this)
}
/**
* Call when something changes in text data
*/
fun selectionWillChange() {
_inputDelegate?.selectionWillChange(this)
}
/**
* Call when something changes in text data
*/
fun selectionDidChange() {
_inputDelegate?.selectionDidChange(this)
}
}
private class IntermediateTextPosition(val position: Long = 0) : UITextPosition()
private fun IntermediateTextRange(start: Int, end: Int) =
IntermediateTextRange(
_start = IntermediateTextPosition(start.toLong()),
_end = IntermediateTextPosition(end.toLong())
)
private class IntermediateTextRange(
private val _start: IntermediateTextPosition,
private val _end: IntermediateTextPosition
) : UITextRange() {
override fun isEmpty() = (_end.position - _start.position) <= 0
override fun start(): UITextPosition = _start
override fun end(): UITextPosition = _end
}
private fun UITextRange.toIntRange(): IntRange {
val start = (start() as IntermediateTextPosition).position.toInt()
val end = (end() as IntermediateTextPosition).position.toInt()
return start until end
}
private fun IntRange.toUITextRange(): UITextRange =
IntermediateTextRange(start = start, end = endInclusive + 1)
private fun NSWritingDirection.directionToStr() =
when (this) {
UITextLayoutDirectionLeft -> "Left"
UITextLayoutDirectionRight -> "Right"
UITextLayoutDirectionUp -> "Up"
UITextLayoutDirectionDown -> "Down"
else -> "unknown direction"
}
......@@ -135,7 +135,7 @@ actual open class SkiaLayer {
skikoView?.onKeyboardEvent(toSkikoEvent(event, SkikoKeyboardEventKind.DOWN))
toSkikoTypeEvent(event.key, event)?.let { inputEvent ->
skikoView?.onInputEvent(inputEvent)
skikoView?.input?.onInputEvent(inputEvent)
}
})
htmlCanvas.addEventListener("keyup", { event ->
......
package org.jetbrains.skiko
actual interface SkikoInput {
fun onInputEvent(event: SkikoInputEvent)
actual object Empty : SkikoInput {
override fun onInputEvent(event: SkikoInputEvent) = Unit
}
}
package org.jetbrains.skiko
actual interface SkikoInput {
actual object Empty : SkikoInput
}
package org.jetbrains.skiko
actual interface SkikoInput {
actual object Empty : SkikoInput
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment