DEV Community

Creating a global configurable shortcut for MacOS apps in Swift

Mitch Stanley on April 17, 2019

Liquid syntax error: 'raw' tag was never closed

Collapse
 
aegerborder profile image
Patrick • Edited

As a beginner I must say that this is a great and in depth tutorial, Very nice! But...

"register:sender needs to be connected with the shortcut button.
unregister:sender needs to be connected to the clear shortcut button.
clearButton property needs to be connected to the clear shortcut button.
shortcutButton needs to be connected to the shortcut button."

Sorry but... what? :D

There's one button and there's one text field. Can't figure out which to connect with which action/outlet because you're calling them all "button"

There's screenshots for all things but this one. Would be incredibly great to know what I'm doing wrong.

Collapse
 
mitchartemis profile image
Mitch Stanley

Hi Patrick,

Thank you for reading and for the kind words!

Sorry for the confusion. There's a mistake in the tutorial. Instead of using a text field and a button I'd recommend two buttons. One is the clear button, and the other is the "set" button.

You may also wish to set default text on the set button so the user knows to click it. You can do that from the storyboard title button property, as well as updating the unregister method

shortcutButton.title = "Set Shortcut"
Collapse
 
aegerborder profile image
Patrick

Hey Mitch,

thank you for your reply which explained a lot and now all makes sense, haha :D

If I may grab another minute off you:
How would you go on if there was no MainWindow? I have an existing app and I have the MainStoryboard wired to a ViewController. Like this: thepracticaldev.s3.amazonaws.com/i...

I want this one to pop up when hitting the shortcut. I've tried a few things but nothing worked so far :-/

Thanks again and best regards
Patrick

Thread Thread
 
mitchartemis profile image
Mitch Stanley

No problem Patrick :)

Can you show me a screenshot of your Storyboard?

Usually a view controller will have window or something else that it is used in.

One of the exceptions is if you're making a menubar app, in which case you can use NSPopover in your AppDelegate

class AppDelegate: NSObject, NSApplicationDelegate {
    // ...

    let popover = NSPopover()

    // ...

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        popover.contentViewController = MenuViewController.freshController()

        eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
            if let strongSelf = self, strongSelf.popover.isShown {
                print("is shown")
                strongSelf.closePopover(sender: event)
            }

    }
    // ...
    @objc func togglePopover(_ sender: Any?) {
        if popover.isShown {
            closePopover(sender: sender)
        } else {
            showPopover(sender: sender)
        }
    }
    func showPopover(sender: Any?) {
        if let button = statusItem.button {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        eventMonitor?.start()
    }
   func closePopover(sender: Any?) {
        popover.performClose(sender)
    }
    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            hotKey.keyDownHandler = { [weak self]  in
                if let strongSelf = self {
                    strongSelf.togglePopover(nil)
                }
            }
        }
    }

This code is a little messy (copied it from an old project) but hopefully it is enough to give you an idea of how it works.

Thread Thread
 
aegerborder profile image
Patrick • Edited

Once again: thank you! I guess I'm coming closer to that goal, hehe. Indeed it's a menu bar app. I ran through a "Swift on sundays" session by Paul Hudson and that's why there's a lot of stuff in the AppDelegate.swift and I don't know exactly how to merge both your code and the code that's already there.

Here's the MainStoryboard: thepracticaldev.s3.amazonaws.com/i...

And here's my AppDelegate. Tried to understand that Markdown stuff here but didn't succeed :(

import Cocoa
import HotKey
import Carbon

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        if Storage.fileExists("globalKeybind.json", in: .documents) {

            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: globalKeybinds.keyCode, carbonModifiers: globalKeybinds.carbonFlags))
        }

        statusItem.button?.title = "⅀"

        statusItem.button?.target = self

        statusItem.button?.action = #selector(showSettings)

    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    @objc func showSettings() {

        let storyboard = NSStoryboard(name: "Main", bundle: nil)

        guard let vc = storyboard.instantiateController(withIdentifier: "ViewController") as? NSViewController else {
            fatalError("ViewController in Storyboard nicht gefunden.")
        }

        guard let button = statusItem.button else {

            fatalError("Button des Status-Items konnte nicht gefunden werden")
        }


        let popoverView = NSPopover()
        popoverView.contentViewController = vc
        popoverView.behavior = .transient
        popoverView.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)

    }


    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            hotKey.keyDownHandler = { [weak self] in
                NSApplication.shared.orderedWindows.forEach({ (window) in
                    if let mainWindow = window as? MainWindow {
                        print("woo")
                        NSApplication.shared.activate(ignoringOtherApps: true)
                        mainWindow.makeKeyAndOrderFront(self)
                        mainWindow.makeKey()
                    }
                })

            }
        }
    }


}
Thread Thread
 
mitchartemis profile image
Mitch Stanley

Is the settings window supposed to be a popover as well? Or is that just a separate window that opens when the setting button is clicked?

I would try

1) Adding a popover instance to the AppDelegate
2) Add the main view controller instance to the new popover in the applicationDidFinishLaunching method

Something like this (This is untested!)

import Cocoa
import HotKey
import Carbon

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    // Add mainPopover and eventMonitor as an instance variable
    let mainPopoverView = NSPopover()
    var eventMonitor: EventMonitor?

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        if Storage.fileExists("globalKeybind.json", in: .documents) {

            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: globalKeybinds.keyCode, carbonModifiers: globalKeybinds.carbonFlags))
        }

        statusItem.button?.title = "⅀"

        statusItem.button?.target = self

        statusItem.button?.action = #selector(showSettings)

        // Fresh controller instance - use your view controller here
        mainPopoverView.contentViewController = MenuViewController.freshController()

        // Check if mouse is clicked outside of menu - if it is then close the popover (optional)
        eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
            if let strongSelf = self, strongSelf.mainPopoverView.isShown {
                print("is shown")
                strongSelf.closePopover(sender: event)
            }
        }
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    // Add in the toggling methods
    @objc func togglePopover(_ sender: Any?) {
        if mainPopoverView.isShown {
            closePopover(sender: sender)
        } else {
            showPopover(sender: sender)
        }
    }

    func showPopover(sender: Any?) {
        if let button = statusItem.button {
            mainPopoverView.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        eventMonitor?.start()
    }

    func closePopover(sender: Any?) {
        mainPopoverView.performClose(sender)
        eventMonitor?.stop()
    }



    @objc func showSettings() {

        let storyboard = NSStoryboard(name: "Main", bundle: nil)

        guard let vc = storyboard.instantiateController(withIdentifier: "ViewController") as? NSViewController else {
            fatalError("ViewController in Storyboard nicht gefunden.")
        }

        guard let button = statusItem.button else {

            fatalError("Button des Status-Items konnte nicht gefunden werden")
        }


        let popoverView = NSPopover()
        popoverView.contentViewController = vc
        popoverView.behavior = .transient
        popoverView.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)

    }


    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            // Toggle popover 
            hotKey.keyDownHandler = { [weak self] in
                if let strongSelf = self {
                    strongSelf.togglePopover(nil)
                }
            }
        }
    }
}

Mildly related, I recommend reading this Ray Wenderlich tutorial on menu bar apps, it helped me understand them better - raywenderlich.com/450-menus-and-po...

Thread Thread
 
aegerborder profile image
Patrick

Thank you! I guess this is far above my skill level. I'm already out on "event monitor" and "instance variable", "fresh controller instance". Funny thing: the moment you sent the reply I was on that exact same site of Ray Wenderlich to find out how to use an image as menu bar icon instead of that mathematical "sum" icon I had used before. I think I will keep learning stuff and get back to this in about 5 years, haha :D

But again: thank you very much. It's doesn't happen too often that people just offer their time to help out in this huge form. And: it shows that there will be a solution in the future when I'm able to understand these things and alter the necessary "things" (Objects? Items? I have no idea at all! :D)

Thread Thread
 
mitchartemis profile image
Mitch Stanley

My pleasure Patrick,

The trick is to keep pushing yourself to try different things in your code. The fact that you're already doing this is a great start.

I hope you continue to pursue Swift. It's truly a wonderful language, especially with the new SwiftUI stuff that's arrived recently!

Thread Thread
 
aegerborder profile image
Patrick

Thanks. And... I'm a fool. I stopped reading the page at "Bet you’re feeling smarter already!" with that little fella flexing. Thought "OK, page end" and quit. I reopened the site to re-read a detail I had missed and saw that there's an entire "Event Monitoring" part following that little dude. NOW I know what you were talking about in the comments of your code.

Well... time for an awkward confession: I'm learning Swift since late 2017 and I even have a few iOS Apps on the App Store. But I am totally new to that thing on macOS. It's very similar in the basics but there're many differences in actions and objects and members of objects etc.
I like Swift a lot. I always like things that show your progress immediately (or tell you what or THAT something went wrong). And as a Mac and iPhone user I can do things for myself. Things I need or always wanted. Yes, it's great. And the day will come when this super small app is done :D

Thanks for motivation - I'll keep practicing :)

Thread Thread
 
mitchartemis profile image
Mitch Stanley • Edited

Yeah MacOS is a completely different beast! and unfortunately it's really hard to find good tutorials and resources for it which makes it that much harder to build for.

Not only that, you'll often have to rely on old APIs that are really awful to write/read. iOS is much nicer in that sense!

Glad you've got the event monitoring stuff figured out 😀

Thread Thread
 
aegerborder profile image
Patrick

Not only figured that out. I got the shortcut thing AND the window/popup issues sorted out and finally both up and running now. And that's only because of your help. Thank you. Again and again :)
There are only 2-3 little things left.
Another good thing is that not everything is different from iOS. I just learned that UserDefaults work the exact same way. At least this was a hattrick scored within seconds, hehe.
Phew... by the way: if you don't understand any things I write feel free to ask. Just a German digging in pale memories of English lessons from over 25 years ago, haha.

Thread Thread
 
mitchartemis profile image
Mitch Stanley

That's great to hear! And yeah being able to share code between iOS and MacOS is really handy :).

Don't worry your English seems pretty good to me, I didn't even noticed until the screenshot you sent had German in it, haha :D.

Collapse
 
kurtbliss profile image
KurtBliss

I'm a bit new to Xcode and swift and I got the hotels working for bringing the window up... But it's doesn't seem to works for the caps lock button.. It'll decide the caps lock button when setting it, but then it just ignores the caps locks key, if you have Command + CapsLock + D for example Command + D will bring up the window still. CapsLock + D set, then try to bring up the window and no luck for me.... Wonder what it is.

Collapse
 
mitchartemis profile image
Mitch Stanley

There are certain keys that can't be used for shortcuts. Capslock is a funny one as it's more of a toggle key rather than an input, so it might not be usable.

It might be best to add in some validation to tell users when certain keys aren't available or certain combinations can't be used (e.g. command+c for example, which is already a system-wide command).

Collapse
 
kurtbliss profile image
KurtBliss • Edited

So just wanted to share this

dev-to-uploads.s3.amazonaws.com/up...