From fffb138b8175b4838b6a063863756b7c6b2db547 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 4 Jun 2014 13:16:34 -0700 Subject: [PATCH] Add IME support on OS X Closes #1247, #1672, #1695. --- src/Makefile | 2 +- src/{ => osx}/osx_utils.mm | 0 src/{ => osx}/retina_helper.mm | 0 src/osx/scintilla_ime.mm | 212 +++++++++++++++++++++++++++++++++ src/subs_edit_box.cpp | 4 +- src/subs_edit_ctrl.cpp | 8 ++ src/utils.cpp | 7 ++ src/utils.h | 11 ++ 8 files changed, 242 insertions(+), 2 deletions(-) rename src/{ => osx}/osx_utils.mm (100%) rename src/{ => osx}/retina_helper.mm (100%) create mode 100644 src/osx/scintilla_ime.mm diff --git a/src/Makefile b/src/Makefile index 90c09b12b..b125b81aa 100644 --- a/src/Makefile +++ b/src/Makefile @@ -16,7 +16,7 @@ LIBS += $(LIBS_FONTCONFIG) $(LIBS_FFTW3) $(LIBS_UCHARDET) $(LIBS_BOOST) LIBS += $(LIBS_ICU) ../vendor/luabins/libluabins.a ifeq (yes, $(BUILD_DARWIN)) -SRC += osx_utils.mm retina_helper.mm +SRC += osx/osx_utils.mm osx/retina_helper.mm osx/scintilla_ime.mm endif lpeg.o: CXXFLAGS += -Wno-unused-function diff --git a/src/osx_utils.mm b/src/osx/osx_utils.mm similarity index 100% rename from src/osx_utils.mm rename to src/osx/osx_utils.mm diff --git a/src/retina_helper.mm b/src/osx/retina_helper.mm similarity index 100% rename from src/retina_helper.mm rename to src/osx/retina_helper.mm 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 2680d9fba..877470034 100644 --- a/src/subs_edit_box.cpp +++ b/src/subs_edit_box.cpp @@ -51,6 +51,7 @@ #include "text_selection_controller.h" #include "timeedit_ctrl.h" #include "tooltip_manager.h" +#include "utils.h" #include "validators.h" #include @@ -421,7 +422,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 33f77cd85..7eb00380a 100644 --- a/src/subs_edit_ctrl.cpp +++ b/src/subs_edit_ctrl.cpp @@ -82,6 +82,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); @@ -183,6 +185,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. @@ -233,6 +236,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() { @@ -285,6 +292,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 056514d2b..44a16ea05 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 b6555a839..7e6a171ee 100644 --- a/src/utils.h +++ b/src/utils.h @@ -37,7 +37,9 @@ #include #include +class wxKeyEvent; class wxMouseEvent; +class wxStyledTextCtrl; class wxWindow; wxString PrettySize(int bytes); @@ -94,3 +96,12 @@ agi::fs::path OpenFileSelector(wxString const& message, std::string const& optio agi::fs::path SaveFileSelector(wxString const& message, std::string const& option_name, std::string const& default_filename, std::string const& default_extension, wxString const& wildcard, wxWindow *parent); wxString LocalizedLanguageName(wxString const& lang); + +namespace osx { 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 &); +} }