FinderSync context menu items beep, never call action - nsmenuitem

I want to generate a menu which will call functions on a class other than that one which generated the menu, and then place that in Finder. However, when I click the menu item, the system error beep plays and my function is never called.
Here's a SSCCE:
import Cocoa
import FinderSync
#objc(FinderSync)
class FinderSync: FIFinderSync {
var observedFolder = URL(fileURLWithPath: "/")
override init() {
super.init()
NSLog("\(FinderSync.self.className()) launched from \(Bundle.main.bundlePath)")
// Set up the directory we are syncing.
FIFinderSyncController.default().directoryURLs = [self.observedFolder]
}
// MARK: - Menu and toolbar item support
override var toolbarItemName: String {
return "Wonderful Test App"
}
override var toolbarItemToolTip: String {
return "This is wonderful"
}
override var toolbarItemImage: NSImage {
return NSImage(named: NSImage.cautionName)!
}
override func menu(for menuKind: FIMenuKind) -> NSMenu {
let menu = NSMenu(title: "")
let menuItem = NSMenuItem(title: "Click me!", action: #selector(SomeOtherClass.remoteAction), keyEquivalent: "")
menuItem.target = SomeOtherClass.shared
menuItem.action = #selector(SomeOtherClass.remoteAction)
menu.addItem(menuItem)
return menu
}
}
#objc(SomeOtherClass)
public class SomeOtherClass: NSObject {
public static let shared = SomeOtherClass()
deinit {
NSLog("Deallocated!")
preconditionFailure("Shared instance should never be deallocated!")
}
#IBAction
#objc(remoteAction:)
public func remoteAction(_ sender: Any?) {
NSLog("Remote!")
}
}
I've verified via the memory debugger that SomeOtherClass.shared is still in memory before, during, and after the menu item is clicked, so it's not being deallocated or anything.

It appears you can't add actions which are within any class other than your FinderSync extension's principal class. Which is about as stupid as everything else about NSMenuItem, so I'm not surprised.
So, you'll have to move the action(s) into your FinderSync class, despite how ugly that might be for organization.

Related

How to pass a selector function as a paramater in Swift?

I have a standard View Controller that has the following selector:
#objc func finished() {
//Do Something here
}
I have an outside class called ButtonFactory that looks something like this:
class ButtonFactory {
static let shared = ButttonFactory()
func createButton(functionParameter: () -> Void ) -> UIButton {
let button = UIButton()
button.addTarget(self, action: #selector(functionParameter), for: .touchUpInside)
return button
}
}
In my view controller I call createButton like this:
ButtonFactory.shared.createButton(functionParamter: self.finished)
However the above code does not compile saying: Argument of '#selector' cannot refer to parameter 'functionParameter'.
Is it possible to pass a selector function as a parameter in Swift 5?
You can pass your function using a Selector:
class ViewController: UIViewController {
#objc func finished() {
//Do Something here
}
}
class ButtonFactory {
static let shared = ButtonFactory()
func createButton(_ action: Selector) -> UIButton {
let button = UIButton()
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
}
let button = ButtonFactory.shared.createButton(#selector(ViewController.finished))
Note that you should pass the view controller instance as well to your method instead of setting the target to your ButtonFactory shared instance.

How to work with Scopes when using Workspaces in TornadoFX?

I'm using the Workspace feature of TornadoFX and I'm trying to make them work with Scopes. When I'm starting up the application I need to set some things up and when it is done I start the TornadoFX application and I supply a Scope to my first view. After that I want to be able to dock other views into my Workspace in a new Scope but this doesn't work for some reason: the original View doesn't get undocked but the new View gets docked I just don't see it on the screen. What could be the problem? Here is my code to reproduce the problem:
fun main(args: Array<String>) {
Application.launch(TestApp::class.java, *args)
}
class ParentScope : Scope() {
val id = UUID.randomUUID().toString().substring(0, 4)
}
class ChildScope : Scope() {
val id = UUID.randomUUID().toString().substring(0, 4)
}
class TestApp : App(Workspace::class) {
override fun onBeforeShow(view: UIComponent) {
workspace.dock<ParentView>(ParentScope())
}
}
class ParentView : View("parent") {
override val scope = super.scope as ParentScope
override val root = hbox {
button("new child") {
action {
workspace.dock<ChildView>(ChildScope())
}
}
}
override fun onDock() {
logger.info("Docking parent (${scope.id})")
}
override fun onUndock() {
logger.info("Undocking parent (${scope.id})")
}
}
class ChildView : View("child") {
override val scope = super.scope as ChildScope
override val root = hbox {
text("In child")
}
override fun onDock() {
logger.info("Docking child (${scope.id})")
}
override fun onUndock() {
logger.info("Undocking child (${scope.id})")
}
}
The output after I click thew new child button is this:
10:56:19.415 [JavaFX Application Thread] INFO Test - Docking parent (d202)
10:56:23.967 [JavaFX Application Thread] INFO Test - Docking child (cbc5)
What I see is the same ParentView. If I click new child again the ChildView gets undocked and a new ChildView gets docked but I still only see the ParentView:
10:56:31.228 [JavaFX Application Thread] INFO Test - Undocking child (cbc5)
10:56:31.228 [JavaFX Application Thread] INFO Test - Docking child (1dd8)
The App already defines a Scope with a corresponding Workspace instance, so while your initial View is docked in the Workspace, your new ParentScope defines a new Workspace instance. When you dock the ChildView into that Workspace it does exactly that, but the Workspace in question is not shown on screen.
To remedy this you can override the primary scope of the your application like this:
class TestApp : WorkspaceApp(ParentView::class) {
init {
scope = ParentScope()
}
}
Notice also that I use the WorkspaceApp subclass so I don't need to dock the ParentView manually.
While looking at this issue I realized that if you dock a UIComponent into a Workspace, it should assume the Workspace it has been docked in. For that reason, I've committed a fix so that your initial code will work without modification.

setStatusBarHidden is deprecated in iOS 9.0

I am upgrading my code from iOS 8 to iOS 9. I have a code snippet in my program
[[UIApplication applicationName] setStatusBarHidden:YES];.
I am getting the warning "setStatusBarHidden is deprecated in iOS 9.0, Use -[UIViewController prefersStatusBarHidden". If I just replace 'setStatusBarHidden' with 'prefersStatusBarHidden', I get 'instance method not found'.
Can someone please suggest me how to solve this problem?
Add below code to your view controller..
- (BOOL)prefersStatusBarHidden {
return NO;
}
Note :
If you change the return value for this method, call the
setNeedsStatusBarAppearanceUpdate method.
For childViewController, To specify that a child view controller
should control preferred status bar hidden/unhidden state, implement
the childViewControllerForStatusBarHidden method.
prefersStatusBarHidden is available from iOS 7+.
Use this in Your UIViewController class
var isHidden = true{
didSet{
self.setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
return isHidden
}
If you change the return value for this method, call the
setNeedsStatusBarAppearanceUpdate() method. To specify that a child
view controller should control preferred status bar hidden/unhidden
state, implement the childViewControllerForStatusBarHidden method.
you have to add method in yourViewController.m
- (BOOL)prefersStatusBarHidden {
return NO;
}
Swift 3.1 Xcode 8.2.1
Change in info.plist the row View controller-based status bar appearance and set it to NO
In your target settings tick "Hide Status bar"
Both steps are required
Here is my swift code for setting status bar hidden and style.
extension UIViewController {
public var privateStatusBarHidden: Bool {
return statusBarHidden
}
public var privateStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
public func setStatusBarHidden(hidden: Bool, animated: Bool = false) {
statusBarHidden = hidden
if animated {
UIView.animate(withDuration: 0.25, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
} else {
self.setNeedsStatusBarAppearanceUpdate()
}
}
public func setStatusBar(style: UIStatusBarStyle) {
statusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
public static func swizzleStatusBarHiddenPropertyForViewController() {
var original = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.prefersStatusBarHidden))
var changeling = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.privateStatusBarHidden))
method_exchangeImplementations(original, changeling)
original = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.preferredStatusBarStyle))
changeling = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.privateStatusBarStyle))
method_exchangeImplementations(original, changeling)
original = class_getClassMethod(UIViewController.self, #selector(UIViewController.swizzleStatusBarHiddenPropertyForViewController))
changeling = class_getClassMethod(UIViewController.self, #selector(UIViewController.emptyFunction))
method_exchangeImplementations(original, changeling)
}
#objc private static func emptyFunction() {}
}
Usage
in lauching function
UIViewController.swizzleStatusBarHiddenPropertyForViewController()
for hide/show statusBar, in UIViewController
. self.setStatusBar(hidden: true/false)
Swift 3 with Xcode 8.3.3
1) Add a row in you Info.plist.
2) In your ViewController ViewDidLoad() override add:
UIApplication.shared.isStatusBarHidden = true

Swift unique property in multiple instances

I know that theoretically it's possible to create multiple instances of the same class with a property that would have a different value for each instance.
The thing is, I can't make it happen.
Each time I'm creating a new instance, it gets the property's value of the other instances, and when I'm changing one value for an instance, it changes the other's too.
So my guess is that I'm doing something wrong (obviously), like accessing the class property value instead of the instance property value... Here's the code.
class CustomUIImageView: UIImageView {
var someParameter: Bool = false // This is the property I want to be different in each version of the instance.
}
class ClassSiege: UIViewController, UIGestureRecognizerDelegate {
var myView: CustomUIImageView! //the instance declaration.
// I use this gesture recognizer to find out the value of the instance I'm tapping on.
func handleTap (sender: UITapGestureRecognizer) {
print("value of someParameter \(self.myView.someParameter)")
}
func handlePan(recognizer: UIPanGestureRecognizer) {
let iv: UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x // 'Blackboard' is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
if centerRondY - centerBoardY < 100 {
self.myView.someParameter = true // If the distance between myView and the blackboard is under 100 I want the instance's property to become true.
} else {
self.myView.someParameter = false // On the other hand, if the distance is greater than 100, I want it to be false.
}
}
// When the user pushes a button, it triggers this method that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
// some code...
// Here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}
}
So clearly that's not the good way to do it, but what's wrong, and how can it be solved?
Basing my answer on your related question.
If what you want to achieve is initializing a property with a value that you provide, just add a new parameter to the initializer. If for instance you are using the initializer with a CGRect passed in, then you can implement an initializer like this:
class CustomUIImageView : UIImageView {
let someParameter : Bool
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect, someParameter: Bool) {
self.someParameter = someParameter
super.init(frame: frame)
}
}
I hope that this is what you are looking for - let me know otherwise.
I've found the solution, and if you've been facing the same issu, here's how to deal with it.
The secret is to downcast the recognizer.view to take the parameter of the subclass CustomUIImageView.
here's how :
func handleTap (sender: UITapGestureRecognizer) {
println("value of someParameter \(self.myView.someParameter)") //I use this gesture recognizer to find out the value of the instance I'm tapping on.
}
func handlePan(recognizer:UIPanGestureRecognizer) {
let iv : UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x //blackboard is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
var myParameter = recognizer.view as CustomUIImageView //<- this is the key point. Downcasting let you access the custom subclass parameters of the object that is currently moved
if centerRondY - centerBoardY < 100 {
myParameter.someParameter = true //so now I'm really changing the parameter's value inside the object rather than changing a global var like I did before.
} else {
myParameter.someParameter = false
}
}
//when user pushes a button, it triggers this func that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
some code...
//here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}

Handle Double-click Mouse Event and Return Pressed for NSTableView

OK, what I need is pretty straightforward, though I can still find nothing specific.
I want to be able to :
track double-click events
track when the NSTableView is in focus, and the "Return" key is pressed.
How would you go about it?
P.S. I've had a look into NSTableViewDelegate specification, but I can't find anything useful.
For double click you need to do just these :
-(void)awakeFromNib{
[self.tableView setDoubleAction:#selector(thisMethod)];
//And if you wish to take selector dynamically, I guess you know how to do :)
}
-(void)thisMethod{
NSLog(#"double clicked");
}
For the return event, subclass your NSTableView and override keyDown:
Swift 5.x:
override func keyDown(with event: NSEvent) {
if event.characters?.count == 1 {
let character = event.keyCode
switch (character) {
// 36 is return
case UInt16(36):
print("return: \(event)")
default:
print("any other key: \(event)")
}
} else {
super.keyDown(with: event)
}
}
There is a way to handle the Return key without having to manually check for its key code.
I'll show the answer in Swift, but it can be applied in Objective-C as well.
First, override keyDown(with:) in your view controller subclass that controls the table view and call interpretKeyEvents(_:):
override func keyDown(with event: NSEvent) {
interpretKeyEvents([event])
}
Second, in the same view controller subclass, override insertNewLine(_:). This is called when the user presses the Return key:
override func insertNewLine(_ sender: Any?) {
// Add your logic to handle the Return key being pressed
}
Here's an example:
class TableViewController: NSViewController {
#IBOutlet var tableView: NSTableView!
override func keyDown(with event: NSEvent) {
interpretKeyEvents([event])
}
override func insertNewLine(_ sender: Any?) {
guard tableView.selectedRow >= 0 else { return }
print("Pressed Return on row \(tableView.selectedRow)")
}
}