feat: add debug profile to feedback message
This commit is contained in:
1 parent
671fdabe79
commit
a14f96550b
27 files changed
+164
-42
No files matched your search
@@ -35,10 +35,12 @@
|
||||
D04BA84074E5FD6221720BC7 /* CollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAACB6648E7C2A4E0339D /* CollectionViewFlowLayout.swift */; };
|
||||
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */; };
|
||||
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
|
||||
D04BA9EE5D34A2789DCB0EE2 /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */; };
|
||||
D04BAAD43731608067734ED3 /* DispatchQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */; };
|
||||
D04BAB5E802C938E78839011 /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8011143819B48F204C2 /* TextArea.swift */; };
|
||||
D04BABF88726DA42B2CBA68B /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAF40D5E54AD1044B3FF7 /* ThumbnailsPanel.swift */; };
|
||||
D04BAC0BF53A80D4F1EE22ED /* AboutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE23C37E0F3B07EEE7B1 /* AboutTab.swift */; };
|
||||
D04BAC4F69FE9563BC1C5E9C /* DebugProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3915020FB9B34555D74 /* DebugProfile.swift */; };
|
||||
D04BAC9C031D482119F6DEB8 /* FeedbackWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAFA84FD0B02215718F94 /* FeedbackWindow.swift */; };
|
||||
D04BACD398A35D82D514A9F7 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BABC180117F8785D250E1 /* TextField.swift */; };
|
||||
D04BAD5A6B2F9EEE6FD4185F /* CollectionViewItemTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA4BABBA0312E0EDBA647 /* CollectionViewItemTitle.swift */; };
|
||||
@@ -63,6 +65,7 @@
|
||||
D04BA2D2BCBA4C47E25315AF /* HyperlinkLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HyperlinkLabel.swift; sourceTree = "<group>"; };
|
||||
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA35456DA0DDA74F9687E /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
|
||||
D04BA3915020FB9B34555D74 /* DebugProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugProfile.swift; sourceTree = "<group>"; };
|
||||
D04BA3D65E7CA78D699EDAB0 /* LabelAndControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAndControl.swift; sourceTree = "<group>"; };
|
||||
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
|
||||
D04BA40A4291E4F310527DBF /* AXUIElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = "<group>"; };
|
||||
@@ -86,6 +89,7 @@
|
||||
D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = "<group>"; };
|
||||
D04BA8011143819B48F204C2 /* TextArea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextArea.swift; sourceTree = "<group>"; };
|
||||
D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sysctl.swift; sourceTree = "<group>"; };
|
||||
D04BA89FAEC4A5734D892C4B /* build_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = build_release.sh; sourceTree = "<group>"; };
|
||||
D04BA8BEE821E2062F23AA97 /* CollectionViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItem.swift; sourceTree = "<group>"; };
|
||||
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensions.swift; sourceTree = "<group>"; };
|
||||
@@ -196,6 +200,7 @@
|
||||
D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */,
|
||||
D04BAF0DFC1F44322973CE1E /* PrivateApis.swift */,
|
||||
D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */,
|
||||
D04BA896E37EFD27CAB61DF0 /* Sysctl.swift */,
|
||||
);
|
||||
path = "api-wrappers";
|
||||
sourceTree = "<group>";
|
||||
@@ -293,6 +298,7 @@
|
||||
D04BAF13DFAA6930676D0492 /* Application.swift */,
|
||||
D04BA66B5B4143D2238F50B9 /* Applications.swift */,
|
||||
D04BA56E285C3FCDA52ED262 /* DispatchQueues.swift */,
|
||||
D04BA3915020FB9B34555D74 /* DebugProfile.swift */,
|
||||
);
|
||||
path = logic;
|
||||
sourceTree = "<group>";
|
||||
@@ -472,6 +478,8 @@
|
||||
D04BA40A1C8B02448D720EA3 /* BaseLabel.swift in Sources */,
|
||||
D04BAD5A6B2F9EEE6FD4185F /* CollectionViewItemTitle.swift in Sources */,
|
||||
D04BA6D9DA2A8BCD93347F0E /* CollectionViewItemFontIcon.swift in Sources */,
|
||||
D04BA9EE5D34A2789DCB0EE2 /* Sysctl.swift in Sources */,
|
||||
D04BAC4F69FE9563BC1C5E9C /* DebugProfile.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension AXUIElement {
|
||||
func cgWindowId() -> CGWindowID {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
typealias CGWindow = [CFString: Any]
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
extension CGWindowID {
|
||||
func title() -> String? {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
extension CGFloat {
|
||||
@@ -101,6 +100,7 @@ extension NSView {
|
||||
}
|
||||
|
||||
extension NSGridView {
|
||||
// set height for all rows
|
||||
func setRowsHeight(_ height: CGFloat) {
|
||||
for i in 0..<numberOfRows {
|
||||
row(at: i).height = height
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
// Private APIs are APIs that we can build the app against, but they are not supported or documented by Apple
|
||||
// We can see their names as symbols in the SDK (see https://github.com/lwouis/MacOSX-SDKs)
|
||||
@@ -231,8 +230,8 @@ func SLSRequestScreenCaptureAccess() -> UInt8
|
||||
//func CGSProcessAssignToAllSpaces(_ cid: CGSConnectionID, _ pid: pid_t) -> CGError
|
||||
//
|
||||
//enum SpaceManagementMode: Int {
|
||||
// case separate = 1
|
||||
// case notSeparate = 0
|
||||
// case checked = 1
|
||||
// case unchecked = 0
|
||||
//}
|
||||
//
|
||||
//// returns the status of the "Displays have separate Spaces" system Preference
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
public struct Sysctl {
|
||||
static func run(_ name: String) -> String {
|
||||
return run(name, { $0.baseAddress.flatMap { String(validatingUTF8: $0) } }) ?? ""
|
||||
}
|
||||
|
||||
static func run<T>(_ name: String, _ type: T.Type) -> T? {
|
||||
return run(name, { $0.baseAddress?.withMemoryRebound(to: T.self, capacity: 1) { $0.pointee } })
|
||||
}
|
||||
|
||||
private static func run<R>(_ name: String, _ fn: (UnsafeBufferPointer<Int8>) -> R?) -> R? {
|
||||
return keys(name).flatMap { keys in data(keys)?.withUnsafeBufferPointer() { fn($0) } }
|
||||
}
|
||||
|
||||
private static func data(_ keys: [Int32]) -> [Int8]? {
|
||||
return keys.withUnsafeBufferPointer() { keysPointer in
|
||||
var requiredSize = 0
|
||||
let preFlightResult = Darwin.sysctl(UnsafeMutablePointer<Int32>(mutating: keysPointer.baseAddress), UInt32(keys.count), nil, &requiredSize, nil, 0)
|
||||
if preFlightResult != 0 {
|
||||
return nil
|
||||
}
|
||||
let data = Array<Int8>(repeating: 0, count: requiredSize)
|
||||
let result = data.withUnsafeBufferPointer() { dataBuffer -> Int32 in
|
||||
return Darwin.sysctl(UnsafeMutablePointer<Int32>(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0)
|
||||
}
|
||||
if result != 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
private static func keys(_ name: String) -> [Int32]? {
|
||||
var keysBufferSize = Int(CTL_MAXNAME)
|
||||
var keysBuffer = Array<Int32>(repeating: 0, count: keysBufferSize)
|
||||
_ = keysBuffer.withUnsafeMutableBufferPointer { (lbp: inout UnsafeMutableBufferPointer<Int32>) in
|
||||
name.withCString { (nbp: UnsafePointer<Int8>) in
|
||||
sysctlnametomib(nbp, lbp.baseAddress, &keysBufferSize)
|
||||
}
|
||||
}
|
||||
if keysBuffer.count > keysBufferSize {
|
||||
keysBuffer.removeSubrange(keysBufferSize..<keysBuffer.count)
|
||||
}
|
||||
return keysBuffer
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Application: NSObject {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Applications {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import Cocoa
|
||||
import Darwin
|
||||
|
||||
class DebugProfile {
|
||||
static let intraSeparator = ": "
|
||||
static let interSeparator = ", "
|
||||
static let bulletPoint = "* "
|
||||
static let nestedSeparator = "\n " + bulletPoint
|
||||
|
||||
static func make() -> String {
|
||||
([
|
||||
// app
|
||||
("App version", App.version),
|
||||
("App preferences", appPreferences()),
|
||||
("Applications count", String(Applications.list.count)),
|
||||
("Windows", appWindows()),
|
||||
// os
|
||||
("OS version", ProcessInfo.processInfo.operatingSystemVersionString),
|
||||
("OS architecture", Sysctl.run("hw.machine")),
|
||||
("Locale", Locale.current.debugDescription),
|
||||
("Spaces count", String((CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary]).map { $0["Spaces"] }.count)),
|
||||
// hardware
|
||||
("Hardware model", Sysctl.run("hw.model")),
|
||||
("Displays count", String(NSScreen.screens.count)),
|
||||
("CPU model", Sysctl.run("machdep.cpu.brand_string")),
|
||||
("Memory size", ByteCountFormatter.string(fromByteCount: Int64(ProcessInfo.processInfo.physicalMemory), countStyle: .file)),
|
||||
// TODO: add gpu model(s)
|
||||
// hardware utilization
|
||||
("Active CPU count", Sysctl.run("hw.activecpu", UInt.self).flatMap { String($0) } ?? ""),
|
||||
("Current CPU frequency", Sysctl.run("hw.cpufrequency", Int.self).map { String(format: "%.1f", Double($0) / Double(1_000_000_000)) + " Ghz" } ?? ""),
|
||||
// TODO: CPU utilization
|
||||
// TODO: Active GPU
|
||||
// TODO: GPU utilization
|
||||
// TODO: Memory utilization
|
||||
// TODO: disk space to detect disk pressure
|
||||
// TODO: thermals to check if overheating
|
||||
// TODO: battery to check if low-energy mode / throttling
|
||||
|
||||
] as [(String, String)])
|
||||
.map { bulletPoint + $0.0 + intraSeparator + $0.1 }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func appPreferences() -> String {
|
||||
return nestedSeparator + Preferences.rawValues
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { $0 + intraSeparator + $1 }
|
||||
.joined(separator: nestedSeparator)
|
||||
}
|
||||
|
||||
private static func appWindows() -> String {
|
||||
return nestedSeparator + Windows.list
|
||||
.sorted { $0.cgWindowId < $1.cgWindowId }
|
||||
.map { appWindow($0) }
|
||||
.joined(separator: nestedSeparator)
|
||||
}
|
||||
|
||||
private static func appWindow(_ window: Window) -> String {
|
||||
return "{" + ([
|
||||
("isMinimized", String(window.isMinimized)),
|
||||
("isHidden", String(window.isHidden)),
|
||||
("isOnAllSpaces", String(window.isOnAllSpaces)),
|
||||
("spaceId", window.spaceId.flatMap { String($0) } ?? ""),
|
||||
("spaceIndex", window.spaceIndex.flatMap { String($0) } ?? ""),
|
||||
] as [(String, String)])
|
||||
.map { $0.0 + intraSeparator + $0.1 }
|
||||
.joined(separator: interSeparator)
|
||||
+ "}"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Screen {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class Spaces {
|
||||
static var currentSpaceId = CGSSpaceID(1)
|
||||
@@ -31,7 +30,7 @@ class Spaces {
|
||||
|
||||
static func allIdsAndIndexes() -> [(CGSSpaceID, SpaceIndex)] {
|
||||
return (CGSCopyManagedDisplaySpaces(cgsMainConnectionId) as! [NSDictionary])
|
||||
.map { return $0["Spaces"] }.joined().enumerated()
|
||||
.map { $0["Spaces"] }.joined().enumerated()
|
||||
.map { (($0.element as! NSDictionary)["id64"]! as! CGSSpaceID, $0.offset + 1) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
// macOS has some privacy restrictions. The user needs to grant certain permissions, app by app, in System Preferences > Security & Privacy
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class Window {
|
||||
var cgWindowId: CGWindowID
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class Windows {
|
||||
// order in the array is important: most-recently-used elements are first
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import Darwin
|
||||
|
||||
let cgsMainConnectionId = CGSMainConnectionID()
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class FeedbackWindow: NSWindow, NSTextViewDelegate {
|
||||
var body: TextArea!
|
||||
var email: TextArea!
|
||||
var sendButton: NSButton!
|
||||
var debugProfile: NSButton!
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
super.init(contentRect: .zero, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
@@ -47,14 +47,16 @@ class FeedbackWindow: NSWindow, NSTextViewDelegate {
|
||||
body = TextArea(80, 20, "I think the app could be improved with…")
|
||||
body.delegate = self
|
||||
email = TextArea(80, 1.1, "Optional: email (if you want a reply)")
|
||||
debugProfile = NSButton(checkboxWithTitle: "Send debug profile", target: nil, action: nil)
|
||||
debugProfile.state = .on
|
||||
let view = GridView.make([
|
||||
[header],
|
||||
[body],
|
||||
[email],
|
||||
[debugProfile],
|
||||
[buttons],
|
||||
])
|
||||
view.cell(atColumnIndex: 0, rowIndex: 3).xPlacement = .trailing
|
||||
view.fit()
|
||||
view.cell(atColumnIndex: 0, rowIndex: 4).xPlacement = .trailing
|
||||
setContentSize(view.fittingSize)
|
||||
contentView = view
|
||||
}
|
||||
@@ -70,24 +72,40 @@ class FeedbackWindow: NSWindow, NSTextViewDelegate {
|
||||
|
||||
@objc
|
||||
private func sendCallback(senderControl: NSControl) {
|
||||
URLSession.shared.dataTask(with: prepareRequest(), completionHandler: { data, response, error in
|
||||
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
|
||||
debugPrint("HTTP call failed:", response ?? "nil", error ?? "nil")
|
||||
}
|
||||
}).resume()
|
||||
close()
|
||||
}
|
||||
|
||||
private func prepareRequest() -> URLRequest {
|
||||
var request = URLRequest(url: URL(string: "https://api.github.com/repos/lwouis/alt-tab-macos/issues")!)
|
||||
request.httpMethod = "POST"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.addValue("application/json", forHTTPHeaderField: "Accept")
|
||||
// access token of the alt-tab-macos-bot github account, with scope repo > public_repo
|
||||
request.addValue("token 6ab65e11bb51e47835fe1f64970b1d7df0341653", forHTTPHeaderField: "Authorization")
|
||||
let preamble = "_This issue was opened by a bot after a user submitted feedback through the in-app form. Here is what they wrote:_\n\n> "
|
||||
let emailNote = !email.string.isEmpty ? "\n\nAuthor's email: " + email.string : ""
|
||||
let parameters: [String: Any] = [
|
||||
request.addValue("token 231413d7bf0e6cc533aae851c83dca25afed86bb", forHTTPHeaderField: "Authorization")
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: [
|
||||
"title": "[In-app feedback]",
|
||||
"body": preamble + body.string.replacingOccurrences(of: "\n", with: "\n> ") + emailNote
|
||||
]
|
||||
request.httpBody = try! JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
|
||||
URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
|
||||
if error != nil || response == nil || (response as! HTTPURLResponse).statusCode != 201 {
|
||||
debugPrint("HTTP call failed:", response, error)
|
||||
}
|
||||
}).resume()
|
||||
close()
|
||||
"body": assembleBody()
|
||||
])
|
||||
return request
|
||||
}
|
||||
|
||||
private func assembleBody() -> String {
|
||||
var result = ""
|
||||
result += "_This issue was opened by a bot after a user submitted feedback through the in-app form._"
|
||||
if !email.string.isEmpty {
|
||||
result += "\n\n__From:__ " + email.string
|
||||
}
|
||||
result += "\n\n__Message:__"
|
||||
result += "\n\n> " + body.string.replacingOccurrences(of: "\n", with: "\n> ")
|
||||
if debugProfile.state == .on {
|
||||
result += "\n\n__Debug profile:__"
|
||||
result += "\n\n" + DebugProfile.make()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class GridView {
|
||||
static let padding = CGFloat(20)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TextArea: NSTextView {
|
||||
static let paddingX = CGFloat(5)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class LabelAndControl: NSObject {
|
||||
static var callbackTarget: PreferencesWindow!
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class PreferencesWindow: NSWindow {
|
||||
var windowCloseRequested = false
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TabViewController: NSTabViewController {
|
||||
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TabViewItem {
|
||||
static func make(_ label: String, _ imageNAme: NSImage.Name, _ view: NSView) -> NSTabViewItem {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class AboutTab: NSObject {
|
||||
static func make() -> NSTabViewItem {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class AppearanceTab {
|
||||
private static let rowHeight = CGFloat(20)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class ShortcutsTab {
|
||||
private static let rowHeight = CGFloat(20)
|
||||
|
||||
Reference in new issue
Block a user