Skip to content

Commit

Permalink
feat: drag-and-drop files on the ui (closes #74)
Browse files Browse the repository at this point in the history
  • Loading branch information
louis.pontoise authored and lwouis committed Mar 10, 2020
1 parent 7eb216d commit e1e3633
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 133 deletions.
4 changes: 4 additions & 0 deletions alt-tab-macos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
D04BAD5A6B2F9EEE6FD4185F /* CollectionViewItemTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA4BABBA0312E0EDBA647 /* CollectionViewItemTitle.swift */; };
D04BAD8346A6A32C9749E0B3 /* TabViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA293C53EC5CE00D11E02 /* TabViewItem.swift */; };
D04BAE369A14C3126A1606FE /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */; };
D04BAE4CE37C303DDD0347B8 /* CollectionViewItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE073DD0B0D65CD4CBB6 /* CollectionViewItemView.swift */; };
D04BAEAB8AB048FF2B16B131 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAE5FA03065C5D23C0C2C /* Localizable.strings */; };
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; };
D04BAF3B6F75E50E9AA3E1D2 /* LabelAndControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3D65E7CA78D699EDAB0 /* LabelAndControl.swift */; };
Expand Down Expand Up @@ -133,6 +134,7 @@
D04BADBAFB42AE72DBE1E59E /* es */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = es; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BADBCA16C1D448D34F473 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
D04BAE073DD0B0D65CD4CBB6 /* CollectionViewItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItemView.swift; sourceTree = "<group>"; };
D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = "<group>"; };
D04BAE23C37E0F3B07EEE7B1 /* AboutTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutTab.swift; sourceTree = "<group>"; };
D04BAE80772D25834E440975 /* Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -442,6 +444,7 @@
D04BAF40D5E54AD1044B3FF7 /* ThumbnailsPanel.swift */,
D04BA4BABBA0312E0EDBA647 /* CollectionViewItemTitle.swift */,
D04BAC0416F29ADE7BC5A544 /* CollectionViewItemFontIcon.swift */,
D04BAE073DD0B0D65CD4CBB6 /* CollectionViewItemView.swift */,
);
path = "main-window";
sourceTree = "<group>";
Expand Down Expand Up @@ -558,6 +561,7 @@
D04BA6D9DA2A8BCD93347F0E /* CollectionViewItemFontIcon.swift in Sources */,
D04BA9EE5D34A2789DCB0EE2 /* Sysctl.swift in Sources */,
D04BAC4F69FE9563BC1C5E9C /* DebugProfile.swift in Sources */,
D04BAE4CE37C303DDD0347B8 /* CollectionViewItemView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class CollectionViewFlowLayout: NSCollectionViewFlowLayout {
var widestRow = CGFloat(0)
var totalHeight = CGFloat(0)
for (index, attribute) in attributes.enumerated() {
let isNewRow = abs(attribute.frame.origin.y - currentRowY) > CollectionViewItem.height(currentScreen!)
let isNewRow = abs(attribute.frame.origin.y - currentRowY) > CollectionViewItemView.height(currentScreen!)
if isNewRow {
currentRowWidth -= Preferences.interCellPadding
widestRow = max(widestRow, currentRowWidth)
Expand Down
130 changes: 3 additions & 127 deletions alt-tab-macos/ui/main-window/CollectionViewItem.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,12 @@
import Cocoa
import WebKit

typealias MouseDownCallback = (Window) -> Void
typealias MouseMovedCallback = (CollectionViewItem) -> Void

class CollectionViewItem: NSCollectionViewItem {
var thumbnail = NSImageView()
var appIcon = NSImageView()
var label = CellTitle(Preferences.fontHeight)
var minimizedIcon = FontIcon(FontIcon.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
var hiddenIcon = FontIcon(FontIcon.sfSymbolCircledDotSign, Preferences.fontIconSize, .white)
var spaceIcon = FontIcon(FontIcon.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
var window: Window?
var mouseDownCallback: MouseDownCallback?
var mouseMovedCallback: MouseMovedCallback?
var view_: CollectionViewItemView { view as! CollectionViewItemView }

override func loadView() {
let hStackView = makeHStackView()
let vStackView = makeVStackView(hStackView)
let shadow = CollectionViewItem.makeShadow(.gray)
thumbnail.shadow = shadow
appIcon.shadow = shadow
view = vStackView
}

override func mouseMoved(with event: NSEvent) {
mouseMovedCallback!(self)
}

override func mouseDown(with theEvent: NSEvent) {
mouseDownCallback!(window!)
view = CollectionViewItemView()
view.wantsLayer = true
}

override var isSelected: Bool {
Expand All @@ -38,105 +15,4 @@ class CollectionViewItem: NSCollectionViewItem {
view.layer!.borderColor = isSelected ? Preferences.highlightBorderColor.cgColor : .clear
}
}

func updateRecycledCellWithNewContent(_ element: Window, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
window = element
thumbnail.image = element.thumbnail
let (thumbnailWidth, thumbnailHeight) = CollectionViewItem.thumbnailSize(element.thumbnail, screen)
let thumbnailSize = NSSize(width: thumbnailWidth.rounded(), height: thumbnailHeight.rounded())
thumbnail.image?.size = thumbnailSize
thumbnail.frame.size = thumbnailSize
appIcon.image = element.icon
let appIconSize = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
appIcon.image?.size = appIconSize
appIcon.frame.size = appIconSize
label.string = element.title
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
label.font = Preferences.font
hiddenIcon.isHidden = !window!.isHidden
minimizedIcon.isHidden = !window!.isMinimized
spaceIcon.isHidden = element.spaceIndex == nil || Spaces.isSingleSpace || Preferences.hideSpaceNumberLabels
if !spaceIcon.isHidden {
if element.isOnAllSpaces {
spaceIcon.setStar()
} else {
spaceIcon.setNumber(UInt32(element.spaceIndex!))
}
}
let fontIconWidth = CGFloat([minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding)
label.textContainer!.size.width = view.frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth
view.subviews.first!.frame.size = view.frame.size
self.mouseDownCallback = mouseDownCallback
self.mouseMovedCallback = mouseMovedCallback
if view.trackingAreas.count > 0 {
view.removeTrackingArea(view.trackingAreas[0])
}
view.addTrackingArea(NSTrackingArea(rect: view.bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
}

static func makeShadow(_ color: NSColor) -> NSShadow {
let shadow = NSShadow()
shadow.shadowColor = color
shadow.shadowOffset = .zero
shadow.shadowBlurRadius = 1
return shadow
}

private func makeHStackView() -> NSStackView {
let hStackView = NSStackView()
hStackView.spacing = Preferences.intraCellPadding
hStackView.setViews([appIcon, label, hiddenIcon, minimizedIcon, spaceIcon], in: .leading)
return hStackView
}

private func makeVStackView(_ hStackView: NSStackView) -> NSStackView {
let vStackView = NSStackView()
vStackView.wantsLayer = true
vStackView.layer!.backgroundColor = .clear
vStackView.layer!.cornerRadius = Preferences.cellCornerRadius
vStackView.layer!.borderWidth = Preferences.cellBorderWidth
vStackView.layer!.borderColor = .clear
vStackView.edgeInsets = NSEdgeInsets(top: Preferences.intraCellPadding, left: Preferences.intraCellPadding, bottom: Preferences.intraCellPadding, right: Preferences.intraCellPadding)
vStackView.orientation = .vertical
vStackView.spacing = Preferences.intraCellPadding
vStackView.setViews([hStackView, thumbnail], in: .leading)
return vStackView
}

static func downscaleFactor() -> CGFloat {
let nCellsBeforePotentialOverflow = Preferences.minRows * Preferences.minCellsPerRow
guard CGFloat(Windows.list.count) > nCellsBeforePotentialOverflow else { return 1 }
// TODO: replace this buggy heuristic with a correct implementation of downscaling
return nCellsBeforePotentialOverflow / (nCellsBeforePotentialOverflow + (sqrt(CGFloat(Windows.list.count) - nCellsBeforePotentialOverflow) * 2))
}

static func widthMax(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.widthMax(screen) / Preferences.minCellsPerRow - Preferences.interCellPadding) * CollectionViewItem.downscaleFactor()
}

static func widthMin(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.widthMax(screen) / Preferences.maxCellsPerRow - Preferences.interCellPadding) * CollectionViewItem.downscaleFactor()
}

static func height(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.heightMax(screen) / Preferences.minRows - Preferences.interCellPadding) * CollectionViewItem.downscaleFactor()
}

static func width(_ image: NSImage?, _ screen: NSScreen) -> CGFloat {
return max(thumbnailSize(image, screen).0 + Preferences.intraCellPadding * 2, CollectionViewItem.widthMin(screen))
}

static func thumbnailSize(_ image: NSImage?, _ screen: NSScreen) -> (CGFloat, CGFloat) {
guard let image = image else { return (0, 0) }
let thumbnailHeightMax = CollectionViewItem.height(screen) - Preferences.intraCellPadding * 3 - Preferences.iconSize
let thumbnailWidthMax = CollectionViewItem.widthMax(screen) - Preferences.intraCellPadding * 2
let thumbnailHeight = min(image.size.height, thumbnailHeightMax)
let thumbnailWidth = min(image.size.width, thumbnailWidthMax)
let imageRatio = image.size.width / image.size.height
let thumbnailRatio = thumbnailWidth / thumbnailHeight
if thumbnailRatio > imageRatio {
return (image.size.width * thumbnailHeight / image.size.height, thumbnailHeight)
}
return (thumbnailWidth, image.size.height * thumbnailWidth / image.size.width)
}
}
2 changes: 1 addition & 1 deletion alt-tab-macos/ui/main-window/CollectionViewItemTitle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CellTitle: BaseLabel {
self.init(NSRect.zero, textContainer)
self.magicOffset = magicOffset
textColor = Preferences.fontColor
shadow = CollectionViewItem.makeShadow(.darkGray)
shadow = CollectionViewItemView.makeShadow(.darkGray)
defaultParagraphStyle = makeParagraphStyle(size)
heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true
}
Expand Down
170 changes: 170 additions & 0 deletions alt-tab-macos/ui/main-window/CollectionViewItemView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import Cocoa

class CollectionViewItemView: NSView {
var window_: Window?
var thumbnail = NSImageView()
var appIcon = NSImageView()
var label = CellTitle(Preferences.fontHeight)
var minimizedIcon = FontIcon(FontIcon.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
var hiddenIcon = FontIcon(FontIcon.sfSymbolCircledDotSign, Preferences.fontIconSize, .white)
var spaceIcon = FontIcon(FontIcon.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
var mouseDownCallback: MouseDownCallback!
var mouseMovedCallback: MouseMovedCallback!
var dragAndDropTimer: Timer?

convenience init() {
self.init(frame: .zero)
let hStackView = makeHStackView()
let vStackView = makeVStackView(hStackView)
let shadow = CollectionViewItemView.makeShadow(.gray)
thumbnail.shadow = shadow
appIcon.shadow = shadow
observeDragAndDrop()
subviews.append(vStackView)
}

private func observeDragAndDrop() {
// NSImageView instances are registered to drag-and-drop by default
thumbnail.unregisterDraggedTypes()
appIcon.unregisterDraggedTypes()
// we only handle URLs (i.e. not text, image, or other draggable things)
registerForDraggedTypes([NSPasteboard.PasteboardType(kUTTypeURL as String)])
}

override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
mouseMovedCallback()
return .link
}

override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
dragAndDropTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { _ in
self.mouseDownCallback()
})
return .link
}

override func draggingExited(_ sender: NSDraggingInfo?) {
dragAndDropTimer?.invalidate()
dragAndDropTimer = nil
}

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self]) as! [URL]
let appUrl = window_!.application.runningApplication.bundleURL!
let open = try? NSWorkspace.shared.open(urls, withApplicationAt: appUrl, options: [], configuration: [:])
(App.shared as! App).hideUi()
return open != nil
}

override func mouseMoved(with event: NSEvent) {
mouseMovedCallback()
}

override func mouseDown(with theEvent: NSEvent) {
mouseDownCallback()
}

func updateRecycledCellWithNewContent(_ element: Window, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
window_ = element
thumbnail.image = element.thumbnail
let (thumbnailWidth, thumbnailHeight) = CollectionViewItemView.thumbnailSize(element.thumbnail, screen)
let thumbnailSize = NSSize(width: thumbnailWidth.rounded(), height: thumbnailHeight.rounded())
thumbnail.image?.size = thumbnailSize
thumbnail.frame.size = thumbnailSize
appIcon.image = element.icon
let appIconSize = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
appIcon.image?.size = appIconSize
appIcon.frame.size = appIconSize
label.string = element.title
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
label.font = Preferences.font
hiddenIcon.isHidden = !window_!.isHidden
minimizedIcon.isHidden = !window_!.isMinimized
spaceIcon.isHidden = element.spaceIndex == nil || Spaces.isSingleSpace || Preferences.hideSpaceNumberLabels
if !spaceIcon.isHidden {
if element.isOnAllSpaces {
spaceIcon.setStar()
} else {
spaceIcon.setNumber(UInt32(element.spaceIndex!))
}
}
let fontIconWidth = CGFloat([minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding)
label.textContainer!.size.width = frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth
subviews.first!.frame.size = frame.size
self.mouseDownCallback = mouseDownCallback
self.mouseMovedCallback = mouseMovedCallback
if trackingAreas.count > 0 {
removeTrackingArea(trackingAreas[0])
}
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
}

static func makeShadow(_ color: NSColor) -> NSShadow {
let shadow = NSShadow()
shadow.shadowColor = color
shadow.shadowOffset = .zero
shadow.shadowBlurRadius = 1
return shadow
}

static func downscaleFactor() -> CGFloat {
let nCellsBeforePotentialOverflow = Preferences.minRows * Preferences.minCellsPerRow
guard CGFloat(Windows.list.count) > nCellsBeforePotentialOverflow else { return 1 }
// TODO: replace this buggy heuristic with a correct implementation of downscaling
return nCellsBeforePotentialOverflow / (nCellsBeforePotentialOverflow + (sqrt(CGFloat(Windows.list.count) - nCellsBeforePotentialOverflow) * 2))
}

static func widthMax(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.widthMax(screen) / Preferences.minCellsPerRow - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
}

static func widthMin(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.widthMax(screen) / Preferences.maxCellsPerRow - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
}

static func height(_ screen: NSScreen) -> CGFloat {
return (ThumbnailsPanel.heightMax(screen) / Preferences.minRows - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
}

static func width(_ image: NSImage?, _ screen: NSScreen) -> CGFloat {
return max(thumbnailSize(image, screen).0 + Preferences.intraCellPadding * 2, CollectionViewItemView.widthMin(screen))
}

static func thumbnailSize(_ image: NSImage?, _ screen: NSScreen) -> (CGFloat, CGFloat) {
guard let image = image else { return (0, 0) }
let thumbnailHeightMax = CollectionViewItemView.height(screen) - Preferences.intraCellPadding * 3 - Preferences.iconSize
let thumbnailWidthMax = CollectionViewItemView.widthMax(screen) - Preferences.intraCellPadding * 2
let thumbnailHeight = min(image.size.height, thumbnailHeightMax)
let thumbnailWidth = min(image.size.width, thumbnailWidthMax)
let imageRatio = image.size.width / image.size.height
let thumbnailRatio = thumbnailWidth / thumbnailHeight
if thumbnailRatio > imageRatio {
return (image.size.width * thumbnailHeight / image.size.height, thumbnailHeight)
}
return (thumbnailWidth, image.size.height * thumbnailWidth / image.size.width)
}

private func makeHStackView() -> NSStackView {
let hStackView = NSStackView()
hStackView.spacing = Preferences.intraCellPadding
hStackView.setViews([appIcon, label, hiddenIcon, minimizedIcon, spaceIcon], in: .leading)
return hStackView
}

private func makeVStackView(_ hStackView: NSStackView) -> NSStackView {
let vStackView = NSStackView()
vStackView.wantsLayer = true
vStackView.layer!.backgroundColor = .clear
vStackView.layer!.cornerRadius = Preferences.cellCornerRadius
vStackView.layer!.borderWidth = Preferences.cellBorderWidth
vStackView.layer!.borderColor = .clear
vStackView.edgeInsets = NSEdgeInsets(top: Preferences.intraCellPadding, left: Preferences.intraCellPadding, bottom: Preferences.intraCellPadding, right: Preferences.intraCellPadding)
vStackView.orientation = .vertical
vStackView.spacing = Preferences.intraCellPadding
vStackView.setViews([hStackView, thumbnail], in: .leading)
return vStackView
}
}

typealias MouseDownCallback = () -> Void
typealias MouseMovedCallback = () -> Void
Loading

0 comments on commit e1e3633

Please sign in to comment.