I'm trying to add an image to an NSPanel, by calling the .addSubvew_(image) method in Python, but I keep getting [NSImage window]: unrecognized selector sent to instance 0x7fbb246a9e10. I'm not sure, whether the problem is an initialization problem of the image, or whether im adding the Subview incorrectly. But it seems to be the case that the initialization works just fine, since it doesn't give any errors when running alone.
Here's the full code:
from Cocoa import NSObject, NSApplication, NSApp, NSWindow, NSPanel, NSColor, NSImage, NSSound
from PyObjCTools import AppHelper
def main():
NSApplication.sharedApplication()
# we must keep a reference to the delegate object ourselves,
# NSApp.setDelegate_() doesn't retain it. A local variable is
# enough here.
delegate = NSPanel.alloc().init()
NSApp().setDelegate_(delegate)
win = NSPanel.alloc()
frame = ((875.0, 140.0), (180.0, 200.0))
win.initWithContentRect_styleMask_backing_defer_(frame, 9395, 0, 5)
win.setLevel_(0) # floating window
image1 = NSImage.alloc()
image1.initWithContentsOfFile_('/Users/yassine/Teams.png')
win.contentView().addSubview_(image1)
win.display()
win.orderFrontRegardless() # but this one does
AppHelper.runEventLoop()
if __name__ == "__main__":
main()
NSView’s addSubview: method adds a view - you need to create a view for the image. Using NSImageView and replacing your image statements would be something like:
#add NSImageView to the import
image = NSImage.alloc().initWithContentsOfFile_('/Users/yassine/Teams.png')
view = NSImageView.alloc().initWithFrame_(((10, 10), (150.0, 150.0)))
view.setImage_(image)
win.contentView().addSubview_(view)
Related
I come from a C/C++ background and am currently learning a bit about Cocoa and Objective-C.
I have a weird behavior involving lazy initialization (unless I'm mistaken) and feel like I'm missing something very basic.
Setup:
Xcode 10.1 (10B61)
macOS High Sierra 10.13.6
started from a scratch Cocoa project
uses Storyboard
add files TestMainView.m/.h
under the View Controller in main.storyboard, set the NSView custom class as TestMainView
tested under debug and release builds
Basically, I create an NSTextView inside a view controller to be able to write some text.
In TestMainView.m, I create the chain of objects programmatically as decribed here
There are two paths:
first one is enabled by setting USE_FUNCTION_CALL to 0, it makes the entire code run inside awakeFromNib().
second path is enabled by setting USE_FUNCTION_CALL to 1. It makes the text container and text view to be allocated from the function call addNewPage() and returns the text container for further usage.
First code path works just as expected: I can write some text.
However second code path just doesn't work because upon return, textContainer.textView is nil (textContainer value itself is totally fine).
What's more troubling though (and this is where I suspect lazy init to be the culprit) is that if I "force" the textContainer.textView value while inside the function call, then everything works just fine. You can try this by setting FORCE_VALUE_LOAD to 1.
It doesn't have to be an if(), it works with NSLog() as well. It even works if you set a breakpoint at the return line and use the debugger to print the value ("p textContainer.textView")
So my questions are:
is this related to lazy initialization ?
is that a bug ? is there a workaround ?
am I thinking about Cocoa/ObjC programming the wrong way ?
I really hope I am missing something here because I cannot be expected to randomly check variables here and there inside Cocoa classes, hoping that they would not turn nil. It even fails silently (no error message, nothing).
TestMainView.m
#import "TestMainView.h"
#define USE_FUNCTION_CALL 1
#define FORCE_VALUE_LOAD 0
#implementation TestMainView
NSTextStorage* m_mainStorage;
- (void)awakeFromNib
{
[super awakeFromNib];
m_mainStorage = [NSTextStorage new];
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
#if USE_FUNCTION_CALL == 1
NSTextContainer* textContainer = [self addNewPage:self.bounds];
#else
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:self.bounds textContainer:textContainer];
#endif
[layoutManager addTextContainer:textContainer];
[m_mainStorage addLayoutManager:layoutManager];
// textContainer.textView is nil unless forced inside function call
[self addSubview:textContainer.textView];
}
#if USE_FUNCTION_CALL == 1
- (NSTextContainer*)addNewPage:(NSRect)containerFrame
{
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:NSMakeSize(FLT_MAX, FLT_MAX)];
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
[textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
#if FORCE_VALUE_LOAD == 1
// Lazy init ? textContainer.textView is nil unless we force it
if (textContainer.textView)
{
}
#endif
return textContainer;
}
#endif
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
}
#end
TestMainView.h
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
#interface TestMainView : NSView
#end
NS_ASSUME_NONNULL_END
I am not familiar with cocoa that much but I think the problem is ARC (Automatic reference counting).
NSTextView* textView = [[NSTextView alloc] initWithFrame:containerFrame textContainer:textContainer];
In the .h file of NSTextContainer you can see NSTextView is a weak reference type.
So after returning from the function it gets deallocated
But if you make the textView an instance variable of TestMainView it works as expected.
Not really sure why it also works if you force it though. ~~(Maybe compiler optimisation?)~~
It seems forcing i.e calling
if (textContainer.textView) {
is triggering retain/autorelease calls so until the next autorelease drain call, textview is still alive.(I am guessing it does not get drained until awakeFromNib function returns). The reason why it works is that you are adding the textView to the view hierarchy(a strong reference) before autorelease pool releases it.
cekisakurek's answer is correct. Objects are deallocated if there is no owning (/"strong") reference to them. Neither the text container nor the text view have owning references to each other. The container has a weak reference to the view, which means that it's set to nil automatically when the view dies. (The view has an non-nilling reference to the container, which means you will have a dangling pointer in textView.textContainer if the container is deallocated while the view is still alive.)
The text container is kept alive because it's returned from the method and assigned to a variable, which creates an owning reference as long as that variable is in scope. The view's only owning reference was inside the addNewPage: method, so it does not outlive that scope.
The "force load" has nothing to do with lazy initialization; as bbum commented, that it "works" is most likely to be accidental. I strongly suspect it wouldn't in an optimized build.
Let me assure you that you do not need to go around poking properties willy-nilly in Cocoa programming. But you do need to consider ownership relations between your objects. In this case, something else needs to own both container and view. That can be your class here, via an ivar/property, or another object that's appropriate given the NSText{Whatever} API (which is not familiar to me).
I'm creating a simple web browser and would like to implement tabs. For this purpose, I'm using an NSTabView. I basically want each tab to have a WebView that will show the website loaded. I'm starting with only one tab and want to add an exact copy of the first one when I create a new tab. I tried something like tabView.addTabViewItem(tabView.tabViewItemAtIndex(0).copy() as NSTableViewItem) but I'm getting an unrecognised selector sent to instance error. I've check the documentation for both NSTableView and NSTableViewItem but can't figure out how to that.
EDIT
My whole error looks like this:
015-03-14 17:15:57.884 Browser[1955:56547] -[NSTabViewItem copyWithZone:]: unrecognized selector sent to instance 0x600000100b40
2015-03-14 17:15:57.884 Browser[1955:56547] -[NSTabViewItem copyWithZone:]: unrecognized selector sent to instance 0x600000100b40
Thanks to the answer on Copy NSView in cocoa and #MattyAyOh, I solved my problem by doing:
var data = NSKeyedArchiver.archivedDataWithRootObject(view)
var newView = NSKeyedUnarchiver.unarchiveObjectWithData(data) as NSView
newTab.view = newView
tabView.addTabViewItem(newTab)
So you are getting that error because you are calling copy() on an object that doesn't have -copyWithZone implemented. (Because NSTabViewItem subclasses NSView which doesn't conform to the NSCopying protocol)
What you can do to get around that is to initialize a new NSTabViewItem, then get the properties from the first item (tabViewItemAtIndex(0)), and then set them to your new NSTabViewItem
from the documentation
you can call setLabel, setIdentifier, setColor, setView, and setTooltip
Once you've initialized and set your new NSTabViewItem, you can then add it to your NSTabView
NSTabViewItem *tempTabViewItem = [NSTabViewItem new];
//set your properties on tempTabViewItem here
tabView.addTabViewItem(tempTabViewItem);
I have defined a subclass of CALayer with an animatable property as discussed here. I would now like to add another (non-animatable) property to that layer to support its internal bookkeeping.
I set the value of the new property in drawInContext: but what I find that it is always reset to 0 when the next call is made. Is this so because Core Animation assumes that this property is also for animation, and that it "animates" its value at constant 0 lacking further instructions? In any case, how can I add truly non-animatable properties to subclasses of CALayer?
I have found a preliminary workaround, which is using a global CGFloat _property instead of #property (assign) CGFloat property but would prefer to use normal property syntax.
UPDATE 1
This is how I try to define the property in MyLayer.m:
#interface MyLayer()
#property (assign) CGFloat property;
#end
And this is how I assign a value to it at the end of drawInContext::
self.property = nonZero;
The property is e.g. read at the start of drawInContext: like so:
NSLog(#"property=%f", self.property);
UPDATE 2
Maybe this it was causes the problem (code inherited from this sample)?
- (id)actionForKey:(NSString *) aKey {
if ([aKey isEqualToString:#"someAnimatableProperty"]) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:aKey];
animation.fromValue = [self.presentationLayer valueForKey:aKey];
return animation;
}
return [super actionForKey:aKey]; // also applies to my "property"
}
To access your standard property from within the drawing method, during an animation, you need to make a few modifications.
Implement initializer
When CoreAnimation performs your animation, it creates shadow copies of your layer, and each copy will be rendered in a different frame. To create such copies, it calls -initWithLayer:.
From Apple's documentation:
If you are implementing a custom layer subclass, you can override this method and use it to copy the values of instance variables into the new object. Subclasses should always invoke the superclass implementation.
Therefore, you need to implement -initWithLayer: and use it to copy manually the value of your property on the new instance, like this:
- (id)initWithLayer:(id)layer
{
if ((self = [super initWithLayer:layer])) {
// Check if it's the right class before casting
if ([layer isKindOfClass:[MyCustomLayer class]]) {
// Copy the value of "myProperty" over from the other layer
self.myProperty = ((MyCustomLayer *)layer).myProperty;
}
}
return self;
}
Access properties through model layer
The copy, anyway, takes place before the animation starts: you can see this by adding a NSLog call to -initWithLayer:. So as far as CoreAnimation knows, your property will always be zero. Moreover, the copies it creates are readonly, if you try to set self.myProperty from within -drawInContext:, when the method is called on one of the presentation copies, you get an exception:
*** Terminating app due to uncaught exception 'CALayerReadOnly', reason:
'attempting to modify read-only layer <MyLayer: 0x8e94010>' ***
Instead of setting self.myProperty, you should write
self.modelLayer.myProperty = 42.0f
as modelLayer will instead refer to the original MyCustomLayer instance, and all the presentation copies share the same model. Note that you must do this also when you read the variable, not only when you set it. For completeness, one should mention as well the property presentationLayer, that instead returns the current (copy of the) layer being displayed.
// This is the line which retrieves the current image on the UIView
CGImageRef originalImage = imageView.image.CGImage;
I declare this in a method I use to edit my image which can be called by several buttons.
Is there anyway I can declare this variable globally so it always holds the first image loaded in the UIView? I can then use a copy variable to do all the editing on e.g.
CGImageRef copyImage = originalImage;
I have tried to make it global by declaring it in the header file and instantiate it in the method file however when I try and access the global variable in another method it is as if it hasn't been made global.
Any advice or better solutions would be greatly appreciated.
It is not entirely clear what you are trying to do, but what I would point out is:
the assignment
CGImageRef copyImage = originalImage;
will not create a copy of your image; it will simply give you a local variable pointing to the same image as originalImage; if you modify copyImage, you also modify originalImage;
if you want to make a copy of an CGImage so that you can modify it freely while keeping the original safe, use:
CGImageRef img1 = CGImageCreateCopy(img2);
if you want to save your original image as under point 2, simply add a property to your class and initialize it soon after you have got your CGImage in place (e.g., in viewDidLoad:
-(void) viewDidLoad {
...
self.originalImage = CGImageCreateCopy(imageView.image.CGImage);
...
}
self.originalImage will be your original image, while you will be able to freely modify imageView.image.CGImage.
Don't forget to release the copied image in your dealloc method.
Hope this helps.
My Setup
XCode 4.3.2
MacRuby 0.12 (ruby 1.9.2) [universal-darwin10.0, x86_64]
Latest nightly as of 4 Jun, 2012
OS 10.7.3
Goal
Have a window with some controls in a separate XIB from MainMenu.xib and be able to open that window programmatically. I do not want it to open at launch.
Attempts
I made a new xib (Woot.xib) and created a window in it
I made a new Ruby class
class WootController < NSWindowController
attr_accessor :window
def windowNibName
return 'Woot'
end
end
I tried to set the class of File's Owner in the Woot.xib to be WootController, but found that it will not if < NSWindowController is in my class definition. If I remove the < NSWindowController from the class definition, then the outlets populate and I can link the window in XIB to the window outlet in my class.
From inside my AppDelegate's applicationDidFinishLaunching method, I've tried
Attempt
newWind = WootController.new
puts newWind #outputs "#<AddCredentialsDialog:0x400191180>"
newWind.window().makeKeyAndOrderFront(self) # results in no method error for nil
Attempt 2
newWind = WootController.initWithWindowNibName 'AddWindow'
puts newWind #outputs "#<AddCredentialsDialog:0x400191180>"
newWind.window().makeKeyAndOrderFront(self) # results in no method error for nil
Questions
Why don't either of my attempts work? I've ready just about everything I can find on macruby and using NSWindowController.
Why can't I link my class WootController if I have it inheriting from NSWindowController
Is there a different way to do this other than putting it all in MainMenu.xib?
This solution works
nib = NSNib.alloc.initWithNibNamed('Woot', bundle: nil)
newWind = WootController.new
nib.instantiateNibWithOwner(newWind, topLevelObjects:nil)
newWind.showWindow(self)
Some things to note
In Macruby, if there are named parameters to a method signature, you must use them even if you just specify nil or the method signatures don't match up and you get a no method error.
ie. obj.foo('hello', to_whom: nil) is not the same as obj.foo('hello')
If there are named parameters you must use parentheses.
ie. this obj.foo('hello', to_whom: nil) will work, not this obj.foo 'hello', to_whom: nil