feat: improve memory image of showing app icons (closes #5144)
This commit is contained in:
1 parent
5c4199ec74
commit
bf58335bbe
14 files changed
+65
-38
No files matched your search
@@ -194,11 +194,6 @@ extension DispatchQoS {
|
||||
}
|
||||
|
||||
extension NSImage {
|
||||
func appIconFixedSize(_ size: NSSize) -> CGImage? {
|
||||
var rect = NSRect(origin: .zero, size: size)
|
||||
return cgImage(forProposedRect: &rect, context: nil, hints: nil)
|
||||
}
|
||||
|
||||
// NSImage(named) caches/reuses NSImage objects; we force separate instances of images by using copy()
|
||||
static func initCopy(_ name: String) -> NSImage {
|
||||
return NSImage(named: name)!.copy() as! NSImage
|
||||
|
||||
@@ -45,16 +45,19 @@ class Application: NSObject {
|
||||
return 0
|
||||
}()
|
||||
|
||||
private static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
|
||||
static func appIconWithoutPadding(_ icon: NSImage?) -> CGImage? {
|
||||
guard let icon else { return nil }
|
||||
// we can render the icon quite big (e.g. windowless app icon), so we store it high-res
|
||||
let iconWidth = CGFloat(1024)
|
||||
// NSRunningApplication.icon returns icons with padding; we remove it manually
|
||||
|
||||
let croppedSize = iconWidth - appIconPadding * 2
|
||||
return icon
|
||||
.appIconFixedSize(NSSize(width: iconWidth, height: iconWidth))?
|
||||
.cropping(to: CGRect(x: appIconPadding, y: appIconPadding, width: croppedSize, height: croppedSize).integral)
|
||||
let finalWidth = max(ThumbnailsPanel.maxPossibleAppIconSize.width, ThumbnailsPanel.maxPossibleAppIconSize.height)
|
||||
let padding = appIconPadding * (finalWidth / (1024 - appIconPadding * 2))
|
||||
let sourceWidth = finalWidth + padding * 2
|
||||
guard let context = CGContext(data: nil, width: Int(finalWidth), height: Int(finalWidth), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { return nil }
|
||||
context.interpolationQuality = .high
|
||||
let drawRect = CGRect(x: -padding, y: -padding, width: sourceWidth, height: sourceWidth)
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false)
|
||||
icon.draw(in: drawRect, from: .zero, operation: .copy, fraction: 1.0, respectFlipped: false, hints: nil)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
return context.makeImage()
|
||||
}
|
||||
|
||||
init(_ runningApplication: NSRunningApplication) {
|
||||
|
||||
@@ -161,4 +161,16 @@ class Applications {
|
||||
static func find(_ pid: pid_t?) -> Application? {
|
||||
return list.first { $0.pid == pid }
|
||||
}
|
||||
|
||||
static func updateAppIcons() {
|
||||
for app in list {
|
||||
BackgroundWork.screenshotsQueue.addOperation { [weak app] in
|
||||
guard let app else { return }
|
||||
let r = Application.appIconWithoutPadding(app.runningApplication.icon)
|
||||
DispatchQueue.main.async { [weak app] in
|
||||
app?.icon = r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class Window {
|
||||
if !view.thumbnail.isHidden {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot.size(), false)
|
||||
let newSize = thumbnailSize.width != view.thumbnail.frame.width || thumbnailSize.height != view.thumbnail.frame.height
|
||||
view.thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
|
||||
view.thumbnail.updateContents(screenshot, thumbnailSize)
|
||||
// if the thumbnail size has changed, we need to refresh the open UI
|
||||
if newSize {
|
||||
App.app.refreshOpenUi([], .refreshOnlyThumbnailsAfterShowUi)
|
||||
|
||||
@@ -8,7 +8,6 @@ class SpacesEvents {
|
||||
@objc private static func handleEvent(_ notification: Notification) {
|
||||
ScreensEvents.debouncerScreenAndSpace.debounce(.spaceEvent) {
|
||||
Logger.debug { notification.name.rawValue }
|
||||
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
|
||||
// Workaround for Safari full-screen videos
|
||||
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
|
||||
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
|
||||
|
||||
@@ -306,6 +306,7 @@ extension App: NSApplicationDelegate {
|
||||
SpacesEvents.observe()
|
||||
ScreensEvents.observe()
|
||||
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
|
||||
ThumbnailsPanel.updateMaxPossibleAppIconSize()
|
||||
SystemAppearanceEvents.observe()
|
||||
SystemScrollerStyleEvents.observe()
|
||||
Applications.initialDiscovery()
|
||||
|
||||
@@ -29,7 +29,7 @@ class FeedbackWindow: NSWindow {
|
||||
|
||||
private func setupView() {
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
|
||||
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
|
||||
appIcon.fit(80, 80)
|
||||
let appText = StackView([
|
||||
BoldLabel(NSLocalizedString("Share improvement ideas, or report bugs", comment: "")),
|
||||
|
||||
@@ -27,12 +27,7 @@ class LightImageView: NSView {
|
||||
layerContentsRedrawPolicy = .never
|
||||
}
|
||||
|
||||
func updateWithResizedCopy(_ caLayerContents: CALayerContents, _ size: NSSize) {
|
||||
let scaleFactor = NSScreen.preferred.backingScaleFactor
|
||||
// alternatively, we could set layer!.contentsGravity to .center, and use the lines bellow to resize ourselves
|
||||
// let scaledSize = NSSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
|
||||
// layer!.contents = image.resizedCopyWithCoreGraphics(scaledSize, fixBitmapInfo)
|
||||
// it would produce subjectively better quality, but the resizing would be done on the CPU so poor performance
|
||||
func updateContents(_ caLayerContents: CALayerContents, _ size: NSSize) {
|
||||
switch caLayerContents {
|
||||
case .cgImage(let image?):
|
||||
layer!.contents = image
|
||||
@@ -40,7 +35,6 @@ class LightImageView: NSView {
|
||||
layer!.contents = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue()
|
||||
default: break
|
||||
}
|
||||
layer!.contentsScale = scaleFactor
|
||||
if frame.size != size {
|
||||
frame.size = size
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class PreviewPanel: NSPanel {
|
||||
func show(_ id: CGWindowID, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
|
||||
if id != currentId {
|
||||
repositionAndResize(position, size)
|
||||
previewView.updateWithResizedCopy(preview, size)
|
||||
previewView.updateContents(preview, size)
|
||||
}
|
||||
if id != currentId || !isVisible {
|
||||
if Preferences.previewFadeInAnimation {
|
||||
@@ -54,7 +54,7 @@ class PreviewPanel: NSPanel {
|
||||
func updateIfShowing(_ id: CGWindowID?, _ preview: CALayerContents, _ position: CGPoint, _ size: CGSize) {
|
||||
if isVisible && id == currentId {
|
||||
repositionAndResize(position, size)
|
||||
previewView.updateWithResizedCopy(preview, size)
|
||||
previewView.updateContents(preview, size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ class ThumbnailView: FlippedView {
|
||||
|
||||
private func updateAppIcon(_ element: Window, _ title: String) {
|
||||
let appIconSize = ThumbnailView.iconSize()
|
||||
appIcon.updateWithResizedCopy(.cgImage(element.icon), appIconSize)
|
||||
appIcon.updateContents(.cgImage(element.icon), appIconSize)
|
||||
appIcon.setAccessibilityLabel(title)
|
||||
}
|
||||
|
||||
@@ -342,11 +342,11 @@ class ThumbnailView: FlippedView {
|
||||
if !thumbnail.isHidden {
|
||||
if let screenshot = element.thumbnail {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(element.size, false)
|
||||
thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
|
||||
thumbnail.updateContents(screenshot, thumbnailSize)
|
||||
} else {
|
||||
// if no thumbnail, show appIcon instead
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
|
||||
thumbnail.updateWithResizedCopy(.cgImage(element.icon), thumbnailSize)
|
||||
thumbnail.updateContents(.cgImage(element.icon), thumbnailSize)
|
||||
}
|
||||
// for Accessibility > "speak items under the pointer"
|
||||
thumbnail.setAccessibilityLabel(element.title)
|
||||
@@ -373,7 +373,7 @@ class ThumbnailView: FlippedView {
|
||||
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
|
||||
if !windowlessIcon.isHidden {
|
||||
let windowlessIconSize = ThumbnailView.thumbnailSize(element.icon?.size(), true)
|
||||
windowlessIcon.updateWithResizedCopy(.cgImage(element.icon), windowlessIconSize)
|
||||
windowlessIcon.updateContents(.cgImage(element.icon), windowlessIconSize)
|
||||
}
|
||||
windowControlIcons.forEach { $0.window_ = element }
|
||||
showOrHideWindowControls(isShowingWindowControls)
|
||||
@@ -575,8 +575,8 @@ class ThumbnailView: FlippedView {
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
static func minThumbnailWidth() -> CGFloat {
|
||||
return ThumbnailsPanel.maxThumbnailsWidth() * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
|
||||
static func minThumbnailWidth(_ screen: NSScreen = NSScreen.preferred) -> CGFloat {
|
||||
return ThumbnailsPanel.maxThumbnailsWidth(screen) * Appearance.windowMinWidthInRow - Appearance.interCellPadding * 2
|
||||
}
|
||||
|
||||
/// The maximum height that a thumbnail can be drawn
|
||||
@@ -622,9 +622,9 @@ class ThumbnailView: FlippedView {
|
||||
return NSSize(width: width.rounded(), height: height.rounded())
|
||||
}
|
||||
|
||||
static func iconSize() -> NSSize {
|
||||
static func iconSize(_ screen: NSScreen = NSScreen.preferred) -> NSSize {
|
||||
if Preferences.appearanceStyle == .appIcons {
|
||||
let widthMin = ThumbnailView.minThumbnailWidth()
|
||||
let widthMin = ThumbnailView.minThumbnailWidth(screen)
|
||||
let contentWidth = Appearance.iconSize
|
||||
let frameWidth = contentWidth + Appearance.edgeInsetsSize * 2
|
||||
let width = max(frameWidth, widthMin).rounded()
|
||||
|
||||
@@ -5,6 +5,7 @@ class ThumbnailsPanel: NSPanel {
|
||||
override var canBecomeKey: Bool { true }
|
||||
private var didDisplayOnce = false
|
||||
static var maxPossibleThumbnailSize = NSSize.zero
|
||||
static var maxPossibleAppIconSize = NSSize.zero
|
||||
|
||||
convenience init() {
|
||||
self.init(contentRect: .zero, styleMask: .nonactivatingPanel, backing: .buffered, defer: false)
|
||||
@@ -96,7 +97,22 @@ class ThumbnailsPanel: NSPanel {
|
||||
(max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
|
||||
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
|
||||
}
|
||||
maxPossibleThumbnailSize = NSSize(width: w, height: h)
|
||||
maxPossibleThumbnailSize = NSSize(width: w.rounded(), height: h.rounded())
|
||||
}
|
||||
|
||||
static func updateMaxPossibleAppIconSize() {
|
||||
let (w, h) = NSScreen.screens.reduce((CGFloat.zero, CGFloat.zero)) { acc, screen in
|
||||
// in Thumbnails Appearance, AppIcons can be used for windowless apps, thus much bigger than the app icon near the title
|
||||
if Preferences.appearanceStyle == .thumbnails {
|
||||
return (max(acc.0, ThumbnailView.maxThumbnailWidth(screen) * screen.backingScaleFactor),
|
||||
max(acc.1, ThumbnailView.maxThumbnailHeight(screen) * screen.backingScaleFactor))
|
||||
} else {
|
||||
let size = ThumbnailView.iconSize(screen)
|
||||
return (max(acc.0, size.width * screen.backingScaleFactor),
|
||||
max(acc.1, size.height * screen.backingScaleFactor))
|
||||
}
|
||||
}
|
||||
maxPossibleAppIconSize = NSSize(width: w.rounded(), height: h.rounded())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,15 @@ class ThumbnailsView {
|
||||
// it would be nicer to remove this whole "reset" logic, and instead update each component to check Appearance properties before showing
|
||||
// Maybe in some Appkit willDraw() function that triggers before drawing it
|
||||
NSScreen.updatePreferred()
|
||||
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
|
||||
Appearance.update()
|
||||
// thumbnails are captured continuously. They will pick up the new size on the next cycle
|
||||
ThumbnailsPanel.updateMaxPossibleThumbnailSize()
|
||||
// app icons are captured once at launch; we need to manually update them if needed
|
||||
let old = ThumbnailsPanel.maxPossibleAppIconSize.width
|
||||
ThumbnailsPanel.updateMaxPossibleAppIconSize()
|
||||
if old != ThumbnailsPanel.maxPossibleAppIconSize.width {
|
||||
Applications.updateAppIcons()
|
||||
}
|
||||
updateBackgroundView()
|
||||
App.app.thumbnailsPanel.contentView = contentView
|
||||
for i in 0..<ThumbnailsView.recycledViews.count {
|
||||
|
||||
@@ -40,7 +40,7 @@ class PermissionsWindow: NSWindow {
|
||||
|
||||
private func setupView() {
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
|
||||
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 80, height: 80))
|
||||
appIcon.fit(80, 80)
|
||||
let appText = TitleLabel(NSLocalizedString("AltTab needs some permissions", comment: ""))
|
||||
appText.preferredMaxLayoutWidth = 380
|
||||
|
||||
@@ -3,7 +3,7 @@ import Cocoa
|
||||
class AboutTab {
|
||||
static func initTab() -> NSView {
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(.cgImage(App.appIcon), NSSize(width: 256, height: 256))
|
||||
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 256, height: 256))
|
||||
appIcon.fit(256, 256)
|
||||
let appText = StackView([
|
||||
BoldLabel(App.name),
|
||||
|
||||
Reference in new issue
Block a user