Open Bug 1108246 Opened 10 years ago Updated 2 years ago

Expose setApplicationIcon from nsMacDockSupport to JS

Categories

(Core :: Widget: Cocoa, enhancement, P5)

35 Branch
x86_64
macOS
enhancement

Tracking

()

People

(Reporter: noitidart, Unassigned)

Details

(Whiteboard: tpi:+)

Attachments

(1 file)

User Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Build ID: 20141201162954

Steps to reproduce:

I'm trying to change the icon of firefox. In windows and linux I was able to do it please see attached winxp-badging-progress_zps1f5d6661.png

The reason I need to use setApplicationIcon is because during downloads a progress bar is drawn on the icon: http://mxr.mozilla.org/mozilla-release/source/widget/cocoa/nsMacDockSupport.mm#140

So it was suggested to me to swizzle `-[NSApplication setApplicationIconImage:]`

> 12:01 mstange if you just overwrite the dock icon with your own one, your replacement is going to be overwritten as soon as nsMacDockSupport::RedrawIcon() is called again
> 12:03 mstange but I don't see how you can get access to mAppIcon or mProgressBackground
> 12:04 mstange I suppose you could swizzle -[NSApplication setApplicationIconImage:]

Would it be possible to get this exposed to javascript?

I have been trying to swizzle it with js-ctypes and have been struggling a lot.
Ideally I would have liked to get this done with js-ctypes so i can support from FF 29 and but I wasn't able to figure it out.

It might be smarter to do this with js-ctypes because there's a lack of js-ctypes work for mac, so this would add a lot to open source, when ppl search they can find this awesome stuff.

I was following here but keep failing: https://developer.mozilla.org/en-US/docs/Mozilla/js-ctypes/Standard_OS_Libraries#Cocoa
This was my horribly failed js-ctypes attempt. I couldn't even figure out how to set the NSImage data of a file path.

/******
//Objective-C code
myImage = [NSImage imageNamed: @"ChangedIcon"];
[NSApp setApplicationIconImage: myImage];
                                    
// pseudo C code
NSImage myImage = (NSImage)objc_msgSend(objc_getClass('NSImage'), sel_registerName('setApplicationIconImage'));
******/
Cu.import('resource://gre/modules/ctypes.jsm');
var objc = ctypes.open(ctypes.libraryName('objc'));

var id = ctypes.voidptr_t;
var SEL = ctypes.voidptr_t;
var objc_getClass = objc.declare('objc_getClass', ctypes.default_abi, id, ctypes.char.ptr);
var sel_registerName = objc.declare('sel_registerName', ctypes.default_abi, SEL, ctypes.char.ptr);
var objc_msgSend = objc.declare('objc_msgSend', ctypes.default_abi, id, id, SEL, '...'); //https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html#//apple_ref/doc/c_ref/objc_msgSend
var obj_msgSend_stret = objc.declare('obj_msgSend_stret', ctypes.default_abi, ctypes.voidptr_t, id, SEL, '...'); //https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html#//apple_ref/doc/c_ref/objc_msgSend_stret
var NSImage = ctypes.voidptr_t;

// note: [NSEvent mouseLocation] returns NSPoint struct, which is small enough to return in register, so we don't need to use objc_msgSend_stret.
// noida note: i doubt NSImage is as small as NSPoint so ill have to use obj_msgSend_stret
var objc_msgSend_NSImage = objc.declare('objc_msgSend', ctypes.default_abi, NSImage, id, SEL, '...');

// pool = [[NSAutoreleasePool alloc] init]
var NSAutoreleasePool = objc_getClass('NSAutoreleasePool');
var alloc = sel_registerName('alloc');
var init = sel_registerName('init');
var pool = objc_msgSend(objc_msgSend(NSAutoreleasePool, alloc), init);

// rez = [NSImage setApplicationIcon]
var NSEvent = objc_getClass('NSImage');
var setApplicationIcon = sel_registerName('setApplicationIcon');
var rez = objc_msgSend_NSPoint(NSImage, setApplicationIcon);

var release = sel_registerName('release');
objc_msgSend(pool, release);

console.log('rez:', rez);

objc.close();
Component: Untriaged → js-ctypes
Product: Firefox → Core
Wow bogus: setApplicaionIconImage was deprecated in 10.10:

However, `[NSApplication setApplicationIconImage:]` is removed from OS X v10.10 API:
  https://developer.apple.com/library/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/frameworks/AppKit.html

What on earth, now we'll have to even change the XPCOM code: http://mxr.mozilla.org/mozilla-release/source/widget/cocoa/nsMacDockSupport.mm#140
Oh actually setApplicationIconImage was not deprecated: http://stackoverflow.com/a/27555524/1828637


[quote]
The setApplicationIconImage method did not get deprecated or removed.
Apple only changed the header to SHOW it as a property instead.

setApplicationIconImage IS the setter for the property. properties are only syntactic sugar for two 'standard' methods: a getter (applicationIconImage) and a setter (setApplicationIconImage)

you might get an ARC warning nowadays because it can't see setApplicationIconImage so use the dot-notation on ALL platforms -> no need for a runtime check of 10.10/10.9-/quote]
js-ctypes of objc done by arai! it is so so so so awesome! he shared it here after i asked, he is so awesome he spoon fed me the code and i learned from it and am applying it to NSDistributionCenter:

arais code: http://www.unmht.org/forum/en/4774.html
uploaded to gist with comments/readme (can set image to icns or png): https://gist.github.com/Noitidart/6e39b7a0b2c3419a1165
he has some awesome blog articles on objc and js-ctypes: http://unmht.blogspot.jp/search/label/js-ctypes
Two small issues remain though with this js-ctypes:

1) the minimized icon is not set to the icon we set (see image here: http://i.imgur.com/hsRTSk6.png)
2) The RedrawIcon from the dock-progress drawing while downloading resets the icon to what it was before - it was recommended to me to swizzle setApplicationIconImage so the dock-progress is drawn over the icon we set it to

I haven't started digging into solving these two issues yet, but just posting to see if anyone had any tips/insight/spoon fed code to provide :P
The js-ctypes workaround doesn't change that properly exposing this would be a Widget::Cocoa bug.
Status: UNCONFIRMED → NEW
Component: js-ctypes → Widget: Cocoa
Ever confirmed: true
OS: Windows 8.1 → Mac OS X
Summary: expose setApplicationIcon from nsMacDockSupport to js (swizzle -[NSApplication setApplicationIconImage:]) → Expose setApplicationIcon from nsMacDockSupport to JS
Thanks Gijs. Do you think if I swizzled setApplicationIcon image in the js-ctypes that it would be right?
(In reply to noitidart from comment #10)
> Thanks Gijs. Do you think if I swizzled setApplicationIcon image in the
> js-ctypes that it would be right?

I don't know. Markus?
Flags: needinfo?(mstange)
Well, it will allow your addon to do what you want, but with a proper Gecko API your code could potentially look much nicer, so let's keep this bug open anyway.
Flags: needinfo?(mstange)
Thanks Gijs and Markus.

Do you know of a preference to disable firefox from doing the download progress drawing? Just in case i cant get the swizzle.
I don't know of one, but if there isn't any I'd be happy to review a patch that adds one.
Thanks very much for that offer Markus! But that's just very greedy of me. I just was wondering for at temp workaround. I will work out the swizzle thing so i was just looking for a temporary thing so i can release my addon earlier, i got a feeling swizzle in jsctypes will drive me (and ppl i pester :P) nuts

Ill just do a temp workaround of using Downloads.jsm to re apply the setApplicationIconImage after download :)
Hi Markus, would you have time to show me please in objc how you would swizzle setApplicationIconImage, I tried following a tutorial here but I can't figure it out:

http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/

It seems its only like 5 lines of codes thats why im asking because I cant figure out those 5 lines and it seems simple :P

Thanks
Flags: needinfo?(mstange)
My issue is, i got my jsctypes setImplementation stuff set up. I just can't figure out how to get into Mozillas objc and get a pointer to it so i can replace it.

heres my jsctypes, i just need to figure out how to replace the NSImage imageNamed used by Firefox here: http://mxr.mozilla.org/mozilla-release/source/widget/cocoa/nsMacDockSupport.mm#140

I'm trying to make my implementation do if the NSString passed to [NSImage imageNamed] is "NSApplicationIcon" then I want to make it take my image. Otherwise dont do anything different.
Oh heres my jsctypes: https://github.com/Noitidart/_scratchpad/blob/a0d3fd2d5477b9cf2ca3bc75fab9f7e9802fe72a/swizzle%20NSImage%20imageNamed%20objc%20ctypes.js

you can see its all set up, ready to call `method_setImplementation` im kind of close
Thanks to @arai we got the swizzle working, but its calling back and crashing after some time. I can't figure out why.


Can copy paste and run this from scratchpad and you can see the console.log message whenever [NSImage imageNamed] is used.

-----------------------------------

Cu.import('resource://gre/modules/osfile.jsm');
Cu.import('resource://gre/modules/ctypes.jsm');
var objc = ctypes.open(ctypes.libraryName('objc'));

/** START - edit these **/
var jsStr_imagePath = OS.Path.join(OS.Constants.Path.desktopDir, 'ff-logos', 'beta48.png');
/** END - edit these **/

// types
var CHAR = ctypes.char;
var CLASS = ctypes.voidptr_t;
var ID = ctypes.voidptr_t;
var SEL = ctypes.voidptr_t;
var BOOL = ctypes.signed_char;
var NSUINTEGER = ctypes.unsigned_long;
var SIZE_T = ctypes.size_t;
var VOID = ctypes.void_t;

// advanced types
var IMP = ctypes.FunctionType(ctypes.default_abi, ID, [ID, SEL, ID]).ptr; //repalced variadic with ID as its specific to my use otherwise doing class_addMethod throws error saying expected pointer blah blah //ctypes.FunctionType(ctypes.default_abi, ID, [ID, SEL, '...']).ptr;

var objc_method = ctypes.StructType('objc_method', [{
	'method_name': SEL
}, {
	'method_types': ctypes.char.ptr
}, {
	'method_imp': IMP
}, ]);
var METHOD = objc_method.ptr;

// constants
var NIL = ctypes.voidptr_t(0);

//common functions
var objc_getClass = objc.declare('objc_getClass', ctypes.default_abi, ID, CHAR.ptr);
var sel_registerName = objc.declare('sel_registerName', ctypes.default_abi, SEL, CHAR.ptr);
var objc_msgSend = objc.declare('objc_msgSend', ctypes.default_abi, ID, ID, SEL, '...');
var method_setImplementation = objc.declare('method_setImplementation', ctypes.default_abi, IMP, METHOD, IMP);
var class_getInstanceMethod = objc.declare('class_getInstanceMethod', ctypes.default_abi, METHOD, CLASS, SEL);
var method_getImplementation = objc.declare('method_getImplementation', ctypes.default_abi, IMP, METHOD);
var method_exchangeImplementations = objc.declare('method_exchangeImplementations', ctypes.default_abi, VOID, METHOD, METHOD);
var objc_disposeClassPair = objc.declare('objc_disposeClassPair', ctypes.default_abi, VOID, CLASS);
var objc_allocateClassPair = objc.declare('objc_allocateClassPair', ctypes.default_abi, CLASS, CLASS, CHAR.ptr, SIZE_T);
var class_addMethod = objc.declare('class_addMethod', ctypes.default_abi, BOOL, CLASS, SEL, IMP, CHAR.ptr);
var objc_registerClassPair = objc.declare('objc_registerClassPair', ctypes.default_abi, VOID, CLASS);
var class_getClassMethod = objc.declare('class_getClassMethod', ctypes.default_abi, METHOD, CLASS, SEL);
var objc_getMetaClass = objc.declare('objc_getMetaClass', ctypes.default_abi, ID, CHAR.ptr);

// COMMON SELECTORS
var alloc = sel_registerName('alloc');
var init = sel_registerName('init');
var release = sel_registerName('release');

// my globals
var myIcon;
var instance__class_NoitSwizzler;
var class_NoitSwizzler;

function shutdown() {
	//put code here to unswizzle it
	if (myIcon) {
		objc_msgSend(myIcon, release);
	}

	if (instance__class_NoitSwizzler) {
		objc_msgSend(instance__class_NoitSwizzler, release);
	}

	if (class_NoitSwizzler) {
		objc_disposeClassPair(class_NoitSwizzler);
	}
	objc.close();
};

var promise_makeMyNSImage = OS.File.read(jsStr_imagePath);

promise_makeMyNSImage.then(
	function(iconData) {
		// NOTE: iconData is Uint8Array
		var length = NSUINTEGER(iconData.length);
		var bytes = ctypes.uint8_t.array()(iconData);

		// data = [NSData dataWithBytes: bytes length: length];
		var NSData = objc_getClass('NSData');
		var dataWithBytes_length = sel_registerName('dataWithBytes:length:');
		var data = objc_msgSend(NSData, dataWithBytes_length, bytes, length);

		// myIcon = [[NSImage alloc] initWithData: data];
		var NSImage = objc_getClass('NSImage');
		var initWithData = sel_registerName('initWithData:');
		myIcon = objc_msgSend(objc_msgSend(NSImage, alloc), initWithData, data);

		if (myIcon.isNull()) {
			throw new Error('Image file is corrupted. Will not continue to swizzle.');
		}

		var imageNamed = sel_registerName('imageNamed:');
		var UTF8String = sel_registerName('UTF8String');
		var selector_swizzled_imageNamed = sel_registerName('swizzled_imageNamed:');

		var classs = sel_registerName('class');
		var js_swizzled_imageNamed = function(c_arg1__self, c_arg2__sel, objc_arg1__NSStringPtr) {
			console.log('SWIZZLED: imageNamed called');
			var icon = objc_msgSend(NSImage, selector_swizzled_imageNamed, objc_arg1__NSStringPtr); //cuz i did method_exchangeImplementations using selector of `selector_swizzled_imageNamed` will call the original
			return icon;
			/*
			console.log('SWIZZLED: imageNamed called');

			var tt_read = objc_msgSend(objc_arg1__NSStringPtr, UTF8String);
			console.info('tt_read:', tt_read, tt_read.toString(), uneval(tt_read), tt_read.isNull());
			var tt_read_casted = ctypes.cast(tt_read, CHAR.ptr);
			console.info('tt_read_casted:', tt_read_casted, tt_read_casted.toString(), uneval(tt_read_casted), tt_read_casted.isNull());
			var tt_read_jsStr = tt_read_casted.readStringReplaceMalformed();
			console.info('tt_read_jsStr:', tt_read_jsStr, tt_read_jsStr.toString(), uneval(tt_read_jsStr)); // TypeError: tt_read_jsStr.isNull is not a function 
			if (tt_read_jsStr == 'NSApplication') {
				// do my hook
				return myIcon;
			} else {
			
				// do normal
				var icon = objc_msgSend(NSImage, selector_swizzled_imageNamed, objc_arg1__NSStringPtr); //cuz i did method_exchangeImplementations using selector of `selector_swizzled_imageNamed` will call the original
				return icon;
			}
            */
		}

		//var IMP_specific = ctypes.FunctionType(ctypes.default_abi, ID, [ID, SEL, ID]).ptr; // return of ID is really NSIMAGE and third arg is NSSTRING
		var swizzled_imageNamed = IMP(js_swizzled_imageNamed); //if use IMP_specific and have variadic IMP defined above, it keeps throwing expecting pointer blah blah. and it wouldnt accept me putting in variadic on this line if do use varidic, on this line it throws `Can't delcare a variadic callback function`

		// add swizzled_imageNamed to NSImage
		var NSImageMeta = objc_getMetaClass('NSImage'); // have to add on meta class otherwise class_getClassMethod will fail to find it
		var rez_class_addMethod = class_addMethod(NSImageMeta, selector_swizzled_imageNamed, swizzled_imageNamed, '@@:@'); // because return of callback is NSImage so `ctypes.voidptr_t`, first argument is c_arg1__self which is `id` and c_arg2__id sel `SEL` and objc_arg1__NSStringPtr is `voidptr_t`
		console.info('rez_class_addMethod:', rez_class_addMethod, rez_class_addMethod.toString(), uneval(rez_class_addMethod));
		if (rez_class_addMethod != 1) {
			// when i ran this twice, so it was already there, the second time it came back as 0
			throw new Error('rez_class_addMethod is not 1, so class_addMethod failed');
		}

		var originalMethod = class_getClassMethod(NSImage, imageNamed); //verified i dont need to do this //may need to use `NSImageClass` instead of `NSImage`
		console.info('originalMethod:', originalMethod, originalMethod.toString(), uneval(originalMethod));
		var alternateMethod = class_getClassMethod(NSImage, selector_swizzled_imageNamed);
		console.info('alternateMethod:', alternateMethod, alternateMethod.toString(), uneval(alternateMethod));

		//results of class_getClassMethod on both:


		//results of class_getInstanceMethod on both:


		var rez = method_exchangeImplementations(originalMethod, alternateMethod);
		// rez is void
		console.log('SUCCESFULLY SWIZZLED');
	},
	function(aReason) {
		var rejObj = {
			nameOfRejector: 'promise_makeMyNSImage',
			aReason: aReason
		};
		console.warn(rejObj);
		throw rejObj;
	}
).catch(function(aCaught) {
	shutdown();
	console.error('promise_makeMyNSImage Catch:', aCaught);
});
Solved, @arai did a bunch of debugging and found that we had to move the `var swizzle_imageNamed` out to global does the trick!! wowwww ctypes is awesome!

im going to try to figure out how to swizzle with the setImplementation method just to have it out there, will let you know how it goes
Flags: needinfo?(mstange)
Ok so this comment here CAN close this, as it successfully works via JS.

Here is code cleaned up and copy paste examples for anyone who wants to see it in action.

To use, create a folder on desktop called "ff-logos" and inside it place a png called "beta48.png" OR edit the variable `jsStr_imgPath` after pasting to scratchpad, but before running the code.

Here is demo of how to swizzle using `method_exchangeImplementations`: https://gist.github.com/Noitidart/3571dbc1b75f28532bc2
Here is demo of how to swizzle using `method_setImplementation`: https://gist.github.com/Noitidart/e8105a5f702dc9e6a4b8

Can see readme below for screenshot, or if you want to see in action, download something and notice how the progress bar is drawn on the custom image supplied.

Really really cool stuff. Thanks Arai and Markus.
Severity: normal → enhancement
Priority: -- → P5
Whiteboard: tpi:+
Should we mark this as invalid as this is easily possible via js-ctypes? As was done in this addon - 
https://addons.mozilla.org/en-US/firefox/addon/os-x-dock-icon-changer/
Severity: normal → S3
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: