Skip to content

Commit

Permalink
feat: hovering thumbnails reveals icons to close/min/max windows (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwouis committed Jul 21, 2020
1 parent 586bb6e commit 11e0d2a
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 24 deletions.
Binary file modified resources/SF-Pro-Text-Regular.otf
Binary file not shown.
2 changes: 1 addition & 1 deletion scripts/subset_fonticon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 7 additions & 0 deletions src/logic/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions src/ui/generic-components/text/BaseLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class BaseLabel: NSTextView {
}

private func setup() {
translatesAutoresizingMaskIntoConstraints = false
drawsBackground = true
backgroundColor = .clear
isSelectable = false
Expand Down
47 changes: 34 additions & 13 deletions src/ui/main-window/ThumbnailFontIconView.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
}
4 changes: 2 additions & 2 deletions src/ui/main-window/ThumbnailTitleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
56 changes: 49 additions & 7 deletions src/ui/main-window/ThumbnailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -45,6 +76,7 @@ class ThumbnailView: NSStackView {
highlightOrNot()
}
}
showOrHideWindowControls()
}

func highlightOrNot() {
Expand Down Expand Up @@ -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()
}
}
}

Expand Down
15 changes: 14 additions & 1 deletion src/ui/main-window/ThumbnailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 11e0d2a

Please sign in to comment.