diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 2b66218..12c5d46 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -108,6 +108,8 @@ C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; }; C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */; }; C44067FB27E25FD70045BD4E /* DomainListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */; }; + C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */; }; + C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; }; C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */; }; C449B4F127EE7FC200C47E8A /* DomainListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* DomainListNameCell.swift */; }; C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; }; @@ -356,6 +358,8 @@ C44067F627E258410045BD4E /* DomainListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListPhpCell.swift; sourceTree = ""; }; C44067F827E2585E0045BD4E /* DomainListTypeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTypeCell.swift; sourceTree = ""; }; C44067FA27E25FD70045BD4E /* DomainListTLSCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListTLSCell.swift; sourceTree = ""; }; + C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIHelper.swift; sourceTree = ""; }; + C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionPopoverView.swift; sourceTree = ""; }; C44C198C276E3A1C0072762D /* ProgressWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindow.swift; sourceTree = ""; }; C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = ""; }; C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = ""; }; @@ -996,10 +1000,12 @@ C4EE55B027708BB2001DF387 /* SwiftUI */ = { isa = PBXGroup; children = ( + C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */, C49E171E27A5736E00787921 /* PMServicesView.swift */, C4EE55A627708B9E001DF387 /* PMHeaderView.swift */, C4EE55A827708B9E001DF387 /* PMStatsView.swift */, C4EE55A727708B9E001DF387 /* Preview.swift */, + C44264BD2850B86C007400F1 /* SwiftUIHelper.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1224,6 +1230,7 @@ C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */, C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */, C4C0E8DF27F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */, + C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */, C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */, C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */, C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */, @@ -1262,6 +1269,7 @@ C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */, C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, + C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */, C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C417DC74277614690015E6EE /* Helpers.swift in Sources */, C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, diff --git a/phpmon/Assets.xcassets/AppColor.colorset/Contents.json b/phpmon/Assets.xcassets/AppColor.colorset/Contents.json index a866234..4759aba 100644 --- a/phpmon/Assets.xcassets/AppColor.colorset/Contents.json +++ b/phpmon/Assets.xcassets/AppColor.colorset/Contents.json @@ -6,7 +6,7 @@ "components" : { "alpha" : "1.000", "blue" : "0.988", - "green" : "0.723", + "green" : "0.580", "red" : "0.277" } }, @@ -19,6 +19,15 @@ "value" : "dark" } ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.723", + "red" : "0.277" + } + }, "idiom" : "universal" } ], diff --git a/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift b/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift index 421e0ae..4241648 100644 --- a/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift +++ b/phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift @@ -91,7 +91,7 @@ public struct PhpVersionNumberCollection: Equatable { } } -public struct PhpVersionNumber: Equatable { +public struct PhpVersionNumber: Equatable, Hashable { let major: Int let minor: Int let patch: Int? diff --git a/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift b/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift index c758fec..54787d8 100644 --- a/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift +++ b/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift @@ -8,6 +8,7 @@ import Cocoa import AppKit +import SwiftUI class DomainListPhpCell: NSTableCellView, DomainListCellProtocol { static let reusableName = "domainListPhpCell" @@ -20,6 +21,9 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol { func populateCell(with site: ValetSite) { self.site = site + buttonPhpVersion.isHidden = false + imageViewPhpVersionOK.isHidden = false + buttonPhpVersion.title = " PHP \(site.servingPhpVersion)" imageViewPhpVersionOK.toolTip = nil @@ -33,9 +37,6 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol { imageViewPhpVersionOK.image = NSImage(named: "Checkmark") imageViewPhpVersionOK.toolTip = "domain_list.tooltips.checkmark".localized(site.composerPhp) } - - buttonPhpVersion.isHidden = false - imageViewPhpVersionOK.isHidden = false } func populateCell(with proxy: ValetProxy) { @@ -47,56 +48,25 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol { @IBAction func pressedPhpVersion(_ sender: Any) { guard let site = self.site else { return } - let alert = NSAlert.init() - alert.alertStyle = .informational + var validPhpSuggestions: [PhpVersionNumber] { + if site.isolatedPhpVersion != nil { + return [] + } - var information = "" - - if self.site?.isolatedPhpVersion != nil { - information += "alert.composer_php_isolated.desc".localized( - self.site!.isolatedPhpVersion!.versionNumber.homebrewVersion, - PhpEnv.phpInstall.version.short - ) - information += "\n\n" - } - - information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)" - .localized - - alert.messageText = "alert.composer_php_requirement.title" - .localized("\(site.name).\(Valet.shared.config.tld)", site.composerPhp) - alert.informativeText = information - - alert.addButton(withTitle: "site_link.close".localized) - - var mapIndex: Int = NSApplication.ModalResponse.alertSecondButtonReturn.rawValue - var map: [Int: String] = [:] - - if site.isolatedPhpVersion == nil { - // Determine which installed versions would be ideal to switch to, - // but make sure to exclude the currently linked version - PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in + return PhpEnv.shared.validVersions(for: site.composerPhp).filter({ version in version.homebrewVersion != PhpEnv.phpInstall.version.short - }).forEach { version in - alert.addButton(withTitle: "site_link.switch_to_php".localized(version.homebrewVersion)) - map[mapIndex] = version.homebrewVersion - mapIndex += 1 - } - - // Site is not isolated, show options to switch global PHP version - alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) { response in - if response.rawValue > NSApplication.ModalResponse.alertFirstButtonReturn.rawValue { - if map.keys.contains(response.rawValue) { - let version = map[response.rawValue]! - Log.info("Pressed button to switch to \(version)") - MainMenu.shared.switchToPhpVersion(version) - } - } - } - } else { - // Site is isolated, do not show any options to switch - alert.beginSheetModal(for: App.shared.domainListWindowController!.window!) + }) } + + let button = self.buttonPhpVersion! + let popover = NSPopover() + + let view = VersionPopoverView(site: site, validPhpVersions: validPhpSuggestions, parent: popover) + + popover.contentViewController = NSHostingController(rootView: view) + popover.behavior = .transient + popover.animates = true + popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) } } diff --git a/phpmon/Domain/Integrations/Valet/Sites/ValetSite+Fake.swift b/phpmon/Domain/Integrations/Valet/Sites/ValetSite+Fake.swift index f068430..433f4c4 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/ValetSite+Fake.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/ValetSite+Fake.swift @@ -23,6 +23,7 @@ extension ValetSite { self.init(name: name, tld: tld, absolutePath: path, aliasPath: nil, makeDeterminations: false) self.secured = secure self.composerPhp = constraint + self.composerPhpSource = constraint != "" ? .require : .unknown self.composerPhpCompatibleWithLinked = self.composerPhp.split(separator: "|") .map { string in @@ -33,6 +34,7 @@ extension ValetSite { self.driver = driver self.driverDeterminedByComposer = true + if linked { self.aliasPath = self.absolutePath } diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 46b016a..797651a 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -11,6 +11,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate static let shared = MainMenu() + override init() { + super.init() + statusItem.isVisible = !isRunningSwiftUIPreview + } + weak var menuDelegate: NSMenuDelegate? /** diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift index a9b665e..f438145 100644 --- a/phpmon/Domain/Menu/StatusMenu+Items.swift +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -98,7 +98,7 @@ extension StatusMenu { let presets = NSMenuItem(title: "mi_presets_title".localized, action: nil, keyEquivalent: "") let presetsMenu = NSMenu() - + presetsMenu.addItem(NSMenuItem.separator()) presetsMenu.addItem(HeaderView.asMenuItem(text: "mi_apply_presets_title".localized)) diff --git a/phpmon/Domain/SwiftUI/SwiftUIHelper.swift b/phpmon/Domain/SwiftUI/SwiftUIHelper.swift new file mode 100644 index 0000000..d23e45e --- /dev/null +++ b/phpmon/Domain/SwiftUI/SwiftUIHelper.swift @@ -0,0 +1,14 @@ +// +// SwiftUIHelper.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 08/06/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +var isRunningSwiftUIPreview: Bool { + return ProcessInfo.processInfo + .environment["XCODE_RUNNING_FOR_PREVIEWS"] != nil +} diff --git a/phpmon/Domain/SwiftUI/VersionPopoverView.swift b/phpmon/Domain/SwiftUI/VersionPopoverView.swift new file mode 100644 index 0000000..3994e39 --- /dev/null +++ b/phpmon/Domain/SwiftUI/VersionPopoverView.swift @@ -0,0 +1,147 @@ +// +// VersionPopoverView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 08/06/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct VersionPopoverView: View { + + @State var site: ValetSite + + @State var validPhpVersions: [PhpVersionNumber] + + @State var parent: NSPopover! + + func getTitle() -> String { + if site.composerPhpSource == .unknown { + return "alert.composer_php_requirement.unable_to_determine".localized + } + + return "alert.composer_php_requirement.title".localized( + "\(site.name).\(Valet.shared.config.tld)", + site.composerPhp + ) + } + + func getSource() -> String { + var information = "" + + if site.isolatedPhpVersion != nil { + information += "alert.composer_php_isolated.desc".localized( + site.isolatedPhpVersion!.versionNumber.homebrewVersion, + PhpEnv.phpInstall.version.short + ) + information += "\n\n" + } + + information += "alert.composer_php_requirement.type.\(site.composerPhpSource.rawValue)" + .localized + + return information + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(getTitle()) + .fontWeight(.bold) + .fixedSize(horizontal: false, vertical: true) + Text(getSource()) + .fixedSize(horizontal: false, vertical: true) + .font(.subheadline) + if !validPhpVersions.isEmpty { + // Suggestions for alternative PHP versions + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .center, spacing: 5) { + Image(systemName: "info.circle.fill") + .renderingMode(.template) + .foregroundColor(Color("AppColor")) + Text("alert.php_suggestions".localized) + .font(.subheadline) + .foregroundColor(Color("AppColor")) + + }.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0)) + HStack { + ForEach(validPhpVersions, id: \.self) { version in + Button("site_link.switch_to_php".localized(version.homebrewVersion), action: { + MainMenu.shared.switchToPhpVersion(version.homebrewVersion) + parent?.close() + }) + } + } + } + } else { + if site.composerPhpSource != .unknown { + HStack(alignment: .center, spacing: 5) { + Image(systemName: "checkmark.seal.fill") + .renderingMode(.template) + .foregroundColor(Color("IconColorGreen")) + Text("alert.php_version_ideal".localized) + .font(.subheadline) + .foregroundColor(Color("IconColorGreen")) + }.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0)) + } else { + HStack(alignment: .firstTextBaseline, spacing: 5) { + Image(systemName: "questionmark.circle.fill") + .renderingMode(.template) + .foregroundColor(Color.secondary) + Text("alert.unable_to_determine_is_fine".localized) + .font(.subheadline) + .foregroundColor(Color.secondary) + }.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0)) + } + } + }.frame(width: 400, height: nil, alignment: .center) + .padding(20) + .background( + Color(NSColor.windowBackgroundColor).padding(-80) + ) + } +} + +struct VersionPopoverView_Previews: PreviewProvider { + static var previews: some View { + VersionPopoverView( + site: ValetSite( + fakeWithName: "amazingwebsite", + tld: "test", + secure: true, + path: "/path/to/site", + linked: true, + constraint: "" + ), + validPhpVersions: [], + parent: nil + ) + VersionPopoverView( + site: ValetSite( + fakeWithName: "amazingwebsite", + tld: "test", + secure: true, + path: "/path/to/site", + linked: true, + constraint: "^8.1" + ), + validPhpVersions: [], + parent: nil + ) + VersionPopoverView( + site: ValetSite( + fakeWithName: "anothersite", + tld: "test", + secure: true, + path: "/path/to/site", + linked: true, + constraint: "^8.0" + ), + validPhpVersions: [ + PhpVersionNumber(major: 8, minor: 0, patch: 0), + PhpVersionNumber(major: 8, minor: 1, patch: 0) + ], + parent: nil + ) + } +} diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 3b88131..995c1fb 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -302,12 +302,17 @@ problem manually, using your own Terminal app (this just shows you the output)." "alert.composer_success.info" = "Your global Composer dependencies have been successfully updated."; // Composer Version + "alert.composer_php_isolated.desc" = "This site has been isolated, which means that Valet serves PHP %@ for this site specifically (the global version is %@)."; -"alert.composer_php_requirement.title" = "`%@` has the following PHP requirement: %@."; -"alert.composer_php_requirement.type.unknown" = "The required PHP version is a mystery."; +"alert.composer_php_requirement.title" = "'%@' requires PHP %@."; +"alert.composer_php_requirement.unable_to_determine" = "Unable to determine PHP requirement"; +"alert.composer_php_requirement.type.unknown" = "PHP Monitor was unable to determine which version of PHP is required for this domain. The constraint may be determined if you have a `composer.json` or a `.valetphprc` file in your project's directory."; "alert.composer_php_requirement.type.require" = "This required PHP version was determined by checking the `require` field in the `composer.json` file when the site list was last refreshed."; "alert.composer_php_requirement.type.platform" = "This required PHP version was determined by checking the `platform` field in the `composer.json` file when the site list was last refreshed."; "alert.composer_php_requirement.type.valetphprc" = "This required PHP version was determined by checking the .valetphprc file in your project's directory."; +"alert.unable_to_determine_is_fine" = "If you have a simple project, there may not be a specified PHP version set as a requirement. In that case, you are free to ignore this warning."; +"alert.php_version_ideal" = "The currently active PHP version is ideal for this site."; +"alert.php_suggestions" = "There may be a different PHP version which is closer to the constraint."; // Suggest Fix My Valet "alert.php_switch_failed.title" = "Switching to PHP %@ seems to have failed.";