diff --git a/Gray.xcodeproj/project.pbxproj b/Gray.xcodeproj/project.pbxproj index cf56964..737cfb6 100644 --- a/Gray.xcodeproj/project.pbxproj +++ b/Gray.xcodeproj/project.pbxproj @@ -41,6 +41,11 @@ BDB20F2B2167A9DB00A72623 /* SearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB20F2A2167A9DB00A72623 /* SearchField.swift */; }; BDBFB09421D0DDB20086F697 /* ApplicationsLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBFB09321D0DDB20086F697 /* ApplicationsLoadingViewController.swift */; }; BDC14C6F21B4502900610983 /* AlertsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC14C6E21B4502900610983 /* AlertsController.swift */; }; + BDE5736022105FDF0065597B /* ApplicationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE5735F22105FDF0065597B /* ApplicationListView.swift */; }; + BDE57364221093440065597B /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE57363221093440065597B /* Component.swift */; }; + BDE57366221098170065597B /* ViewToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE57365221098170065597B /* ViewToolbarItem.swift */; }; + BDE573682210A2CB0065597B /* AppearanceAware.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE573672210A2CB0065597B /* AppearanceAware.swift */; }; + BDE5736D2210A9850065597B /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE5736C2210A9850065597B /* UserDefaults.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -86,6 +91,11 @@ BDB20F2A2167A9DB00A72623 /* SearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchField.swift; sourceTree = ""; }; BDBFB09321D0DDB20086F697 /* ApplicationsLoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationsLoadingViewController.swift; sourceTree = ""; }; BDC14C6E21B4502900610983 /* AlertsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsController.swift; sourceTree = ""; }; + BDE5735F22105FDF0065597B /* ApplicationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationListView.swift; sourceTree = ""; }; + BDE57363221093440065597B /* Component.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = ""; }; + BDE57365221098170065597B /* ViewToolbarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewToolbarItem.swift; sourceTree = ""; }; + BDE573672210A2CB0065597B /* AppearanceAware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceAware.swift; sourceTree = ""; }; + BDE5736C2210A9850065597B /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -122,6 +132,7 @@ children = ( BDC14C6D21B4501B00610983 /* Alerts */, BD30ED4C216C011F00FD2D91 /* Application */, + BDE5736B2210A9780065597B /* Extensions */, BD30ED4B216C010400FD2D91 /* Features */, BDAAC490216A2A810093B27D /* Shared */, BD6F243621620A570032338F /* Utilities */, @@ -246,11 +257,14 @@ BD9D3D79215C2D3F00233333 /* Applications */ = { isa = PBXGroup; children = ( + BDE573672210A2CB0065597B /* AppearanceAware.swift */, BD750FDB215C1AC60024E70A /* Application.swift */, BD1BA9BF215E6DBB0052633B /* ApplicationGridView.swift */, + BDE5735F22105FDF0065597B /* ApplicationListView.swift */, BD9D3D7E215C30B000233333 /* ApplicationsFeatureViewController.swift */, BDBFB09321D0DDB20086F697 /* ApplicationsLoadingViewController.swift */, BD308449215C198D002A9349 /* ApplicationsLogicController.swift */, + BDE57363221093440065597B /* Component.swift */, ); path = Applications; sourceTree = ""; @@ -288,6 +302,7 @@ BDB20F252167A64100A72623 /* Toolbar.swift */, BDB20F282167A73C00A72623 /* SearchToolbarItem.swift */, BDB20F2A2167A9DB00A72623 /* SearchField.swift */, + BDE57365221098170065597B /* ViewToolbarItem.swift */, ); path = Toolbar; sourceTree = ""; @@ -300,6 +315,14 @@ path = Alerts; sourceTree = ""; }; + BDE5736B2210A9780065597B /* Extensions */ = { + isa = PBXGroup; + children = ( + BDE5736C2210A9850065597B /* UserDefaults.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -450,6 +473,7 @@ buildActionMask = 2147483647; files = ( BD2349312207446D0047F910 /* StatefulItem.swift in Sources */, + BDE57364221093440065597B /* Component.swift in Sources */, BDB20F2B2167A9DB00A72623 /* SearchField.swift in Sources */, BDB20F292167A73C00A72623 /* SearchToolbarItem.swift in Sources */, BD23492D220744330047F910 /* ViewControllerFactory-macOS.generated.swift in Sources */, @@ -461,13 +485,17 @@ BD67CE1C215BF4FE00216FDB /* MainContainerViewController.swift in Sources */, BD23492E220744330047F910 /* CollectionViewItemComponent-macOS.generated.swift in Sources */, BD67CE0E215BF15F00216FDB /* AppDelegate.swift in Sources */, + BDE57366221098170065597B /* ViewToolbarItem.swift in Sources */, BD30844A215C198D002A9349 /* ApplicationsLogicController.swift in Sources */, BDAAC492216A2A920093B27D /* CollectionViewHeader.swift in Sources */, BD1D9545215E435F003ABBCF /* VersionController.swift in Sources */, + BDE5736022105FDF0065597B /* ApplicationListView.swift in Sources */, BD9F0C462166764E00608FD9 /* SystemPreference.swift in Sources */, BDC14C6F21B4502900610983 /* AlertsController.swift in Sources */, BDB20F262167A64100A72623 /* Toolbar.swift in Sources */, + BDE5736D2210A9850065597B /* UserDefaults.swift in Sources */, BD9F0C4C2166773300608FD9 /* SystemPreferenceView.swift in Sources */, + BDE573682210A2CB0065597B /* AppearanceAware.swift in Sources */, BD9F0C43216675F300608FD9 /* SystemPreferenceFeatureViewController.swift in Sources */, BD23492F220744330047F910 /* StatefulItem-macOS.generated.swift in Sources */, BD2349302207446D0047F910 /* CollectionViewItemComponent.swift in Sources */, diff --git a/Podfile.lock b/Podfile.lock index fa5dae9..c330aa1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -29,4 +29,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9a4a4208e8595d7fa51cbc758521284dec9359cc -COCOAPODS: 1.6.0.rc.2 +COCOAPODS: 1.6.0 diff --git a/Resources/Assets.xcassets/Grid.imageset/Contents.json b/Resources/Assets.xcassets/Grid.imageset/Contents.json new file mode 100644 index 0000000..f654343 --- /dev/null +++ b/Resources/Assets.xcassets/Grid.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Grid.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true, + "auto-scaling" : "auto" + } +} \ No newline at end of file diff --git a/Resources/Assets.xcassets/Grid.imageset/Grid.pdf b/Resources/Assets.xcassets/Grid.imageset/Grid.pdf new file mode 100644 index 0000000..d098ddd Binary files /dev/null and b/Resources/Assets.xcassets/Grid.imageset/Grid.pdf differ diff --git a/Resources/Assets.xcassets/List.imageset/Contents.json b/Resources/Assets.xcassets/List.imageset/Contents.json new file mode 100644 index 0000000..f1f5159 --- /dev/null +++ b/Resources/Assets.xcassets/List.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "List.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true, + "auto-scaling" : "auto" + } +} \ No newline at end of file diff --git a/Resources/Assets.xcassets/List.imageset/List.pdf b/Resources/Assets.xcassets/List.imageset/List.pdf new file mode 100644 index 0000000..2368dad Binary files /dev/null and b/Resources/Assets.xcassets/List.imageset/List.pdf differ diff --git a/Resources/Base.lproj/MainMenu.xib b/Resources/Base.lproj/MainMenu.xib index b3dd971..f5bd2d7 100644 --- a/Resources/Base.lproj/MainMenu.xib +++ b/Resources/Base.lproj/MainMenu.xib @@ -1,7 +1,7 @@ - + - + @@ -470,6 +470,16 @@ + + + + + + + + + + diff --git a/Sources/Application/AppDelegate.swift b/Sources/Application/AppDelegate.swift index e2e1022..ad5c5f0 100644 --- a/Sources/Application/AppDelegate.swift +++ b/Sources/Application/AppDelegate.swift @@ -29,7 +29,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let dependencyContainer = DependencyContainer() let contentViewController = MainContainerViewController(iconStore: dependencyContainer) let toolbar = Toolbar(identifier: .init("MainApplicationWindowToolbar")) - toolbar.searchDelegate = contentViewController + toolbar.toolbarDelegate = contentViewController let windowSize = CGSize(width: 400, height: 640) let window = NSWindow(contentViewController: contentViewController) window.setFrameAutosaveName(NSWindow.FrameAutosaveName.init("MainApplicationWindow")) @@ -64,6 +64,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Actions + @IBAction func switchToGrid(_ sender: Any?) { + guard let toolbar = window?.toolbar as? Toolbar else { return } + (window?.contentViewController as? MainContainerViewController)?.toolbar(toolbar, + didChangeMode: ApplicationsFeatureViewController.Mode.grid.rawValue) + NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) + } + + @IBAction func switchToList(_ sender: Any?) { + guard let toolbar = window?.toolbar as? Toolbar else { return } + (window?.contentViewController as? MainContainerViewController)?.toolbar(toolbar, + didChangeMode: ApplicationsFeatureViewController.Mode.list.rawValue) + NotificationCenter.default.post(name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) + } + @IBAction func search(_ sender: Any?) { toolbar?.searchField?.becomeFirstResponder() } diff --git a/Sources/Application/LayoutFactory.swift b/Sources/Application/LayoutFactory.swift index eeac3f0..dacd075 100644 --- a/Sources/Application/LayoutFactory.swift +++ b/Sources/Application/LayoutFactory.swift @@ -11,4 +11,15 @@ class LayoutFactory { animator: DefaultLayoutAnimator(animation: .fade)) return layout } + + func createListLayout() -> VerticalBlueprintLayout { + let layout = VerticalBlueprintLayout( + itemsPerRow: 1.0, + height: 50, + minimumInteritemSpacing: 10, + minimumLineSpacing: 10, + sectionInset: .init(top: 0, left: 10, bottom: 20, right: 10), + animator: DefaultLayoutAnimator(animation: .fade)) + return layout + } } diff --git a/Sources/Application/Toolbar/Toolbar.swift b/Sources/Application/Toolbar/Toolbar.swift index 4f94ef1..38945c8 100644 --- a/Sources/Application/Toolbar/Toolbar.swift +++ b/Sources/Application/Toolbar/Toolbar.swift @@ -1,11 +1,12 @@ import Cocoa -protocol ToolbarSearchDelegate: class { +protocol ToolbarDelegate: class { func toolbar(_ toolbar: Toolbar, didSearchFor string: String) + func toolbar(_ toolbar: Toolbar, didChangeMode mode: String) } -class Toolbar: NSToolbar, NSToolbarDelegate { - weak var searchDelegate: ToolbarSearchDelegate? +class Toolbar: NSToolbar, NSToolbarDelegate, ViewToolbarItemDelegate { + weak var toolbarDelegate: ToolbarDelegate? weak var searchField: SearchField? override init(identifier: NSToolbar.Identifier) { @@ -19,19 +20,25 @@ class Toolbar: NSToolbar, NSToolbarDelegate { return [ NSToolbarItem.Identifier.space, NSToolbarItem.Identifier.flexibleSpace, + ViewToolbarItem.itemIdentifier, SearchToolbarItem.itemIdentifier ] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { return [ + ViewToolbarItem.itemIdentifier, NSToolbarItem.Identifier.flexibleSpace, - SearchToolbarItem.itemIdentifier + SearchToolbarItem.itemIdentifier, ] } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { switch itemIdentifier { + case ViewToolbarItem.itemIdentifier: + let viewToolbarItem = ViewToolbarItem() + viewToolbarItem.delegate = self + return viewToolbarItem case SearchToolbarItem.itemIdentifier: let searchToolbarItem = SearchToolbarItem(text: "") searchToolbarItem.titleLabel.target = self @@ -46,6 +53,12 @@ class Toolbar: NSToolbar, NSToolbarDelegate { } @objc func search(_ label: SearchField) { - searchDelegate?.toolbar(self, didSearchFor: label.stringValue) + toolbarDelegate?.toolbar(self, didSearchFor: label.stringValue) + } + + // MARK: - ViewToolbarItemDelegate + + func viewToolbarItem(_ toolbarItem: ViewToolbarItem, didChange mode: String) { + toolbarDelegate?.toolbar(self, didChangeMode: mode) } } diff --git a/Sources/Application/Toolbar/ViewToolbarItem.swift b/Sources/Application/Toolbar/ViewToolbarItem.swift new file mode 100644 index 0000000..35e58d9 --- /dev/null +++ b/Sources/Application/Toolbar/ViewToolbarItem.swift @@ -0,0 +1,63 @@ +import Cocoa + +protocol ViewToolbarItemDelegate: class { + func viewToolbarItem(_ toolbarItem: ViewToolbarItem, didChange mode: String) +} + +class ViewToolbarItem: NSToolbarItem { + static var itemIdentifier: NSToolbarItem.Identifier = .init("View") + + weak var delegate: ViewToolbarItemDelegate? + + lazy var segmentedControl = NSSegmentedControl.init(images: ApplicationsFeatureViewController.Mode.allCases.compactMap({ $0.image }), + trackingMode: .selectOne, + target: self, + action: #selector(didChangeView(_:))) + lazy var customView = NSView() + + init() { + super.init(itemIdentifier: ViewToolbarItem.itemIdentifier) + view = customView + view?.addSubview(segmentedControl) + minSize = .init(width: 80, height: 25) + maxSize = .init(width: 80, height: 25) + segmentedControl.setToolTip("Grid", forSegment: 0) + segmentedControl.setTag(0, forSegment: 0) + segmentedControl.setToolTip("List", forSegment: 1) + segmentedControl.setTag(1, forSegment: 1) + configureSegmentControl() + setupConstraints() + + NotificationCenter.default.addObserver(self, selector: #selector(configureSegmentControl), + name: NSNotification.Name.init(rawValue: "featureViewControllerMode"), object: nil) + } + + @objc func configureSegmentControl() { + if let mode = UserDefaults.standard.featureViewControllerMode { + switch mode { + case .grid: + segmentedControl.selectSegment(withTag: 0) + case .list: + segmentedControl.selectSegment(withTag: 1) + } + } else { + segmentedControl.selectSegment(withTag: 0) + } + } + + func setupConstraints() { + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + segmentedControl.centerXAnchor.constraint(equalTo: customView.centerXAnchor).isActive = true + segmentedControl.centerYAnchor.constraint(equalTo: customView.centerYAnchor).isActive = true + segmentedControl.widthAnchor.constraint(equalTo: customView.widthAnchor).isActive = true + segmentedControl.heightAnchor.constraint(equalToConstant: 25).isActive = true + } + + @objc func didChangeView(_ segmentControl: NSSegmentedControl) { + guard let label = segmentedControl.toolTip(forSegment: segmentedControl.indexOfSelectedItem) else { + return + } + + delegate?.viewToolbarItem(self, didChange: label) + } +} diff --git a/Sources/Extensions/UserDefaults.swift b/Sources/Extensions/UserDefaults.swift new file mode 100644 index 0000000..000be69 --- /dev/null +++ b/Sources/Extensions/UserDefaults.swift @@ -0,0 +1,13 @@ +import Foundation + +extension UserDefaults { + var featureViewControllerMode: ApplicationsFeatureViewController.Mode? { + get { + let rawValue = UserDefaults.standard.string(forKey: #function) ?? "" + return ApplicationsFeatureViewController.Mode.init(rawValue: rawValue) + } + set { + UserDefaults.standard.set(newValue?.rawValue, forKey: #function) + } + } +} diff --git a/Sources/Features/Applications/AppearanceAware.swift b/Sources/Features/Applications/AppearanceAware.swift new file mode 100644 index 0000000..b7de085 --- /dev/null +++ b/Sources/Features/Applications/AppearanceAware.swift @@ -0,0 +1,69 @@ +import Cocoa + +protocol AppearanceAware { + var titleLabel: NSTextField { get } + var subtitleLabel: NSTextField { get } + var view: NSView { get } + func update(with appearance: Application.Appearance, duration: TimeInterval, then handler: (() -> Void)?) +} + +extension AppearanceAware { + func update(with appearance: Application.Appearance, duration: TimeInterval = 0, then handler: (() -> Void)? = nil) { + if duration > 0 { + NSAnimationContext.current.allowsImplicitAnimation = true + NSAnimationContext.runAnimationGroup({ (context) in + context.duration = duration + switch appearance { + case .dark: + view.animator().layer?.backgroundColor = NSColor(named: "Dark")?.cgColor + titleLabel.animator().textColor = .white + subtitleLabel.animator().textColor = .controlAccentColor + view.layer?.borderWidth = 0.0 + case .system: + view.animator().layer?.backgroundColor = NSColor.gray.cgColor + titleLabel.animator().textColor = .white + subtitleLabel.animator().textColor = .lightGray + view.layer?.borderWidth = 0.0 + case .light: + view.animator().layer?.backgroundColor = .white + titleLabel.animator().textColor = .black + subtitleLabel.animator().textColor = .controlAccentColor + view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor + view.layer?.borderWidth = 0 + } + }, completionHandler:{ + handler?() + }) + } else { + switch appearance { + case .dark: + view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor + titleLabel.textColor = .white + subtitleLabel.textColor = .controlAccentColor + view.layer?.borderWidth = 0.0 + case .light: + view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor + titleLabel.textColor = .black + subtitleLabel.textColor = .controlAccentColor + view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor + view.layer?.borderWidth = 1.0 + case .system: + switch view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) { + case .darkAqua?: + view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor + titleLabel.textColor = .white + subtitleLabel.textColor = .lightGray + view.layer?.borderWidth = 0.0 + case .aqua?: + view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor + titleLabel.textColor = .black + subtitleLabel.textColor = .controlAccentColor + view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor + view.layer?.borderWidth = 1.0 + default: + break + } + } + } + } +} diff --git a/Sources/Features/Applications/Application.swift b/Sources/Features/Applications/Application.swift index 0a369bf..675947a 100644 --- a/Sources/Features/Applications/Application.swift +++ b/Sources/Features/Applications/Application.swift @@ -9,16 +9,19 @@ struct Application: Hashable { let bundleIdentifier: String let name: String + let metadata: String let url: URL let preferencesUrl: URL let appearance: Appearance let restricted: Bool init(bundleIdentifier: String, name: String, + metadata: String, url: URL, preferencesUrl: URL, appearance: Appearance, restricted: Bool) { self.bundleIdentifier = bundleIdentifier self.name = name + self.metadata = metadata self.url = url self.preferencesUrl = preferencesUrl self.appearance = appearance diff --git a/Sources/Features/Applications/ApplicationGridView.swift b/Sources/Features/Applications/ApplicationGridView.swift index e0b30ce..fd5ef16 100644 --- a/Sources/Features/Applications/ApplicationGridView.swift +++ b/Sources/Features/Applications/ApplicationGridView.swift @@ -6,8 +6,8 @@ protocol ApplicationGridViewDelegate: class { } // sourcery: let application = Application -class ApplicationGridView: NSCollectionViewItem, CollectionViewItemComponent { - override func loadView() { self.view = NSView(); self.view.wantsLayer = true } +class ApplicationGridView: NSCollectionViewItem, CollectionViewItemComponent, AppearanceAware { + lazy var baseView = NSView() weak var delegate: ApplicationGridViewDelegate? // sourcery: currentAppearance = model.application.appearance @@ -26,6 +26,11 @@ class ApplicationGridView: NSCollectionViewItem, CollectionViewItemComponent { // sourcery: let subtitle: String = "subtitleLabel.stringValue = model.subtitle" lazy var subtitleLabel = NSTextField() + override func loadView() { + self.view = baseView + baseView.wantsLayer = true + } + override func viewDidLoad() { super.viewDidLoad() @@ -80,64 +85,5 @@ class ApplicationGridView: NSCollectionViewItem, CollectionViewItemComponent { @objc func resetApplication() { delegate?.applicationView(self, didResetApplication: currentAppearance) } - - func update(with appearance: Application.Appearance, duration: TimeInterval = 0, then handler: (() -> Void)? = nil) { - if duration > 0 { - NSAnimationContext.current.allowsImplicitAnimation = true - NSAnimationContext.runAnimationGroup({ (context) in - context.duration = duration - switch appearance { - case .dark: - view.animator().layer?.backgroundColor = NSColor(named: "Dark")?.cgColor - titleLabel.animator().textColor = .white - subtitleLabel.animator().textColor = .controlAccentColor - view.layer?.borderWidth = 0.0 - case .system: - view.animator().layer?.backgroundColor = NSColor.gray.cgColor - titleLabel.animator().textColor = .white - subtitleLabel.animator().textColor = .lightGray - view.layer?.borderWidth = 0.0 - case .light: - view.animator().layer?.backgroundColor = .white - titleLabel.animator().textColor = .black - subtitleLabel.animator().textColor = .controlAccentColor - view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor - view.layer?.borderWidth = 0 - } - }, completionHandler:{ - handler?() - }) - } else { - switch appearance { - case .dark: - view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor - titleLabel.textColor = .white - subtitleLabel.textColor = .controlAccentColor - view.layer?.borderWidth = 0.0 - case .light: - view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor - titleLabel.textColor = .black - subtitleLabel.textColor = .controlAccentColor - view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor - view.layer?.borderWidth = 1.0 - case .system: - switch view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) { - case .darkAqua?: - view.layer?.backgroundColor = NSColor(named: "Dark")?.cgColor - titleLabel.textColor = .white - subtitleLabel.textColor = .lightGray - view.layer?.borderWidth = 0.0 - case .aqua?: - view.layer?.backgroundColor = NSColor(named: "Light")?.cgColor - titleLabel.textColor = .black - subtitleLabel.textColor = .controlAccentColor - view.layer?.borderColor = NSColor.gray.withAlphaComponent(0.25).cgColor - view.layer?.borderWidth = 1.0 - default: - break - } - } - } - } } diff --git a/Sources/Features/Applications/ApplicationListView.swift b/Sources/Features/Applications/ApplicationListView.swift new file mode 100644 index 0000000..136e6dc --- /dev/null +++ b/Sources/Features/Applications/ApplicationListView.swift @@ -0,0 +1,82 @@ +import Cocoa + +protocol ApplicationListViewDelegate: class { + func applicationView(_ view: ApplicationListView, didResetApplication currentAppearance: Application.Appearance?) +} + +// sourcery: let application = Application +class ApplicationListView: NSCollectionViewItem, CollectionViewItemComponent, AppearanceAware { + let baseView = NSView() + weak var delegate: ApplicationListViewDelegate? + + // sourcery: currentAppearance = model.application.appearance + var currentAppearance: Application.Appearance? { + didSet { + if let currentAppearance = self.currentAppearance { + update(with: currentAppearance) + } + } + } + + // sourcery: $RawBinding = "iconStore.loadIcon(for: model.application) { image in view.iconView.image = image }" + lazy var iconView: NSImageView = .init() + // sourcery: let title: String = "titleLabel.stringValue = model.title" + lazy var titleLabel: NSTextField = .init() + // sourcery: let subtitle: String = "subtitleLabel.stringValue = model.subtitle" + lazy var subtitleLabel: NSTextField = .init() + + private var layoutConstraints = [NSLayoutConstraint]() + + override func loadView() { + self.view = baseView + self.view.wantsLayer = true + } + + override func viewDidLoad() { + super.viewDidLoad() + + let menu = NSMenu() + menu.addItem(NSMenuItem(title: "Reset", action: #selector(resetApplication), keyEquivalent: "")) + view.menu = menu + + let verticalStackView = NSStackView(views: [titleLabel, subtitleLabel]) + verticalStackView.alignment = .leading + verticalStackView.orientation = .vertical + verticalStackView.spacing = 0 + let stackView = NSStackView(views: [iconView, verticalStackView]) + stackView.orientation = .horizontal + + titleLabel.isEditable = false + titleLabel.drawsBackground = false + titleLabel.isBezeled = false + titleLabel.font = NSFont.boldSystemFont(ofSize: 13) + + subtitleLabel.isEditable = false + subtitleLabel.drawsBackground = false + subtitleLabel.isBezeled = false + + stackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stackView) + view.layer?.cornerRadius = 4 + + NSLayoutConstraint.deactivate(layoutConstraints) + layoutConstraints = [ + stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8) + ] + NSLayoutConstraint.activate(layoutConstraints) + } + + override func viewDidLayout() { + super.viewDidLayout() + + guard let currentAppearance = currentAppearance else { return } + update(with: currentAppearance) + } + + @objc func resetApplication() { + delegate?.applicationView(self, didResetApplication: currentAppearance) + } +} diff --git a/Sources/Features/Applications/ApplicationsFeatureViewController.swift b/Sources/Features/Applications/ApplicationsFeatureViewController.swift index 28ae765..29526a4 100644 --- a/Sources/Features/Applications/ApplicationsFeatureViewController.swift +++ b/Sources/Features/Applications/ApplicationsFeatureViewController.swift @@ -6,34 +6,74 @@ protocol ApplicationsFeatureViewControllerDelegate: class { func applicationViewController(_ controller: ApplicationsFeatureViewController, finishedLoading: Bool) func applicationViewController(_ controller: ApplicationsFeatureViewController, - didLoad application: ApplicationGridViewModel, + didLoad application: Application, offset: Int, total: Int) func applicationViewController(_ controller: ApplicationsFeatureViewController, toggleAppearance newAppearance: Application.Appearance, - application: ApplicationGridViewModel) + application: Application) } class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDelegate, - ApplicationGridViewDelegate, ApplicationsLogicControllerDelegate { +ApplicationGridViewDelegate, ApplicationsLogicControllerDelegate, ApplicationListViewDelegate { + enum Mode: String, CaseIterable { + case grid = "Grid" + case list = "List" + + var image: NSImage { + switch self { + case .grid: + return NSImage.init(named: "Grid")! + case .list: + return NSImage.init(named: "List")! + } + } + } + enum State { - case loading(application: ApplicationGridViewModel, offset: Int, total: Int) - case view([ApplicationGridViewModel]) + case loading(application: Application, offset: Int, total: Int) + case view([Application]) } weak var delegate: ApplicationsFeatureViewControllerDelegate? - let component: ApplicationGridViewController + let listComponent: ApplicationListViewController + let gridComponent: ApplicationGridViewController let logicController = ApplicationsLogicController() let iconStore: IconStore - var applicationCache = [ApplicationGridViewModel]() + var mode: Mode { + didSet { + switch mode { + case .grid: + self.component = gridComponent + case .list: + self.component = listComponent + } + self.view = component.view + configureComponent() + } + } + var component: Component + var applicationCache = [Application]() var query: String = "" - init(iconStore: IconStore, models: [Application] = []) { + init(iconStore: IconStore, mode: Mode?, models: [Application] = []) { let layoutFactory = LayoutFactory() self.iconStore = iconStore - self.component = ApplicationGridViewController(title: "Applications", - layout: layoutFactory.createGridLayout(), - iconStore: iconStore) + self.mode = mode ?? .grid + self.gridComponent = ApplicationGridViewController(title: "Applications", + layout: layoutFactory.createGridLayout(), + iconStore: iconStore) + self.listComponent = ApplicationListViewController(title: "Applications", + layout: layoutFactory.createListLayout(), + iconStore: iconStore) + + switch self.mode { + case .grid: + self.component = gridComponent + case .list: + self.component = listComponent + } + super.init(nibName: nil, bundle: nil) } @@ -48,9 +88,7 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg override func viewDidLoad() { super.viewDidLoad() logicController.delegate = self - component.collectionView.delegate = self - component.collectionView.isSelectable = true - component.collectionView.allowsMultipleSelection = false + configureComponent() } override func viewDidAppear() { @@ -58,20 +96,44 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg logicController.load() } - func toggle(_ newAppearance: Application.Appearance, for model: ApplicationGridViewModel) { + func configureComponent() { + component.collectionView.delegate = self + component.collectionView.isSelectable = true + component.collectionView.allowsMultipleSelection = false + } + + func toggle(_ newAppearance: Application.Appearance, for model: Application) { logicController.toggleAppearance(newAppearance, for: model) } func performSearch(with string: String) { query = string.lowercased() + let filtered: [Application] switch string.count { case 0: - component.reload(with: applicationCache) + filtered = applicationCache default: - // This can be improved! - let results = applicationCache.filter({ $0.application.name.lowercased().contains(query) }) - component.reload(with: results) + filtered = applicationCache.filter({ $0.name.lowercased().contains(query) }) } + + switch mode { + case .grid: + gridComponent.reload(with: gridModels(from: filtered)) + case .list: + listComponent.reload(with: listModels(from: filtered)) + } + } + + private func listModels(from applications: [Application]) -> [ApplicationListViewModel] { + return applications.compactMap({ + ApplicationListViewModel(title: $0.name, subtitle: $0.metadata, application: $0) + }) + } + + private func gridModels(from applications: [Application]) -> [ApplicationGridViewModel] { + return applications.compactMap({ + ApplicationGridViewModel(title: $0.name, subtitle: $0.metadata, application: $0) + }) } private func render(_ newState: State) { @@ -81,10 +143,18 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg case .view(let applications): delegate?.applicationViewController(self, finishedLoading: true) applicationCache = applications - component.reload(with: applications) { [weak self] in + + let completion = { [weak self] in guard let strongSelf = self else { return } strongSelf.performSearch(with: strongSelf.query) } + + switch mode { + case .grid: + gridComponent.reload(with: gridModels(from: applications), completion: completion) + case .list: + listComponent.reload(with: listModels(from: applications), completion: completion) + } } } @@ -103,21 +173,28 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg // MARK: - ApplicationsLogicControllerDelegate - func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplication application: ApplicationGridViewModel, offset: Int, total: Int) { + func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplication application: Application, offset: Int, total: Int) { render(.loading(application: application, offset: offset, total: total)) } - func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [ApplicationGridViewModel]) { + func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [Application]) { render(.view(applications)) } // MARK: - ApplicationGridViewDelegate func applicationView(_ view: ApplicationGridView, didResetApplication currentAppearance: Application.Appearance?) { - guard let indexPath = component.indexPath(for: view) else { return } + guard let indexPath = component.collectionView.indexPath(for: view) else { return } + let model = gridComponent.model(at: indexPath) + toggle(.system, for: model.application) + } + + // MARK: - ApplicationListViewDelegate - let model = component.model(at: indexPath) - toggle(.system, for: model) + func applicationView(_ view: ApplicationListView, didResetApplication currentAppearance: Application.Appearance?) { + guard let indexPath = component.collectionView.indexPath(for: view) else { return } + let model = gridComponent.model(at: indexPath) + toggle(.system, for: model.application) } // MARK: - NSCollectionViewDelegate @@ -126,20 +203,38 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg if let view = item as? ApplicationGridView { view.delegate = self } + + if let view = item as? ApplicationListView { + view.delegate = self + } } func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { - guard let indexPath = indexPaths.first, - let item = collectionView.item(at: indexPath) as? ApplicationGridView else { - return - } + guard let indexPath = indexPaths.first else { return } + guard let item: NSCollectionViewItem = collectionView.item(at: indexPath) else { return } collectionView.deselectAll(nil) - let model = component.model(at: indexPath) - let newAppearance: Application.Appearance = model.application.appearance == .light - ? .dark - : .light + let restricted: Bool + let application: Application + let newAppearance: Application.Appearance + + if collectionView.item(at: indexPath) is ApplicationGridView { + let model = gridComponent.model(at: indexPath) + restricted = model.application.restricted + application = model.application + newAppearance = model.application.appearance == .light + ? .dark + : .light + } else if collectionView.item(at: indexPath) is ApplicationListView { + let model = listComponent.model(at: indexPath) + restricted = model.application.restricted + application = model.application + newAppearance = model.application.appearance == .light + ? .dark + : .light + } else { return } + let duration: TimeInterval = 0.15 NSAnimationContext.runAnimationGroup({ (context) in @@ -158,18 +253,18 @@ class ApplicationsFeatureViewController: NSViewController, NSCollectionViewDeleg context.allowsImplicitAnimation = true item.view.animator().layer?.setAffineTransform(.identity) }, completionHandler: { - if model.application.restricted { - self.showPermissionsDialog(for: model.application) { result in + if restricted { + self.showPermissionsDialog(for: application) { result in guard result else { return } let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")! NSWorkspace.shared.open(url) } } else { - item.update(with: newAppearance, duration: 0.5) { [weak self] in + (item as? AppearanceAware)?.update(with: newAppearance, duration: 0.5) { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.applicationViewController(strongSelf, toggleAppearance: newAppearance, - application: model) + application: application) } } }) diff --git a/Sources/Features/Applications/ApplicationsLogicController.swift b/Sources/Features/Applications/ApplicationsLogicController.swift index cb70e44..8afe40a 100644 --- a/Sources/Features/Applications/ApplicationsLogicController.swift +++ b/Sources/Features/Applications/ApplicationsLogicController.swift @@ -3,9 +3,9 @@ import Cocoa protocol ApplicationsLogicControllerDelegate: class { func applicationsLogicController(_ controller: ApplicationsLogicController, - didLoadApplication application: ApplicationGridViewModel, + didLoadApplication application: Application, offset: Int, total: Int) - func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [ApplicationGridViewModel]) + func applicationsLogicController(_ controller: ApplicationsLogicController, didLoadApplications applications: [Application]) } class ApplicationsLogicController { @@ -40,10 +40,9 @@ class ApplicationsLogicController { } func toggleAppearance(_ newAppearance: Application.Appearance, - for model: ApplicationGridViewModel) { + for application: Application) { queue.async { [weak self] in let shell = Shell() - let application = model.application // The cfprefsd is killed for the current user to avoid plist caching. // PlistBuddy is used to set new values. @@ -150,8 +149,8 @@ class ApplicationsLogicController { } private func parseApplicationUrls(_ appUrls: [URL], - excludedBundles: [String] = []) throws -> [ApplicationGridViewModel] { - var applications = [ApplicationGridViewModel]() + excludedBundles: [String] = []) throws -> [Application] { + var applications = [Application]() let shell = Shell() let sip = shell.execute(command: "csrutil status").contains("enabled") let libraryDirectory = try FileManager.default.url(for: .libraryDirectory, @@ -193,38 +192,41 @@ class ApplicationsLogicController { FileManager.default.fileExists(atPath: appContainerPreferenceUrl.path) && NSDictionary.init(contentsOfFile: appContainerPreferenceUrl.path) == nil - let application = Application(bundleIdentifier: bundleIdentifier, - name: bundleName, url: url, - preferencesUrl: resolvedAppPreferenceUrl, - appearance: applicationPlist?.appearance() ?? .system, - restricted: restricted) - var subtitle: String - switch application.appearance { + let appearance = applicationPlist?.appearance() ?? .system + var metadata: String + switch appearance { case .dark: - subtitle = "Dark appearance" + metadata = "Dark appearance" case .light: - subtitle = "Light appearance" + metadata = "Light appearance" case .system: - subtitle = "System appearance" + metadata = "System appearance" } - if application.restricted { - subtitle = "🔐 Locked" + if restricted { + metadata = "🔐 Locked" } - let app = ApplicationGridViewModel( - title: bundleName, - subtitle: subtitle, - application: application) + let application = Application(bundleIdentifier: bundleIdentifier, + name: bundleName, metadata: metadata, + url: url, + preferencesUrl: resolvedAppPreferenceUrl, + appearance: appearance, + restricted: restricted) + +// let app = ApplicationGridViewModel( +// title: bundleName, +// subtitle: subtitle, +// application: application) DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - strongSelf.delegate?.applicationsLogicController(strongSelf, didLoadApplication: app, offset: offset, total: total) + strongSelf.delegate?.applicationsLogicController(strongSelf, didLoadApplication: application, offset: offset, total: total) } - applications.append(app) + applications.append(application) addedApplicationNames.append(bundleName) } - return applications.sorted(by: { $0.application.name.lowercased() < $1.application.name.lowercased() }) + return applications.sorted(by: { $0.name.lowercased() < $1.name.lowercased() }) } private func shouldExcludeApplication(with plist: NSDictionary, applicationUrl url: URL) -> Bool { diff --git a/Sources/Features/Applications/Component.swift b/Sources/Features/Applications/Component.swift new file mode 100644 index 0000000..25bd1d0 --- /dev/null +++ b/Sources/Features/Applications/Component.swift @@ -0,0 +1,6 @@ +import Cocoa + +protocol Component { + var collectionView: NSCollectionView { get } + var view: NSView { get } +} diff --git a/Sources/Features/Main/MainContainerViewController.swift b/Sources/Features/Main/MainContainerViewController.swift index a2c961a..dcff47d 100644 --- a/Sources/Features/Main/MainContainerViewController.swift +++ b/Sources/Features/Main/MainContainerViewController.swift @@ -4,7 +4,7 @@ import Family class MainContainerViewController: FamilyViewController, ApplicationsFeatureViewControllerDelegate, SystemPreferenceFeatureViewControllerDelegate, -ToolbarSearchDelegate { +ToolbarDelegate { lazy var loadingLabelController = ApplicationsLoadingViewController(text: "Loading...") let preferencesViewController: SystemPreferenceFeatureViewController let applicationsViewController: ApplicationsFeatureViewController @@ -12,7 +12,8 @@ ToolbarSearchDelegate { init(iconStore: IconStore) { self.preferencesViewController = SystemPreferenceFeatureViewController(iconStore: iconStore) - self.applicationsViewController = ApplicationsFeatureViewController(iconStore: iconStore) + self.applicationsViewController = ApplicationsFeatureViewController(iconStore: iconStore, + mode: UserDefaults.standard.featureViewControllerMode) super.init(nibName: nil, bundle: nil) } @@ -59,6 +60,16 @@ ToolbarSearchDelegate { performSearch(with: string) } + func toolbar(_ toolbar: Toolbar, didChangeMode mode: String) { + guard let mode = ApplicationsFeatureViewController.Mode.init(rawValue: mode) else { + return + } + UserDefaults.standard.featureViewControllerMode = mode + applicationsViewController.mode = mode + applicationsViewController.removeFromParent() + addChild(applicationsViewController) + } + // MARK: - ApplicationCollectionViewControllerDelegate func applicationViewController(_ controller: ApplicationsFeatureViewController, finishedLoading: Bool) { @@ -66,8 +77,7 @@ ToolbarSearchDelegate { } func applicationViewController(_ controller: ApplicationsFeatureViewController, - didLoad application: ApplicationGridViewModel, offset: Int, total: Int) { - let application = application.application + didLoad application: Application, offset: Int, total: Int) { let progress = Double(offset + 1) / Double(total) * Double(100) loadingLabelController.progress.doubleValue = floor(progress) loadingLabelController.textField.stringValue = "Loading (\(offset)/\(total)): \(application.name)" @@ -75,7 +85,7 @@ ToolbarSearchDelegate { func applicationViewController(_ controller: ApplicationsFeatureViewController, toggleAppearance newAppearance: Application.Appearance, - application: ApplicationGridViewModel) { + application: Application) { applicationsViewController.toggle(newAppearance, for: application) } diff --git a/Voodoo/Output/CollectionViewItemComponent-macOS.generated.swift b/Voodoo/Output/CollectionViewItemComponent-macOS.generated.swift index 4a572dc..b83bd42 100644 --- a/Voodoo/Output/CollectionViewItemComponent-macOS.generated.swift +++ b/Voodoo/Output/CollectionViewItemComponent-macOS.generated.swift @@ -4,7 +4,7 @@ import Cocoa import Differific -class ApplicationGridViewController: NSViewController { +class ApplicationGridViewController: NSViewController, Component { private let layout: NSCollectionViewFlowLayout private let dataSource: ApplicationGridDataSource let collectionView: NSCollectionView @@ -137,7 +137,142 @@ struct ApplicationGridViewModel: Hashable { let subtitle: String let application: Application } -class SystemPreferenceViewController: NSViewController { + +class ApplicationListViewController: NSViewController, Component { + private let layout: NSCollectionViewFlowLayout + private let dataSource: ApplicationListDataSource + let collectionView: NSCollectionView + + init(title: String? = nil, + layout: NSCollectionViewFlowLayout, + iconStore: IconStore, + collectionView: NSCollectionView? = nil) { + self.layout = layout + self.dataSource = ApplicationListDataSource(title: title, iconStore: iconStore) + if let collectionView = collectionView { + self.collectionView = collectionView + } else { + self.collectionView = NSCollectionView() + } + self.collectionView.collectionViewLayout = layout + super.init(nibName: nil, bundle: nil) + self.title = title + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View lifecycle + + override func loadView() { + self.view = collectionView + } + + override func viewDidLoad() { + super.viewDidLoad() + collectionView.dataSource = dataSource + let headerIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationListViewHeader") + collectionView.register(CollectionViewHeader.self, + forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, + withIdentifier: headerIdentifier) + let itemIdentifier = NSUserInterfaceItemIdentifier.init("ApplicationListView") + collectionView.register(ApplicationListView.self, forItemWithIdentifier: itemIdentifier) + + if title != nil { + layout.headerReferenceSize.height = 60 + } + } + + // MARK: - Public API + + func indexPath(for item: NSCollectionViewItem) -> IndexPath? { + return collectionView.indexPath(for: item) + } + + func model(at indexPath: IndexPath) -> ApplicationListViewModel { + return dataSource.model(at: indexPath) + } + + func reload(with models: [ApplicationListViewModel], completion: (() -> Void)? = nil) { + dataSource.reload(collectionView, with: models, then: completion) + } +} + +class ApplicationListDataSource: NSObject, NSCollectionViewDataSource { + + private var title: String? + private var models = [ApplicationListViewModel]() + private let iconStore: IconStore + + init(title: String? = nil, + models: [ApplicationListViewModel] = [], + iconStore: IconStore) { + self.title = title + self.models = models + self.iconStore = iconStore + super.init() + } + + // MARK: - Public API + + func model(at indexPath: IndexPath) -> ApplicationListViewModel { + return models[indexPath.item] + } + + func reload(_ collectionView: NSCollectionView, + with models: [ApplicationListViewModel], + then handler: (() -> Void)? = nil) { + let manager = DiffManager() + let changes = manager.diff(self.models, models) + collectionView.reload(with: changes, + updateDataSource: { self.models = models }, + completion: handler) + } + + // MARK: - NSCollectionViewDataSource + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return models.count + } + + func collectionView(_ collectionView: NSCollectionView, + viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, + at indexPath: IndexPath) -> NSView { + let identifier = NSUserInterfaceItemIdentifier.init("ApplicationListViewHeader") + let item = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, + withIdentifier: identifier, for: indexPath) + + if let title = title, let header = item as? CollectionViewHeader { + header.setText(title) + } + + return item + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let identifier = NSUserInterfaceItemIdentifier.init("ApplicationListView") + let item = collectionView.makeItem(withIdentifier: identifier, for: indexPath) + let model = self.model(at: indexPath) + + if let view = item as? ApplicationListView { + view.currentAppearance = model.application.appearance + iconStore.loadIcon(for: model.application) { image in view.iconView.image = image } + view.titleLabel.stringValue = model.title + view.subtitleLabel.stringValue = model.subtitle + } + + return item + } +} + +struct ApplicationListViewModel: Hashable { + let title: String + let subtitle: String + let application: Application +} + +class SystemPreferenceViewController: NSViewController, Component { private let layout: NSCollectionViewFlowLayout private let dataSource: SystemPreferenceDataSource let collectionView: NSCollectionView @@ -270,3 +405,4 @@ struct SystemPreferenceViewModel: Hashable { let subtitle: String let preference: SystemPreference } + diff --git a/Voodoo/Output/ViewControllerFactory-macOS.generated.swift b/Voodoo/Output/ViewControllerFactory-macOS.generated.swift index 9895935..d2ebf61 100644 --- a/Voodoo/Output/ViewControllerFactory-macOS.generated.swift +++ b/Voodoo/Output/ViewControllerFactory-macOS.generated.swift @@ -9,6 +9,10 @@ class ViewControllerFactory { let viewController = ApplicationGridViewController(layout: layout, iconStore: iconStore) return viewController } + public func createApplicationListViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> ApplicationListViewController { + let viewController = ApplicationListViewController(layout: layout, iconStore: iconStore) + return viewController + } public func createSystemPreferenceViewController(layout: NSCollectionViewFlowLayout, iconStore: IconStore) -> SystemPreferenceViewController { let viewController = SystemPreferenceViewController(layout: layout, iconStore: iconStore) return viewController diff --git a/Voodoo/Templates/CollectionViewItemComponent-macOS.stencil b/Voodoo/Templates/CollectionViewItemComponent-macOS.stencil index 5d29896..4362cee 100644 --- a/Voodoo/Templates/CollectionViewItemComponent-macOS.stencil +++ b/Voodoo/Templates/CollectionViewItemComponent-macOS.stencil @@ -2,7 +2,7 @@ import Cocoa import Differific {% for type in types.implementing.CollectionViewItemComponent %} -class {{type.name|replace:"View",""}}ViewController: NSViewController { +class {{type.name|replace:"View",""}}ViewController: NSViewController, Component { private let layout: NSCollectionViewFlowLayout private let dataSource: {{type.name|replace:"View",""}}DataSource let collectionView: NSCollectionView @@ -155,4 +155,5 @@ struct {{type.name}}Model: Hashable { {{key}}: {{type.annotations[key]}} {% endfor %} } + {% endfor %}