mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
iOS: Add proper UITextInput implementation
This provides an improved user experience, allowing cursor movement directly from the keyboard.
This commit is contained in:
parent
09c107698b
commit
5cf1a964fc
8 changed files with 1022 additions and 255 deletions
|
|
@ -271,7 +271,7 @@ namespace juce
|
|||
#include "native/juce_MultiTouchMapper.h"
|
||||
#endif
|
||||
|
||||
#if JUCE_ANDROID || JUCE_WINDOWS || JUCE_UNIT_TESTS
|
||||
#if JUCE_ANDROID || JUCE_WINDOWS || JUCE_IOS || JUCE_UNIT_TESTS
|
||||
#include "native/accessibility/juce_AccessibilityTextHelpers.h"
|
||||
#endif
|
||||
|
||||
|
|
@ -279,8 +279,8 @@ namespace juce
|
|||
#include "native/accessibility/juce_mac_AccessibilitySharedCode.mm"
|
||||
|
||||
#if JUCE_IOS
|
||||
#include "native/accessibility/juce_ios_Accessibility.mm"
|
||||
#include "native/juce_ios_UIViewComponentPeer.mm"
|
||||
#include "native/accessibility/juce_ios_Accessibility.mm"
|
||||
#include "native/juce_ios_Windowing.mm"
|
||||
#include "native/juce_ios_FileChooser.mm"
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,32 @@ public:
|
|||
/** Inserts some text, overwriting the selected text region, if there is one. */
|
||||
virtual void insertTextAtCaret (const String& textToInsert) = 0;
|
||||
|
||||
/** Returns the current index of the caret. */
|
||||
virtual int getCaretPosition() const = 0;
|
||||
|
||||
/** Returns the position of the caret, relative to the component's origin. */
|
||||
virtual Rectangle<int> getCaretRectangle() = 0;
|
||||
Rectangle<int> getCaretRectangle() const { return getCaretRectangleForCharIndex (getCaretPosition()); }
|
||||
|
||||
/** Returns the bounding box of the character at the given index. */
|
||||
virtual Rectangle<int> getCaretRectangleForCharIndex (int characterIndex) const = 0;
|
||||
|
||||
/** Returns the total number of codepoints in the string. */
|
||||
virtual int getTotalNumChars() const = 0;
|
||||
|
||||
/** Returns the index closest to the given point.
|
||||
|
||||
This is the location where the cursor might be placed after clicking at the given
|
||||
point in a text field.
|
||||
*/
|
||||
virtual int getCharIndexForPoint (Point<int> point) const = 0;
|
||||
|
||||
/** Returns the bounding box for a range of text in the editor. As the range may span
|
||||
multiple lines, this method returns a RectangleList.
|
||||
|
||||
The bounds are relative to the component's top-left and may extend beyond the bounds
|
||||
of the component if the text is long and word wrapping is disabled.
|
||||
*/
|
||||
virtual RectangleList<int> getTextBounds (Range<int> textRange) const = 0;
|
||||
|
||||
/** A set of possible on-screen keyboard types, for use in the
|
||||
getKeyboardType() method.
|
||||
|
|
|
|||
|
|
@ -43,33 +43,45 @@ namespace juce
|
|||
|
||||
#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
|
||||
|
||||
//==============================================================================
|
||||
static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler)
|
||||
{
|
||||
const auto children = handler.getChildren();
|
||||
template <typename> struct Signature;
|
||||
|
||||
NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()];
|
||||
template <typename Result, typename... Args>
|
||||
struct Signature<Result (Args...)> {};
|
||||
|
||||
for (auto* childHandler : children)
|
||||
{
|
||||
id accessibleElement = [&childHandler]
|
||||
{
|
||||
id native = static_cast<id> (childHandler->getNativeImplementation());
|
||||
|
||||
if (! childHandler->getChildren().empty())
|
||||
return [native accessibilityContainer];
|
||||
|
||||
return native;
|
||||
}();
|
||||
|
||||
if (accessibleElement != nil)
|
||||
[accessibleChildren addObject: accessibleElement];
|
||||
}
|
||||
|
||||
[accessibleChildren addObject: static_cast<id> (handler.getNativeImplementation())];
|
||||
|
||||
return accessibleChildren;
|
||||
}
|
||||
// @selector isn't constexpr, so the 'sel' members are functions rather than static constexpr data members
|
||||
struct SignatureHasText : Signature<BOOL()> { static auto sel() { return @selector (hasText); } };
|
||||
struct SignatureSetSelectedTextRange : Signature<void (UITextRange*)> { static auto sel() { return @selector (setSelectedTextRange:); } };
|
||||
struct SignatureSelectedTextRange : Signature<UITextRange*()> { static auto sel() { return @selector (selectedTextRange); } };
|
||||
struct SignatureMarkedTextRange : Signature<UITextRange*()> { static auto sel() { return @selector (markedTextRange); } };
|
||||
struct SignatureSetMarkedTextSelectedRange : Signature<void (NSString*, NSRange)> { static auto sel() { return @selector (setMarkedText:selectedRange:); } };
|
||||
struct SignatureUnmarkText : Signature<void()> { static auto sel() { return @selector (unmarkText); } };
|
||||
struct SignatureMarkedTextStyle : Signature<NSDictionary<NSAttributedStringKey, id>*()> { static auto sel() { return @selector (markedTextStyle); } };
|
||||
struct SignatureSetMarkedTextStyle : Signature<void (NSDictionary<NSAttributedStringKey, id>*)> { static auto sel() { return @selector (setMarkedTextStyle:); } };
|
||||
struct SignatureBeginningOfDocument : Signature<UITextPosition*()> { static auto sel() { return @selector (beginningOfDocument); } };
|
||||
struct SignatureEndOfDocument : Signature<UITextPosition*()> { static auto sel() { return @selector (endOfDocument); } };
|
||||
struct SignatureTokenizer : Signature<id<UITextInputTokenizer>()> { static auto sel() { return @selector (tokenizer); } };
|
||||
struct SignatureBaseWritingDirection : Signature<NSWritingDirection (UITextPosition*, UITextStorageDirection)> { static auto sel() { return @selector (baseWritingDirectionForPosition:inDirection:); } };
|
||||
struct SignatureCaretRectForPosition : Signature<CGRect (UITextPosition*)> { static auto sel() { return @selector (caretRectForPosition:); } };
|
||||
struct SignatureCharacterRangeByExtending : Signature<UITextRange* (UITextPosition*, UITextLayoutDirection)> { static auto sel() { return @selector (characterRangeByExtendingPosition:inDirection:); } };
|
||||
struct SignatureCharacterRangeAtPoint : Signature<UITextRange* (CGPoint)> { static auto sel() { return @selector (characterRangeAtPoint:); } };
|
||||
struct SignatureClosestPositionToPoint : Signature<UITextPosition* (CGPoint)> { static auto sel() { return @selector (closestPositionToPoint:); } };
|
||||
struct SignatureClosestPositionToPointInRange : Signature<UITextPosition* (CGPoint, UITextRange*)> { static auto sel() { return @selector (closestPositionToPoint:withinRange:); } };
|
||||
struct SignatureComparePositionToPosition : Signature<NSComparisonResult (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (comparePosition:toPosition:); } };
|
||||
struct SignatureOffsetFromPositionToPosition : Signature<NSInteger (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (offsetFromPosition:toPosition:); } };
|
||||
struct SignaturePositionFromPositionInDirection : Signature<UITextPosition* (UITextPosition*, UITextLayoutDirection, NSInteger)> { static auto sel() { return @selector (positionFromPosition:inDirection:offset:); } };
|
||||
struct SignaturePositionFromPositionOffset : Signature<UITextPosition* (UITextPosition*, NSInteger)> { static auto sel() { return @selector (positionFromPosition:offset:); } };
|
||||
struct SignatureFirstRectForRange : Signature<CGRect (UITextRange*)> { static auto sel() { return @selector (firstRectForRange:); } };
|
||||
struct SignatureSelectionRectsForRange : Signature<NSArray<UITextSelectionRect*>* (UITextRange*)> { static auto sel() { return @selector (selectionRectsForRange:); } };
|
||||
struct SignaturePositionWithinRange : Signature<UITextPosition* (UITextRange*, UITextLayoutDirection)> { static auto sel() { return @selector (positionWithinRange:farthestInDirection:); } };
|
||||
struct SignatureReplaceRangeWithText : Signature<void (UITextRange*, NSString*)> { static auto sel() { return @selector (replaceRange:withText:); } };
|
||||
struct SignatureSetBaseWritingDirection : Signature<void (NSWritingDirection, UITextRange*)> { static auto sel() { return @selector (setBaseWritingDirection:forRange:); } };
|
||||
struct SignatureTextInRange : Signature<NSString* (UITextRange*)> { static auto sel() { return @selector (textInRange:); } };
|
||||
struct SignatureTextRangeFromPosition : Signature<UITextRange* (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (textRangeFromPosition:toPosition:); } };
|
||||
struct SignatureSetInputDelegate : Signature<void (id)> { static auto sel() { return @selector (setInputDelegate:); } };
|
||||
struct SignatureInputDelegate : Signature<id()> { static auto sel() { return @selector (inputDelegate); } };
|
||||
struct SignatureKeyboardType : Signature<UIKeyboardType()> { static auto sel() { return @selector (keyboardType); } };
|
||||
struct SignatureAutocapitalizationType : Signature<UITextAutocapitalizationType()> { static auto sel() { return @selector (autocapitalizationType); } };
|
||||
struct SignatureAutocorrectionType : Signature<UITextAutocorrectionType()> { static auto sel() { return @selector (autocorrectionType); } };
|
||||
|
||||
//==============================================================================
|
||||
class AccessibilityHandler::AccessibilityNativeImpl
|
||||
|
|
@ -208,6 +220,12 @@ private:
|
|||
//==============================================================================
|
||||
class AccessibilityElement : public AccessibleObjCClass<UIAccessibilityElement>
|
||||
{
|
||||
template <typename Func, typename... Items>
|
||||
static constexpr void forEach (Func&& func, Items&&... items)
|
||||
{
|
||||
(void) std::initializer_list<int> { ((void) func (std::forward<Items> (items)), 0)... };
|
||||
}
|
||||
|
||||
public:
|
||||
enum class Type { defaultElement, textElement };
|
||||
|
||||
|
|
@ -318,7 +336,8 @@ private:
|
|||
case AccessibilityRole::hyperlink: return UIAccessibilityTraitLink;
|
||||
case AccessibilityRole::ignored: return UIAccessibilityTraitNotEnabled;
|
||||
|
||||
case AccessibilityRole::editableText:
|
||||
case AccessibilityRole::editableText: return UIAccessibilityTraitKeyboardKey;
|
||||
|
||||
case AccessibilityRole::slider:
|
||||
case AccessibilityRole::menuItem:
|
||||
case AccessibilityRole::menuBar:
|
||||
|
|
@ -394,7 +413,7 @@ private:
|
|||
handler->giveAwayFocus();
|
||||
});
|
||||
|
||||
addMethod (@selector (accessibilityElementIsFocused), [] (id self, SEL)
|
||||
addMethod (@selector (accessibilityElementIsFocused), [] (id self, SEL) -> BOOL
|
||||
{
|
||||
if (auto* handler = getHandler (self))
|
||||
return handler->hasFocus (false);
|
||||
|
|
@ -447,63 +466,45 @@ private:
|
|||
|
||||
if (elementType == Type::textElement)
|
||||
{
|
||||
addMethod (@selector (accessibilityLineNumberForPoint:), [] (id self, SEL, CGPoint point)
|
||||
{
|
||||
if (auto* handler = getHandler (self))
|
||||
{
|
||||
if (auto* textInterface = handler->getTextInterface())
|
||||
{
|
||||
auto pointInt = roundToIntPoint (point);
|
||||
addMethod (@selector (deleteBackward), [] (id, SEL) {});
|
||||
addMethod (@selector (insertText:), [] (id, SEL, NSString*) {});
|
||||
|
||||
if (handler->getComponent().getScreenBounds().contains (pointInt))
|
||||
{
|
||||
auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() });
|
||||
forEach ([this] (auto signature) { addPassthroughMethodWithSignature (signature); },
|
||||
SignatureHasText{},
|
||||
SignatureSetSelectedTextRange{},
|
||||
SignatureSelectedTextRange{},
|
||||
SignatureMarkedTextRange{},
|
||||
SignatureSetMarkedTextSelectedRange{},
|
||||
SignatureUnmarkText{},
|
||||
SignatureMarkedTextStyle{},
|
||||
SignatureSetMarkedTextStyle{},
|
||||
SignatureBeginningOfDocument{},
|
||||
SignatureEndOfDocument{},
|
||||
SignatureTokenizer{},
|
||||
SignatureBaseWritingDirection{},
|
||||
SignatureCaretRectForPosition{},
|
||||
SignatureCharacterRangeByExtending{},
|
||||
SignatureCharacterRangeAtPoint{},
|
||||
SignatureClosestPositionToPoint{},
|
||||
SignatureClosestPositionToPointInRange{},
|
||||
SignatureComparePositionToPosition{},
|
||||
SignatureOffsetFromPositionToPosition{},
|
||||
SignaturePositionFromPositionInDirection{},
|
||||
SignaturePositionFromPositionOffset{},
|
||||
SignatureFirstRectForRange{},
|
||||
SignatureSelectionRectsForRange{},
|
||||
SignaturePositionWithinRange{},
|
||||
SignatureReplaceRangeWithText{},
|
||||
SignatureSetBaseWritingDirection{},
|
||||
SignatureTextInRange{},
|
||||
SignatureTextRangeFromPosition{},
|
||||
SignatureSetInputDelegate{},
|
||||
SignatureInputDelegate{},
|
||||
SignatureKeyboardType{},
|
||||
SignatureAutocapitalizationType{},
|
||||
SignatureAutocorrectionType{});
|
||||
|
||||
for (int i = 0; i < textBounds.getNumRectangles(); ++i)
|
||||
if (textBounds.getRectangle (i).contains (pointInt))
|
||||
return (NSInteger) i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NSNotFound;
|
||||
});
|
||||
|
||||
addMethod (@selector (accessibilityContentForLineNumber:), [] (id self, SEL, NSInteger lineNumber) -> NSString*
|
||||
{
|
||||
if (auto* textInterface = getTextInterface (self))
|
||||
{
|
||||
auto lines = StringArray::fromLines (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }));
|
||||
|
||||
if ((int) lineNumber < lines.size())
|
||||
return juceStringToNS (lines[(int) lineNumber]);
|
||||
}
|
||||
|
||||
return nil;
|
||||
});
|
||||
|
||||
addMethod (@selector (accessibilityFrameForLineNumber:), [] (id self, SEL, NSInteger lineNumber)
|
||||
{
|
||||
if (auto* textInterface = getTextInterface (self))
|
||||
{
|
||||
auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() });
|
||||
|
||||
if (lineNumber < textBounds.getNumRectangles())
|
||||
return convertToCGRect (textBounds.getRectangle ((int) lineNumber));
|
||||
}
|
||||
|
||||
return CGRectZero;
|
||||
});
|
||||
|
||||
addMethod (@selector (accessibilityPageContent), [] (id self, SEL) -> NSString*
|
||||
{
|
||||
if (auto* textInterface = getTextInterface (self))
|
||||
return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }));
|
||||
|
||||
return nil;
|
||||
});
|
||||
|
||||
addProtocol (@protocol (UIAccessibilityReadingContent));
|
||||
addProtocol (@protocol (UITextInput));
|
||||
}
|
||||
|
||||
#if JUCE_IOS_CONTAINER_API_AVAILABLE
|
||||
|
|
@ -521,15 +522,69 @@ private:
|
|||
}
|
||||
|
||||
private:
|
||||
template <typename Return, typename Method>
|
||||
void addMethodWithReturn (SEL selector, Method method) { addMethod (selector, static_cast<Return (*) (id, SEL)> (method)); }
|
||||
template <typename Result>
|
||||
static auto getResult (NSInvocation* invocation, detail::Tag<Result>)
|
||||
{
|
||||
Result result{};
|
||||
[invocation getReturnValue: &result];
|
||||
return result;
|
||||
}
|
||||
|
||||
static void getResult (NSInvocation*, detail::Tag<void>) {}
|
||||
|
||||
template <typename HasSelector, typename Result, typename... Args>
|
||||
auto makePassthroughCallback (HasSelector, Signature<Result (Args...)>)
|
||||
{
|
||||
return [] (id self, SEL, Args... args) -> Result
|
||||
{
|
||||
if (auto* input = getPeerTextInput (self))
|
||||
{
|
||||
const auto s = detail::makeCompileTimeStr (@encode (Result), @encode (id), @encode (SEL), @encode (Args)...);
|
||||
const auto signature = [NSMethodSignature signatureWithObjCTypes: s.data()];
|
||||
const auto invocation = [NSInvocation invocationWithMethodSignature: signature];
|
||||
invocation.selector = HasSelector::sel();
|
||||
|
||||
// Indices 0 and 1 are 'id self' and 'SEL _cmd' respectively
|
||||
auto counter = 2;
|
||||
forEach ([&] (auto& arg) { [invocation setArgument: &arg atIndex: counter++]; }, args...);
|
||||
|
||||
[invocation invokeWithTarget: input];
|
||||
|
||||
return getResult (invocation, detail::Tag<Result>{});
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Signature>
|
||||
void addPassthroughMethodWithSignature (Signature signature)
|
||||
{
|
||||
addMethod (Signature::sel(), makePassthroughCallback (signature, signature));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static UIAccessibilityElement* getContainer (id self)
|
||||
{
|
||||
return getIvar<UIAccessibilityElement*> (self, "container");
|
||||
}
|
||||
|
||||
static UIViewComponentPeer* getPeer (id self)
|
||||
{
|
||||
if (auto* handler = getHandler (self))
|
||||
return static_cast<UIViewComponentPeer*> (handler->getComponent().getPeer());
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
static JuceTextView* getPeerTextInput (id self)
|
||||
{
|
||||
if (auto* peer = getPeer (self))
|
||||
return peer->hiddenTextInput.get();
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,6 +39,34 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler)
|
||||
{
|
||||
const auto children = handler.getChildren();
|
||||
|
||||
NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()];
|
||||
|
||||
for (auto* childHandler : children)
|
||||
{
|
||||
id accessibleElement = [&childHandler]
|
||||
{
|
||||
id native = static_cast<id> (childHandler->getNativeImplementation());
|
||||
|
||||
if (! childHandler->getChildren().empty())
|
||||
return [native accessibilityContainer];
|
||||
|
||||
return native;
|
||||
}();
|
||||
|
||||
if (accessibleElement != nil)
|
||||
[accessibleChildren addObject: accessibleElement];
|
||||
}
|
||||
|
||||
[accessibleChildren addObject: static_cast<id> (handler.getNativeImplementation())];
|
||||
|
||||
return accessibleChildren;
|
||||
}
|
||||
|
||||
class UIViewComponentPeer;
|
||||
|
||||
static UIInterfaceOrientation getWindowOrientation()
|
||||
|
|
@ -119,6 +147,107 @@ enum class MouseEventFlags
|
|||
|
||||
using namespace juce;
|
||||
|
||||
@interface JuceUITextPosition : UITextPosition
|
||||
{
|
||||
@public
|
||||
int index;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation JuceUITextPosition
|
||||
|
||||
+ (instancetype) withIndex: (int) indexIn
|
||||
{
|
||||
auto* result = [[JuceUITextPosition alloc] init];
|
||||
result->index = indexIn;
|
||||
return [result autorelease];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
@interface JuceUITextRange : UITextRange
|
||||
{
|
||||
@public
|
||||
int from, to;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation JuceUITextRange
|
||||
|
||||
+ (instancetype) withRange: (juce::Range<int>) range
|
||||
{
|
||||
return [JuceUITextRange from: range.getStart() to: range.getEnd()];
|
||||
}
|
||||
|
||||
+ (instancetype) from: (int) from to: (int) to
|
||||
{
|
||||
auto* result = [[JuceUITextRange alloc] init];
|
||||
result->from = from;
|
||||
result->to = to;
|
||||
return [result autorelease];
|
||||
}
|
||||
|
||||
- (UITextPosition*) start
|
||||
{
|
||||
return [JuceUITextPosition withIndex: from];
|
||||
}
|
||||
|
||||
- (UITextPosition*) end
|
||||
{
|
||||
return [JuceUITextPosition withIndex: to];
|
||||
}
|
||||
|
||||
- (Range<int>) range
|
||||
{
|
||||
return Range<int>::between (from, to);
|
||||
}
|
||||
|
||||
- (BOOL) isEmpty
|
||||
{
|
||||
return from == to;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
// UITextInputStringTokenizer doesn't handle 'line' granularities correctly by default, hence
|
||||
// this subclass.
|
||||
@interface JuceTextInputTokenizer : UITextInputStringTokenizer
|
||||
{
|
||||
UIViewComponentPeer* peer;
|
||||
}
|
||||
|
||||
- (instancetype) initWithPeer: (UIViewComponentPeer*) peer;
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
@interface JuceUITextSelectionRect : UITextSelectionRect
|
||||
{
|
||||
CGRect _rect;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation JuceUITextSelectionRect
|
||||
|
||||
+ (instancetype) withRect: (CGRect) rect
|
||||
{
|
||||
auto* result = [[JuceUITextSelectionRect alloc] init];
|
||||
result->_rect = rect;
|
||||
return [result autorelease];
|
||||
}
|
||||
|
||||
- (CGRect) rect { return _rect; }
|
||||
- (NSWritingDirection) writingDirection { return NSWritingDirectionNatural; }
|
||||
- (BOOL) containsStart { return NO; }
|
||||
- (BOOL) containsEnd { return NO; }
|
||||
- (BOOL) isVertical { return NO; }
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
struct CADisplayLinkDeleter
|
||||
{
|
||||
void operator() (CADisplayLink* displayLink) const noexcept
|
||||
|
|
@ -128,11 +257,21 @@ struct CADisplayLinkDeleter
|
|||
}
|
||||
};
|
||||
|
||||
@interface JuceUIView : UIView <UITextViewDelegate>
|
||||
@interface JuceTextView : UIView <UITextInput>
|
||||
{
|
||||
@public
|
||||
UIViewComponentPeer* owner;
|
||||
id<UITextInputDelegate> delegate;
|
||||
}
|
||||
|
||||
- (instancetype) initWithOwner: (UIViewComponentPeer*) owner;
|
||||
|
||||
@end
|
||||
|
||||
@interface JuceUIView : UIView
|
||||
{
|
||||
@public
|
||||
UIViewComponentPeer* owner;
|
||||
UITextView* hiddenTextView;
|
||||
std::unique_ptr<CADisplayLink, CADisplayLinkDeleter> displayLink;
|
||||
}
|
||||
|
||||
|
|
@ -159,8 +298,6 @@ struct CADisplayLinkDeleter
|
|||
- (BOOL) resignFirstResponder;
|
||||
- (BOOL) canBecomeFirstResponder;
|
||||
|
||||
- (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text;
|
||||
|
||||
- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection;
|
||||
|
||||
- (BOOL) isAccessibilityElement;
|
||||
|
|
@ -212,6 +349,7 @@ struct UIViewPeerControllerReceiver
|
|||
virtual void setViewController (UIViewController*) = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class UIViewComponentPeer : public ComponentPeer,
|
||||
private UIViewPeerControllerReceiver
|
||||
{
|
||||
|
|
@ -264,9 +402,9 @@ public:
|
|||
void grabFocus() override;
|
||||
void textInputRequired (Point<int>, TextInputTarget&) override;
|
||||
void dismissPendingTextInput() override;
|
||||
void closeInputMethodContext() override;
|
||||
|
||||
BOOL textViewReplaceCharacters (Range<int>, const String&);
|
||||
void updateHiddenTextContent (TextInputTarget&);
|
||||
|
||||
void updateScreenBounds();
|
||||
|
||||
|
|
@ -286,7 +424,10 @@ public:
|
|||
JuceUIView* view = nil;
|
||||
UIViewController* controller = nil;
|
||||
const bool isSharedWindow, isAppex;
|
||||
String stringBeingComposed;
|
||||
bool fullScreen = false, insideDrawRect = false;
|
||||
NSUniquePtr<JuceTextView> hiddenTextInput { [[JuceTextView alloc] initWithOwner: this] };
|
||||
NSUniquePtr<JuceTextInputTokenizer> tokenizer { [[JuceTextInputTokenizer alloc] initWithPeer: this] };
|
||||
|
||||
static int64 getMouseTime (NSTimeInterval timestamp) noexcept
|
||||
{
|
||||
|
|
@ -306,6 +447,22 @@ public:
|
|||
|
||||
static MultiTouchMapper<UITouch*> currentTouches;
|
||||
|
||||
static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType type) noexcept
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TextInputTarget::textKeyboard: return UIKeyboardTypeDefault;
|
||||
case TextInputTarget::numericKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
|
||||
case TextInputTarget::decimalKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
|
||||
case TextInputTarget::urlKeyboard: return UIKeyboardTypeURL;
|
||||
case TextInputTarget::emailAddressKeyboard: return UIKeyboardTypeEmailAddress;
|
||||
case TextInputTarget::phoneNumberKeyboard: return UIKeyboardTypePhonePad;
|
||||
default: jassertfalse; break;
|
||||
}
|
||||
|
||||
return UIKeyboardTypeDefault;
|
||||
}
|
||||
|
||||
private:
|
||||
void appStyleChanged() override
|
||||
{
|
||||
|
|
@ -491,14 +648,7 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
|
|||
[displayLink.get() addToRunLoop: [NSRunLoop mainRunLoop]
|
||||
forMode: NSDefaultRunLoopMode];
|
||||
|
||||
hiddenTextView = [[UITextView alloc] initWithFrame: CGRectZero];
|
||||
[self addSubview: hiddenTextView];
|
||||
hiddenTextView.delegate = self;
|
||||
|
||||
hiddenTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
hiddenTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
hiddenTextView.inputAssistantItem.leadingBarButtonGroups = @[];
|
||||
hiddenTextView.inputAssistantItem.trailingBarButtonGroups = @[];
|
||||
[self addSubview: owner->hiddenTextInput.get()];
|
||||
|
||||
#if JUCE_HAS_IOS_POINTER_SUPPORT
|
||||
if (@available (iOS 13.4, *))
|
||||
|
|
@ -522,9 +672,7 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
|
|||
|
||||
- (void) dealloc
|
||||
{
|
||||
[hiddenTextView removeFromSuperview];
|
||||
[hiddenTextView release];
|
||||
|
||||
[owner->hiddenTextInput.get() removeFromSuperview];
|
||||
displayLink = nullptr;
|
||||
|
||||
[super dealloc];
|
||||
|
|
@ -623,13 +771,6 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
|
|||
return owner != nullptr && owner->canBecomeKeyWindow();
|
||||
}
|
||||
|
||||
- (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text
|
||||
{
|
||||
ignoreUnused (textView);
|
||||
return owner->textViewReplaceCharacters (Range<int> ((int) range.location, (int) (range.location + range.length)),
|
||||
nsStringToJuce (text));
|
||||
}
|
||||
|
||||
- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection
|
||||
{
|
||||
[super traitCollectionDidChange: previousTraitCollection];
|
||||
|
|
@ -689,6 +830,542 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
|
|||
|
||||
@end
|
||||
|
||||
/** see https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/LowerLevelText-HandlingTechnologies/LowerLevelText-HandlingTechnologies.html */
|
||||
@implementation JuceTextView
|
||||
|
||||
- (TextInputTarget*) getTextInputTarget
|
||||
{
|
||||
if (owner != nullptr)
|
||||
return owner->findCurrentTextInputTarget();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
- (instancetype) initWithOwner: (UIViewComponentPeer*) ownerIn
|
||||
{
|
||||
[super init];
|
||||
owner = ownerIn;
|
||||
delegate = nil;
|
||||
|
||||
// The frame must have a finite size, otherwise some accessibility events will be ignored
|
||||
self.frame = CGRectMake (0.0, 0.0, 1.0, 1.0);
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL) canPerformAction: (SEL) action withSender: (id) sender
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (action == @selector (paste:))
|
||||
{
|
||||
if (@available (iOS 10, *))
|
||||
return [[UIPasteboard generalPasteboard] hasStrings];
|
||||
|
||||
return [[UIPasteboard generalPasteboard] string] != nil;
|
||||
}
|
||||
}
|
||||
|
||||
return [super canPerformAction: action withSender: sender];
|
||||
}
|
||||
|
||||
- (void) cut: (id) sender
|
||||
{
|
||||
[self copy: sender];
|
||||
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (delegate != nil)
|
||||
[delegate textWillChange: self];
|
||||
|
||||
target->insertTextAtCaret ("");
|
||||
|
||||
if (delegate != nil)
|
||||
[delegate textDidChange: self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) copy: (id) sender
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
[[UIPasteboard generalPasteboard] setString: juceStringToNS (target->getTextInRange (target->getHighlightedRegion()))];
|
||||
}
|
||||
|
||||
- (void) paste: (id) sender
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (auto* string = [[UIPasteboard generalPasteboard] string])
|
||||
{
|
||||
if (delegate != nil)
|
||||
[delegate textWillChange: self];
|
||||
|
||||
target->insertTextAtCaret (nsStringToJuce (string));
|
||||
|
||||
if (delegate != nil)
|
||||
[delegate textDidChange: self];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) selectAll: (id) sender
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
target->setHighlightedRegion ({ 0, target->getTotalNumChars() });
|
||||
}
|
||||
|
||||
- (void) deleteBackward
|
||||
{
|
||||
auto* target = [self getTextInputTarget];
|
||||
|
||||
if (target == nullptr)
|
||||
return;
|
||||
|
||||
const auto range = target->getHighlightedRegion();
|
||||
const auto rangeToDelete = range.isEmpty() ? range.withStartAndLength (jmax (range.getStart() - 1, 0),
|
||||
range.getStart() != 0 ? 1 : 0)
|
||||
: range;
|
||||
|
||||
target->setHighlightedRegion (rangeToDelete);
|
||||
target->insertTextAtCaret ("");
|
||||
}
|
||||
|
||||
- (void) insertText: (NSString*) text
|
||||
{
|
||||
if (owner == nullptr)
|
||||
return;
|
||||
|
||||
owner->stringBeingComposed.clear();
|
||||
|
||||
if (auto* target = owner->findCurrentTextInputTarget())
|
||||
target->insertTextAtCaret (nsStringToJuce (text));
|
||||
}
|
||||
|
||||
- (BOOL) hasText
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
return target->getTextInRange ({ 0, 1 }).isNotEmpty();
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL) accessibilityElementsHidden
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (UITextRange*) selectedTextRangeForTarget: (TextInputTarget*) target
|
||||
{
|
||||
if (target != nullptr)
|
||||
return [JuceUITextRange withRange: target->getHighlightedRegion()];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UITextRange*) selectedTextRange
|
||||
{
|
||||
return [self selectedTextRangeForTarget: [self getTextInputTarget]];
|
||||
}
|
||||
|
||||
- (void) setSelectedTextRange: (JuceUITextRange*) range
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
target->setHighlightedRegion (range != nil ? [range range] : Range<int>());
|
||||
}
|
||||
|
||||
- (UITextRange*) markedTextRange
|
||||
{
|
||||
if (owner != nullptr && owner->stringBeingComposed.isNotEmpty())
|
||||
if (auto* target = owner->findCurrentTextInputTarget())
|
||||
return [JuceUITextRange withRange: target->getHighlightedRegion()];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) setMarkedText: (NSString*) markedText
|
||||
selectedRange: (NSRange) selectedRange
|
||||
{
|
||||
ignoreUnused (selectedRange);
|
||||
|
||||
if (owner == nullptr)
|
||||
return;
|
||||
|
||||
owner->stringBeingComposed = nsStringToJuce (markedText);
|
||||
|
||||
auto* target = owner->findCurrentTextInputTarget();
|
||||
|
||||
if (target == nullptr)
|
||||
return;
|
||||
|
||||
const auto currentHighlight = target->getHighlightedRegion();
|
||||
target->insertTextAtCaret (owner->stringBeingComposed);
|
||||
target->setHighlightedRegion (currentHighlight.withLength (0));
|
||||
target->setHighlightedRegion (currentHighlight.withLength (owner->stringBeingComposed.length()));
|
||||
}
|
||||
|
||||
- (void) unmarkText
|
||||
{
|
||||
if (owner == nullptr)
|
||||
return;
|
||||
|
||||
auto* target = owner->findCurrentTextInputTarget();
|
||||
|
||||
if (target == nullptr)
|
||||
return;
|
||||
|
||||
target->insertTextAtCaret (owner->stringBeingComposed);
|
||||
owner->stringBeingComposed.clear();
|
||||
}
|
||||
|
||||
- (NSDictionary<NSAttributedStringKey, id>*) markedTextStyle
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void) setMarkedTextStyle: (NSDictionary<NSAttributedStringKey, id>*) dict
|
||||
{
|
||||
}
|
||||
|
||||
- (UITextPosition*) beginningOfDocument
|
||||
{
|
||||
return [JuceUITextPosition withIndex: 0];
|
||||
}
|
||||
|
||||
- (UITextPosition*) endOfDocument
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
return [JuceUITextPosition withIndex: target->getTotalNumChars()];
|
||||
|
||||
return [JuceUITextPosition withIndex: 0];
|
||||
}
|
||||
|
||||
- (id<UITextInputTokenizer>) tokenizer
|
||||
{
|
||||
return owner->tokenizer.get();
|
||||
}
|
||||
|
||||
- (NSWritingDirection) baseWritingDirectionForPosition: (UITextPosition*) position
|
||||
inDirection: (UITextStorageDirection) direction
|
||||
{
|
||||
return NSWritingDirectionNatural;
|
||||
}
|
||||
|
||||
- (CGRect) caretRectForPosition: (JuceUITextPosition*) position
|
||||
{
|
||||
if (position == nil)
|
||||
return CGRectZero;
|
||||
|
||||
// Currently we ignore the requested position and just return the text editor's caret position
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (auto* comp = dynamic_cast<Component*> (target))
|
||||
{
|
||||
const auto areaOnDesktop = comp->localAreaToGlobal (target->getCaretRectangle());
|
||||
return convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
|
||||
}
|
||||
}
|
||||
|
||||
return CGRectZero;
|
||||
}
|
||||
|
||||
- (UITextRange*) characterRangeByExtendingPosition: (JuceUITextPosition*) position
|
||||
inDirection: (UITextLayoutDirection) direction
|
||||
{
|
||||
const auto newPosition = [self indexFromPosition: position inDirection: direction offset: 1];
|
||||
return [JuceUITextRange from: position->index to: newPosition];
|
||||
}
|
||||
|
||||
- (int) closestIndexToPoint: (CGPoint) point
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (auto* comp = dynamic_cast<Component*> (target))
|
||||
{
|
||||
const auto pointOnDesktop = ScalingHelpers::unscaledScreenPosToScaled (convertToPointFloat (point));
|
||||
return target->getCharIndexForPoint (comp->getLocalPoint (nullptr, pointOnDesktop).roundToInt());
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (UITextRange*) characterRangeAtPoint: (CGPoint) point
|
||||
{
|
||||
const auto index = [self closestIndexToPoint: point];
|
||||
const auto result = index != -1 ? [JuceUITextRange from: index to: index] : nil;
|
||||
jassert (result != nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (UITextPosition*) closestPositionToPoint: (CGPoint) point
|
||||
{
|
||||
const auto index = [self closestIndexToPoint: point];
|
||||
const auto result = index != -1 ? [JuceUITextPosition withIndex: index] : nil;
|
||||
jassert (result != nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (UITextPosition*) closestPositionToPoint: (CGPoint) point
|
||||
withinRange: (JuceUITextRange*) range
|
||||
{
|
||||
const auto index = [self closestIndexToPoint: point];
|
||||
const auto result = index != -1 ? [JuceUITextPosition withIndex: [range range].clipValue (index)] : nil;
|
||||
jassert (result != nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSComparisonResult) comparePosition: (JuceUITextPosition*) position
|
||||
toPosition: (JuceUITextPosition*) other
|
||||
{
|
||||
const auto a = position != nil ? makeOptional (position->index) : nullopt;
|
||||
const auto b = other != nil ? makeOptional (other ->index) : nullopt;
|
||||
|
||||
if (a < b)
|
||||
return NSOrderedAscending;
|
||||
|
||||
if (b < a)
|
||||
return NSOrderedDescending;
|
||||
|
||||
return NSOrderedSame;
|
||||
}
|
||||
|
||||
- (NSInteger) offsetFromPosition: (JuceUITextPosition*) from
|
||||
toPosition: (JuceUITextPosition*) toPosition
|
||||
{
|
||||
if (from != nil && toPosition != nil)
|
||||
return toPosition->index - from->index;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (int) indexFromPosition: (JuceUITextPosition*) position
|
||||
inDirection: (UITextLayoutDirection) direction
|
||||
offset: (NSInteger) offset
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case UITextLayoutDirectionLeft:
|
||||
case UITextLayoutDirectionRight:
|
||||
return position->index + (int) (offset * (direction == UITextLayoutDirectionLeft ? -1 : 1));
|
||||
|
||||
case UITextLayoutDirectionUp:
|
||||
case UITextLayoutDirectionDown:
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
const auto originalRectangle = target->getCaretRectangleForCharIndex (position->index);
|
||||
|
||||
auto testIndex = position->index;
|
||||
|
||||
for (auto lineOffset = 0; lineOffset < offset; ++lineOffset)
|
||||
{
|
||||
const auto caretRect = target->getCaretRectangleForCharIndex (testIndex);
|
||||
const auto newY = direction == UITextLayoutDirectionUp ? caretRect.getY() - 1
|
||||
: caretRect.getBottom() + 1;
|
||||
testIndex = target->getCharIndexForPoint ({ originalRectangle.getX(), newY });
|
||||
}
|
||||
|
||||
return testIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return position->index;
|
||||
}
|
||||
|
||||
- (UITextPosition*) positionFromPosition: (JuceUITextPosition*) position
|
||||
inDirection: (UITextLayoutDirection) direction
|
||||
offset: (NSInteger) offset
|
||||
{
|
||||
return [JuceUITextPosition withIndex: [self indexFromPosition: position inDirection: direction offset: offset]];
|
||||
}
|
||||
|
||||
- (UITextPosition*) positionFromPosition: (JuceUITextPosition*) position
|
||||
offset: (NSInteger) offset
|
||||
{
|
||||
if (position != nil)
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
const auto newIndex = position->index + (int) offset;
|
||||
|
||||
if (isPositiveAndBelow (newIndex, target->getTotalNumChars() + 1))
|
||||
return [JuceUITextPosition withIndex: newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGRect) firstRectForRange: (JuceUITextRange*) range
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (auto* comp = dynamic_cast<Component*> (target))
|
||||
{
|
||||
const auto list = target->getTextBounds ([range range]);
|
||||
|
||||
if (! list.isEmpty())
|
||||
{
|
||||
const auto areaOnDesktop = comp->localAreaToGlobal (list.getRectangle (0));
|
||||
return convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
- (NSArray<UITextSelectionRect*>*) selectionRectsForRange: (JuceUITextRange*) range
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
{
|
||||
if (auto* comp = dynamic_cast<Component*> (target))
|
||||
{
|
||||
const auto list = target->getTextBounds ([range range]);
|
||||
|
||||
auto* result = [NSMutableArray arrayWithCapacity: (NSUInteger) list.getNumRectangles()];
|
||||
|
||||
for (const auto& rect : list)
|
||||
{
|
||||
const auto areaOnDesktop = comp->localAreaToGlobal (rect);
|
||||
const auto nativeArea = convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
|
||||
|
||||
[result addObject: [JuceUITextSelectionRect withRect: nativeArea]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UITextPosition*) positionWithinRange: (JuceUITextRange*) range
|
||||
farthestInDirection: (UITextLayoutDirection) direction
|
||||
{
|
||||
return direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionLeft
|
||||
? [range start]
|
||||
: [range end];
|
||||
}
|
||||
|
||||
- (void) replaceRange: (JuceUITextRange*) range
|
||||
withText: (NSString*) text
|
||||
{
|
||||
if (owner == nullptr)
|
||||
return;
|
||||
|
||||
owner->stringBeingComposed.clear();
|
||||
|
||||
if (auto* target = owner->findCurrentTextInputTarget())
|
||||
{
|
||||
target->setHighlightedRegion ([range range]);
|
||||
target->insertTextAtCaret (nsStringToJuce (text));
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setBaseWritingDirection: (NSWritingDirection) writingDirection
|
||||
forRange: (UITextRange*) range
|
||||
{
|
||||
}
|
||||
|
||||
- (NSString*) textInRange: (JuceUITextRange*) range
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
return juceStringToNS (target->getTextInRange ([range range]));
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UITextRange*) textRangeFromPosition: (JuceUITextPosition*) fromPosition
|
||||
toPosition: (JuceUITextPosition*) toPosition
|
||||
{
|
||||
const auto from = fromPosition != nil ? fromPosition->index : 0;
|
||||
const auto to = toPosition != nil ? toPosition ->index : 0;
|
||||
|
||||
return [JuceUITextRange withRange: Range<int>::between (from, to)];
|
||||
}
|
||||
|
||||
- (void) setInputDelegate: (id<UITextInputDelegate>) delegateIn
|
||||
{
|
||||
delegate = delegateIn;
|
||||
}
|
||||
|
||||
- (id<UITextInputDelegate>) inputDelegate
|
||||
{
|
||||
return delegate;
|
||||
}
|
||||
|
||||
- (UIKeyboardType) keyboardType
|
||||
{
|
||||
if (auto* target = [self getTextInputTarget])
|
||||
return UIViewComponentPeer::getUIKeyboardType (target->getKeyboardType());
|
||||
|
||||
return UIKeyboardTypeDefault;
|
||||
}
|
||||
|
||||
- (UITextAutocapitalizationType) autocapitalizationType
|
||||
{
|
||||
return UITextAutocapitalizationTypeNone;
|
||||
}
|
||||
|
||||
- (UITextAutocorrectionType) autocorrectionType
|
||||
{
|
||||
return UITextAutocorrectionTypeNo;
|
||||
}
|
||||
|
||||
- (BOOL) canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
@implementation JuceTextInputTokenizer
|
||||
|
||||
- (instancetype) initWithPeer: (UIViewComponentPeer*) peerIn
|
||||
{
|
||||
[super initWithTextInput: peerIn->hiddenTextInput.get()];
|
||||
peer = peerIn;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UITextRange*) rangeEnclosingPosition: (JuceUITextPosition*) position
|
||||
withGranularity: (UITextGranularity) granularity
|
||||
inDirection: (UITextDirection) direction
|
||||
{
|
||||
if (granularity != UITextGranularityLine)
|
||||
return [super rangeEnclosingPosition: position withGranularity: granularity inDirection: direction];
|
||||
|
||||
auto* target = peer->findCurrentTextInputTarget();
|
||||
|
||||
if (target == nullptr)
|
||||
return nullptr;
|
||||
|
||||
const auto numChars = target->getTotalNumChars();
|
||||
|
||||
if (! isPositiveAndBelow (position->index, numChars))
|
||||
return nullptr;
|
||||
|
||||
const auto allText = target->getTextInRange ({ 0, numChars });
|
||||
|
||||
const auto begin = AccessibilityTextHelpers::makeCharPtrIteratorAdapter (allText.begin());
|
||||
const auto end = AccessibilityTextHelpers::makeCharPtrIteratorAdapter (allText.end());
|
||||
const auto positionIter = begin + position->index;
|
||||
|
||||
const auto nextNewlineIter = std::find (positionIter, end, '\n');
|
||||
const auto lastNewlineIter = std::find (std::make_reverse_iterator (positionIter),
|
||||
std::make_reverse_iterator (begin),
|
||||
'\n').base();
|
||||
|
||||
const auto from = std::distance (begin, lastNewlineIter);
|
||||
const auto to = std::distance (begin, nextNewlineIter);
|
||||
|
||||
return [JuceUITextRange from: from to: to];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
//==============================================================================
|
||||
//==============================================================================
|
||||
namespace juce
|
||||
|
|
@ -975,6 +1652,9 @@ static float getTouchForce (UITouch* touch) noexcept
|
|||
|
||||
void UIViewComponentPeer::handleTouches (UIEvent* event, MouseEventFlags mouseEventFlags)
|
||||
{
|
||||
if (event == nullptr)
|
||||
return;
|
||||
|
||||
NSArray* touches = [[event touchesForView: view] allObjects];
|
||||
|
||||
for (unsigned int i = 0; i < [touches count]; ++i)
|
||||
|
|
@ -1126,41 +1806,27 @@ void UIViewComponentPeer::grabFocus()
|
|||
}
|
||||
}
|
||||
|
||||
void UIViewComponentPeer::textInputRequired (Point<int> pos, TextInputTarget& target)
|
||||
void UIViewComponentPeer::textInputRequired (Point<int>, TextInputTarget&)
|
||||
{
|
||||
view->hiddenTextView.frame = CGRectMake (pos.x, pos.y, 0, 0);
|
||||
[hiddenTextInput.get() becomeFirstResponder];
|
||||
}
|
||||
|
||||
updateHiddenTextContent (target);
|
||||
[view->hiddenTextView becomeFirstResponder];
|
||||
void UIViewComponentPeer::closeInputMethodContext()
|
||||
{
|
||||
if (auto* input = hiddenTextInput.get())
|
||||
{
|
||||
if (auto* delegate = [input inputDelegate])
|
||||
{
|
||||
[delegate selectionWillChange: input];
|
||||
[delegate selectionDidChange: input];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UIViewComponentPeer::dismissPendingTextInput()
|
||||
{
|
||||
closeInputMethodContext();
|
||||
[view->hiddenTextView resignFirstResponder];
|
||||
}
|
||||
|
||||
static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType type) noexcept
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TextInputTarget::textKeyboard: return UIKeyboardTypeAlphabet;
|
||||
case TextInputTarget::numericKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
|
||||
case TextInputTarget::decimalKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
|
||||
case TextInputTarget::urlKeyboard: return UIKeyboardTypeURL;
|
||||
case TextInputTarget::emailAddressKeyboard: return UIKeyboardTypeEmailAddress;
|
||||
case TextInputTarget::phoneNumberKeyboard: return UIKeyboardTypePhonePad;
|
||||
default: jassertfalse; break;
|
||||
}
|
||||
|
||||
return UIKeyboardTypeDefault;
|
||||
}
|
||||
|
||||
void UIViewComponentPeer::updateHiddenTextContent (TextInputTarget& target)
|
||||
{
|
||||
view->hiddenTextView.keyboardType = getUIKeyboardType (target.getKeyboardType());
|
||||
view->hiddenTextView.text = juceStringToNS (target.getTextInRange (Range<int> (0, target.getHighlightedRegion().getStart())));
|
||||
view->hiddenTextView.selectedRange = NSMakeRange ((NSUInteger) target.getHighlightedRegion().getStart(), 0);
|
||||
[hiddenTextInput.get() resignFirstResponder];
|
||||
}
|
||||
|
||||
BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const String& text)
|
||||
|
|
@ -1179,9 +1845,6 @@ BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const Str
|
|||
handleKeyPress (KeyPress::returnKey, text[0]);
|
||||
else
|
||||
target->insertTextAtCaret (text);
|
||||
|
||||
if (deletionChecker != nullptr)
|
||||
updateHiddenTextContent (*target);
|
||||
}
|
||||
|
||||
return NO;
|
||||
|
|
|
|||
|
|
@ -1168,7 +1168,7 @@ void TextEditor::updateCaretPosition()
|
|||
{
|
||||
Iterator i (*this);
|
||||
caret->setCaretPosition (getCaretRectangle().translated (leftIndent,
|
||||
topIndent + roundToInt (i.getYOffset())));
|
||||
topIndent + roundToInt (i.getYOffset())) - getTextOffset());
|
||||
|
||||
if (auto* handler = getAccessibilityHandler())
|
||||
handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
|
||||
|
|
@ -1451,18 +1451,13 @@ void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
|
|||
viewport->setViewPosition (vx, vy);
|
||||
}
|
||||
|
||||
Rectangle<int> TextEditor::getCaretRectangle()
|
||||
{
|
||||
return getCaretRectangleFloat().getSmallestIntegerContainer();
|
||||
}
|
||||
|
||||
Rectangle<float> TextEditor::getCaretRectangleFloat() const
|
||||
Rectangle<int> TextEditor::getCaretRectangleForCharIndex (int index) const
|
||||
{
|
||||
Point<float> anchor;
|
||||
auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
|
||||
getCharPosition (caretPosition, anchor, cursorHeight);
|
||||
getCharPosition (index, anchor, cursorHeight);
|
||||
|
||||
return { anchor.x, anchor.y, 2.0f, cursorHeight };
|
||||
return Rectangle<float> { anchor.x, anchor.y, 2.0f, cursorHeight }.getSmallestIntegerContainer() + getTextOffset();
|
||||
}
|
||||
|
||||
Point<int> TextEditor::getTextOffset() const noexcept
|
||||
|
|
@ -1474,7 +1469,7 @@ Point<int> TextEditor::getTextOffset() const noexcept
|
|||
roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + yOffset) - viewport->getViewPositionY() };
|
||||
}
|
||||
|
||||
RectangleList<int> TextEditor::getTextBounds (Range<int> textRange)
|
||||
RectangleList<int> TextEditor::getTextBounds (Range<int> textRange) const
|
||||
{
|
||||
RectangleList<int> boundingBox;
|
||||
Iterator i (*this);
|
||||
|
|
@ -1564,7 +1559,7 @@ void TextEditor::scrollToMakeSureCursorIsVisible()
|
|||
if (keepCaretOnScreen)
|
||||
{
|
||||
auto viewPos = viewport->getViewPosition();
|
||||
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent);
|
||||
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent) - getTextOffset();
|
||||
auto relativeCursor = caretRect.getPosition() - viewPos;
|
||||
|
||||
if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
|
||||
|
|
@ -1647,6 +1642,16 @@ int TextEditor::getTextIndexAt (const int x, const int y) const
|
|||
(float) (y - offset.y));
|
||||
}
|
||||
|
||||
int TextEditor::getTextIndexAt (const Point<int> pt) const
|
||||
{
|
||||
return getTextIndexAt (pt.x, pt.y);
|
||||
}
|
||||
|
||||
int TextEditor::getCharIndexForPoint (const Point<int> point) const
|
||||
{
|
||||
return getTextIndexAt (isMultiLine() ? point : getTextBounds ({ 0, getTotalNumChars() }).getBounds().getConstrainedPoint (point));
|
||||
}
|
||||
|
||||
void TextEditor::insertTextAtCaret (const String& t)
|
||||
{
|
||||
String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
|
||||
|
|
@ -1670,8 +1675,13 @@ void TextEditor::insertTextAtCaret (const String& t)
|
|||
|
||||
void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
|
||||
{
|
||||
moveCaretTo (newSelection.getStart(), false);
|
||||
moveCaretTo (newSelection.getEnd(), true);
|
||||
if (newSelection == getHighlightedRegion())
|
||||
return;
|
||||
|
||||
const auto cursorAtStart = newSelection.getEnd() == getHighlightedRegion().getStart()
|
||||
|| newSelection.getEnd() == getHighlightedRegion().getEnd();
|
||||
moveCaretTo (cursorAtStart ? newSelection.getEnd() : newSelection.getStart(), false);
|
||||
moveCaretTo (cursorAtStart ? newSelection.getStart() : newSelection.getEnd(), true);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -1853,8 +1863,7 @@ void TextEditor::mouseDown (const MouseEvent& e)
|
|||
{
|
||||
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
|
||||
{
|
||||
moveCaretTo (getTextIndexAt (e.x, e.y),
|
||||
e.mods.isShiftDown());
|
||||
moveCaretTo (getTextIndexAt (e.getPosition()), e.mods.isShiftDown());
|
||||
|
||||
if (auto* peer = getPeer())
|
||||
peer->closeInputMethodContext();
|
||||
|
|
@ -1889,7 +1898,7 @@ void TextEditor::mouseDrag (const MouseEvent& e)
|
|||
|
||||
if (wasFocused || ! selectAllTextWhenFocused)
|
||||
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
|
||||
moveCaretTo (getTextIndexAt (e.x, e.y), true);
|
||||
moveCaretTo (getTextIndexAt (e.getPosition()), true);
|
||||
}
|
||||
|
||||
void TextEditor::mouseUp (const MouseEvent& e)
|
||||
|
|
@ -1902,7 +1911,7 @@ void TextEditor::mouseUp (const MouseEvent& e)
|
|||
|
||||
if (wasFocused || ! selectAllTextWhenFocused)
|
||||
if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
|
||||
moveCaret (getTextIndexAt (e.x, e.y));
|
||||
moveCaret (getTextIndexAt (e.getPosition()));
|
||||
|
||||
wasFocused = true;
|
||||
}
|
||||
|
|
@ -1912,7 +1921,7 @@ void TextEditor::mouseDoubleClick (const MouseEvent& e)
|
|||
if (! mouseDownInEditor)
|
||||
return;
|
||||
|
||||
int tokenEnd = getTextIndexAt (e.x, e.y);
|
||||
int tokenEnd = getTextIndexAt (e.getPosition());
|
||||
int tokenStart = 0;
|
||||
|
||||
if (e.getNumberOfClicks() > 3)
|
||||
|
|
@ -2026,7 +2035,7 @@ bool TextEditor::moveCaretUp (bool selecting)
|
|||
if (! isMultiLine())
|
||||
return moveCaretToStartOfLine (selecting);
|
||||
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2035,7 +2044,7 @@ bool TextEditor::moveCaretDown (bool selecting)
|
|||
if (! isMultiLine())
|
||||
return moveCaretToEndOfLine (selecting);
|
||||
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2044,7 +2053,7 @@ bool TextEditor::pageUp (bool selecting)
|
|||
if (! isMultiLine())
|
||||
return moveCaretToStartOfLine (selecting);
|
||||
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - (float) viewport->getViewHeight()), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2053,7 +2062,7 @@ bool TextEditor::pageDown (bool selecting)
|
|||
if (! isMultiLine())
|
||||
return moveCaretToEndOfLine (selecting);
|
||||
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + (float) viewport->getViewHeight()), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2081,7 +2090,7 @@ bool TextEditor::moveCaretToTop (bool selecting)
|
|||
|
||||
bool TextEditor::moveCaretToStartOfLine (bool selecting)
|
||||
{
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2092,7 +2101,7 @@ bool TextEditor::moveCaretToEnd (bool selecting)
|
|||
|
||||
bool TextEditor::moveCaretToEndOfLine (bool selecting)
|
||||
{
|
||||
auto caretPos = getCaretRectangleFloat();
|
||||
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
|
||||
return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
|
||||
}
|
||||
|
||||
|
|
@ -2727,20 +2736,7 @@ private:
|
|||
|
||||
void setSelection (Range<int> r) override
|
||||
{
|
||||
if (r == textEditor.getHighlightedRegion())
|
||||
return;
|
||||
|
||||
if (r.isEmpty())
|
||||
{
|
||||
textEditor.setCaretPosition (r.getStart());
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto cursorAtStart = r.getEnd() == textEditor.getHighlightedRegion().getStart()
|
||||
|| r.getEnd() == textEditor.getHighlightedRegion().getEnd();
|
||||
textEditor.moveCaretTo (cursorAtStart ? r.getEnd() : r.getStart(), false);
|
||||
textEditor.moveCaretTo (cursorAtStart ? r.getStart() : r.getEnd(), true);
|
||||
}
|
||||
textEditor.setHighlightedRegion (r);
|
||||
}
|
||||
|
||||
String getText (Range<int> r) const override
|
||||
|
|
@ -2772,8 +2768,7 @@ private:
|
|||
|
||||
int getOffsetAtPoint (Point<int> point) const override
|
||||
{
|
||||
auto localPoint = textEditor.getLocalPoint (nullptr, point);
|
||||
return textEditor.getTextIndexAt (localPoint.x, localPoint.y);
|
||||
return textEditor.getTextIndexAt (textEditor.getLocalPoint (nullptr, point));
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ public:
|
|||
/** Returns the current index of the caret.
|
||||
@see setCaretPosition
|
||||
*/
|
||||
int getCaretPosition() const;
|
||||
int getCaretPosition() const override;
|
||||
|
||||
/** Moves the caret to be in front of a given character.
|
||||
@see getCaretPosition, moveCaretToEnd
|
||||
|
|
@ -443,12 +443,11 @@ public:
|
|||
*/
|
||||
void scrollEditorToPositionCaret (int desiredCaretX, int desiredCaretY);
|
||||
|
||||
/** Get the graphical position of the caret.
|
||||
/** Get the graphical position of the caret for a particular index in the text.
|
||||
|
||||
The rectangle returned is relative to the component's top-left corner.
|
||||
@see scrollEditorToPositionCaret
|
||||
*/
|
||||
Rectangle<int> getCaretRectangle() override;
|
||||
Rectangle<int> getCaretRectangleForCharIndex (int index) const override;
|
||||
|
||||
/** Selects a section of the text. */
|
||||
void setHighlightedRegion (const Range<int>& newSelection) override;
|
||||
|
|
@ -467,12 +466,22 @@ public:
|
|||
*/
|
||||
int getTextIndexAt (int x, int y) const;
|
||||
|
||||
/** Finds the index of the character at a given position.
|
||||
The coordinates are relative to the component's top-left.
|
||||
*/
|
||||
int getTextIndexAt (Point<int>) const;
|
||||
|
||||
/** Like getTextIndexAt, but doesn't snap to the beginning/end of the range for
|
||||
points vertically outside the text.
|
||||
*/
|
||||
int getCharIndexForPoint (Point<int> point) const override;
|
||||
|
||||
/** Counts the number of characters in the text.
|
||||
|
||||
This is quicker than getting the text as a string if you just need to know
|
||||
the length.
|
||||
*/
|
||||
int getTotalNumChars() const;
|
||||
int getTotalNumChars() const override;
|
||||
|
||||
/** Returns the total width of the text, as it is currently laid-out.
|
||||
|
||||
|
|
@ -541,7 +550,7 @@ public:
|
|||
The bounds are relative to the component's top-left and may extend beyond the bounds
|
||||
of the component if the text is long and word wrapping is disabled.
|
||||
*/
|
||||
RectangleList<int> getTextBounds (Range<int> textRange);
|
||||
RectangleList<int> getTextBounds (Range<int> textRange) const override;
|
||||
|
||||
//==============================================================================
|
||||
void moveCaretToEnd();
|
||||
|
|
@ -826,7 +835,6 @@ private:
|
|||
void reinsert (int insertIndex, const OwnedArray<UniformTextSection>&);
|
||||
void remove (Range<int>, UndoManager*, int caretPositionToMoveTo);
|
||||
void getCharPosition (int index, Point<float>&, float& lineHeight) const;
|
||||
Rectangle<float> getCaretRectangleFloat() const;
|
||||
void updateCaretPosition();
|
||||
void updateValueFromText();
|
||||
void textWasChangedByValue();
|
||||
|
|
|
|||
|
|
@ -71,21 +71,7 @@ private:
|
|||
|
||||
void setSelection (Range<int> r) override
|
||||
{
|
||||
if (r == codeEditorComponent.getHighlightedRegion())
|
||||
return;
|
||||
|
||||
if (r.isEmpty())
|
||||
{
|
||||
codeEditorComponent.caretPos.setPosition (r.getStart());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& doc = codeEditorComponent.document;
|
||||
|
||||
const auto cursorAtStart = r.getEnd() == codeEditorComponent.getHighlightedRegion().getStart()
|
||||
|| r.getEnd() == codeEditorComponent.getHighlightedRegion().getEnd();
|
||||
codeEditorComponent.selectRegion (CodeDocument::Position (doc, cursorAtStart ? r.getEnd() : r.getStart()),
|
||||
CodeDocument::Position (doc, cursorAtStart ? r.getStart() : r.getEnd()));
|
||||
codeEditorComponent.setHighlightedRegion (r);
|
||||
}
|
||||
|
||||
String getText (Range<int> r) const override
|
||||
|
|
@ -108,32 +94,7 @@ private:
|
|||
|
||||
RectangleList<int> getTextBounds (Range<int> textRange) const override
|
||||
{
|
||||
auto& doc = codeEditorComponent.document;
|
||||
|
||||
RectangleList<int> localRects;
|
||||
|
||||
CodeDocument::Position startPosition (doc, textRange.getStart());
|
||||
CodeDocument::Position endPosition (doc, textRange.getEnd());
|
||||
|
||||
for (int line = startPosition.getLineNumber(); line <= endPosition.getLineNumber(); ++line)
|
||||
{
|
||||
CodeDocument::Position lineStart (doc, line, 0);
|
||||
CodeDocument::Position lineEnd (doc, line, doc.getLine (line).length());
|
||||
|
||||
if (line == startPosition.getLineNumber())
|
||||
lineStart = lineStart.movedBy (startPosition.getIndexInLine());
|
||||
|
||||
if (line == endPosition.getLineNumber())
|
||||
lineEnd = { doc, line, endPosition.getIndexInLine() };
|
||||
|
||||
auto startPos = codeEditorComponent.getCharacterBounds (lineStart).getTopLeft();
|
||||
auto endPos = codeEditorComponent.getCharacterBounds (lineEnd).getTopLeft();
|
||||
|
||||
localRects.add (startPos.x,
|
||||
startPos.y,
|
||||
jmax (1, endPos.x - startPos.x),
|
||||
codeEditorComponent.getLineHeight());
|
||||
}
|
||||
const auto localRects = codeEditorComponent.getTextBounds (textRange);
|
||||
|
||||
RectangleList<int> globalRects;
|
||||
|
||||
|
|
@ -213,11 +174,23 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
void getHighlightArea (RectangleList<float>& area, float x, int y, int lineH, float characterWidth) const
|
||||
Optional<Rectangle<float>> getHighlightArea (float x, int y, int lineH, float characterWidth) const
|
||||
{
|
||||
if (highlightColumnStart < highlightColumnEnd)
|
||||
area.add (Rectangle<float> (x + (float) highlightColumnStart * characterWidth - 1.0f, (float) y - 0.5f,
|
||||
(float) (highlightColumnEnd - highlightColumnStart) * characterWidth + 1.5f, (float) lineH + 1.0f));
|
||||
return getHighlightArea (x, y, lineH, characterWidth, { highlightColumnStart, highlightColumnEnd });
|
||||
}
|
||||
|
||||
Optional<Rectangle<float>> getHighlightArea (float x,
|
||||
int y,
|
||||
int lineH,
|
||||
float characterWidth,
|
||||
Range<int> highlightColumns) const
|
||||
{
|
||||
if (highlightColumns.isEmpty())
|
||||
return {};
|
||||
|
||||
return Rectangle<float> (x + (float) highlightColumns.getStart() * characterWidth - 1.0f, (float) y - 0.5f,
|
||||
(float) (highlightColumns.getEnd() - highlightColumns.getStart()) * characterWidth + 1.5f, (float) lineH + 1.0f);
|
||||
|
||||
}
|
||||
|
||||
void draw (CodeEditorComponent& owner, Graphics& g, const Font& fontToUse,
|
||||
|
|
@ -533,14 +506,6 @@ void CodeEditorComponent::setTemporaryUnderlining (const Array<Range<int>>&)
|
|||
jassertfalse; // TODO Windows IME not yet supported for this comp..
|
||||
}
|
||||
|
||||
Rectangle<int> CodeEditorComponent::getCaretRectangle()
|
||||
{
|
||||
if (caret != nullptr)
|
||||
return getLocalArea (caret.get(), caret->getLocalBounds());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void CodeEditorComponent::setLineNumbersShown (const bool shouldBeShown)
|
||||
{
|
||||
if (showLineNumbers != shouldBeShown)
|
||||
|
|
@ -598,25 +563,26 @@ void CodeEditorComponent::paint (Graphics& g)
|
|||
{
|
||||
g.fillAll (findColour (CodeEditorComponent::backgroundColourId));
|
||||
|
||||
auto gutterSize = getGutterSize();
|
||||
auto bottom = horizontalScrollBar.isVisible() ? horizontalScrollBar.getY() : getHeight();
|
||||
auto right = verticalScrollBar.isVisible() ? verticalScrollBar.getX() : getWidth();
|
||||
const auto gutterSize = getGutterSize();
|
||||
const auto bottom = horizontalScrollBar.isVisible() ? horizontalScrollBar.getY() : getHeight();
|
||||
const auto right = verticalScrollBar.isVisible() ? verticalScrollBar.getX() : getWidth();
|
||||
|
||||
g.reduceClipRegion (gutterSize, 0, right - gutterSize, bottom);
|
||||
|
||||
g.setFont (font);
|
||||
|
||||
auto clip = g.getClipBounds();
|
||||
auto firstLineToDraw = jmax (0, clip.getY() / lineHeight);
|
||||
auto lastLineToDraw = jmin (lines.size(), clip.getBottom() / lineHeight + 1);
|
||||
auto x = (float) (gutterSize - xOffset * charWidth);
|
||||
auto rightClip = (float) clip.getRight();
|
||||
const auto clip = g.getClipBounds();
|
||||
const auto firstLineToDraw = jmax (0, clip.getY() / lineHeight);
|
||||
const auto lastLineToDraw = jmin (lines.size(), clip.getBottom() / lineHeight + 1);
|
||||
const auto x = (float) (gutterSize - xOffset * charWidth);
|
||||
const auto rightClip = (float) clip.getRight();
|
||||
|
||||
{
|
||||
RectangleList<float> highlightArea;
|
||||
|
||||
for (int i = firstLineToDraw; i < lastLineToDraw; ++i)
|
||||
lines.getUnchecked(i)->getHighlightArea (highlightArea, x, lineHeight * i, lineHeight, charWidth);
|
||||
if (const auto area = lines.getUnchecked(i)->getHighlightArea (x, lineHeight * i, lineHeight, charWidth))
|
||||
highlightArea.add (*area);
|
||||
|
||||
g.setColour (findColour (CodeEditorComponent::highlightColourId));
|
||||
g.fillRectList (highlightArea);
|
||||
|
|
@ -893,6 +859,37 @@ CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) const
|
|||
return CodeDocument::Position (document, line, index);
|
||||
}
|
||||
|
||||
int CodeEditorComponent::getCharIndexForPoint (Point<int> point) const
|
||||
{
|
||||
return getPositionAt (point.x, point.y).getPosition();
|
||||
}
|
||||
|
||||
RectangleList<int> CodeEditorComponent::getTextBounds (Range<int> textRange) const
|
||||
{
|
||||
RectangleList<int> localRects;
|
||||
|
||||
const CodeDocument::Position startPosition (document, textRange.getStart());
|
||||
const CodeDocument::Position endPosition (document, textRange.getEnd());
|
||||
|
||||
for (int line = startPosition.getLineNumber(); line <= endPosition.getLineNumber(); ++line)
|
||||
{
|
||||
const CodeDocument::Position lineStartColumn0 { document, line, 0 };
|
||||
|
||||
const auto lineStart = line == startPosition.getLineNumber() ? lineStartColumn0.movedBy (startPosition.getIndexInLine())
|
||||
: lineStartColumn0;
|
||||
|
||||
const CodeDocument::Position lineEnd { document, line, line == endPosition.getLineNumber() ? endPosition.getIndexInLine()
|
||||
: document.getLine (line).length() };
|
||||
|
||||
const auto startPos = getCharacterBounds (lineStart).getTopLeft();
|
||||
const auto endPos = getCharacterBounds (lineEnd) .getTopLeft();
|
||||
|
||||
localRects.add (startPos.x, startPos.y, jmax (1, endPos.x - startPos.x), getLineHeight());
|
||||
}
|
||||
|
||||
return localRects;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void CodeEditorComponent::insertTextAtCaret (const String& newText)
|
||||
{
|
||||
|
|
@ -1336,8 +1333,13 @@ bool CodeEditorComponent::isHighlightActive() const noexcept
|
|||
|
||||
void CodeEditorComponent::setHighlightedRegion (const Range<int>& newRange)
|
||||
{
|
||||
selectRegion (CodeDocument::Position (document, newRange.getStart()),
|
||||
CodeDocument::Position (document, newRange.getEnd()));
|
||||
if (newRange == getHighlightedRegion())
|
||||
return;
|
||||
|
||||
const auto cursorAtStart = newRange.getEnd() == getHighlightedRegion().getStart()
|
||||
|| newRange.getEnd() == getHighlightedRegion().getEnd();
|
||||
selectRegion (CodeDocument::Position (document, cursorAtStart ? newRange.getEnd() : newRange.getStart()),
|
||||
CodeDocument::Position (document, cursorAtStart ? newRange.getStart() : newRange.getEnd()));
|
||||
}
|
||||
|
||||
String CodeEditorComponent::getTextInRange (const Range<int>& range) const
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ public:
|
|||
/** Returns the current caret position. */
|
||||
CodeDocument::Position getCaretPos() const { return caretPos; }
|
||||
|
||||
/** Returns the position of the caret, relative to the editor's origin. */
|
||||
Rectangle<int> getCaretRectangle() override;
|
||||
/** Returns the total number of codepoints in the string. */
|
||||
int getTotalNumChars() const override { return document.getNumCharacters(); }
|
||||
|
||||
/** Moves the caret.
|
||||
If selecting is true, the section of the document between the current
|
||||
|
|
@ -121,6 +121,26 @@ public:
|
|||
/** Enables or disables the line-number display in the gutter. */
|
||||
void setLineNumbersShown (bool shouldBeShown);
|
||||
|
||||
/** Returns the number of characters from the beginning of the document to the caret. */
|
||||
int getCaretPosition() const override { return getCaretPos().getPosition(); }
|
||||
|
||||
/** @see getPositionAt */
|
||||
int getCharIndexForPoint (Point<int> point) const override;
|
||||
|
||||
/** Returns the bounds of the caret at a particular location in the text. */
|
||||
Rectangle<int> getCaretRectangleForCharIndex (int index) const override
|
||||
{
|
||||
return getCharacterBounds ({ document, index });
|
||||
}
|
||||
|
||||
/** Returns the bounding box for a range of text in the editor. As the range may span
|
||||
multiple lines, this method returns a RectangleList.
|
||||
|
||||
The bounds are relative to the component's top-left and may extend beyond the bounds
|
||||
of the component if the text is long and word wrapping is disabled.
|
||||
*/
|
||||
RectangleList<int> getTextBounds (Range<int> textRange) const override;
|
||||
|
||||
//==============================================================================
|
||||
bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting);
|
||||
bool moveCaretRight (bool moveInWholeWordSteps, bool selecting);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue