I have an NSTextField bound to some object "zoom" property.
In this object's class implementation, I have the following
- (void)setZoom:(CGFloat)zoom
{
_zoom = MAX(0, MIN(10, zoom));
}
If I write "-5" in the textfield, setZoom: will be called with "-5" as argument and _zoom will be set to 0.
Then problem is that the textfield is not updating itself and it shows "-5" instead of re-reading the property value it has just set.
If I do myObject.zoom = -5; in code, the text field will display 0 correctly.
I tried to wrap _zoom =... inside willChangeValueForKey/didChangeValueForKey calls but it didn't change anything.
You can try to do in such way:
- (void)setZoom:(CGFloat)zoom
{
CGFloat corectedValue = MAX(0, MIN(10, zoom));
if (zoom != corectedValue)
{
[self setZoom:correctedValue];
} else {
_zoom = zoom;
}
}
Related
I'm trying to programmatically add a MouseEntered event to a custom NSButton class, and I can't seem to get it to fire. I'm writing a Mac OS application in Visual Studio for Mac, using Xamarin.Mac. I need to add the event in code because I'm creating the buttons dynamically.
Here's my ViewController where these buttons are being created. They're instantiated in the DrawHexMap method near the bottom.
public partial class MapController : NSViewController
{
public MapController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
DrawHexMap();
}
partial void GoToHex(Foundation.NSObject sender)
{
string[] coordinateStrings = ((NSButton)sender).Title.Split(',');
Hex chosenHex = HexRepo.GetHex(coordinateStrings[0], coordinateStrings[1]);
HexService.currentHex = chosenHex;
NSTabViewController tp = (NSTabViewController)ParentViewController;
tp.TabView.SelectAt(1);
}
private void DrawHexMap()
{
double height = 60;
for (int x = 0; x < 17; x++) {
for (int y = 0; y < 13; y++) {
HexButton button = new HexButton(x, y, height);
var handle = ObjCRuntime.Selector.GetHandle("GoToHex:");
button.Action = ObjCRuntime.Selector.FromHandle(handle);
button.Target = this;
View.AddSubview(button);
}
}
}
}
And here's the custom button class.
public class HexButton : NSButton
{
public NSTrackingArea _trackingArea;
public HexButton(int x, int y, double height)
{
double width = height/Math.Sqrt(3);
double doubleWidth = width * 2;
double halfHeight = height/2;
double columnNudge = decimal.Remainder(x, 2) == 0 ? 0 : halfHeight;
Title = x + "," + y;
//Bordered = false;
//ShowsBorderOnlyWhileMouseInside = true;
SetFrameSize(new CGSize(width, height));
SetFrameOrigin(new CGPoint(width + (x * doubleWidth), (y * height) + columnNudge));
_trackingArea = new NSTrackingArea(Frame, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
AddTrackingArea(_trackingArea);
}
public override void MouseEntered(NSEvent theEvent)
{
base.MouseEntered(theEvent);
Console.WriteLine("mouse enter");
}
}
So as you can see, I'm creating a tracking area for the button and adding it in the constructor. Yet I can't seem to get a MouseEntered to fire. I know the MouseEntered override in this class works, because when I call button.MouseEntered() directly from my code, the method fires.
A few other things I've tried include: Commenting out the lines that set the Action and Target in the ViewController, in case those were overriding the MouseEntered handler somehow. Setting those values inside the HexButton constructor so that the Target was the button instead of the ViewController. Putting the MouseEntered override in the ViewController instead of the button class. Creating the tracking area after the button was added as a subview to the ViewController. None of these made a difference.
Any help would be much appreciated! It's quite difficult to find documentation for Xamarin.Mac...
Thanks!
You are adding the Frame as the region tracked, but you are attaching the tracking area to the view that you are creating the region from, thus you need to track the Bounds coords.
_trackingArea = new NSTrackingArea(Bounds, NSTrackingAreaOptions.ActiveInKeyWindow | NSTrackingAreaOptions.MouseEnteredAndExited, this, null);
Note: If you were tracking from the view that you are adding the buttons to, then you would need to track the "Frame" as it is the tracking region is relative to the view being tracked, not its children.
I want to make horizontal collection and add new cell at the first position.
I make custom UICollectionViewFlowLayout and override
LayoutAttributesForElementsInRect like this (to see reverse order of items):
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
var attributes = new UICollectionViewLayoutAttributes[cellCount];
NSIndexPath indexPath;
// Setting first position
int shift = 25, reverseXCenter;
for (int i = 0; i < cellCount; i++)
{
indexPath = NSIndexPath.FromItemSection(i, 0);
attributes[i] = LayoutAttributesForItem(indexPath);
// We have to make opposite order
reverseXCenter = (cellCount -1 - i) * 50;
attributes[i].Center = new CGPoint(reverseXCenter + shift, attributes[i].Center.Y);
}
return attributes;
}
I think is bad solution, because when I have more than 5 cells, the displayed collection is filled and I can't scroll.
I had debugger in GetOrCreateCellFor in CollectionViewSource
and indexPath.Item's had value for example (10,9,8,7,6...), where 10 is the last I added.
Maybe is another way to do this, or just I miss something?
Ok I am gonna help you out with a flow .
Suppose you are in a ViewController called "A" which will present a controller ViewController called "B"
Inside A use this function to push B
Public void PushNewHorizontalCollectionController(){
using (var vc =
new B(this.PageViewControllerLayout(), indexPath))
{
this.controller.PushViewController(pageViewController, true);
}
}
public UICollectionViewFlowLayout PageViewControllerLayout()
{
var flowLayout = new UICollectionViewFlowLayout();
var itemSize = this.controller.NavigationBarHidden ? new CGSize(Constants.ScreenWidth, Constants.ScreenHeight + 20) : new CGSize(Constants.ScreenWidth, Constants.ScreenHeight - Constants.NavigationHeaderAndStatusbarHeight);
flowLayout.ItemSize = itemSize;
flowLayout.MinimumLineSpacing = 0;
flowLayout.MinimumInteritemSpacing = 0;
flowLayout.ScrollDirection = UICollectionViewScrollDirection.Horizontal;
return flowLayout;
}
Here itemSize is CGSize which u can assign what size you can ignore my constants which are self explainable .
Inside B your constructor should look like this .
public HorizontalViewController(UICollectionViewLayout layout, NSIndexPath indexpath) : base(layout)
{
Do your initialisation like registering UICollectionViewCell class nibs for use with this Controller
}
Handling real requirement from comment .
You wanna add something always as 1st element in collectionView ,Go to this method which should look like below
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
Console.WriteLine("getcellHori");
var isQuoteType = Photo.PhotoCollection[(int)indexPath.Item].Type;
if (isQuoteType == 3 || isQuoteType == 4)
{
var item = Photo.PhotoCollection[(int)indexPath.Item];
var cell = (HorizontalviewForQuoteType)collectionView.DequeueReusableCell("HorizontalviewForQuoteType", indexPath);
cell.PopulateCell(item.Caption,item.TextLabel,item.Likes,item.TimeLabel,item.Id);
HorizontalviewForQuoteType.pullAction = (offset) =>
{
this.pullOffset = offset;
this.NavigationController.PopViewController(true);
};
cell.SetNeedsLayout();
return cell;
}
Here you this line
var item = Photo.PhotoCollection[(int)indexPath.Item];
this is the data source , so whenever you adding something at index first and everything will be solved .
I have a custom UISplitViewController subclass in iOS 8. When in landscape I want the default behavior of both the primary and secondary VCs being visible (preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible), but when I rotate to portrait I would like the primary VC to be displayed in the default popover (preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay).
By implementing -viewWillTransitionToSize: on the subclass I can kind of get this working, but after rotating a couple times the primary VC disappears and will not reappear until I tap the split view's bar button item.
The log in the rotation animation completion block on the first couple rotations shows the preferred display mode being the same as the actual display mode, but after a couple rotations the actual display mode is stuck as UISplitViewControllerDisplayModePrimaryOverlay in landscape, even when the preferred display mode is UISplitViewControllerDisplayModeAllVisible.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
if (self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
BOOL isPortrait = size.height > size.width;
if (isPortrait) {
self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
}
else {
self.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible;
}
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.dividerView.hidden = isPortrait;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
NSLog(#"Preferred display mode: %ld | Actual display mode: %ld", self.preferredDisplayMode, self.displayMode);
}];
}
}
Thanks to https://devforums.apple.com/message/1024928#1024928 I got it figured.
In the completion block for the animation coordinator, setting the preferredDisplayMode to UISplitViewControllerDisplayModeAutomatic makes it work.
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
if (self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
BOOL isPortrait = size.height > size.width;
if (isPortrait) {
self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
}
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if (isPortrait) {
self.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
}
self.dividerView.hidden = isPortrait;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// ADD THIS TO THE COMPLETION BLOCK
self.preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
NSLog(#"Preferred display mode: %ld | Actual display mode: %ld", self.preferredDisplayMode, self.displayMode);
}];
}
}
I have created a usercontrol just to contain an Image, because I haveto use Measureoverride and arrangeoverride methods and I can't create a subclass (Image is seled)... anyway, the thing is that when i call this.Image.Measure(SizeIwantto giveto the image) the desiredSize field of the image is not set... anybody know why?
I have been managing the Layouting methods before and It worked...
Here is the code (I have already checked all the other sizes and none of them is 0 or NaN)
protected override Size MeasureOverride(Size availableSize)
{
//the size that the element wants to have
Size imageDesiredSize = new Size();
imageDesiredSize = availableSize;
//if the current element's size is a pertentage
if ((futureHeight != 0))
{
imageDesiredSize.Height = availableSize.Height * futureHeight / 100;
}
if ((futureWidth != 0))
{
imageDesiredSize.Width = availableSize.Width * futureWidth / 100;
}
if (widthWrap)
{
imageDesiredSize.Width = ((BitmapImage)this.Source).PixelWidth;
}
if (heightWrap)
{
imageDesiredSize.Height = ((BitmapImage)this.Source).PixelHeight;
}
System.Diagnostics.Debug.WriteLine("imagedesired" + imageDesiredSize);
this.image.Measure(imageDesiredSize);
System.Diagnostics.Debug.WriteLine("desiredsize" + this.image.DesiredSize);
return imageDesiredSize;
}
In the end It was a really simple solution... In the constructor of the UserControl I added this line: this.Content = image;
And now the content of the Usercontrol is drawn in the screen :-)
Let's say you have a custom control similar to NSSlider but with support for choosing a range of values, instead of a single value. The obvious choice of properties is minValue, maxValue, leftValue, rightValue, or however you wanna name them.
You'd probably also want to make sure that leftValue and rightValue always lay in between minValue and maxValue. Same for minValue and maxValue. You don't want them to potentially invalidate the existing leftValue and rightValue, like by—let's say—setting maxValue to a value lower than the current rightValue.
Pretty straight forward so far.
What however do you do in case of ensuring proper KVO accessibility/compliency? You simply can't assure that KVO sets the properties in the proper order (that being first setting the min and max limits and then the other values).
I happen to have such an UI element and simply can't figure out hot to get this running without opening the doors for falsely set values.
The custom control is meant to be bound to a model object.
If I now create such a model with the values (min: 0.00 max: 42.00 left: 14.00 right: 28.00) I end up with the following setup in the custom control (min: 0.00 max: 42.00 left: 1.00 right: 1.00)
The reason for this is that instead of calling minValue and maxValue first, it calls leftValue and rightValue first, which causes both values to end up with 1.0 (which is the default maxValue). (After which the maxValue is then set to its proper value.)
//[self prepare] gets called by both, [self initWithCoder:] and [self initWithFrame:]
- (void)prepare
{
minValue = 0.0;
maxValue = 1.0;
leftValue = 0.0;
rightValue = 1.0;
}
- (void)setMinValue:(double)aMinValue
{
if (aMinValue < maxValue && aMinValue < leftValue) {
minValue = aMinValue;
[self propagateValue:[NSNumber numberWithDouble:minValue] forBinding:#"minValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setMaxValue:(double)aMaxValue
{
if (aMaxValue > minValue && aMaxValue > rightValue) {
maxValue = aMaxValue;
[self propagateValue:[NSNumber numberWithDouble:maxValue] forBinding:#"maxValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setLeftValue:(double)aLeftValue
{
double newValue = leftValue;
if (aLeftValue < minValue) {
newValue = minValue;
} else if (aLeftValue > rightValue) {
newValue = rightValue;
} else {
newValue = aLeftValue;
}
if (newValue != leftValue) {
leftValue = newValue;
[self propagateValue:[NSNumber numberWithDouble:leftValue] forBinding:#"leftValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setRightValue:(double)aRightValue
{
double newValue = leftValue;
if (aRightValue > maxValue) {
newValue = maxValue;
} else if (aRightValue < leftValue) {
newValue = leftValue;
} else {
newValue = aRightValue;
}
if (newValue != rightValue) {
rightValue = newValue;
[self propagateValue:[NSNumber numberWithDouble:rightValue] forBinding:#"rightValue"];
[self setNeedsDisplay:YES];
}
It's times like these when you wish Apple had opened up the source of Cocoa. Or at least some of the controls for deeper inspection.
How would you have implemented such a control?
Or more general: What's the best practice for implementing a class that does have cross-dependent properties while complying to KVO?
Wow. This is a tricky problem. As you've already identified, there's nothing you can do to ensure that KVO messages are sent in any particular order.
Here's one thought: in your setLeftValue: and setRightValue: accessors, instead of trimming the value, why not just store the value that's passed in, and then trim the value on access.
For example:
- (void)setLeftValue:(double)value {
leftValue = value;
[self propagateValue:[NSNumber numberWithDouble:self.leftValue] forBinding:#"leftValue"]; // Note the use of the accessor here
[self setNeedsDisplay:YES];
}
- (double)leftValue {
return MAX(self.minValue, MIN(self.maxValue, leftValue));
}
This way, once minValue and maxValue are set, leftValue and rightValue will represent the correct values. And, by using custom accessors, you can ensure that your constraint that minValue <= leftValue/rightValue <= maxValue is preserved.
Also, for the code you're writing, the MIN and MAX preprocessor macros will save you a lot of effort.
Not exactly sure I understand what your trying to do but your problem has nothing to do with KVO.
The first time that setLeftValue: is called after prepare to set it to 14.0, you see the following with values of variables in parethesis:
- (void)setLeftValue:(double)aLeftValue(14.0)
{
double newValue = leftValue(0.0);
if (aLeftValue(14.0) < minValue(0.0)) {
newValue = minValue(0.0);
} else if (aLeftValue(14.0) > rightValue(1.0)) { // always evaluates true for all aleftValue>1
newValue = rightValue(1.0); // now newValue==1.0
} else {
newValue = aLeftValue(14.0);
}
if (newValue(1.0) != leftValue(0.0)) {
leftValue = newValue(1.0); // now leftvalue==1.0
[self propagateValue:[NSNumber numberWithDouble:leftValue] forBinding:#"leftValue"];
[self setNeedsDisplay:YES];
}
}
For all aLeftvalues>1, this code will always set leftValue==1.0.
Since you never call either of the minValue and maxValue setters, they have no effect on this code.