Remove license and update about window.

This commit is contained in:
2025-01-15 17:11:18 -08:00
parent e1146840f6
commit d31763e66f
5 changed files with 115 additions and 377 deletions

View File

@@ -9,12 +9,13 @@ import AppKit
// MARK: - Constants
fileprivate enum AboutLinks {
static let privacyLink = "https://cmdbar.app/#privacy-policy"
static let purchaseLink = "https://cmdbar.app/purchase"
static let website = "https://cmdbar.app"
static let documentation = "https://cmdbar.app/documentation"
static let privacy = "https://cmdbar.app/#privacy-policy"
}
enum Strings {
static let copyright = "Copyright © 2024 GarikMI. All rights reserved."
static let copyright = "Copyright © 2024\nGarikMI. All rights reserved."
static let evaluationTitle = "License - Evaluation"
static let evaluationMessage = "You are currently using evaluation license. CmdBar will quit after 20 minutes. If you already own a license, enter it below or purchase a license."
static let activate = "Activate"
@@ -30,7 +31,8 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appIconImage: NSImageView = {
//let image = NSImageView(image: NSApp.applicationIconImage)
let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.image = NSWorkspace.shared
.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false
return image
@@ -38,12 +40,16 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
private var appNameLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = (Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "NOT FOUND"
textField.stringValue =
(Bundle.main.infoDictionary?["CFBundleName"] as? String) ??
"NOT FOUND"
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold)
textField.alignment = .center
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .title1).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
@@ -54,9 +60,11 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.alignment = .center
textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
@@ -69,52 +77,11 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.alignment = .center
textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var licenseLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = Strings.evaluationTitle
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .title2).pointSize, weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var licenseInfoLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = Strings.evaluationMessage
textField.cell?.truncatesLastVisibleLine = true
textField.maximumNumberOfLines = 6
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.textColor = NSColor.systemGray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .subheadline).pointSize, weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var emailInput: EditableNSTextField = {
let textField = EditableNSTextField()
textField.placeholderString = "Email"
textField.bezelStyle = .roundedBezel
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var licenseKeyInput: EditableNSTextField = {
let textField = EditableNSTextField()
textField.placeholderString = "License Key"
textField.bezelStyle = .roundedBezel
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .subheadline).pointSize,
weight: .regular)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
@@ -129,73 +96,46 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
return button
}()
private var closeButton: NSButton = {
private var documentationButton: NSButton = {
let button = NSButton()
button.title = "Close"
button.title = "Docs"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(close)
button.action = #selector(documentation)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var purchaseButton: NSButton = {
private var websiteButton: NSButton = {
let button = NSButton()
button.title = "Purchase"
button.title = "CmdBar.app"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(purchase)
button.action = #selector(website)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var activateButton: NSButton = {
let button = NSButton()
button.isEnabled = false
button.title = "Activate"
button.keyEquivalent = "\r"
button.sizeToFit()
button.bezelStyle = .rounded
button.action = #selector(activate)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private var dividerBox: NSBox = {
let box = NSBox()
box.boxType = .separator
box.translatesAutoresizingMaskIntoConstraints = false
return box
private var buttonsContainer: NSLayoutGuide = {
let container = NSLayoutGuide()
return container
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Input delegates
emailInput.delegate = self
licenseKeyInput.delegate = self
// Program info
view.addSubview(appIconImage)
view.addSubview(appNameLabel)
view.addSubview(versionLabel)
view.addSubview(copyrightLabel)
// Buttons
view.addLayoutGuide(buttonsContainer)
view.addSubview(privacyButton)
// Divider
view.addSubview(dividerBox)
// License
view.addSubview(licenseLabel)
view.addSubview(licenseInfoLabel)
view.addSubview(emailInput)
view.addSubview(licenseKeyInput)
// Action buttons
view.addSubview(closeButton)
view.addSubview(purchaseButton)
view.addSubview(activateButton)
view.addSubview(documentationButton)
view.addSubview(websiteButton)
setupConstraints()
}
@@ -203,7 +143,6 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.center()
checkLicense()
}
override func loadView() {
@@ -214,155 +153,88 @@ class AboutViewController: NSViewController, NSTextFieldDelegate {
private func setupConstraints() {
// View.
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: 600),
view.widthAnchor.constraint(equalToConstant: 300),
view.heightAnchor.constraint(lessThanOrEqualToConstant: 500),
])
// App image.
NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 60),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing20),
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
appIconImage.widthAnchor.constraint(equalToConstant: 100),
appIconImage.heightAnchor
.constraint(equalTo: appIconImage.widthAnchor,
multiplier: 1),
appIconImage.topAnchor
.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing20),
appIconImage.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
])
// Title
NSLayoutConstraint.activate([
appNameLabel.topAnchor.constraint(equalTo: appIconImage.topAnchor),
appNameLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10),
appNameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
appNameLabel.topAnchor
.constraint(equalTo: appIconImage.bottomAnchor,
constant: ViewConstants.spacing20),
appNameLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
versionLabel.topAnchor.constraint(equalTo: appNameLabel.bottomAnchor, constant: ViewConstants.spacing2),
versionLabel.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
versionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
versionLabel.topAnchor
.constraint(equalTo: appNameLabel.bottomAnchor,
constant: ViewConstants.spacing2),
versionLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
copyrightLabel.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: ViewConstants.spacing2),
copyrightLabel.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
copyrightLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
privacyButton.topAnchor.constraint(equalTo: copyrightLabel.bottomAnchor, constant: ViewConstants.spacing10),
privacyButton.leadingAnchor.constraint(equalTo: appNameLabel.leadingAnchor),
copyrightLabel.topAnchor
.constraint(equalTo: versionLabel.bottomAnchor,
constant: ViewConstants.spacing10),
copyrightLabel.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
])
// Buttons
NSLayoutConstraint.activate([
dividerBox.topAnchor.constraint(equalTo: privacyButton.bottomAnchor, constant: ViewConstants.spacing10),
dividerBox.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
dividerBox.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
])
buttonsContainer.topAnchor
.constraint(equalTo: copyrightLabel.bottomAnchor,
constant: ViewConstants.spacing20),
buttonsContainer.bottomAnchor
.constraint(equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing20),
buttonsContainer.centerXAnchor
.constraint(equalTo: view.centerXAnchor),
// License labels.
NSLayoutConstraint.activate([
licenseLabel.topAnchor.constraint(equalTo: dividerBox.bottomAnchor, constant: ViewConstants.spacing10),
licenseLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing20),
licenseLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
privacyButton.topAnchor
.constraint(equalTo: buttonsContainer.topAnchor),
privacyButton.bottomAnchor
.constraint(equalTo: buttonsContainer.bottomAnchor),
privacyButton.leadingAnchor
.constraint(equalTo: buttonsContainer.leadingAnchor),
licenseInfoLabel.topAnchor.constraint(equalTo: licenseLabel.bottomAnchor, constant: ViewConstants.spacing2),
licenseInfoLabel.leadingAnchor.constraint(equalTo: licenseLabel.leadingAnchor),
licenseInfoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
])
documentationButton.firstBaselineAnchor
.constraint(equalTo: privacyButton.firstBaselineAnchor),
documentationButton.leadingAnchor
.constraint(equalTo: privacyButton.trailingAnchor,
constant: ViewConstants.spacing10),
// License inputs.
NSLayoutConstraint.activate([
emailInput.topAnchor.constraint(equalTo: licenseInfoLabel.bottomAnchor, constant: ViewConstants.spacing10),
emailInput.leadingAnchor.constraint(equalTo: licenseInfoLabel.leadingAnchor),
emailInput.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
licenseKeyInput.topAnchor.constraint(equalTo: emailInput.bottomAnchor, constant: ViewConstants.spacing10),
licenseKeyInput.leadingAnchor.constraint(equalTo: licenseInfoLabel.leadingAnchor),
licenseKeyInput.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
])
// License buttons.
NSLayoutConstraint.activate([
activateButton.topAnchor.constraint(equalTo: licenseKeyInput.bottomAnchor, constant: ViewConstants.spacing20),
activateButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -ViewConstants.spacing20),
activateButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -ViewConstants.spacing20),
purchaseButton.trailingAnchor.constraint(equalTo: activateButton.leadingAnchor, constant: -ViewConstants.spacing10),
purchaseButton.lastBaselineAnchor.constraint(equalTo: activateButton.lastBaselineAnchor),
closeButton.trailingAnchor.constraint(equalTo: purchaseButton.leadingAnchor, constant: -ViewConstants.spacing10),
closeButton.lastBaselineAnchor.constraint(equalTo: purchaseButton.lastBaselineAnchor),
websiteButton.firstBaselineAnchor
.constraint(equalTo: privacyButton.firstBaselineAnchor),
websiteButton.leadingAnchor
.constraint(equalTo: documentationButton.trailingAnchor,
constant: ViewConstants.spacing10),
websiteButton.trailingAnchor
.constraint(equalTo: buttonsContainer.trailingAnchor),
])
}
// MARK: - Button Functions
@objc private func privacy() {
NSWorkspace.shared.open(URL(string: AboutLinks.privacyLink)!)
NSWorkspace.shared.open(URL(string: AboutLinks.privacy)!)
}
@objc private func close() {
view.window?.orderOut(nil)
@objc private func documentation() {
NSWorkspace.shared.open(URL(string: AboutLinks.documentation)!)
}
@objc private func purchase() {
guard let url = URL(string: AboutLinks.purchaseLink) else { return }
NSWorkspace.shared.open(url)
}
@objc private func activate() {
let email = emailInput.stringValue
let key = licenseKeyInput.stringValue
if email.isEmpty || key.isEmpty { return }
activateButton.isEnabled = false
activateButton.title = Strings.activating
// TODO: Notify the user if registration failed.
LicenseManager.standard.activateLicense(email: email, key: key) { [weak self] _ in
DispatchQueue.main.async { self?.checkLicense() }
}
}
@objc private func deactivate() {
LicenseManager.standard.deactivateLicense()
checkLicense()
}
private func checkLicense() {
if LicenseManager.standard.isRegistered() {
activateButton.isEnabled = true
activateButton.title = Strings.deactivate
activateButton.action = #selector(deactivate)
licenseLabel.stringValue = Strings.proTitle
licenseInfoLabel.stringValue = Strings.proMessage
emailInput.isEnabled = false
licenseKeyInput.isEnabled = false
let license = LicenseManager.standard.getLicense()
emailInput.stringValue = license.email
licenseKeyInput.stringValue =
"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXX" +
license.key.dropFirst(license.key.count - 5)
} else {
activateButton.isEnabled = false
activateButton.title = Strings.activate
activateButton.action = #selector(activate)
licenseLabel.stringValue = Strings.evaluationTitle
licenseInfoLabel.stringValue = Strings.evaluationMessage
emailInput.isEnabled = true
licenseKeyInput.isEnabled = true
emailInput.stringValue = ""
licenseKeyInput.stringValue = ""
emailInput.becomeFirstResponder()
}
#if(DEBUG)
emailInput.stringValue = "igorkolokolnikov@icloud.com"
licenseKeyInput.stringValue = "137A5010-A95A-4A8C-A037-6DF16B6CA666"
#endif
}
}
extension AboutViewController {
func controlTextDidChange(_ obj: Notification) {
activateButton.isEnabled = !(emailInput.stringValue.isEmpty || licenseKeyInput.stringValue.isEmpty)
@objc private func website() {
NSWorkspace.shared.open(URL(string: AboutLinks.website)!)
}
}

View File

@@ -12,28 +12,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
setupUpdateWindow()
setupNotifications()
setupLicense()
CmdManager.standard.configure()
}
private func setupLicense() {
let licenseManager = LicenseManager.standard
if !licenseManager.isRegistered() {
showAbout()
licenseManager.clearLicense()
licenseManager.startTimer()
}
}
private func setupUpdateWindow() {
let controller = UpdateViewController()
controller.setUpdateDelegate()
updateWindow = MenulessWindow(viewController: controller)
updateWindow.title = "Software Update"
}
func applicationWillTerminate(_ notification: Notification) {
persistMenuBar(false)
CmdManager.standard.clearItems()
@@ -72,6 +53,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Setup About Window
private func setupAboutWindow() {
aboutWindow = MenulessWindow(viewController: AboutViewController())
aboutWindow.level = .statusBar
}
private func setupUpdateWindow() {
let controller = UpdateViewController()
controller.setUpdateDelegate()
updateWindow = MenulessWindow(viewController: controller)
updateWindow.title = "Software Update"
updateWindow.level = .statusBar
}
// MARK: - Show About Window

View File

@@ -1,129 +0,0 @@
//
// LicenseManager.swift
// CmdBar
//
// Created by Igor Kolokolnikov on 8/26/23.
//
import AppKit
import CryptoKit
fileprivate struct LicenseModel: Codable {
let valid: Bool
let error: String
let email: String
let key: String
}
// Converts string to a hash.
fileprivate func hashString(_ string: String) -> String {
return SHA256.hash(data: Data(string.utf8)).compactMap { String(format: "%02x", $0) }.joined()
}
// Converts passphrase to a SymmetricKey.
fileprivate func passToKey(from passphrase: String) -> SymmetricKey {
let passphraseData = Data(passphrase.utf8)
let keyData = SHA256.hash(data: passphraseData).withUnsafeBytes { buffer in
return Data(buffer)
}
return SymmetricKey(data: keyData)
}
final class LicenseManager {
static let standard = LicenseManager()
private let passkey = passToKey(from: "B2B67FB0E22B814CE5A6E7DA729A068BF21C9490919EA0B42FCEB322F9C9BA82")
private let defaults = UserDefaults.standard
private var timer: DispatchSourceTimer?
private var quitAfterMinutes = 20.0 * 60.0 // quit after 20 minutes
func isRegistered() -> Bool {
guard let email = defaults.object(forKey: "Email") as? String,
let key = defaults.object(forKey: "Key") as? String,
let licenseData = defaults.object(forKey: "License") as? Data,
let unsealedBox = try? AES.GCM.SealedBox(combined: licenseData),
let licenseHashData = try? AES.GCM.open(unsealedBox, using: passkey),
!email.isEmpty,
!key.isEmpty
else { return false }
let currentHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
.compactMap { String(format: "%02x", $0) }.joined().utf8)
return licenseHashData == currentHashData
}
func activateLicense(email: String, key: String, completionHandler: @escaping (_ data: Resulting<Bool, String>) -> Void) {
guard let url = URL(string: EndpointConstants.validateURL)?.appending(component: key)
else { completionHandler(.failure); return }
URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
guard
let data = data,
error == nil,
let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode)
else { completionHandler(.failure); return }
guard let jsonResponse = try? JSONDecoder().decode(LicenseModel.self, from: data)
else { completionHandler(.failure); return }
if jsonResponse.email == email && jsonResponse.key == key {
self?.saveLicense(with: email, and: key)
self?.stopTimer()
completionHandler(.success)
} else {
completionHandler(.failure)
}
}.resume()
}
func deactivateLicense() {
clearLicense()
startTimer()
}
func getLicense() -> (email: String, key: String) {
guard
let email = defaults.object(forKey: "Email") as? String,
let key = defaults.object(forKey: "Key") as? String
else { return ("", "") }
return (email, key)
}
private func saveLicense(with email: String, and key: String) {
let defaults = UserDefaults.standard
let licenseHashData = Data(SHA256.hash(data: Data(hashString("\(email)\(key)\(NSUserName())").utf8))
.compactMap { String(format: "%02x", $0) }.joined().utf8)
guard let sealedBoxData = try? AES.GCM.seal(licenseHashData, using: passkey).combined else { return }
defaults.set(email, forKey: "Email")
defaults.set(key, forKey: "Key")
defaults.set(sealedBoxData, forKey: "License")
}
func clearLicense() {
defaults.set(nil, forKey: "Email")
defaults.set(nil, forKey: "Key")
defaults.set(nil, forKey: "License")
}
// MARK: - Timer
func startTimer() {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(
deadline: .now() + quitAfterMinutes,
repeating: DispatchTimeInterval.never
)
timer?.setEventHandler {
DispatchQueue.main.async { NSApplication.shared.terminate(self) }
}
timer?.resume()
}
func stopTimer() {
timer?.cancel()
timer = nil
}
}

View File

@@ -7,12 +7,12 @@ XCODE_PATH = $(shell xcode-select --print-path)
EXEC = CmdBar
SRCMODULES = UpdateManager.swift LicenseManager.swift \
AboutViewController.swift AppDelegate.swift CBMenuBarItem.swift \
CircularProgressView.swift CmdManager.swift CmdViewController.swift \
CopyableTextView.swift EditableNSTextField.swift EventMonitor.swift \
Helpers.swift MenulessWindow.swift PopoverPanel.swift \
QuietView.swift UpdateViewController.swift main.swift
SRCMODULES = UpdateManager.swift AboutViewController.swift \
AppDelegate.swift CBMenuBarItem.swift CircularProgressView.swift \
CmdManager.swift CmdViewController.swift CopyableTextView.swift \
EditableNSTextField.swift EventMonitor.swift Helpers.swift \
MenulessWindow.swift PopoverPanel.swift QuietView.swift \
UpdateViewController.swift main.swift
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))

View File

@@ -1,4 +1,5 @@
import AppKit
import Carbon
class MenulessWindow: NSWindow {
init(viewController: NSViewController) {
@@ -18,14 +19,17 @@ class MenulessWindow: NSWindow {
}
override func performKeyEquivalent(with event: NSEvent) -> Bool {
let commandKey = NSEvent.ModifierFlags.command.rawValue
let modifiers = event.modifierFlags.rawValue
let key = event.keyCode
if event.type == NSEvent.EventType.keyDown {
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey,
event.charactersIgnoringModifiers! == "w" {
if modsContains(keys: OSCmd, in: modifiers) &&
key == kVK_ANSI_W
{
performClose(nil)
}
}
return super.performKeyEquivalent(with: event)
}
}