diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-12-04 17:55:38 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-12-04 17:55:38 +0200 |
commit | 368eddea42200ec95faa4f4767a72b2b3cf7bdff (patch) | |
tree | 5de42312f4ff697d7a141e926be41731e957d043 | |
parent | f8c0c817c2ce1919f71ef333439c9f4742fc6a12 (diff) |
iOS: Setting up a system-provided text input control
-rw-r--r-- | src/ios.h | 20 | ||||
-rw-r--r-- | src/ios.m | 111 | ||||
-rw-r--r-- | src/ui/inputwidget.c | 50 |
3 files changed, 176 insertions, 5 deletions
@@ -61,3 +61,23 @@ iBool isPaused_AVFAudioPlayer (const iAVFAudioPlayer *); | |||
61 | 61 | ||
62 | void clearNowPlayingInfo_iOS (void); | 62 | void clearNowPlayingInfo_iOS (void); |
63 | void updateNowPlayingInfo_iOS (void); | 63 | void updateNowPlayingInfo_iOS (void); |
64 | |||
65 | /*----------------------------------------------------------------------------------------------*/ | ||
66 | |||
67 | enum iSystemTextInputFlags { | ||
68 | multiLine_SystemTextInputFlags = iBit(1), | ||
69 | returnGo_SystemTextInputFlags = iBit(2), | ||
70 | returnSend_SystemTextInputFlags = iBit(3), | ||
71 | disableAutocorrect_SystemTextInputFlag = iBit(4), | ||
72 | alignRight_SystemTextInputFlag = iBit(5), | ||
73 | }; | ||
74 | |||
75 | iDeclareType(SystemTextInput) | ||
76 | iDeclareTypeConstructionArgs(SystemTextInput, int flags) | ||
77 | |||
78 | void setRect_SystemTextInput (iSystemTextInput *, iRect rect); | ||
79 | void setText_SystemTextInput (iSystemTextInput *, const iString *text); | ||
80 | void setFont_SystemTextInput (iSystemTextInput *, int fontId); | ||
81 | void setTextChangedFunc_SystemTextInput (iSystemTextInput *, void (*textChangedFunc)(iSystemTextInput *, void *), void *); | ||
82 | |||
83 | const iString * text_SystemTextInput (const iSystemTextInput *); | ||
@@ -60,6 +60,8 @@ static UIViewController *viewController_(iWindow *window) { | |||
60 | return NULL; | 60 | return NULL; |
61 | } | 61 | } |
62 | 62 | ||
63 | static void notifyChange_SystemTextInput_(iSystemTextInput *); | ||
64 | |||
63 | /*----------------------------------------------------------------------------------------------*/ | 65 | /*----------------------------------------------------------------------------------------------*/ |
64 | 66 | ||
65 | API_AVAILABLE(ios(13.0)) | 67 | API_AVAILABLE(ios(13.0)) |
@@ -159,9 +161,10 @@ API_AVAILABLE(ios(13.0)) | |||
159 | 161 | ||
160 | /*----------------------------------------------------------------------------------------------*/ | 162 | /*----------------------------------------------------------------------------------------------*/ |
161 | 163 | ||
162 | @interface AppState : NSObject<UIDocumentPickerDelegate> { | 164 | @interface AppState : NSObject<UIDocumentPickerDelegate, UITextFieldDelegate> { |
163 | iString *fileBeingSaved; | 165 | iString *fileBeingSaved; |
164 | iString *pickFileCommand; | 166 | iString *pickFileCommand; |
167 | iSystemTextInput *sysCtrl; | ||
165 | } | 168 | } |
166 | @property (nonatomic, assign) BOOL isHapticsAvailable; | 169 | @property (nonatomic, assign) BOOL isHapticsAvailable; |
167 | @property (nonatomic, strong) NSObject *haptic; | 170 | @property (nonatomic, strong) NSObject *haptic; |
@@ -175,9 +178,18 @@ static AppState *appState_; | |||
175 | self = [super init]; | 178 | self = [super init]; |
176 | fileBeingSaved = NULL; | 179 | fileBeingSaved = NULL; |
177 | pickFileCommand = NULL; | 180 | pickFileCommand = NULL; |
181 | sysCtrl = NULL; | ||
178 | return self; | 182 | return self; |
179 | } | 183 | } |
180 | 184 | ||
185 | -(void)setSystemTextInput:(iSystemTextInput *)sys { | ||
186 | sysCtrl = sys; | ||
187 | } | ||
188 | |||
189 | -(iSystemTextInput *)systemTextInput { | ||
190 | return sysCtrl; | ||
191 | } | ||
192 | |||
181 | -(void)setPickFileCommand:(const char *)command { | 193 | -(void)setPickFileCommand:(const char *)command { |
182 | if (!pickFileCommand) { | 194 | if (!pickFileCommand) { |
183 | pickFileCommand = new_String(); | 195 | pickFileCommand = new_String(); |
@@ -256,6 +268,21 @@ didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls { | |||
256 | -(void)keyboardOffScreen:(NSNotification *)notification { | 268 | -(void)keyboardOffScreen:(NSNotification *)notification { |
257 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); | 269 | setKeyboardHeight_MainWindow(get_MainWindow(), 0); |
258 | } | 270 | } |
271 | |||
272 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { | ||
273 | SDL_Event ev = { .type = SDL_KEYDOWN }; | ||
274 | ev.key.keysym.sym = SDLK_RETURN; | ||
275 | SDL_PushEvent(&ev); | ||
276 | printf("Return pressed\n"); | ||
277 | return NO; | ||
278 | } | ||
279 | |||
280 | - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { | ||
281 | iSystemTextInput *sysCtrl = [appState_ systemTextInput]; | ||
282 | notifyChange_SystemTextInput_(sysCtrl); | ||
283 | return YES; | ||
284 | } | ||
285 | |||
259 | @end | 286 | @end |
260 | 287 | ||
261 | static void enableMouse_(iBool yes) { | 288 | static void enableMouse_(iBool yes) { |
@@ -607,3 +634,85 @@ iBool isStarted_AVFAudioPlayer(const iAVFAudioPlayer *d) { | |||
607 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { | 634 | iBool isPaused_AVFAudioPlayer(const iAVFAudioPlayer *d) { |
608 | return d->state == paused_AVFAudioPlayerState; | 635 | return d->state == paused_AVFAudioPlayerState; |
609 | } | 636 | } |
637 | |||
638 | /*----------------------------------------------------------------------------------------------*/ | ||
639 | |||
640 | struct Impl_SystemTextInput { | ||
641 | void *ctrl; | ||
642 | void (*textChangedFunc)(iSystemTextInput *, void *); | ||
643 | void *textChangedContext; | ||
644 | }; | ||
645 | |||
646 | iDefineTypeConstructionArgs(SystemTextInput, (int flags), flags) | ||
647 | |||
648 | #define REF_d_ctrl (__bridge UITextField *)d->ctrl | ||
649 | |||
650 | void init_SystemTextInput(iSystemTextInput *d, int flags) { | ||
651 | d->ctrl = (void *) CFBridgingRetain([[UITextField alloc] init]); | ||
652 | UITextField *field = REF_d_ctrl; | ||
653 | // TODO: Use the right font: https://developer.apple.com/documentation/uikit/text_display_and_fonts/adding_a_custom_font_to_your_app?language=objc | ||
654 | [[viewController_(get_Window()) view] addSubview:REF_d_ctrl]; | ||
655 | if (flags & returnGo_SystemTextInputFlags) { | ||
656 | [field setReturnKeyType:UIReturnKeyGo]; | ||
657 | } | ||
658 | if (flags & returnSend_SystemTextInputFlags) { | ||
659 | [field setReturnKeyType:UIReturnKeySend]; | ||
660 | } | ||
661 | if (flags & disableAutocorrect_SystemTextInputFlag) { | ||
662 | [field setAutocorrectionType:UITextAutocorrectionTypeNo]; | ||
663 | [field setAutocapitalizationType:UITextAutocapitalizationTypeNone]; | ||
664 | [field setSpellCheckingType:UITextSpellCheckingTypeNo]; | ||
665 | } | ||
666 | if (flags & alignRight_WidgetFlag) { | ||
667 | [field setTextAlignment:NSTextAlignmentRight]; | ||
668 | } | ||
669 | [field setDelegate:appState_]; | ||
670 | [field becomeFirstResponder]; | ||
671 | d->textChangedFunc = NULL; | ||
672 | d->textChangedContext = NULL; | ||
673 | [appState_ setSystemTextInput:d]; | ||
674 | } | ||
675 | |||
676 | void deinit_SystemTextInput(iSystemTextInput *d) { | ||
677 | [appState_ setSystemTextInput:nil]; | ||
678 | [REF_d_ctrl removeFromSuperview]; | ||
679 | d->ctrl = nil; // TODO: Does this need to be released?? | ||
680 | } | ||
681 | |||
682 | void setText_SystemTextInput(iSystemTextInput *d, const iString *text) { | ||
683 | [REF_d_ctrl setText:[NSString stringWithUTF8String:cstr_String(text)]]; | ||
684 | [REF_d_ctrl selectAll:nil]; | ||
685 | } | ||
686 | |||
687 | void setFont_SystemTextInput(iSystemTextInput *d, int fontId) { | ||
688 | int height = lineHeight_Text(fontId); | ||
689 | UIFont *font = [UIFont systemFontOfSize:0.65f * height / get_Window()->pixelRatio]; | ||
690 | [REF_d_ctrl setFont:font]; | ||
691 | } | ||
692 | |||
693 | const iString *text_SystemTextInput(const iSystemTextInput *d) { | ||
694 | return collectNewCStr_String([[REF_d_ctrl text] cStringUsingEncoding:NSUTF8StringEncoding]);; | ||
695 | } | ||
696 | |||
697 | void setRect_SystemTextInput(iSystemTextInput *d, iRect rect) { | ||
698 | const iWindow *win = get_Window(); | ||
699 | CGRect frame; | ||
700 | frame.origin.x = rect.pos.x / win->pixelRatio; | ||
701 | frame.origin.y = (rect.pos.y - gap_UI + 2) / win->pixelRatio; | ||
702 | frame.size.width = rect.size.x / win->pixelRatio; | ||
703 | frame.size.height = rect.size.y / win->pixelRatio; | ||
704 | [REF_d_ctrl setFrame:frame]; | ||
705 | } | ||
706 | |||
707 | void setTextChangedFunc_SystemTextInput(iSystemTextInput *d, | ||
708 | void (*textChangedFunc)(iSystemTextInput *, void *), | ||
709 | void *context) { | ||
710 | d->textChangedFunc = textChangedFunc; | ||
711 | d->textChangedContext = context; | ||
712 | } | ||
713 | |||
714 | static void notifyChange_SystemTextInput_(iSystemTextInput *d) { | ||
715 | if (d && d->textChangedFunc) { | ||
716 | d->textChangedFunc(d, d->textChangedContext); | ||
717 | } | ||
718 | } | ||
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index bd9b63b5..bb3851df 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c | |||
@@ -40,6 +40,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
40 | # include "macos.h" | 40 | # include "macos.h" |
41 | #endif | 41 | #endif |
42 | 42 | ||
43 | #if defined (iPlatformAppleMobile) | ||
44 | # include "ios.h" | ||
45 | # define LAGRANGE_ENABLE_SYSTEM_INPUT 1 | ||
46 | #endif | ||
47 | |||
43 | static const int refreshInterval_InputWidget_ = 512; | 48 | static const int refreshInterval_InputWidget_ = 512; |
44 | static const size_t maxUndo_InputWidget_ = 64; | 49 | static const size_t maxUndo_InputWidget_ = 64; |
45 | static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ | 50 | static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ |
@@ -233,6 +238,7 @@ struct Impl_InputWidget { | |||
233 | void * validatorContext; | 238 | void * validatorContext; |
234 | iString * backupPath; | 239 | iString * backupPath; |
235 | int backupTimer; | 240 | int backupTimer; |
241 | iSystemTextInput *sysCtrl; | ||
236 | }; | 242 | }; |
237 | 243 | ||
238 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) | 244 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) |
@@ -721,10 +727,12 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { | |||
721 | d->buffered = NULL; | 727 | d->buffered = NULL; |
722 | d->backupPath = NULL; | 728 | d->backupPath = NULL; |
723 | d->backupTimer = 0; | 729 | d->backupTimer = 0; |
730 | d->sysCtrl = NULL; | ||
724 | updateMetrics_InputWidget_(d); | 731 | updateMetrics_InputWidget_(d); |
725 | } | 732 | } |
726 | 733 | ||
727 | void deinit_InputWidget(iInputWidget *d) { | 734 | void deinit_InputWidget(iInputWidget *d) { |
735 | delete_SystemTextInput(d->sysCtrl); | ||
728 | if (d->backupTimer) { | 736 | if (d->backupTimer) { |
729 | SDL_RemoveTimer(d->backupTimer); | 737 | SDL_RemoveTimer(d->backupTimer); |
730 | } | 738 | } |
@@ -953,6 +961,9 @@ void setText_InputWidget(iInputWidget *d, const iString *text) { | |||
953 | iString *nfcText = collect_String(copy_String(text)); | 961 | iString *nfcText = collect_String(copy_String(text)); |
954 | normalize_String(nfcText); | 962 | normalize_String(nfcText); |
955 | splitToLines_(nfcText, &d->lines); | 963 | splitToLines_(nfcText, &d->lines); |
964 | if (d->sysCtrl) { | ||
965 | setText_SystemTextInput(d->sysCtrl, nfcText); | ||
966 | } | ||
956 | iAssert(!isEmpty_Array(&d->lines)); | 967 | iAssert(!isEmpty_Array(&d->lines)); |
957 | iForEach(Array, i, &d->lines) { | 968 | iForEach(Array, i, &d->lines) { |
958 | updateLine_InputWidget_(d, i.value); /* count number of visible lines */ | 969 | updateLine_InputWidget_(d, i.value); /* count number of visible lines */ |
@@ -1008,6 +1019,16 @@ iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { | |||
1008 | return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; | 1019 | return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; |
1009 | } | 1020 | } |
1010 | 1021 | ||
1022 | static void contentsWereChanged_InputWidget_(iInputWidget *); | ||
1023 | |||
1024 | #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) | ||
1025 | void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { | ||
1026 | iInputWidget *d = widget; | ||
1027 | splitToLines_(text_SystemTextInput(sysCtrl), &d->lines); | ||
1028 | contentsWereChanged_InputWidget_(d); | ||
1029 | } | ||
1030 | #endif | ||
1031 | |||
1011 | void begin_InputWidget(iInputWidget *d) { | 1032 | void begin_InputWidget(iInputWidget *d) { |
1012 | iWidget *w = as_Widget(d); | 1033 | iWidget *w = as_Widget(d); |
1013 | if (isEditing_InputWidget_(d)) { | 1034 | if (isEditing_InputWidget_(d)) { |
@@ -1016,7 +1037,18 @@ void begin_InputWidget(iInputWidget *d) { | |||
1016 | } | 1037 | } |
1017 | invalidateBuffered_InputWidget_(d); | 1038 | invalidateBuffered_InputWidget_(d); |
1018 | setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); | 1039 | setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); |
1040 | setFlags_Widget(w, selected_WidgetFlag, iTrue); | ||
1019 | mergeLines_(&d->lines, &d->oldText); | 1041 | mergeLines_(&d->lines, &d->oldText); |
1042 | #if defined (LAGRANGE_ENABLE_SYSTEM_INPUT) | ||
1043 | d->sysCtrl = new_SystemTextInput((d->inFlags & isUrl_InputWidgetFlag ? disableAutocorrect_SystemTextInputFlag : 0) | | ||
1044 | (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) ); | ||
1045 | // (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0)); | ||
1046 | setFont_SystemTextInput(d->sysCtrl, d->font); | ||
1047 | setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); | ||
1048 | setText_SystemTextInput(d->sysCtrl, &d->oldText); | ||
1049 | setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); | ||
1050 | return; | ||
1051 | #endif | ||
1020 | if (d->mode == overwrite_InputMode) { | 1052 | if (d->mode == overwrite_InputMode) { |
1021 | d->cursor = zero_I2(); | 1053 | d->cursor = zero_I2(); |
1022 | } | 1054 | } |
@@ -1025,7 +1057,6 @@ void begin_InputWidget(iInputWidget *d) { | |||
1025 | d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); | 1057 | d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); |
1026 | } | 1058 | } |
1027 | SDL_StartTextInput(); | 1059 | SDL_StartTextInput(); |
1028 | setFlags_Widget(w, selected_WidgetFlag, iTrue); | ||
1029 | showCursor_InputWidget_(d); | 1060 | showCursor_InputWidget_(d); |
1030 | refresh_Widget(w); | 1061 | refresh_Widget(w); |
1031 | startOrStopCursorTimer_InputWidget_(d, iTrue); | 1062 | startOrStopCursorTimer_InputWidget_(d, iTrue); |
@@ -1048,15 +1079,22 @@ void end_InputWidget(iInputWidget *d, iBool accept) { | |||
1048 | /* Was not active. */ | 1079 | /* Was not active. */ |
1049 | return; | 1080 | return; |
1050 | } | 1081 | } |
1051 | enableEditorKeysInMenus_(iTrue); | 1082 | if (d->sysCtrl) { |
1052 | if (!accept) { | 1083 | if (accept) { |
1084 | splitToLines_(text_SystemTextInput(d->sysCtrl), &d->lines); | ||
1085 | } | ||
1086 | delete_SystemTextInput(d->sysCtrl); | ||
1087 | d->sysCtrl = NULL; | ||
1088 | } | ||
1089 | else if (!accept) { | ||
1053 | /* Overwrite the edited lines. */ | 1090 | /* Overwrite the edited lines. */ |
1054 | splitToLines_(&d->oldText, &d->lines); | 1091 | splitToLines_(&d->oldText, &d->lines); |
1092 | SDL_StopTextInput(); | ||
1055 | } | 1093 | } |
1094 | enableEditorKeysInMenus_(iTrue); | ||
1056 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; | 1095 | d->inFlags |= needUpdateBuffer_InputWidgetFlag; |
1057 | d->inFlags &= ~isMarking_InputWidgetFlag; | 1096 | d->inFlags &= ~isMarking_InputWidgetFlag; |
1058 | startOrStopCursorTimer_InputWidget_(d, iFalse); | 1097 | startOrStopCursorTimer_InputWidget_(d, iFalse); |
1059 | SDL_StopTextInput(); | ||
1060 | setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); | 1098 | setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); |
1061 | const char *id = cstr_String(id_Widget(as_Widget(d))); | 1099 | const char *id = cstr_String(id_Widget(as_Widget(d))); |
1062 | if (!*id) id = "_"; | 1100 | if (!*id) id = "_"; |
@@ -2324,6 +2362,10 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
2324 | isFocused ? gap_UI / 4 : 1, | 2362 | isFocused ? gap_UI / 4 : 1, |
2325 | isFocused ? uiInputFrameFocused_ColorId | 2363 | isFocused ? uiInputFrameFocused_ColorId |
2326 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); | 2364 | : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); |
2365 | if (d->sysCtrl) { | ||
2366 | drawChildren_Widget(w); | ||
2367 | return; | ||
2368 | } | ||
2327 | setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), | 2369 | setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), |
2328 | init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); | 2370 | init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); |
2329 | const iRect contentBounds = contentBounds_InputWidget_(d); | 2371 | const iRect contentBounds = contentBounds_InputWidget_(d); |