PageRenderTime 26ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/platform/util/ui/src/com/intellij/ui/mac/foundation/Foundation.java

http://github.com/JetBrains/intellij-community
Java | 594 lines | 457 code | 111 blank | 26 comment | 50 complexity | c03c5757fb75472a64fd2e571a74f045 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0, MPL-2.0-no-copyleft-exception, MIT, EPL-1.0, AGPL-1.0
  1. // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
  2. package com.intellij.ui.mac.foundation;
  3. import com.intellij.jna.JnaLoader;
  4. import com.intellij.openapi.util.text.StringUtil;
  5. import com.intellij.openapi.vfs.CharsetToolkit;
  6. import com.intellij.util.ImageLoader;
  7. import com.sun.jna.*;
  8. import com.sun.jna.ptr.PointerByReference;
  9. import org.jetbrains.annotations.NonNls;
  10. import org.jetbrains.annotations.NotNull;
  11. import org.jetbrains.annotations.Nullable;
  12. import java.awt.*;
  13. import java.io.File;
  14. import java.util.List;
  15. import java.util.*;
  16. /**
  17. * @author spleaner
  18. * see <a href="http://developer.apple.com/documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html">Documentation</a>
  19. */
  20. @NonNls
  21. public class Foundation {
  22. private static final FoundationLibrary myFoundationLibrary;
  23. static {
  24. assert JnaLoader.isLoaded() : "JNA library is not available";
  25. myFoundationLibrary = Native.load("Foundation", FoundationLibrary.class, Collections.singletonMap("jna.encoding", "UTF8"));
  26. }
  27. public static void init() { /* fake method to init foundation */ }
  28. private Foundation() { }
  29. /**
  30. * Get the ID of the NSClass with className
  31. */
  32. public static ID getObjcClass(String className) {
  33. return myFoundationLibrary.objc_getClass(className);
  34. }
  35. public static ID getProtocol(String name) {
  36. return myFoundationLibrary.objc_getProtocol(name);
  37. }
  38. public static Pointer createSelector(String s) {
  39. return myFoundationLibrary.sel_registerName(s);
  40. }
  41. public static ID invoke(final ID id, final Pointer selector, Object... args) {
  42. return myFoundationLibrary.objc_msgSend(id, selector, args);
  43. }
  44. public static ID invoke(final String cls, final String selector, Object... args) {
  45. return invoke(getObjcClass(cls), createSelector(selector), args);
  46. }
  47. public static ID safeInvoke(final String stringCls, final String stringSelector, Object... args) {
  48. ID cls = getObjcClass(stringCls);
  49. Pointer selector = createSelector(stringSelector);
  50. if (invoke(cls, "respondsToSelector:", selector).intValue() == 0) {
  51. throw new RuntimeException(String.format("Missing selector %s for %s", stringSelector, stringCls));
  52. }
  53. return invoke(cls, selector, args);
  54. }
  55. public static ID invoke(final ID id, final String selector, Object... args) {
  56. return invoke(id, createSelector(selector), args);
  57. }
  58. public static double invoke_fpret(ID receiver, Pointer selector, Object... args) { return myFoundationLibrary.objc_msgSend_fpret(receiver, selector, args); }
  59. public static double invoke_fpret(ID receiver, String selector, Object... args) { return myFoundationLibrary.objc_msgSend_fpret(receiver, createSelector(selector), args); }
  60. public static boolean isNil(ID id) {
  61. return id == null || ID.NIL.equals(id);
  62. }
  63. public static ID safeInvoke(final ID id, final String stringSelector, Object... args) {
  64. Pointer selector = createSelector(stringSelector);
  65. if (!id.equals(ID.NIL) && invoke(id, "respondsToSelector:", selector).intValue() == 0) {
  66. throw new RuntimeException(String.format("Missing selector %s for %s", stringSelector, toStringViaUTF8(invoke(id, "description"))));
  67. }
  68. return invoke(id, selector, args);
  69. }
  70. public static ID allocateObjcClassPair(ID superCls, String name) {
  71. return myFoundationLibrary.objc_allocateClassPair(superCls, name, 0);
  72. }
  73. public static void registerObjcClassPair(ID cls) {
  74. myFoundationLibrary.objc_registerClassPair(cls);
  75. }
  76. public static boolean isClassRespondsToSelector(ID cls, Pointer selectorName) {
  77. return myFoundationLibrary.class_respondsToSelector(cls, selectorName);
  78. }
  79. /**
  80. *
  81. * @param cls The class to which to add a method.
  82. * @param selectorName A selector that specifies the name of the method being added.
  83. * @param impl A function which is the implementation of the new method. The function must take at least two arguments-self and _cmd.
  84. * @param types An array of characters that describe the types of the arguments to the method.
  85. * See <a href="https://developer.apple.com/library/IOs/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100"></a>
  86. * @return true if the method was added successfully, otherwise false (for example, the class already contains a method implementation with that name).
  87. */
  88. public static boolean addMethod(ID cls, Pointer selectorName, Callback impl, String types) {
  89. return myFoundationLibrary.class_addMethod(cls, selectorName, impl, types);
  90. }
  91. public static boolean addProtocol(ID aClass, ID protocol) {
  92. return myFoundationLibrary.class_addProtocol(aClass, protocol);
  93. }
  94. public static boolean addMethodByID(ID cls, Pointer selectorName, ID impl, String types) {
  95. return myFoundationLibrary.class_addMethod(cls, selectorName, impl, types);
  96. }
  97. public static boolean isMetaClass(ID cls) {
  98. return myFoundationLibrary.class_isMetaClass(cls);
  99. }
  100. @Nullable
  101. public static String stringFromSelector(Pointer selector) {
  102. ID id = myFoundationLibrary.NSStringFromSelector(selector);
  103. if (id.intValue() > 0) {
  104. return toStringViaUTF8(id);
  105. }
  106. return null;
  107. }
  108. public static Pointer getClass(Pointer clazz) {
  109. return myFoundationLibrary.objc_getClass(clazz);
  110. }
  111. public static String fullUserName() {
  112. return toStringViaUTF8(myFoundationLibrary.NSFullUserName());
  113. }
  114. public static ID class_replaceMethod(ID cls, Pointer selector, Callback impl, String types) {
  115. return myFoundationLibrary.class_replaceMethod(cls, selector, impl, types);
  116. }
  117. public static ID getMetaClass(String className) {
  118. return myFoundationLibrary.objc_getMetaClass(className);
  119. }
  120. public static boolean isPackageAtPath(@NotNull final String path) {
  121. final ID workspace = invoke("NSWorkspace", "sharedWorkspace");
  122. final ID result = invoke(workspace, createSelector("isFilePackageAtPath:"), nsString(path));
  123. return result.intValue() == 1;
  124. }
  125. public static boolean isPackageAtPath(@NotNull final File file) {
  126. if (!file.isDirectory()) return false;
  127. return isPackageAtPath(file.getPath());
  128. }
  129. private static class NSString {
  130. private static final ID nsStringCls = getObjcClass("NSString");
  131. private static final Pointer stringSel = createSelector("string");
  132. private static final Pointer allocSel = createSelector("alloc");
  133. private static final Pointer autoreleaseSel = createSelector("autorelease");
  134. private static final Pointer initWithBytesLengthEncodingSel = createSelector("initWithBytes:length:encoding:");
  135. private static final long nsEncodingUTF16LE = convertCFEncodingToNS(FoundationLibrary.kCFStringEncodingUTF16LE);
  136. @NotNull
  137. public static ID create(@NotNull String s) {
  138. // Use a byte[] rather than letting jna do the String -> char* marshalling itself.
  139. // Turns out about 10% quicker for long strings.
  140. if (s.isEmpty()) {
  141. return invoke(nsStringCls, stringSel);
  142. }
  143. byte[] utf16Bytes = s.getBytes(CharsetToolkit.UTF_16LE_CHARSET);
  144. return invoke(invoke(invoke(nsStringCls, allocSel),
  145. initWithBytesLengthEncodingSel, utf16Bytes, utf16Bytes.length, nsEncodingUTF16LE),
  146. autoreleaseSel);
  147. }
  148. }
  149. @NotNull
  150. public static ID nsString(@Nullable String s) {
  151. return s == null ? ID.NIL : NSString.create(s);
  152. }
  153. public static ID nsUUID(@NotNull UUID uuid) {
  154. return nsUUID(uuid.toString());
  155. }
  156. public static ID nsUUID(@NotNull String uuid) {
  157. return invoke(invoke(invoke("NSUUID", "alloc"), "initWithUUIDString:", nsString(uuid)), "autorelease");
  158. }
  159. @Nullable
  160. public static String toStringViaUTF8(ID cfString) {
  161. if (cfString.intValue() == 0) return null;
  162. int lengthInChars = myFoundationLibrary.CFStringGetLength(cfString);
  163. int potentialLengthInBytes = 3 * lengthInChars + 1; // UTF8 fully escaped 16 bit chars, plus nul
  164. byte[] buffer = new byte[potentialLengthInBytes];
  165. byte ok = myFoundationLibrary.CFStringGetCString(cfString, buffer, buffer.length, FoundationLibrary.kCFStringEncodingUTF8);
  166. if (ok == 0) throw new RuntimeException("Could not convert string");
  167. return Native.toString(buffer);
  168. }
  169. @Nullable
  170. public static String getNSErrorText(@Nullable ID error) {
  171. if (error == null || error.byteValue() == 0) return null;
  172. String description = toStringViaUTF8(invoke(error, "localizedDescription"));
  173. String recovery = toStringViaUTF8(invoke(error, "localizedRecoverySuggestion"));
  174. if (recovery != null) description += "\n" + recovery;
  175. return StringUtil.notNullize(description);
  176. }
  177. @Nullable
  178. public static String getEncodingName(long nsStringEncoding) {
  179. long cfEncoding = myFoundationLibrary.CFStringConvertNSStringEncodingToEncoding(nsStringEncoding);
  180. ID pointer = myFoundationLibrary.CFStringConvertEncodingToIANACharSetName(cfEncoding);
  181. String name = toStringViaUTF8(pointer);
  182. if ("macintosh".equals(name)) name = "MacRoman"; // JDK8 does not recognize IANA's "macintosh" alias
  183. return name;
  184. }
  185. public static long getEncodingCode(@Nullable String encodingName) {
  186. if (StringUtil.isEmptyOrSpaces(encodingName)) return -1;
  187. ID converted = nsString(encodingName);
  188. long cfEncoding = myFoundationLibrary.CFStringConvertIANACharSetNameToEncoding(converted);
  189. ID restored = myFoundationLibrary.CFStringConvertEncodingToIANACharSetName(cfEncoding);
  190. if (ID.NIL.equals(restored)) return -1;
  191. return convertCFEncodingToNS(cfEncoding);
  192. }
  193. private static long convertCFEncodingToNS(long cfEncoding) {
  194. return myFoundationLibrary.CFStringConvertEncodingToNSStringEncoding(cfEncoding) & 0xffffffffffL; // trim to C-type limits
  195. }
  196. public static void cfRetain(ID id) {
  197. myFoundationLibrary.CFRetain(id);
  198. }
  199. public static ID cgWindowListCreateImage(Foundation.NSRect screenBounds, int windowOption, ID windowID, int imageOption) {
  200. return myFoundationLibrary.CGWindowListCreateImage(screenBounds, windowOption, windowID, imageOption);
  201. }
  202. public static void cfRelease(ID... ids) {
  203. for (ID id : ids) {
  204. if (id != null) {
  205. myFoundationLibrary.CFRelease(id);
  206. }
  207. }
  208. }
  209. public static ID autorelease(ID id){
  210. return Foundation.invoke(id, "autorelease");
  211. }
  212. public static boolean isMainThread() {
  213. return invoke("NSThread", "isMainThread").intValue() > 0;
  214. }
  215. private static Callback ourRunnableCallback;
  216. private static final Map<String, RunnableInfo> ourMainThreadRunnables = new HashMap<>();
  217. private static long ourCurrentRunnableCount = 0;
  218. private static final Object RUNNABLE_LOCK = new Object();
  219. static class RunnableInfo {
  220. RunnableInfo(Runnable runnable, boolean useAutoreleasePool) {
  221. myRunnable = runnable;
  222. myUseAutoreleasePool = useAutoreleasePool;
  223. }
  224. Runnable myRunnable;
  225. boolean myUseAutoreleasePool;
  226. }
  227. public static void executeOnMainThread(final boolean withAutoreleasePool, final boolean waitUntilDone, final Runnable runnable) {
  228. String runnableCountString;
  229. synchronized (RUNNABLE_LOCK) {
  230. initRunnableSupport();
  231. runnableCountString = String.valueOf(++ourCurrentRunnableCount);
  232. ourMainThreadRunnables.put(runnableCountString, new RunnableInfo(runnable, withAutoreleasePool));
  233. }
  234. // fixme: Use Grand Central Dispatch instead?
  235. final ID ideaRunnable = getObjcClass("IdeaRunnable");
  236. final ID runnableObject = invoke(invoke(ideaRunnable, "alloc"), "init");
  237. final ID keyObject = invoke(nsString(runnableCountString), "retain");
  238. invoke(runnableObject, "performSelectorOnMainThread:withObject:waitUntilDone:", createSelector("run:"),
  239. keyObject, Boolean.valueOf(waitUntilDone));
  240. invoke(runnableObject, "release");
  241. }
  242. /**
  243. * Registers idea runnable adapter class in ObjC runtime, if not registered yet.
  244. * <p>
  245. * Warning: NOT THREAD-SAFE! Must be called under lock. Danger of segmentation fault.
  246. */
  247. private static void initRunnableSupport() {
  248. if (ourRunnableCallback == null) {
  249. final ID runnableClass = allocateObjcClassPair(getObjcClass("NSObject"), "IdeaRunnable");
  250. registerObjcClassPair(runnableClass);
  251. final Callback callback = new Callback() {
  252. @SuppressWarnings("UnusedDeclaration")
  253. public void callback(ID self, String selector, ID keyObject) {
  254. final String key = toStringViaUTF8(keyObject);
  255. invoke(keyObject, "release");
  256. RunnableInfo info;
  257. synchronized (RUNNABLE_LOCK) {
  258. info = ourMainThreadRunnables.remove(key);
  259. }
  260. if (info == null) {
  261. return;
  262. }
  263. ID pool = null;
  264. try {
  265. if (info.myUseAutoreleasePool) {
  266. pool = invoke("NSAutoreleasePool", "new");
  267. }
  268. info.myRunnable.run();
  269. }
  270. finally {
  271. if (pool != null) {
  272. invoke(pool, "release");
  273. }
  274. }
  275. }
  276. };
  277. if (!addMethod(runnableClass, createSelector("run:"), callback, "v@:*")) {
  278. throw new RuntimeException("Unable to add method to objective-c runnableClass class!");
  279. }
  280. ourRunnableCallback = callback;
  281. }
  282. }
  283. public static class NSDictionary {
  284. private final ID myDelegate;
  285. public NSDictionary(ID delegate) {
  286. myDelegate = delegate;
  287. }
  288. public ID get(ID key) {
  289. return invoke(myDelegate, "objectForKey:", key);
  290. }
  291. public ID get(String key) {
  292. return get(nsString(key));
  293. }
  294. public int count() {
  295. return invoke(myDelegate, "count").intValue();
  296. }
  297. public NSArray keys() { return new NSArray(invoke(myDelegate, "allKeys")); }
  298. @NotNull
  299. public static Map<String, String> toStringMap(@Nullable ID delegate) {
  300. Map<String, String> result = new HashMap<>();
  301. if (isNil(delegate)) {
  302. return result;
  303. }
  304. NSDictionary dict = new NSDictionary(delegate);
  305. NSArray keys = dict.keys();
  306. for (int i = 0; i < keys.count(); i++) {
  307. String key = toStringViaUTF8(keys.at(i));
  308. String val = toStringViaUTF8(dict.get(key));
  309. result.put(key, val);
  310. }
  311. return result;
  312. }
  313. public static ID toStringDictionary(@NotNull Map<String, String> map) {
  314. ID dict = invoke("NSMutableDictionary", "dictionaryWithCapacity:", map.size());
  315. for (Map.Entry<String, String> entry : map.entrySet()) {
  316. invoke(dict, "setObject:forKey:", nsString(entry.getValue()), nsString(entry.getKey()));
  317. }
  318. return dict;
  319. }
  320. }
  321. public static class NSArray {
  322. private final ID myDelegate;
  323. public NSArray(ID delegate) {
  324. myDelegate = delegate;
  325. }
  326. public int count() {
  327. return invoke(myDelegate, "count").intValue();
  328. }
  329. public ID at(int index) {
  330. return invoke(myDelegate, "objectAtIndex:", index);
  331. }
  332. @NotNull
  333. public List<ID> getList() {
  334. List<ID> result = new ArrayList<>();
  335. for (int i = 0; i < count(); i++) {
  336. result.add(at(i));
  337. }
  338. return result;
  339. }
  340. }
  341. public static class NSData {
  342. private final ID myDelegate;
  343. // delegate should not be nil
  344. public NSData(@NotNull ID delegate) {
  345. myDelegate = delegate;
  346. }
  347. public int length() {
  348. return invoke(myDelegate, "length").intValue();
  349. }
  350. public byte @NotNull [] bytes() {
  351. Pointer data = new Pointer(invoke(myDelegate, "bytes").longValue());
  352. return data.getByteArray(0, length());
  353. }
  354. @NotNull
  355. public Image createImageFromBytes() {
  356. return ImageLoader.loadFromBytes(bytes());
  357. }
  358. }
  359. public static class NSAutoreleasePool {
  360. private final ID myDelegate;
  361. public NSAutoreleasePool() {
  362. myDelegate = invoke(invoke("NSAutoreleasePool", "alloc"), "init");
  363. }
  364. public void drain() {
  365. invoke(myDelegate, "drain");
  366. }
  367. }
  368. @Structure.FieldOrder({"origin", "size"})
  369. public static class NSRect extends Structure implements Structure.ByValue {
  370. public NSPoint origin;
  371. public NSSize size;
  372. public NSRect(double x, double y, double w, double h) {
  373. origin = new NSPoint(x, y);
  374. size = new NSSize(w, h);
  375. }
  376. }
  377. @Structure.FieldOrder({"x", "y"})
  378. public static class NSPoint extends Structure implements Structure.ByValue {
  379. public CGFloat x;
  380. public CGFloat y;
  381. @SuppressWarnings("UnusedDeclaration")
  382. public NSPoint() {
  383. this(0, 0);
  384. }
  385. public NSPoint(double x, double y) {
  386. this.x = new CGFloat(x);
  387. this.y = new CGFloat(y);
  388. }
  389. }
  390. @Structure.FieldOrder({"width", "height"})
  391. public static class NSSize extends Structure implements Structure.ByValue {
  392. public CGFloat width;
  393. public CGFloat height;
  394. @SuppressWarnings("UnusedDeclaration")
  395. public NSSize() {
  396. this(0, 0);
  397. }
  398. public NSSize(double width, double height) {
  399. this.width = new CGFloat(width);
  400. this.height = new CGFloat(height);
  401. }
  402. }
  403. public static class CGFloat implements NativeMapped {
  404. private final double value;
  405. @SuppressWarnings("UnusedDeclaration")
  406. public CGFloat() {
  407. this(0);
  408. }
  409. public CGFloat(double d) {
  410. value = d;
  411. }
  412. @Override
  413. public Object fromNative(Object o, FromNativeContext fromNativeContext) {
  414. switch (Native.LONG_SIZE) {
  415. case 4:
  416. return new CGFloat((Float)o);
  417. case 8:
  418. return new CGFloat((Double)o);
  419. }
  420. throw new IllegalStateException();
  421. }
  422. @Override
  423. public Object toNative() {
  424. switch (Native.LONG_SIZE) {
  425. case 4:
  426. return (float)value;
  427. case 8:
  428. return value;
  429. }
  430. throw new IllegalStateException();
  431. }
  432. @Override
  433. public Class<?> nativeType() {
  434. switch (Native.LONG_SIZE) {
  435. case 4:
  436. return Float.class;
  437. case 8:
  438. return Double.class;
  439. }
  440. throw new IllegalStateException();
  441. }
  442. }
  443. public static ID fillArray(final Object[] a) {
  444. final ID result = invoke("NSMutableArray", "array");
  445. for (Object s : a) {
  446. invoke(result, "addObject:", convertType(s));
  447. }
  448. return result;
  449. }
  450. public static ID createDict(final String @NotNull [] keys, final Object @NotNull [] values) {
  451. final ID nsKeys = invoke("NSArray", "arrayWithObjects:", convertTypes(keys));
  452. final ID nsData = invoke("NSArray", "arrayWithObjects:", convertTypes(values));
  453. return invoke("NSDictionary", "dictionaryWithObjects:forKeys:", nsData, nsKeys);
  454. }
  455. @NotNull
  456. public static PointerType createPointerReference() {
  457. PointerType reference = new PointerByReference(new Memory(Native.POINTER_SIZE));
  458. reference.getPointer().clear(Native.POINTER_SIZE);
  459. return reference;
  460. }
  461. @NotNull
  462. public static ID castPointerToNSError(@NotNull PointerType pointerType) {
  463. return new ID(pointerType.getPointer().getLong(0));
  464. }
  465. private static Object[] convertTypes(Object @NotNull [] v) {
  466. final Object[] result = new Object[v.length];
  467. for (int i = 0; i < v.length; i++) {
  468. result[i] = convertType(v[i]);
  469. }
  470. return result;
  471. }
  472. private static Object convertType(@NotNull Object o) {
  473. if (o instanceof Pointer || o instanceof ID) {
  474. return o;
  475. }
  476. else if (o instanceof String) {
  477. return nsString((String)o);
  478. }
  479. else {
  480. throw new IllegalArgumentException("Unsupported type! " + o.getClass());
  481. }
  482. }
  483. }