feat: hovering thumbnails reveals icons to close/min/max windows (#9)

This commit is contained in:
Louis Pontoise authored and lwouis committed 2020-07-22 01:23:25 +09:00
1 parent 586bb6e5e6
commit 11e0d2aac3
8 files changed
+108 -24

No files matched your search

Binary file not shown.
+1 -1
View File
@@ -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="􀀁􀁑􀁏􀁍􀁎􀁌􀕧􀕬􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀘠􀚗􀚙􀚛􀚝􀚟􀚡􀚣􀚥􀚧􀚩􀚫􀚭􀚯􀚱􀚳􀚵􀚷􀚹􀚻"
+7
View File
@@ -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
+34 -13
View File
@@ -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)
}
}
+2 -2
View File
@@ -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
}
+49 -7
View File
@@ -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()
}
}
}
+14 -1
View File
@@ -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) {