Accessing NSWindow-like properties in Catalyst macOS app - swift

I am thinking about porting my macOS app to Catalyst.
My app shows a transparent window (no title bar, clear background) on top of all other apps windows (dock included).
To do that, in the non-catalyst code I use:
window.isOpaque = false
window.hasShadow = false
window.backgroundColor = .clear
window.styleMask = .borderless
window.isMovableByWindowBackground = true
window.level = .statusBar
Using UIKit, I was only able to remove the toolbar so far:
window.titleBar.titleVisibility
...But no clue about the other settings.
I plan to make the app available on the App Store in the future, but if the only way to do so is and hack with a private API, that's fine.
Any ideas?
Thanks in advance

There is no official API for doing that, but you can easily access the NSWindow instance and modify it directly. You can do that manually or using some library like Dynamic (Full disclosure: I'm the author):
let window = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(uiWindow)
window.isOpaque = false
window.hasShadow = false
window.backgroundColor = Dynamic.NSColor.clearColor
window.styleMask = 0 /*borderless*/
window.isMovableByWindowBackground = true
window.level = 25 /*statusBar*/

I have some success removing the close button on Catalyst by calling a function from the viewDidAppear(). I called it AppDelegate().disableTitleBarButtons(). Has to be from view did appear.
AppDelegate().disableTitleBarButtons() is as follows
func disableTitleBarButtons() {
func bitSet(_ bits: [Int]) -> UInt {
return bits.reduce(0) { $0 | (1 << $1) }
}
func property(_ property: String, object: NSObject, set: [Int], clear: [Int]) {
if let value = object.value(forKey: property) as? UInt {
object.setValue((value & ~bitSet(clear)) | bitSet(set), forKey: property)
}
}
// disable full-screen button
if let NSApplication = NSClassFromString("NSApplication") as? NSObject.Type,
let sharedApplication = NSApplication.value(forKeyPath: "sharedApplication") as? NSObject,
let windows = sharedApplication.value(forKeyPath: "windows") as? [NSObject]
{
for window in windows {
let resizable = 4
property("styleMask", object: window, set: [], clear: [resizable])
let fullScreenPrimary = 7
let fullScreenAuxiliary = 8
let fullScreenNone = 9
property("collectionBehavior", object: window, set: [fullScreenNone], clear: [fullScreenPrimary, fullScreenAuxiliary])
}
}
}
Where is says let resizable = 4,
Change to 3 for no Maximise,
Change to 2 for No minimise,
Change to 1 of No Close button.
Play with the other numbers or stylemask settings also. Good luck

Related

Adding a new label where clicked SWIFT

I would like to write a simple swift code for a MacOS app which add a new label wherever the user perform a mouse click in a window.
This code compiles but make the app crash:
#IBOutlet var here2 = [NSTextField]()
var count: Int = 0
func getCoordinates(){
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) {
if self.location.x < 700 && self.location.y<750 {
self.here2.append(NSTextField.init())
self.here2[self.count].frame.origin = CGPoint(x: self.location.x, y: self.location.y)
self.here2[self.count].stringValue = String(self.count)
print("count is: " + String(self.here2.count))
self.count+=1
}
return $0
}
Here is a programmatic approach which may be run in Terminal. Note that window.acceptsMouseMovedEvents is set to true.
//May be run in Terminal with:
//swiftc label.swift -framework Cocoa -o label && ./label
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var window : NSWindow!
var here2 = [NSTextField]()
var count: Int = 0
let _wndW : CGFloat = 700
let _wndH : CGFloat = 750
let _labelW : CGFloat = 24
let _labelH : CGFloat = 24
#objc func getCoordinates(_ sender:AnyObject ) {
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) {
var pt: NSPoint? { self.window.mouseLocationOutsideOfEventStream }
if let location = pt {
let msPt:NSPoint = self.window.contentView!.convert(location, from: nil)
print("x = \(msPt.x) : y = \(msPt.y)")
if msPt.x < (self._wndW - self._labelW) && msPt.y < (self._wndH - self._labelH ) {
self.here2.append(NSTextField.init())
self.here2[self.count].frame = NSMakeRect(msPt.x, msPt.y, self._labelW, self._labelH)
print(self.here2[self.count].frame)
self.window.contentView!.addSubview (self.here2[self.count])
self.here2[self.count].backgroundColor = NSColor.white
self.here2[self.count].isSelectable = false
self.here2[self.count].stringValue = String(self.count)
print("count is: " + String(self.here2.count))
self.count+=1
}
}
return $0
}
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable, .resizable], backing:.buffered, defer:false)
window.center()
window.title = "Swift Test Window"
window.makeKeyAndOrderFront(window)
window.acceptsMouseMovedEvents = true
// **** Button **** //
let myBtn = NSButton (frame:NSMakeRect( _wndW - 180, 15, 135, 24 ))
myBtn.bezelStyle = .rounded
myBtn.autoresizingMask = [.maxXMargin,.minYMargin]
myBtn.title = "Start Label Maker"
myBtn.action = #selector(self.getCoordinates(_:))
window.contentView!.addSubview (myBtn)
// **** Quit btn **** //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// **** main.swift **** //
let app = NSApplication.shared
app.delegate = appDelegate
app.setActivationPolicy(.regular)
app.activate(ignoringOtherApps:true)
app.run()
You cannot call copy on NSTextView, that causes the error message.
You implement all the required methods by subclassing the view, but I'd suggest you allocate new text views programmatically and set their style in code instead of Interface builder.
Here's another thread dealing with copying objects: "[something copyWithZone:]: unrecognized selector sent to instance" when using Bindings / Core Data
You can't connect and array of NSTextField in a storyboard. In my opinion it's a bug in Xcode, IB shouldn't make the connection. It's also a bug in AppKit, I tried this and got this weird error:
Failed to set (contentViewController) user defined inspected property on (NSWindow): [<__NSTimeZone 0x6040000a2280> valueForUndefinedKey:]: this class is not key value coding-compliant for the key identifier.
Solution: remove the connection in IB and change
#IBOutlet var here2 = [NSTextField]()
to
var here2 = [NSTextField]()
#IBOutlet weak var here2TextField: NSTextField!
Connect here2TextField in IB and append here2TextField to here2 in viewDidLoad().
#Marco: The code which I posted is a template (aka boilerplate) example of the programmatic approach to creating applications. All that I did was copy/paste your function into the AppDelegate class of the template and connect it to a button. From there I revised your code until the desired result was obtained. Instead of using a XIB or storyboard the technique requires that source code be written by the author (or use templates). The good news is that you have control of your application and nothing is done behind your back; what you see is what you get. The downside is that the technique is cumbersome for really large projects; for this I rely on Xcode with XIBs. The autocompletion feature of Xcode is also very helpful, while with the programmatic approach you have to rely on Xcode/Help/DeveloperDocumentation as well as internet searches, especially StackOverflow (usually someone else has had the same problem that you are experiencing). In lieu of using the CommandLine from Terminal for every compile I automated the process with a very simple editor which was written in Swift using Xcode (with XIB). However, the posted code may also be run in Xcode by doing the following:
1) Create a new macOS App using Swift (User Interface is XIB but it won’t be needed for the demo)
2) Use File/New/File to add a Swift file and name it ‘main.swift’
3) In the new main.swift file change ‘import Foundation’ to ‘import Cocoa’
4) Copy/paste the last five lines of the demo (labelled main.swift) into this file
5) Go into the AppDelegate file that Apple provided and delete everything there except for ‘import Cocoa’
6) Copy/paste the entire AppDelegate class of the posted code (down to the main.swift section which you have already used)
7) Hit the ‘Run’ button, and it should compile without error.
Hope it helps and good luck with your project. Thanks for posting.

Add 'PageSetupAccessory' to PrintPanel for PDFDocument

I have an app which displays a PDFView, and I want it to print the PDF file from the view. Also, I want the Print panel to show the Page Setup Accessory (i.e. the Paper Size, Orientation and Scale settings, like in Preview, which appear as one panel of the drop-down list of options).
Currently, I'm mistakenly printing the PDFView, not the PDF document itself. This only gives me one page and includes the scrollbars in the print-out! I can't see how to init an NSPrintOperation referencing a PDFDocument rather than the PDFView.
Here's my code, which works, but isn't what I want. I presume I'll have to override either the printDocument or printOperation functions of NSDocument with similar code that defines the Panel and the Info.
func thePrintInfo() -> NSPrintInfo {
let thePrintInfo = NSPrintInfo()
thePrintInfo.horizontalPagination = .automatic // Tried fit
thePrintInfo.verticalPagination = .automatic // Tried fit
thePrintInfo.isHorizontallyCentered = true // Tried false
thePrintInfo.isVerticallyCentered = true // Tried false
thePrintInfo.leftMargin = 0.0
thePrintInfo.rightMargin = 0.0
thePrintInfo.topMargin = 0.0
thePrintInfo.bottomMargin = 0.0
thePrintInfo.jobDisposition = .spool
return thePrintInfo
}
// Need to show the 'Page Setup' Options as an Accessory
// e.g. Paper size, orientation.
#IBAction func printContent(_ sender: Any) {
let printOperation = NSPrintOperation(view: thePDFView, printInfo: thePrintInfo())
let printPanel = NSPrintPanel()
printPanel.options = [
NSPrintPanel.Options.showsCopies,
NSPrintPanel.Options.showsPrintSelection,
NSPrintPanel.Options.showsPageSetupAccessory,
NSPrintPanel.Options.showsPreview
]
printOperation.printPanel = printPanel
printOperation.run()
}
Based on #Willeke's comments, I've come up with the following, which seems to work well. (Minor quibble is that the Print dialog isn't a sheet.) If anyone has any improvements, please post a better answer.
#IBAction func printContent(_ sender: Any) {
let printOperation = thePDFView.document?.printOperation(for: thePrintInfo(), scalingMode: .pageScaleNone, autoRotate: true)
printOperation?.printPanel = thePrintPanel()
printOperation?.run()
}

Not open NSWindow has isVisible property set to true

I'm trying to detect if NSWindow is open or closed using the isVisible property of NSWindow, but it is not working as I expected. For example I overrode the loadWindow method of my NSWindowController (I need to show a fullscreen web on the extended screen):
override func loadWindow() {
self.contentController = WKUserContentController();
guard let contentController = self.contentController else {
return
}
let config = WKWebViewConfiguration()
config.userContentController = contentController
let externalScreens = NSScreen.externalScreens()
let screen = externalScreens.count == 0 ? NSScreen.main()! : externalScreens[0]
window = KeyWindow(
contentRect: NSRect(x: 0, y: 0, width: screen.frame.width, height: screen.frame.height),
styleMask: NSBorderlessWindowMask,
backing: NSBackingStoreType.buffered,
defer: false,
screen: screen
)
if let w = window {
w.level = Int(CGShieldingWindowLevel())
w.backgroundColor = NSColor.black
w.makeKeyAndOrderFront(self)
w.makeFirstResponder(self)
webView = WKWebView(frame: w.frame, configuration: config)
w.contentView = webView!
debugPrint("Window is visible = \(w.isVisible)")
}
}
KeyWindow:
import Foundation
import AppKit
class KeyWindow : NSWindow {
override var canBecomeKey: Bool {
return true
}
}
but debugPrint shows that isVisible property is set to true, although the window was not opened yet (self.showWindow(self) method of controller was not called yet).
How can I reliably find out if window is open (displayed on screen) or not?
in your code
if let w = window {
w.level = Int(CGShieldingWindowLevel())
w.backgroundColor = NSColor.black
w.makeKeyAndOrderFront(self)
w.makeFirstResponder(self)
webView = WKWebView(frame: w.frame, configuration: config)
w.contentView = webView!
debugPrint("Window is visible = \(w.isVisible)")
}
you are calling w.makeKeyAndOrderFront(self) before the check
according to the Apple documentation:
func makeKeyAndOrderFront(Any?) Moves the window to the front of the
screen list, within its level, and makes it the key window; that is,
it shows the window.
So technically the isVisible works as advertised :)
isVisible
The value of this property is true when the window is onscreen (even
if it’s obscured by other windows); otherwise, false.
You window should be onscreen - despite the fact that you have set it's level as CGShieldingWindowLevel it maybe just invisible due to call of
w.makeKeyAndOrderFront(self) you can try to call func orderFrontRegardless()
Apple Doc
In this case it shall show the window immediately - but I think it is another SO Question

How can I disable the close button?

I need some help with figuring out how to disable/hide the close, minimize, and resize buttons in OS X Cocoa and Swift 2. Here's the code I tried. I know it's for the Title Bar, but I thought I'd try it anyway:
self.window.titleVisibility = NSWindowTitleVisibility.Hidden;
Does any one know how to do that? I'm using Swift 2, OS X Cocoa, and Xcode 7.2. Thanks!
Also try;
self.window!.standardWindowButton(NSWindow.ButtonType.closeButton)!.hidden = true
self.window!.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)!.hidden = true
etc.
In Xcode 9.1 you can use following in the ViewController,
override func viewWillAppear() {
self.view.window?.titleVisibility = .hidden
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.styleMask.insert(.fullSizeContentView)
self.view.window?.styleMask.remove(.closable)
self.view.window?.styleMask.remove(.fullScreen)
self.view.window?.styleMask.remove(.miniaturizable)
self.view.window?.styleMask.remove(.resizable)
//self.view.window?.isMovable = false
}
override func viewWillAppear() {
self.view.window?.titleVisibility = .hidden
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.styleMask.insert(.fullSizeContentView)
//self.view.window?.styleMask.remove(.closable)
self.view.window?.styleMask.remove(.fullScreen)
self.view.window?.styleMask.remove(.miniaturizable)
self.view.window?.styleMask.remove(.resizable)
//self.view.window?.isMovable = false
}
See the NSWindow.styleMask property and the Window Style Masks.
Clearing the NSClosableWindowMask, NSMiniaturizableWindowMask, and NSResizableWindowMask flags will remove all of the buttons from a window's title bar.
window.styleMask &= ~(NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask)
I find that the following effectively disables without completely hiding the close button:
self.view.window?.standardWindowButton(NSWindow.ButtonType.closeButton)?.isEnabled = false
a side note: do NOT disable "close" red button: apple will reject app saying:
"The user interface of your app is not consistent with the macOS Human Interface Guidelines. Specifically:
Specifically, the red light was disabled."
:(
Expanding on the accepted answer above by #JohnElemans, this is what worked for me when presenting modally using a storyboard segue on macOS 10.15:
// On NSViewController.viewDidAppear()
if let window = self.view.window {
window.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true
window.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true
window.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
}
I had to add the third line (NSWindow.ButtonType.zoomButton) to get rid of the green ('fullscreen') button.
If you are using Storyboards and you can change your window styleMask property inside an IBAction or viewDidLoad as follow:
NSApplication.sharedApplication().windows.first?.styleMask = NSTitledWindowMask // | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask
If you would like to enable them again just uncomment the rest of the style mask:
NSApplication.sharedApplication().windows.first?.styleMask = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask
If you are using Flutter, you can do like this:
import Cocoa
import FlutterMacOS
import window_manager
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
self.standardWindowButton(.closeButton)?.isHidden = true
self.standardWindowButton(.miniaturizeButton)?.isHidden = true
self.standardWindowButton(.zoomButton)?.isHidden = true
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
super.order(place, relativeTo: otherWin)
hiddenWindowAtLaunch()
}
}

How can I make an editable text field in a swift shell application

I'm trying to make a editable text region in a NSWindow. So far I can make
a window and add a text field - but when I select it and type characters the characters are echoed in the shell and NOT the text area.
NOTE: this is NOT an Xcode project - I am trying to do this in a single
file in the shell - my goal is to do this in code only
To replicate the error put the following code into a file (experiment.swift) and give the shell command
> swift experiment.swift
Here's the code
import Cocoa
class MyAppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow()
let ed = NSTextField(frame: NSMakeRect(20, 10, 180, 160))
func applicationDidFinishLaunching(aNotification: NSNotification) {
window.setContentSize(NSSize(width:600, height:200))
window.styleMask = NSTitledWindowMask | NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask
window.opaque = false
window.center();
window.title = "My window"
ed.font = NSFont(name:"Helvetica Bold", size:20)
ed.stringValue = "edit me"
ed.editable = true
ed.selectable = true
window.contentView!.addSubview(ed)
window.makeKeyAndOrderFront(window)
window.level = 1
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
let app = NSApplication.sharedApplication()
let obj = MyAppDelegate()
app.delegate = obj
app.run()
Before app.run(), add
app.setActivationPolicy(.Regular)
According to the docs, the default activationPolicy is Prohibited:
Prohibited
The application does not appear in the Dock and may not create windows or be activated. [...] This is also the default for unbundled executables that do not have Info.plists.