echoes 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/echoes/gui.rb +182 -5
- data/lib/echoes/objc.rb +8 -0
- data/lib/echoes/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ef610bfece0d2ba0f400475abf738bc9fa373ae3f64574652c6a090a9c63425
|
|
4
|
+
data.tar.gz: 24c08669c3f540953c06f60d2e39f91351e359866ebf4bded0012faddd637095
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ed9ff19f3c30e359a5595544dc849a3247d4c947a2b3422e7139bc28ce7eb05cc8ff21e45b9d10aa87dc3ba94472a295804e7fa31297a141a906c6b47a4ab54
|
|
7
|
+
data.tar.gz: a8a967dce6e4b25108a58660f213d5697a7a440d6a06c32ff542444e9d8b8547783919cdb98b656d2fa2a32380ab61946b6bc991ead382adea022644ad7835cc
|
data/lib/echoes/gui.rb
CHANGED
|
@@ -785,10 +785,45 @@ module Echoes
|
|
|
785
785
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP]
|
|
786
786
|
) { |_self, _cmd, _loc, _len, _actual| Fiddle::Pointer.new(0) }
|
|
787
787
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
788
|
+
# firstRectForCharacterRange:actualRange: returns NSRect (4
|
|
789
|
+
# doubles). Fiddle::Closure has no way to return a struct, so
|
|
790
|
+
# we can't implement it directly — a TYPE_DOUBLE closure only
|
|
791
|
+
# writes one of the four NSRect fields to v0 and leaves the
|
|
792
|
+
# rest as register garbage, which makes the IME candidate
|
|
793
|
+
# window appear off-screen or get suppressed entirely.
|
|
794
|
+
#
|
|
795
|
+
# Forward it through methodSignatureForSelector: + forward-
|
|
796
|
+
# Invocation: instead — NSInvocation.setReturnValue: handles
|
|
797
|
+
# the struct ABI for us. We just write 4 doubles into a buffer
|
|
798
|
+
# and hand the pointer to AppKit. The wiring lives in
|
|
799
|
+
# @method_sig_closure / @forward_inv_closure below.
|
|
800
|
+
@method_sig_closure = Fiddle::Closure::BlockCaller.new(
|
|
801
|
+
Fiddle::TYPE_VOIDP,
|
|
802
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
|
|
803
|
+
) do |_self, _cmd, sel|
|
|
804
|
+
gui.method_signature_for_selector(_self, sel)
|
|
805
|
+
rescue => e
|
|
806
|
+
gui.log_crash(e, context: 'methodSignatureForSelector')
|
|
807
|
+
Fiddle::Pointer.new(0)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
@forward_inv_closure = Fiddle::Closure::BlockCaller.new(
|
|
811
|
+
Fiddle::TYPE_VOID,
|
|
812
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
|
|
813
|
+
) do |_self, _cmd, inv|
|
|
814
|
+
gui.forward_invocation(_self, inv)
|
|
815
|
+
rescue => e
|
|
816
|
+
gui.log_crash(e, context: 'forwardInvocation')
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# respondsToSelector: needs to claim YES for the forwarded
|
|
820
|
+
# selector — AppKit's IME code checks before calling, and
|
|
821
|
+
# the runtime's default class_respondsToSelector returns NO
|
|
822
|
+
# for selectors we don't implement directly (only forward).
|
|
823
|
+
@responds_to_sel_closure = Fiddle::Closure::BlockCaller.new(
|
|
824
|
+
Fiddle::TYPE_CHAR,
|
|
825
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
|
|
826
|
+
) { |_self, _cmd, sel| gui.responds_to_selector?(_self, sel) }
|
|
792
827
|
|
|
793
828
|
@char_index_closure = Fiddle::Closure::BlockCaller.new(
|
|
794
829
|
Fiddle::TYPE_LONG,
|
|
@@ -1067,7 +1102,14 @@ module Echoes
|
|
|
1067
1102
|
'selectedRange' => ['{_NSRange=QQ}@:', @selected_range_closure],
|
|
1068
1103
|
'validAttributesForMarkedText' => ['@@:', @valid_attrs_closure],
|
|
1069
1104
|
'attributedSubstringForProposedRange:actualRange:' => ['@@:{_NSRange=QQ}^{_NSRange=QQ}', @attr_substring_closure],
|
|
1070
|
-
|
|
1105
|
+
# firstRectForCharacterRange:actualRange: is intentionally
|
|
1106
|
+
# not in the methods dict — it returns NSRect (4 doubles)
|
|
1107
|
+
# which Fiddle::Closure can't model. The runtime falls
|
|
1108
|
+
# through to the methodSignatureForSelector / forwardInvocation
|
|
1109
|
+
# pair below, which writes the rect via NSInvocation.
|
|
1110
|
+
'methodSignatureForSelector:' => ['@@::', @method_sig_closure],
|
|
1111
|
+
'forwardInvocation:' => ['v@:@', @forward_inv_closure],
|
|
1112
|
+
'respondsToSelector:' => ['c@::', @responds_to_sel_closure],
|
|
1071
1113
|
'characterIndexForPoint:' => ['Q@:{CGPoint=dd}', @char_index_closure],
|
|
1072
1114
|
'draggingEntered:' => ['Q@:@', @dragging_entered_closure],
|
|
1073
1115
|
'draggingUpdated:' => ['Q@:@', @dragging_updated_closure],
|
|
@@ -1806,6 +1848,141 @@ module Echoes
|
|
|
1806
1848
|
@marked_text ? 0 : 0x7FFFFFFFFFFFFFFF # NSNotFound
|
|
1807
1849
|
end
|
|
1808
1850
|
|
|
1851
|
+
# --- IME candidate-window positioning via message forwarding ---
|
|
1852
|
+
#
|
|
1853
|
+
# firstRectForCharacterRange:actualRange: returns NSRect (4 doubles).
|
|
1854
|
+
# Fiddle::Closure can't model a struct return — only single-register
|
|
1855
|
+
# scalar returns. Without a real implementation, AppKit reads the
|
|
1856
|
+
# rect from v0..v3 with only v0 set to our scalar return value, so
|
|
1857
|
+
# the IME sees a garbage rect and either positions the candidate
|
|
1858
|
+
# window off-screen or suppresses it entirely.
|
|
1859
|
+
#
|
|
1860
|
+
# The workaround: don't implement the selector directly; let it
|
|
1861
|
+
# fall through to forwardInvocation:. Inside forward_invocation we
|
|
1862
|
+
# pack four doubles into an NSRect-sized buffer and hand it to
|
|
1863
|
+
# NSInvocation.setReturnValue:, which handles the struct ABI for
|
|
1864
|
+
# us correctly. methodSignatureForSelector: is the gateway that
|
|
1865
|
+
# tells the runtime to use the forwarding path.
|
|
1866
|
+
IME_FORWARDED_SELECTORS = ['firstRectForCharacterRange:actualRange:'].freeze
|
|
1867
|
+
FIRST_RECT_SIG = '{CGRect={CGPoint=dd}{CGSize=dd}}@:{_NSRange=QQ}^{_NSRange=QQ}'.freeze
|
|
1868
|
+
|
|
1869
|
+
def method_signature_for_selector(self_ptr, sel)
|
|
1870
|
+
name = sel_name_string(sel)
|
|
1871
|
+
if name == 'firstRectForCharacterRange:actualRange:'
|
|
1872
|
+
return ObjC::MSG_PTR_1.call(
|
|
1873
|
+
ObjC.cls('NSMethodSignature'),
|
|
1874
|
+
ObjC.sel('signatureWithObjCTypes:'),
|
|
1875
|
+
Fiddle::Pointer[FIRST_RECT_SIG.b])
|
|
1876
|
+
end
|
|
1877
|
+
# Default: defer to whatever the runtime would have computed
|
|
1878
|
+
# from the class's method table. class_getInstanceMethod walks
|
|
1879
|
+
# the hierarchy so directly-implemented + inherited methods
|
|
1880
|
+
# all resolve.
|
|
1881
|
+
self_class = ObjC::MSG_PTR.call(self_ptr, ObjC.sel('class'))
|
|
1882
|
+
method = ObjC::ClassGetInstanceMethod.call(self_class, sel)
|
|
1883
|
+
return Fiddle::Pointer.new(0) if method.null?
|
|
1884
|
+
enc = ObjC::MethodGetTypeEncoding.call(method)
|
|
1885
|
+
ObjC::MSG_PTR_1.call(
|
|
1886
|
+
ObjC.cls('NSMethodSignature'),
|
|
1887
|
+
ObjC.sel('signatureWithObjCTypes:'),
|
|
1888
|
+
enc)
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
def forward_invocation(_self_ptr, inv)
|
|
1892
|
+
sel = ObjC::MSG_PTR.call(inv, ObjC.sel('selector'))
|
|
1893
|
+
name = sel_name_string(sel)
|
|
1894
|
+
return unless name == 'firstRectForCharacterRange:actualRange:'
|
|
1895
|
+
|
|
1896
|
+
rect = first_rect_for_marked_text_screen_coords
|
|
1897
|
+
buf = Fiddle::Pointer.malloc(32, Fiddle::RUBY_FREE)
|
|
1898
|
+
buf[0, 32] = rect.pack('d4')
|
|
1899
|
+
ObjC::MSG_VOID_1.call(inv, ObjC.sel('setReturnValue:'), buf)
|
|
1900
|
+
end
|
|
1901
|
+
|
|
1902
|
+
def responds_to_selector?(self_ptr, sel)
|
|
1903
|
+
name = sel_name_string(sel)
|
|
1904
|
+
return 1 if IME_FORWARDED_SELECTORS.include?(name)
|
|
1905
|
+
self_class = ObjC::MSG_PTR.call(self_ptr, ObjC.sel('class'))
|
|
1906
|
+
method = ObjC::ClassGetInstanceMethod.call(self_class, sel)
|
|
1907
|
+
method.null? ? 0 : 1
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
def sel_name_string(sel)
|
|
1911
|
+
ptr = ObjC::SelGetName.call(sel)
|
|
1912
|
+
ptr.null? ? '' : ptr.to_s
|
|
1913
|
+
end
|
|
1914
|
+
|
|
1915
|
+
# Compute the on-screen rect for the marked-text caret, used by
|
|
1916
|
+
# the IME to anchor its candidate window. Returns 4 doubles
|
|
1917
|
+
# [origin_x, origin_y, width, height] in screen coords. Origin
|
|
1918
|
+
# is bottom-left (NSScreen convention, y from bottom).
|
|
1919
|
+
#
|
|
1920
|
+
# We anchor on the active pane's cursor (the marked text is
|
|
1921
|
+
# drawn there, see draw_pane_content). When @marked_text is set,
|
|
1922
|
+
# use its measured width so the candidate window aligns with the
|
|
1923
|
+
# right edge of the composition; otherwise fall back to a single
|
|
1924
|
+
# cell so the IME still has something sensible to anchor on.
|
|
1925
|
+
def first_rect_for_marked_text_screen_coords
|
|
1926
|
+
return [0.0, 0.0, 0.0, 0.0] unless @view && @window && @tabs && @tabs.any?
|
|
1927
|
+
tab = current_tab
|
|
1928
|
+
pane = tab&.active_pane
|
|
1929
|
+
return [0.0, 0.0, 0.0, 0.0] unless pane
|
|
1930
|
+
|
|
1931
|
+
rect_info = tab.pane_tree.layout(0, 0, @cols, @rows).find { |r| r[:pane] == pane }
|
|
1932
|
+
return [0.0, 0.0, 0.0, 0.0] unless rect_info
|
|
1933
|
+
|
|
1934
|
+
cell_w = @cell_width.to_f
|
|
1935
|
+
cell_h = @cell_height.to_f
|
|
1936
|
+
gy_off = grid_y_offset
|
|
1937
|
+
pane_px = rect_info[:x] * cell_w
|
|
1938
|
+
pane_py = gy_off + rect_info[:y] * cell_h
|
|
1939
|
+
cursor = pane.screen.cursor
|
|
1940
|
+
mx_view = pane_px + cursor.col * cell_w
|
|
1941
|
+
my_view = pane_py + cursor.row * cell_h
|
|
1942
|
+
|
|
1943
|
+
width = if @marked_text && !@marked_text.empty?
|
|
1944
|
+
@marked_text.each_char.sum { |c| c.ord > 0x7F ? cell_w * 2 : cell_w }
|
|
1945
|
+
else
|
|
1946
|
+
cell_w
|
|
1947
|
+
end
|
|
1948
|
+
|
|
1949
|
+
# View y is flipped (top-origin); NSWindow / NSScreen y is
|
|
1950
|
+
# bottom-origin. The rect's BOTTOM in window coords sits at
|
|
1951
|
+
# (view_frame_height − rect_bottom_in_view_coords).
|
|
1952
|
+
vfh = view_frame_height
|
|
1953
|
+
win_x = mx_view
|
|
1954
|
+
win_y = vfh - (my_view + cell_h)
|
|
1955
|
+
|
|
1956
|
+
sx, sy = window_to_screen_point(win_x, win_y)
|
|
1957
|
+
[sx, sy, width, cell_h]
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1960
|
+
# [NSWindow convertPointToScreen:] returns NSPoint (2 doubles).
|
|
1961
|
+
# Same trick as event_location / dragging_location uses for the
|
|
1962
|
+
# other NSPoint-returning calls — Fiddle can't model the return
|
|
1963
|
+
# directly, so go through NSInvocation.
|
|
1964
|
+
def window_to_screen_point(x, y)
|
|
1965
|
+
sig = ObjC::MSG_PTR_1.call(
|
|
1966
|
+
ObjC.cls('NSWindow'),
|
|
1967
|
+
ObjC.sel('instanceMethodSignatureForSelector:'),
|
|
1968
|
+
ObjC.sel('convertPointToScreen:'))
|
|
1969
|
+
inv = ObjC::MSG_PTR_1.call(
|
|
1970
|
+
ObjC.cls('NSInvocation'),
|
|
1971
|
+
ObjC.sel('invocationWithMethodSignature:'), sig)
|
|
1972
|
+
ObjC::MSG_VOID_1.call(inv, ObjC.sel('setSelector:'),
|
|
1973
|
+
ObjC.sel('convertPointToScreen:'))
|
|
1974
|
+
arg = Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
|
|
1975
|
+
arg[0, 16] = [x, y].pack('dd')
|
|
1976
|
+
# setArgument:atIndex: signature is (id, SEL, void*, NSInteger).
|
|
1977
|
+
# No existing reusable msgSend variant matches; compose inline.
|
|
1978
|
+
set_arg_sig = ObjC.new_msg([ObjC::P, ObjC::P, ObjC::P, ObjC::L], ObjC::V)
|
|
1979
|
+
set_arg_sig.call(inv, ObjC.sel('setArgument:atIndex:'), arg, 2)
|
|
1980
|
+
ObjC::MSG_VOID_1.call(inv, ObjC.sel('invokeWithTarget:'), @window)
|
|
1981
|
+
out = Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
|
|
1982
|
+
ObjC::MSG_VOID_1.call(inv, ObjC.sel('getReturnValue:'), out)
|
|
1983
|
+
out[0, 16].unpack('dd')
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1809
1986
|
def timer_fired
|
|
1810
1987
|
save_window_state
|
|
1811
1988
|
|
data/lib/echoes/objc.rb
CHANGED
|
@@ -24,6 +24,14 @@ module Echoes
|
|
|
24
24
|
GetMethodImpl = Fiddle::Function.new(LIBOBJC['class_getMethodImplementation'], [P, P], P)
|
|
25
25
|
AddProtocol = Fiddle::Function.new(LIBOBJC['class_addProtocol'], [P, P], I)
|
|
26
26
|
GetProtocol = Fiddle::Function.new(LIBOBJC['objc_getProtocol'], [P], P)
|
|
27
|
+
# SEL → const char *name. Used by the message-forwarding hooks
|
|
28
|
+
# so a single closure can dispatch on selector name.
|
|
29
|
+
SelGetName = Fiddle::Function.new(LIBOBJC['sel_getName'], [P], P)
|
|
30
|
+
# Look up a Method by (Class, SEL) and read its type encoding —
|
|
31
|
+
# lets methodSignatureForSelector: fall back to the runtime's
|
|
32
|
+
# default behavior for any selector we don't explicitly forward.
|
|
33
|
+
ClassGetInstanceMethod = Fiddle::Function.new(LIBOBJC['class_getInstanceMethod'], [P, P], P)
|
|
34
|
+
MethodGetTypeEncoding = Fiddle::Function.new(LIBOBJC['method_getTypeEncoding'], [P], P)
|
|
27
35
|
|
|
28
36
|
# objc_msgSend variants for different signatures
|
|
29
37
|
def self.new_msg(args, ret)
|
data/lib/echoes/version.rb
CHANGED