diff --git a/src/meson.build b/src/meson.build index 72587d366..f604e8e15 100644 --- a/src/meson.build +++ b/src/meson.build @@ -168,6 +168,7 @@ if host_machine.system() == 'darwin' 'font_file_lister_coretext.mm', 'osx/osx_utils.mm', 'osx/retina_helper.mm', + 'osx/scintilla_ime.mm', ) elif host_machine.system() == 'windows' aegisub_src += files( diff --git a/src/osx/scintilla_ime.mm b/src/osx/scintilla_ime.mm new file mode 100644 index 000000000..4806d1797 --- /dev/null +++ b/src/osx/scintilla_ime.mm @@ -0,0 +1,212 @@ +// Copyright (c) 2014, Thomas Goyne +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// Aegisub Project http://www.aegisub.org/ + +#import +#import +#import + +// from src/osx/cocoa/window.mm +@interface wxNSView : NSView { + BOOL _hasToolTip; + NSTrackingRectTag _lastToolTipTrackTag; + id _lastToolTipOwner; + void *_lastUserData; +} +@end + +@interface IMEState : NSObject +@property (nonatomic) NSRange markedRange; +@property (nonatomic) bool undoActive; +@end + +@implementation IMEState +- (id)init { + self = [super init]; + self.markedRange = NSMakeRange(NSNotFound, 0); + self.undoActive = false; + return self; +} +@end + +@interface ScintillaNSView : wxNSView +@property (nonatomic, readonly) wxStyledTextCtrl *stc; +@property (nonatomic, readonly) IMEState *state; +@end + +@implementation ScintillaNSView +- (Class)superclass { + return [wxNSView superclass]; +} + +- (wxStyledTextCtrl *)stc { + return static_cast(wxWidgetImpl::FindFromWXWidget(self)->GetWXPeer()); +} + +- (IMEState *)state { + return objc_getAssociatedObject(self, [IMEState class]); +} + +- (void)invalidate { + self.state.markedRange = NSMakeRange(NSNotFound, 0); + [self.inputContext discardMarkedText]; +} + +#pragma mark - NSTextInputClient + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange + actualRange:(NSRangePointer)actualRange +{ + return nil; +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)point { + return self.stc->PositionFromPoint(wxPoint(point.x, point.y)); +} + +- (BOOL)drawsVerticallyForCharacterAtIndex:(NSUInteger)charIndex { + return NO; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)range + actualRange:(NSRangePointer)actualRange +{ + auto stc = self.stc; + int line = stc->LineFromPosition(range.location); + int height = stc->TextHeight(line); + auto pt = stc->PointFromPosition(range.location); + + int width = 0; + if (range.length > 0) { + // If the end of the range is on the next line, the range should be + // truncated to the current line and actualRange should be set to the + // truncated range + int end_line = stc->LineFromPosition(range.location + range.length); + if (end_line > line) { + range.length = stc->PositionFromLine(line + 1) - 1 - range.location; + *actualRange = range; + } + + auto end_pt = stc->PointFromPosition(range.location + range.length); + width = end_pt.x - pt.x; + } + + auto rect = NSMakeRect(pt.x, pt.y, width, height); + rect = [self convertRect:rect toView:nil]; + return [self.window convertRectToScreen:rect]; +} + +- (BOOL)hasMarkedText { + return self.state.markedRange.length > 0; +} + +- (void)insertText:(id)str replacementRange:(NSRange)replacementRange { + [self unmarkText]; + [super insertText:str replacementRange:replacementRange]; +} + +- (NSRange)markedRange { + return self.state.markedRange; +} + +- (NSRange)selectedRange { + long from = 0, to = 0; + self.stc->GetSelection(&from, &to); + return NSMakeRange(from, to - from); +} + +- (void)setMarkedText:(id)str + selectedRange:(NSRange)range + replacementRange:(NSRange)replacementRange +{ + if ([str isKindOfClass:[NSAttributedString class]]) + str = [str string]; + + auto stc = self.stc; + auto state = self.state; + + int pos = stc->GetInsertionPoint(); + if (state.markedRange.length > 0) { + pos = state.markedRange.location; + stc->DeleteRange(pos, state.markedRange.length); + stc->SetSelection(pos, pos); + } else { + state.undoActive = stc->GetUndoCollection(); + if (state.undoActive) + stc->SetUndoCollection(false); + } + + auto utf8 = [str UTF8String]; + auto utf8len = strlen(utf8); + stc->AddTextRaw(utf8, utf8len); + + state.markedRange = NSMakeRange(pos, utf8len); + + stc->SetIndicatorCurrent(1); + stc->IndicatorFillRange(pos, utf8len); + + // Re-enable undo if we got a zero-length string as that means we're done + if (!utf8len && state.undoActive) + stc->SetUndoCollection(true); + else { + int start = pos; + // Range is in utf-16 code units + if (range.location > 0) + start += [[str substringToIndex:range.location] lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + int length = [[str substringWithRange:range] lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + stc->SetSelection(start, start + length); + } +} + +- (void)unmarkText { + auto state = self.state; + if (state.markedRange.length > 0) { + self.stc->DeleteRange(state.markedRange.location, state.markedRange.length); + state.markedRange = NSMakeRange(NSNotFound, 0); + if (state.undoActive) + self.stc->SetUndoCollection(true); + } +} + +- (NSArray *)validAttributesForMarkedText { + return @[]; +} +@end + +namespace osx { namespace ime { +void inject(wxStyledTextCtrl *ctrl) { + id view = (id)ctrl->GetHandle(); + object_setClass(view, [ScintillaNSView class]); + + auto state = [IMEState new]; + objc_setAssociatedObject(view, [IMEState class], state, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [state release]; +} + +void invalidate(wxStyledTextCtrl *ctrl) { + [(ScintillaNSView *)ctrl->GetHandle() invalidate]; +} + +bool process_key_event(wxStyledTextCtrl *ctrl, wxKeyEvent &evt) { + if (evt.GetModifiers() != 0) return false; + if (evt.GetKeyCode() != WXK_RETURN && evt.GetKeyCode() != WXK_TAB) return false; + if (![(ScintillaNSView *)ctrl->GetHandle() hasMarkedText]) return false; + + evt.Skip(); + return true; +} + +} } diff --git a/src/subs_edit_box.cpp b/src/subs_edit_box.cpp index 1e8b32849..491a8d6ee 100644 --- a/src/subs_edit_box.cpp +++ b/src/subs_edit_box.cpp @@ -53,6 +53,7 @@ #include "text_selection_controller.h" #include "timeedit_ctrl.h" #include "tooltip_manager.h" +#include "utils.h" #include "validators.h" #include @@ -429,7 +430,8 @@ void SubsEditBox::UpdateFrameTiming(agi::vfr::Framerate const& fps) { } void SubsEditBox::OnKeyDown(wxKeyEvent &event) { - hotkey::check("Subtitle Edit Box", c, event); + if (!osx::ime::process_key_event(edit_ctrl, event)) + hotkey::check("Subtitle Edit Box", c, event); } void SubsEditBox::OnChange(wxStyledTextEvent &event) { diff --git a/src/subs_edit_ctrl.cpp b/src/subs_edit_ctrl.cpp index dab71585b..263018022 100644 --- a/src/subs_edit_ctrl.cpp +++ b/src/subs_edit_ctrl.cpp @@ -98,6 +98,8 @@ SubsTextEditCtrl::SubsTextEditCtrl(wxWindow* parent, wxSize wsize, long style, a , thesaurus(agi::make_unique()) , context(context) { + osx::ime::inject(this); + // Set properties SetWrapMode(wxSTC_WRAP_WORD); SetMarginWidth(1,0); @@ -200,6 +202,7 @@ void SubsTextEditCtrl::OnLoseFocus(wxFocusEvent &event) { } void SubsTextEditCtrl::OnKeyDown(wxKeyEvent &event) { + if (osx::ime::process_key_event(this, event)) return; event.Skip(); // Workaround for wxSTC eating tabs. @@ -257,6 +260,10 @@ void SubsTextEditCtrl::SetStyles() { // Misspelling indicator IndicatorSetStyle(0,wxSTC_INDIC_SQUIGGLE); IndicatorSetForeground(0,wxColour(255,0,0)); + + // IME pending text indicator + IndicatorSetStyle(1, wxSTC_INDIC_PLAIN); + IndicatorSetUnder(1, true); } void SubsTextEditCtrl::UpdateStyle() { @@ -321,6 +328,7 @@ void SubsTextEditCtrl::UpdateCallTip() { } void SubsTextEditCtrl::SetTextTo(std::string const& text) { + osx::ime::invalidate(this); SetEvtHandlerEnabled(false); Freeze(); diff --git a/src/utils.cpp b/src/utils.cpp index a014b0a29..69c53bd1c 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -217,6 +217,13 @@ void SetFloatOnParent(wxWindow *) { } RetinaHelper::RetinaHelper(wxWindow *) { } RetinaHelper::~RetinaHelper() { } int RetinaHelper::GetScaleFactor() const { return 1; } + +// OS X implementation in scintilla_ime.mm +namespace osx { namespace ime { + void inject(wxStyledTextCtrl *) { } + void invalidate(wxStyledTextCtrl *) { } + bool process_key_event(wxStyledTextCtrl *, wxKeyEvent&) { return false; } +} } #endif wxString FontFace(std::string opt_prefix) { diff --git a/src/utils.h b/src/utils.h index 02ef78aac..269100de8 100644 --- a/src/utils.h +++ b/src/utils.h @@ -106,4 +106,13 @@ namespace osx { bool activate_top_window_other_than(wxFrame *wx); // Bring all windows to the front, maintaining relative z-order void bring_to_front(); + +namespace ime { + /// Inject the IME helper into the given wxSTC + void inject(wxStyledTextCtrl *ctrl); + /// Invalidate any pending text from the IME + void invalidate(wxStyledTextCtrl *ctrl); + /// Give the IME a chance to process a key event and return whether it did + bool process_key_event(wxStyledTextCtrl *ctrl, wxKeyEvent &); +} }