diff --git a/resources/SF-Pro-Text-Regular.otf b/resources/SF-Pro-Text-Regular.otf index d67604108..2eb6dffc3 100644 Binary files a/resources/SF-Pro-Text-Regular.otf and b/resources/SF-Pro-Text-Regular.otf differ diff --git a/scripts/subset_fonticon.sh b/scripts/subset_fonticon.sh index 0102bd60b..e2fb67ad8 100755 --- a/scripts/subset_fonticon.sh +++ b/scripts/subset_fonticon.sh @@ -4,4 +4,4 @@ set -exu "$(pipenv --venv)"/bin/pyftsubset resources/SF-Pro-Text-Regular-Full.otf \ --output-file=resources/SF-Pro-Text-Regular.otf \ - --text="􀁎􀁌􀕧􀕬􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀘠􀚗􀚙􀚛􀚝􀚟􀚡􀚣􀚥􀚧􀚩􀚫􀚭􀚯􀚱􀚳􀚵􀚷􀚹􀚻" + --text="􀀁􀁑􀁏􀁍􀁎􀁌􀕧􀕬􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀘠􀚗􀚙􀚛􀚝􀚟􀚡􀚣􀚥􀚧􀚩􀚫􀚭􀚯􀚱􀚳􀚵􀚷􀚹􀚻" diff --git a/src/logic/Window.swift b/src/logic/Window.swift index a1c2da535..c2e87b06e 100644 --- a/src/logic/Window.swift +++ b/src/logic/Window.swift @@ -109,6 +109,13 @@ class Window { } } + func toggleFullscreen() { + BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in + guard let self = self else { return } + self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen) + } + } + func quitApp() { // prevent users from quitting Finder if application.runningApplication.bundleIdentifier == "com.apple.finder" { return } diff --git a/src/ui/generic-components/text/BaseLabel.swift b/src/ui/generic-components/text/BaseLabel.swift index f99b8f705..7e7e06b01 100644 --- a/src/ui/generic-components/text/BaseLabel.swift +++ b/src/ui/generic-components/text/BaseLabel.swift @@ -13,6 +13,7 @@ class BaseLabel: NSTextView { } private func setup() { + translatesAutoresizingMaskIntoConstraints = false drawsBackground = true backgroundColor = .clear isSelectable = false diff --git a/src/ui/main-window/ThumbnailFontIconView.swift b/src/ui/main-window/ThumbnailFontIconView.swift index e77cdd1ad..585ff3943 100644 --- a/src/ui/main-window/ThumbnailFontIconView.swift +++ b/src/ui/main-window/ThumbnailFontIconView.swift @@ -1,19 +1,29 @@ import Cocoa +enum Symbols: String { + case circledPlusSign = "􀁌" + case circledMinusSign = "􀁎" + case circledSlashSign = "􀕧" + case circledNumber0 = "􀀸" + case circledNumber10 = "􀓵" + case circledStar = "􀕬" + case filledCircled = "􀀁" + case filledCircledMultiplySign = "􀁑" + case filledCircledMinusSign = "􀁏" + case filledCircledPlusSign = "􀁍" +} + // Font icon using SF Symbols from the SF Pro font from Apple // see https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/ class ThumbnailFontIconView: ThumbnailTitleView { - static let sfSymbolCircledPlusSign = "􀁌" - static let sfSymbolCircledMinusSign = "􀁎" - static let sfSymbolCircledSlashSign = "􀕧" - static let sfSymbolCircledNumber0 = "􀀸" - static let sfSymbolCircledNumber10 = "􀓵" - static let sfSymbolCircledStart = "􀕬" - - convenience init(_ text: String, _ size: CGFloat, _ color: NSColor) { + convenience init(_ symbol: Symbols, _ size: CGFloat = Preferences.fontIconSize, _ color: NSColor = .white, _ isBackground: Bool = false) { // This helps SF symbols display vertically centered and not clipped at the bottom - self.init(size, 3) - string = text + if isBackground { + self.init(size, 3, shadow: nil) + } else { + self.init(size, 3) + } + string = symbol.rawValue font = NSFont(name: "SF Pro Text", size: size) textColor = color // This helps SF symbols not be clipped on the right @@ -27,15 +37,26 @@ class ThumbnailFontIconView: ThumbnailTitleView { } func setStar() { - assignIfDifferent(&string, ThumbnailFontIconView.sfSymbolCircledStart) + assignIfDifferent(&string, Symbols.circledStar.rawValue) } private func baseCharacterAndOffset(_ number: UInt32) -> (String, UInt32) { if number <= 9 { // numbers alternate between empty and full circles; we skip the full circles - return (ThumbnailFontIconView.sfSymbolCircledNumber0, number * UInt32(2)) + return (Symbols.circledNumber0.rawValue, number * UInt32(2)) } else { - return (ThumbnailFontIconView.sfSymbolCircledNumber10, number - 10) + return (Symbols.circledNumber10.rawValue, number - 10) } } } + +class ThumbnailFilledFontIconView: NSView { + convenience init(_ thumbnailFontIconView: ThumbnailFontIconView, _ backgroundColor: NSColor) { + self.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + var backgroundView = ThumbnailFontIconView(.filledCircled, thumbnailFontIconView.font!.pointSize, backgroundColor, true) + addSubview(backgroundView) + addSubview(thumbnailFontIconView, positioned: .above, relativeTo: nil) + fit(backgroundView.fittingSize.width, backgroundView.fittingSize.height) + } +} diff --git a/src/ui/main-window/ThumbnailTitleView.swift b/src/ui/main-window/ThumbnailTitleView.swift index 34b412d3f..9e74692ac 100644 --- a/src/ui/main-window/ThumbnailTitleView.swift +++ b/src/ui/main-window/ThumbnailTitleView.swift @@ -3,7 +3,7 @@ import Cocoa class ThumbnailTitleView: BaseLabel { var magicOffset = CGFloat(0) - convenience init(_ size: CGFloat, _ magicOffset: CGFloat = 0) { + convenience init(_ size: CGFloat, _ magicOffset: CGFloat = 0, shadow: NSShadow? = ThumbnailView.makeShadow(.darkGray)) { let textStorage = NSTextStorage() let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) @@ -15,7 +15,7 @@ class ThumbnailTitleView: BaseLabel { font = Preferences.font self.magicOffset = magicOffset textColor = Preferences.fontColor - shadow = ThumbnailView.makeShadow(.darkGray) + self.shadow = shadow defaultParagraphStyle = makeParagraphStyle(size) heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true } diff --git a/src/ui/main-window/ThumbnailView.swift b/src/ui/main-window/ThumbnailView.swift index 52cb33dde..8f9a13d90 100644 --- a/src/ui/main-window/ThumbnailView.swift +++ b/src/ui/main-window/ThumbnailView.swift @@ -5,15 +5,20 @@ class ThumbnailView: NSStackView { var thumbnail = NSImageView() var appIcon = NSImageView() var label = ThumbnailTitleView(Preferences.fontHeight) - var fullscreenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledPlusSign, Preferences.fontIconSize, .white) - var minimizedIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white) - var hiddenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledSlashSign, Preferences.fontIconSize, .white) - var spaceIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledNumber0, Preferences.fontIconSize, .white) + var fullscreenIcon = ThumbnailFontIconView(.circledPlusSign) + var minimizedIcon = ThumbnailFontIconView(.circledMinusSign) + var hiddenIcon = ThumbnailFontIconView(.circledSlashSign) + var spaceIcon = ThumbnailFontIconView(.circledNumber0) + var closeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMultiplySign, Preferences.fontIconSize, NSColor(srgbRed: 1, green: 0.35, blue: 0.32, alpha: 1)), NSColor(srgbRed: 0.64, green: 0.03, blue: 0.02, alpha: 1)) + var minimizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledMinusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.91, green: 0.75, blue: 0.16, alpha: 1)), NSColor(srgbRed: 0.71, green: 0.55, blue: 0.09, alpha: 1)) + var maximizeIcon = ThumbnailFilledFontIconView(ThumbnailFontIconView(.filledCircledPlusSign, Preferences.fontIconSize, NSColor(srgbRed: 0.32, green: 0.76, blue: 0.17, alpha: 1)), NSColor(srgbRed: 0.04, green: 0.39, blue: 0.02, alpha: 1)) var hStackView: NSStackView! var mouseUpCallback: (() -> Void)! var mouseMovedCallback: (() -> Void)! var dragAndDropTimer: Timer? var isHighlighted = false + var shouldShowWindowControls = false + var isShowingWindowControls = false convenience init() { self.init(frame: .zero) @@ -36,6 +41,32 @@ class ThumbnailView: NSStackView { hStackView = NSStackView(views: [appIcon, label, hiddenIcon, fullscreenIcon, minimizedIcon, spaceIcon]) hStackView.spacing = Preferences.intraCellPadding setViews([hStackView, thumbnail], in: .leading) + addWindowControls() + } + + func addWindowControls() { + thumbnail.addSubview(closeIcon, positioned: .above, relativeTo: nil) + thumbnail.addSubview(minimizeIcon, positioned: .above, relativeTo: nil) + thumbnail.addSubview(maximizeIcon, positioned: .above, relativeTo: nil) + let windowsControlSpacing = CGFloat(3) + [closeIcon, minimizeIcon, maximizeIcon].forEach { + $0.topAnchor.constraint(equalTo: thumbnail.topAnchor, constant: 1).isActive = true + } + closeIcon.leftAnchor.constraint(equalTo: thumbnail.leftAnchor).isActive = true + minimizeIcon.leftAnchor.constraint(equalTo: closeIcon.rightAnchor, constant: windowsControlSpacing).isActive = true + maximizeIcon.leftAnchor.constraint(equalTo: minimizeIcon.rightAnchor, constant: windowsControlSpacing).isActive = true + [closeIcon, minimizeIcon, maximizeIcon].forEach { $0.isHidden = true } + } + + func showOrHideWindowControls(_ shouldShowWindowControls_: Bool? = nil) { + if let shouldShowWindowControls = shouldShowWindowControls_ { + self.shouldShowWindowControls = shouldShowWindowControls + } + let shouldShow = shouldShowWindowControls && isHighlighted + if isShowingWindowControls != shouldShow { + isShowingWindowControls = shouldShow + [closeIcon, minimizeIcon, maximizeIcon].forEach { $0.isHidden = !shouldShow } + } } func highlight(_ highlight: Bool) { @@ -45,6 +76,7 @@ class ThumbnailView: NSStackView { highlightOrNot() } } + showOrHideWindowControls() } func highlightOrNot() { @@ -139,14 +171,24 @@ class ThumbnailView: NSStackView { } func mouseMoved() { + showOrHideWindowControls(true) if !isHighlighted { mouseMovedCallback() } } - override func mouseUp(with theEvent: NSEvent) { - if theEvent.clickCount >= 1 { - mouseUpCallback() + override func mouseUp(with event: NSEvent) { + if event.clickCount >= 1 { + let target = thumbnail.hitTest(convert(event.locationInWindow, from: nil))?.superview + if target == closeIcon { + window_!.close() + } else if target == minimizeIcon { + window_!.minDemin() + } else if target == maximizeIcon { + window_!.toggleFullscreen() + } else { + mouseUpCallback() + } } } diff --git a/src/ui/main-window/ThumbnailsView.swift b/src/ui/main-window/ThumbnailsView.swift index c94ef8b7e..9b80a0614 100644 --- a/src/ui/main-window/ThumbnailsView.swift +++ b/src/ui/main-window/ThumbnailsView.swift @@ -120,7 +120,7 @@ class ThumbnailsView: NSVisualEffectView { if let existingTrackingArea = scrollView.trackingAreas.first { scrollView.documentView!.removeTrackingArea(existingTrackingArea) } - scrollView.addTrackingArea(NSTrackingArea(rect: scrollView.bounds, options: [.mouseMoved, .activeAlways], owner: scrollView, userInfo: nil)) + scrollView.addTrackingArea(NSTrackingArea(rect: scrollView.bounds, options: [.mouseMoved, .mouseEnteredAndExited, .activeAlways], owner: scrollView, userInfo: nil)) } func centerRows(_ maxX: CGFloat) { @@ -168,6 +168,7 @@ class ScrollView: NSScrollView { override class var isCompatibleWithResponsiveScrolling: Bool { true } var isCurrentlyScrolling = false + var previousTarget: ThumbnailView? convenience init() { self.init(frame: .zero) @@ -200,11 +201,23 @@ class ScrollView: NSScrollView { target = target!.superview } if let target = target, target is ThumbnailView { + if previousTarget != target { + previousTarget?.showOrHideWindowControls(false) + previousTarget = target as! ThumbnailView + } (target as! ThumbnailView).mouseMoved() + } else { + previousTarget?.showOrHideWindowControls(false) } + } else { + previousTarget?.showOrHideWindowControls(false) } } + override func mouseExited(with event: NSEvent) { + previousTarget?.showOrHideWindowControls(false) + } + // holding shift and using the scrolling wheel will generate a horizontal movement // shift can be part of shortcuts so we force shift scrolls to be vertical override func scrollWheel(with event: NSEvent) {