feat: hovering thumbnails reveals icons to close/min/max windows (#9)
This commit is contained in:
1 parent
586bb6e5e6
commit
11e0d2aac3
8 files changed
+108
-24
No files matched your search
Binary file not shown.
@@ -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=""
|
||||
@@ -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 }
|
||||
|
||||
@@ -13,6 +13,7 @@ class BaseLabel: NSTextView {
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
drawsBackground = true
|
||||
backgroundColor = .clear
|
||||
isSelectable = false
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user