feat: improve thumbnails quality and performance (closes #4183)
This commit is contained in:
1 parent
2cd8b96d38
commit
9d6fc68642
18 files changed
+265
-125
No files matched your search
@@ -85,6 +85,7 @@
|
||||
BF0C812C068CD751CE9BFB67 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D5993267A74D4875C1A /* Localizable.strings */; };
|
||||
BF0C81480366BAD71BC9A158 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8ABA423E72218605CA4F /* InfoPlist.strings */; };
|
||||
BF0C816911CAD3F1B5865BFA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82DF98B6161C45361197 /* Localizable.strings */; };
|
||||
BF0C81A1933D59E1063E350D /* 20MB-photo.jpg in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8E1C49845D813673D7A2 /* 20MB-photo.jpg */; };
|
||||
BF0C81A79128CF877FE895CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8A48BBB3BF649864DB08 /* InfoPlist.strings */; };
|
||||
BF0C81B64A1205F066AC796A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C80404D3455D496C02F29 /* Localizable.strings */; };
|
||||
BF0C81D836A8A4CA393B135F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82E55127D38ABF969CA0 /* InfoPlist.strings */; };
|
||||
@@ -105,6 +106,7 @@
|
||||
BF0C8406805BDEC5B69E8803 /* screen-recording@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8E25373671B528AD448D /* screen-recording@2x.png */; };
|
||||
BF0C84D7DC2AC65BB1C111EB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8349BA94516E8760BAFA /* Localizable.strings */; };
|
||||
BF0C84E5AC1D423625ABF3E6 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C829EAA93A4C4ADB42CF8 /* Mocks.swift */; };
|
||||
BF0C8521DA3078516615C414 /* HelperExtensionsTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */; };
|
||||
BF0C852D28E458CDD6FA3E44 /* app.icns in Resources */ = {isa = PBXBuildFile; fileRef = BF0C84E11784B3B0900D0963 /* app.icns */; };
|
||||
BF0C853B271C4407E87FA183 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8096A88BC18C4902C037 /* InfoPlist.strings */; };
|
||||
BF0C8571D70170B73B46A262 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C86631FA950CE81FB8542 /* Localizable.strings */; };
|
||||
@@ -118,6 +120,7 @@
|
||||
BF0C863526C260F4155F9D2C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C852BFEEFB55B6E4EF18D /* Localizable.strings */; };
|
||||
BF0C8658CC748011E57691B7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C84F4B25A7C26A88ADE92 /* Localizable.strings */; };
|
||||
BF0C865E3C3F3B353A7A7CBC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C86FFF1C801761B7CE363 /* Localizable.strings */; };
|
||||
BF0C866DE5D740AF7756CDCF /* ImageScalingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C854910CCD4FB493C3A5D /* ImageScalingTests.swift */; };
|
||||
BF0C86BB79E417163E813179 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C80851C26EDB3DB98556C /* InfoPlist.strings */; };
|
||||
BF0C875A28B98904F8F4A66A /* ATShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8BA452332236D972C60E /* ATShortcut.swift */; };
|
||||
BF0C87732A6E660899D13BFD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C886B1D6E707E9E6BE46C /* InfoPlist.strings */; };
|
||||
@@ -133,6 +136,7 @@
|
||||
BF0C896B5D08B7DA8F2024AC /* KeyRepeatTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8D606B53D315F2D9927B /* KeyRepeatTimer.swift */; };
|
||||
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8C69316EAAF6DC9AE391 /* CliEvents.swift */; };
|
||||
BF0C898511686611E4D7D81E /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8B2C057099EDABE46779 /* Button.swift */; };
|
||||
BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */; };
|
||||
BF0C89B4B17D7843C32A102E /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C8353097840D7DB1E0E2E /* Markdown.swift */; };
|
||||
BF0C89C49CFBAAB4646BB8CB /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82A8848224E76B880A83 /* InfoPlist.strings */; };
|
||||
BF0C89F0632E0A2EC52D9168 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C87749F1A64E2D3305181 /* InfoPlist.strings */; };
|
||||
@@ -168,6 +172,7 @@
|
||||
BF0C8EA4654F8059F0AED40E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C82992C31AA542EF9E638 /* InfoPlist.strings */; };
|
||||
BF0C8EF58A07D9752A6392FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C8D928E2ABECB92738113 /* Localizable.strings */; };
|
||||
BF0C8F3526BC393290D5BE59 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C81EC1C5F3052A75C7D7B /* InfoPlist.strings */; };
|
||||
BF0C8F9B8014AD21E68B1349 /* HelperExtensionsTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */; };
|
||||
BF0C8FF2A4B00D409494EA80 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0C88D79FA0C773D94F351A /* InfoPlist.strings */; };
|
||||
D04BA004884A273D4D2D3EF1 /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD91161791D42FEC4A60 /* HelperExtensions.swift */; };
|
||||
D04BA0182F4734A5C8BB63DC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA5461DAEE9B5350C3020 /* Localizable.strings */; };
|
||||
@@ -347,6 +352,7 @@
|
||||
BF0C809C9DE36EA1317816FB /* nn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nn; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C80A05C11C8ADC366EC4A /* lb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = lb; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C80CD087C6384E0340F4E /* convert_font_to_png.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = convert_font_to_png.sh; sourceTree = "<group>"; };
|
||||
BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelperExtensionsTestable.swift; sourceTree = "<group>"; };
|
||||
BF0C8113492C0FFCB4454BC9 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C8118F91A00A04AD4DE26 /* bg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = bg; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C813DE8B55A735B1720B5 /* subset_font.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = subset_font.sh; sourceTree = "<group>"; };
|
||||
@@ -388,6 +394,7 @@
|
||||
BF0C84E11784B3B0900D0963 /* app.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = app.icns; sourceTree = "<group>"; };
|
||||
BF0C850C46CCD8B978168CCD /* sv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sv; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
BF0C855CD9DBCAEFE48F9EA5 /* AXUIElementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AXUIElementTests.swift; sourceTree = "<group>"; };
|
||||
BF0C854910CCD4FB493C3A5D /* ImageScalingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageScalingTests.swift; sourceTree = "<group>"; };
|
||||
BF0C85C20516C4210508F349 /* tsconfig.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tsconfig.json; sourceTree = "<group>"; };
|
||||
BF0C861255F42BF7B23408EF /* sq */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sq; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C863EDD60D925950B223B /* convert_iconset_to_icns.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = convert_iconset_to_icns.sh; sourceTree = "<group>"; };
|
||||
@@ -473,11 +480,13 @@
|
||||
BF0C8DF008F1EB5F23291A13 /* SpacesEvents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacesEvents.swift; sourceTree = "<group>"; };
|
||||
BF0C8DF173A246A0E0C30649 /* override_tags_on_poeditor.ts */ = {isa = PBXFileReference; lastKnownFileType = file.ts; path = override_tags_on_poeditor.ts; sourceTree = "<group>"; };
|
||||
BF0C8DF400A1AF64EB0CD0B3 /* TrafficLightButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrafficLightButton.swift; sourceTree = "<group>"; };
|
||||
BF0C8E1C49845D813673D7A2 /* 20MB-photo.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "20MB-photo.jpg"; sourceTree = "<group>"; };
|
||||
BF0C8E1FBCBC7B64C69D933C /* titles-style-full.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "titles-style-full.jpg"; sourceTree = "<group>"; };
|
||||
BF0C8E25373671B528AD448D /* screen-recording@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "screen-recording@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8E478D68ED85DAE6BF43 /* ga */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ga; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E54658B4DAEA0528200 /* menubar-2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menubar-2@2x.png"; sourceTree = "<group>"; };
|
||||
BF0C8E5D2BD5F7F93DF77DF5 /* sk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sk; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightImageView.swift; sourceTree = "<group>"; };
|
||||
BF0C8E7B087DCC97A454C0AE /* uk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = uk; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
BF0C8EAF02309B9C489E65F4 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
BF0C8EC400A560FDF99F8C35 /* el */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = el; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -975,6 +984,8 @@
|
||||
BF0C829EAA93A4C4ADB42CF8 /* Mocks.swift */,
|
||||
BF0C8C6810FF50908E637A48 /* ConcurrentScreenshots.swift */,
|
||||
BF0C855CD9DBCAEFE48F9EA5 /* AXUIElementTests.swift */,
|
||||
BF0C854910CCD4FB493C3A5D /* ImageScalingTests.swift */,
|
||||
BF0C8AE65DDFD6359BE12CB3 /* resources */,
|
||||
);
|
||||
path = "unit-tests";
|
||||
sourceTree = "<group>";
|
||||
@@ -1041,6 +1052,14 @@
|
||||
path = ca.lproj;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF0C8AE65DDFD6359BE12CB3 /* resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF0C8E1C49845D813673D7A2 /* 20MB-photo.jpg */,
|
||||
);
|
||||
path = resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF0C8B1DA0DFC696489608FF /* bg.lproj */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1344,6 +1363,7 @@
|
||||
1C96189273B7482124C36F4C /* Switch.swift */,
|
||||
1C9617C5839903D752070CF2 /* ImageTextButtonView.swift */,
|
||||
BF0C896FD4907CD85D62C124 /* WindowlessAppIndicator.swift */,
|
||||
BF0C8E63BE7394A6591E74E0 /* LightImageView.swift */,
|
||||
);
|
||||
path = "generic-components";
|
||||
sourceTree = "<group>";
|
||||
@@ -1437,6 +1457,7 @@
|
||||
BF0C8B841E890CA0D68FFD86 /* Logger.swift */,
|
||||
BF0C8353097840D7DB1E0E2E /* Markdown.swift */,
|
||||
BF0C88D101728C6798C7FC07 /* private-apis */,
|
||||
BF0C810FA0F6BC70E5886E11 /* HelperExtensionsTestable.swift */,
|
||||
);
|
||||
path = "api-wrappers";
|
||||
sourceTree = "<group>";
|
||||
@@ -1950,6 +1971,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF0C81A1933D59E1063E350D /* 20MB-photo.jpg in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2246,6 +2268,8 @@
|
||||
BF0C84E5AC1D423625ABF3E6 /* Mocks.swift in Sources */,
|
||||
BF0C8CBED8D88DFBADE7EC03 /* ConcurrentScreenshots.swift in Sources */,
|
||||
BF0C88FFEE7645A1966C7C4E /* AXUIElementTests.swift in Sources */,
|
||||
BF0C866DE5D740AF7756CDCF /* ImageScalingTests.swift in Sources */,
|
||||
BF0C8F9B8014AD21E68B1349 /* HelperExtensionsTestable.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2337,6 +2361,8 @@
|
||||
BF0C803A13C409E7F01FBCAB /* SkyLight.framework.swift in Sources */,
|
||||
BF0C80C69EFEF93C18539925 /* PreferencesMigrations.swift in Sources */,
|
||||
BF0C8973EC078169170F2634 /* CliEvents.swift in Sources */,
|
||||
BF0C8521DA3078516615C414 /* HelperExtensionsTestable.swift in Sources */,
|
||||
BF0C8991399A3277A9A02FFA /* LightImageView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -193,13 +193,6 @@ extension DispatchQoS {
|
||||
}
|
||||
}
|
||||
|
||||
extension NSImageView {
|
||||
func setSize(_ size: NSSize) {
|
||||
image?.size = size
|
||||
frame.size = size
|
||||
}
|
||||
}
|
||||
|
||||
extension NSTextField {
|
||||
func setWidth(_ width: CGFloat) {
|
||||
frame.size.width = width
|
||||
@@ -210,36 +203,28 @@ extension NSTextField {
|
||||
}
|
||||
|
||||
extension NSImage {
|
||||
/// if an image can be used in different contexts (e.g. multiple NSTextViews), it's safer to copy it
|
||||
/// see https://www.noodlesoft.com/blog/2011/04/15/the-proper-care-and-feeding-of-nsimage/
|
||||
func copyToSeparateContexts() -> NSImage {
|
||||
return copy() as! NSImage
|
||||
func cgImage(maxSize: NSSize) -> CGImage {
|
||||
// some images like NSRunningApp.icon are from icns. They hosts multiple representations and it's hard to know the highest resolution
|
||||
// by setting a maxSize, the returned CGImage will be the biggest it can under that maxSize
|
||||
var rect = NSRect(origin: .zero, size: maxSize)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
static func initTemplateCopy(_ name: String) -> NSImage {
|
||||
let image = initCopy(name)
|
||||
image.isTemplate = true
|
||||
return image
|
||||
extension CGImage {
|
||||
func nsImage() -> NSImage {
|
||||
return NSImage(cgImage: self, size: NSSize(width: width, height: height))
|
||||
}
|
||||
|
||||
// copy and resize an image using high quality interpolation
|
||||
static func initResizedCopy(_ name: String, _ width: CGFloat, _ height: CGFloat) -> NSImage {
|
||||
let original = initCopy(name)
|
||||
let img = NSImage(size: CGSize(width: width, height: height))
|
||||
img.lockFocus()
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
original.draw(in: NSMakeRect(0, 0, width, height), from: NSMakeRect(0, 0, original.size.width, original.size.height), operation: .copy, fraction: 1)
|
||||
img.unlockFocus()
|
||||
return img
|
||||
}
|
||||
|
||||
static func fromCgImage(_ cgImage: CGImage) -> NSImage {
|
||||
return NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
|
||||
static func named(_ imageName: String) -> CGImage {
|
||||
let imageURL = Bundle.main.url(forResource: imageName, withExtension: nil)!
|
||||
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, nil)!
|
||||
return CGImageSourceCreateImageAtIndex(imageSource, 0, nil)!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import Cocoa
|
||||
|
||||
extension CGImage {
|
||||
func resizedCopyWithCoreGraphics(_ newSize: NSSize, _ fixBitmapInfo: Bool) -> CGImage {
|
||||
let finalBitmapInfo = fixBitmapInfo
|
||||
? CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue).union(.byteOrder32Little)
|
||||
: bitmapInfo
|
||||
let context = CGContext(data: nil,
|
||||
width: Int(newSize.width),
|
||||
height: Int(newSize.height),
|
||||
bitsPerComponent: bitsPerComponent,
|
||||
bytesPerRow: 0,
|
||||
space: colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
|
||||
bitmapInfo: finalBitmapInfo.rawValue
|
||||
)!
|
||||
context.interpolationQuality = .high
|
||||
context.draw(self, in: CGRect(origin: .zero, size: newSize))
|
||||
return context.makeImage()!
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class Application: NSObject {
|
||||
var pid: pid_t
|
||||
var isHidden: Bool
|
||||
var hasBeenActiveOnce: Bool
|
||||
var icon: NSImage?
|
||||
var icon: CGImage?
|
||||
var dockLabel: String?
|
||||
var focusedWindow: Window? = nil
|
||||
var alreadyRequestedToQuit = false
|
||||
@@ -25,7 +25,7 @@ class Application: NSObject {
|
||||
pid = runningApplication.processIdentifier
|
||||
isHidden = runningApplication.isHidden
|
||||
hasBeenActiveOnce = runningApplication.isActive
|
||||
icon = runningApplication.icon
|
||||
icon = runningApplication.icon?.cgImage(maxSize: NSSize(width: 1024, height: 1024))
|
||||
localizedName = runningApplication.localizedName
|
||||
bundleIdentifier = runningApplication.bundleIdentifier
|
||||
bundleURL = runningApplication.bundleURL
|
||||
|
||||
+8
-10
@@ -6,9 +6,8 @@ class Window {
|
||||
var lastFocusOrder = Int.zero
|
||||
var creationOrder = Int.zero
|
||||
var title: String!
|
||||
var thumbnail: NSImage?
|
||||
var thumbnailFullSize: NSSize?
|
||||
var icon: NSImage? { get { application.icon } }
|
||||
var thumbnail: CGImage?
|
||||
var icon: CGImage? { get { application.icon } }
|
||||
var shouldShowTheUser = true
|
||||
var isTabbed: Bool = false
|
||||
var isHidden: Bool { get { application.isHidden } }
|
||||
@@ -96,17 +95,16 @@ class Window {
|
||||
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .commonModes)
|
||||
}
|
||||
|
||||
func refreshThumbnail(_ screenshot: NSImage) {
|
||||
func refreshThumbnail(_ screenshot: CGImage) {
|
||||
thumbnail = screenshot
|
||||
thumbnailFullSize = screenshot.size
|
||||
if !App.app.appIsBeingUsed || !shouldShowTheUser { return }
|
||||
if let view = (ThumbnailsView.recycledViews.first { $0.window_?.cgWindowId == cgWindowId }) {
|
||||
if let size,
|
||||
let view = (ThumbnailsView.recycledViews.first { $0.window_?.cgWindowId == cgWindowId }) {
|
||||
if !view.thumbnail.isHidden {
|
||||
view.thumbnail.image = thumbnail?.copyToSeparateContexts()
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(thumbnail, false)
|
||||
view.thumbnail.setSize(thumbnailSize)
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot, false)
|
||||
view.thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
|
||||
}
|
||||
App.app.previewPanel.updateImageIfShowing(cgWindowId, screenshot, screenshot.size)
|
||||
App.app.previewPanel.updateImageIfShowing(cgWindowId, screenshot, size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Windows {
|
||||
order = .orderedSame
|
||||
} else if $0.isOnAllSpaces {
|
||||
order = .orderedAscending
|
||||
} else if $1.isOnAllSpaces {
|
||||
} else if $1.isOnAllSpaces {
|
||||
order = .orderedDescending
|
||||
} else if let spaceIndex0 = $0.spaceIndexes.first, let spaceIndex1 = $1.spaceIndexes.first {
|
||||
order = spaceIndex0.compare(spaceIndex1)
|
||||
@@ -323,7 +323,7 @@ class Windows {
|
||||
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
|
||||
DispatchQueue.main.async { [weak window] in
|
||||
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
|
||||
window?.refreshThumbnail(NSImage.fromCgImage(cgImage))
|
||||
window?.refreshThumbnail(cgImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class App: AppCenterApplication {
|
||||
static let licence = Bundle.main.object(forInfoDictionaryKey: "NSHumanReadableCopyright") as! String
|
||||
static let repository = "https://github.com/lwouis/alt-tab-macos"
|
||||
static let website = "https://alt-tab-macos.netlify.app"
|
||||
static let appIcon = CGImage.named("app.icns")
|
||||
static var app: App!
|
||||
var thumbnailsPanel: ThumbnailsPanel!
|
||||
var previewPanel: PreviewPanel!
|
||||
|
||||
@@ -28,14 +28,16 @@ class FeedbackWindow: NSWindow {
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
let appIcon = NSImageView(image: NSImage.initResizedCopy("app", 80, 80))
|
||||
appIcon.imageScaling = .scaleNone
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(App.appIcon, NSSize(width: 80, height: 80), fixBitmapInfo: true)
|
||||
appIcon.fit(80, 80)
|
||||
let appText = StackView([
|
||||
BoldLabel(NSLocalizedString("Share improvement ideas, or report bugs", comment: "")),
|
||||
HyperlinkLabel(NSLocalizedString("View existing discussions", comment: ""), App.repository + "/issues"),
|
||||
], .vertical)
|
||||
appText.spacing = GridView.interPadding / 2
|
||||
let header = NSStackView(views: [appIcon, appText])
|
||||
header.translatesAutoresizingMaskIntoConstraints = false
|
||||
header.spacing = GridView.interPadding
|
||||
sendButton = NSButton(title: NSLocalizedString("Send", comment: ""), target: nil, action: #selector(sendCallback))
|
||||
sendButton.keyEquivalent = "\r"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import Cocoa
|
||||
|
||||
/// this is a lightweight view which displays an image using its CALayer
|
||||
/// it is an alternative to NSImageView, which doesn't have internal complexities and performance costs
|
||||
class LightImageView: NSView {
|
||||
var image: CGImage? { get { layer!.contents as! CGImage? } }
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Class only supports programmatic initialization")
|
||||
}
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
wantsLayer = true
|
||||
layer!.contentsGravity = .resize
|
||||
layer!.magnificationFilter = .trilinear
|
||||
layer!.minificationFilter = .trilinear
|
||||
layer!.minificationFilterBias = 0.0
|
||||
layer!.shouldRasterize = false
|
||||
}
|
||||
|
||||
func updateWithResizedCopy(_ image: CGImage?, _ size: NSSize, fixBitmapInfo: Bool = false) {
|
||||
let scaleFactor = NSScreen.preferred.backingScaleFactor
|
||||
if let image {
|
||||
CATransaction.begin()
|
||||
// disable implicit fade-in animation from CALayer
|
||||
CATransaction.setDisableActions(true)
|
||||
// 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
|
||||
layer!.contents = image
|
||||
layer!.contentsScale = scaleFactor
|
||||
CATransaction.commit()
|
||||
}
|
||||
frame.size = size
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ class TrafficLightButton: NSButton {
|
||||
var window_: Window?
|
||||
|
||||
init(_ type: TrafficLightButtonType, _ tooltip: String) {
|
||||
super.init(frame: .init(origin: .zero, size: .init(width: TrafficLightButton.size, height: TrafficLightButton.size)))
|
||||
super.init(frame: NSRect(origin: .zero, size: NSSize(width: TrafficLightButton.size, height: TrafficLightButton.size)))
|
||||
self.type = type
|
||||
target = self
|
||||
action = #selector(onClick)
|
||||
fit(TrafficLightButton.size, TrafficLightButton.size)
|
||||
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: nil))
|
||||
toolTip = tooltip
|
||||
appearance = .init(named: .aqua)
|
||||
appearance = NSAppearance(named: .aqua)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Cocoa
|
||||
|
||||
class PreviewPanel: NSPanel {
|
||||
private let previewView = NSImageView()
|
||||
private let previewView = LightImageView()
|
||||
private let borderView = BorderView()
|
||||
private var currentId: CGWindowID?
|
||||
|
||||
@@ -27,17 +27,15 @@ class PreviewPanel: NSPanel {
|
||||
setAccessibilitySubrole(.unknown)
|
||||
}
|
||||
|
||||
func updateImageIfShowing(_ id: CGWindowID?, _ preview: NSImage, _ size: CGSize) {
|
||||
if id == currentId && previewView.image != preview {
|
||||
previewView.image = preview.copyToSeparateContexts()
|
||||
previewView.image!.size = size
|
||||
func updateImageIfShowing(_ id: CGWindowID?, _ preview: CGImage, _ size: CGSize) {
|
||||
if id == currentId {
|
||||
previewView.updateWithResizedCopy(preview, size)
|
||||
}
|
||||
}
|
||||
|
||||
func show(_ id: CGWindowID, _ preview: NSImage, _ position: CGPoint, _ size: CGSize) {
|
||||
if previewView.image != preview {
|
||||
previewView.image = preview.copyToSeparateContexts()
|
||||
previewView.image!.size = size
|
||||
func show(_ id: CGWindowID, _ preview: CGImage, _ position: CGPoint, _ size: CGSize) {
|
||||
if id != currentId {
|
||||
previewView.updateWithResizedCopy(preview, size)
|
||||
var frame = NSRect(origin: position, size: size)
|
||||
// Flip Y coordinate from Quartz (0,0 at bottom-left) to Cocoa coordinates (0,0 at top-left)
|
||||
// Always use the primary screen as reference since all coordinates are relative to it
|
||||
|
||||
@@ -3,10 +3,10 @@ import Cocoa
|
||||
class ThumbnailView: FlippedView {
|
||||
static let noOpenWindowToolTip = NSLocalizedString("App is running but has no open window", comment: "")
|
||||
var window_: Window?
|
||||
var thumbnail = NSImageView()
|
||||
var windowlessIcon = NSImageView()
|
||||
var thumbnail = LightImageView()
|
||||
var windowlessIcon = LightImageView()
|
||||
let thumbnailContainer = FlippedView()
|
||||
var appIcon = NSImageView()
|
||||
var appIcon = LightImageView()
|
||||
var label = ThumbnailTitleView(shadow: ThumbnailView.makeShadow(Appearance.titleShadowColor), font: Appearance.font)
|
||||
var fullscreenIcon = ThumbnailFontIconView(symbol: .circledPlusSign, tooltip: NSLocalizedString("Window is fullscreen", comment: ""))
|
||||
var minimizedIcon = ThumbnailFontIconView(symbol: .circledMinusSign, tooltip: NSLocalizedString("Window is minimized", comment: ""))
|
||||
@@ -137,8 +137,7 @@ class ThumbnailView: FlippedView {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updateDockLabelIcon(_ dockLabel: String?) -> Bool {
|
||||
func updateDockLabelIcon(_ dockLabel: String?) {
|
||||
assignIfDifferent(&dockLabelIcon.isHidden, dockLabel == nil || Preferences.hideAppBadges || Appearance.iconSize == 0)
|
||||
if !dockLabelIcon.isHidden, let dockLabel {
|
||||
let view = dockLabelIcon.subviews[1] as! ThumbnailFontIconView
|
||||
@@ -149,9 +148,7 @@ class ThumbnailView: FlippedView {
|
||||
view.setNumber(dockLabelInt!, true)
|
||||
}
|
||||
view.setAccessibilityLabel(getAccessibilityTextForBadge(dockLabel))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
@@ -170,12 +167,9 @@ class ThumbnailView: FlippedView {
|
||||
thumbnail.shadow = shadow
|
||||
windowlessIcon.shadow = shadow
|
||||
appIcon.shadow = shadow
|
||||
thumbnail.imageScaling = .scaleProportionallyUpOrDown
|
||||
windowlessIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
windowlessIcon.imageScaling = .scaleProportionallyUpOrDown
|
||||
windowlessIcon.toolTip = ThumbnailView.noOpenWindowToolTip
|
||||
appIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
appIcon.imageScaling = .scaleProportionallyUpOrDown
|
||||
appIcon.setSubviewAbove(dockLabelIcon)
|
||||
label.fixHeight()
|
||||
vStackView.wantsLayer = true
|
||||
@@ -306,28 +300,10 @@ class ThumbnailView: FlippedView {
|
||||
view.label.toolTip = view.label.cell!.cellSize.width >= view.label.frame.size.width ? view.label.stringValue : nil
|
||||
}
|
||||
|
||||
private func updateThumbnail(_ element: Window) {
|
||||
if !thumbnail.isHidden && thumbnail != element.thumbnail {
|
||||
if let screenshot = element.thumbnail {
|
||||
thumbnail.image = screenshot.copyToSeparateContexts()
|
||||
} else {
|
||||
thumbnail.image = element.icon?.copyToSeparateContexts()
|
||||
// if no thumbnail, show appIcon instead
|
||||
// .icon is always 32x32 by default. We scale it so it can be "thumbnail-ed" down
|
||||
thumbnail.setSize(NSSize(width: 1024, height: 1024))
|
||||
}
|
||||
// for Accessibility > "speak items under the pointer"
|
||||
thumbnail.setAccessibilityLabel(element.title)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAppIcon(_ element: Window, _ title: String) -> Bool {
|
||||
let appIconChanged = appIcon.image != element.icon
|
||||
if appIconChanged {
|
||||
appIcon.image = element.icon?.copyToSeparateContexts()
|
||||
appIcon.setAccessibilityLabel(title)
|
||||
}
|
||||
return appIconChanged
|
||||
private func updateAppIcon(_ element: Window, _ title: String) {
|
||||
let appIconSize = ThumbnailView.iconSize()
|
||||
appIcon.updateWithResizedCopy(element.icon, appIconSize)
|
||||
appIcon.setAccessibilityLabel(title)
|
||||
}
|
||||
|
||||
private func updateValues(_ element: Window, _ index: Int, _ newHeight: CGFloat) {
|
||||
@@ -342,7 +318,18 @@ class ThumbnailView: FlippedView {
|
||||
NSScreen.screens.count < 2 || Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab
|
||||
)
|
||||
))
|
||||
updateThumbnail(element)
|
||||
if !thumbnail.isHidden {
|
||||
if let screenshot = element.thumbnail {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(screenshot, false)
|
||||
thumbnail.updateWithResizedCopy(screenshot, thumbnailSize)
|
||||
} else {
|
||||
// if no thumbnail, show appIcon instead
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(element.icon, true)
|
||||
thumbnail.updateWithResizedCopy(element.icon, thumbnailSize)
|
||||
}
|
||||
// for Accessibility > "speak items under the pointer"
|
||||
thumbnail.setAccessibilityLabel(element.title)
|
||||
}
|
||||
let title = getAppOrAndWindowTitle()
|
||||
let labelChanged = label.stringValue != title
|
||||
if labelChanged {
|
||||
@@ -360,13 +347,12 @@ class ThumbnailView: FlippedView {
|
||||
spaceIcon.toolTip = String(format: NSLocalizedString("Window is on Space %d", comment: ""), spaceIndex)
|
||||
}
|
||||
}
|
||||
let appIconChanged = updateAppIcon(element, title)
|
||||
let dockLabelChanged = updateDockLabelIcon(element.dockLabel)
|
||||
if appIconChanged || dockLabelChanged {
|
||||
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
|
||||
}
|
||||
if element.isWindowlessApp && windowlessIcon.image != element.icon {
|
||||
windowlessIcon.image = element.icon?.copyToSeparateContexts()
|
||||
updateAppIcon(element, title)
|
||||
updateDockLabelIcon(element.dockLabel)
|
||||
setAccessibilityHelp(getAccessibilityHelp(element.application.localizedName, element.dockLabel))
|
||||
if !windowlessIcon.isHidden {
|
||||
let windowlessIconSize = ThumbnailView.thumbnailSize(element.icon, true)
|
||||
windowlessIcon.updateWithResizedCopy(element.icon, windowlessIconSize)
|
||||
}
|
||||
windowControlIcons.forEach { $0.window_ = element }
|
||||
showOrHideWindowControls(isShowingWindowControls)
|
||||
@@ -375,19 +361,7 @@ class ThumbnailView: FlippedView {
|
||||
}
|
||||
|
||||
private func updateSizes(_ newHeight: CGFloat) {
|
||||
if Preferences.appearanceStyle == .thumbnails {
|
||||
if !thumbnail.isHidden {
|
||||
let thumbnailSize = ThumbnailView.thumbnailSize(thumbnail.image, false)
|
||||
thumbnail.setSize(thumbnailSize)
|
||||
}
|
||||
if !windowlessIcon.isHidden {
|
||||
let windowlessIconSize = ThumbnailView.thumbnailSize(windowlessIcon.image, true)
|
||||
windowlessIcon.setSize(windowlessIconSize)
|
||||
}
|
||||
}
|
||||
setFrameWidthHeight(newHeight)
|
||||
let appIconSize = ThumbnailView.iconSize()
|
||||
appIcon.setSize(appIconSize)
|
||||
if Preferences.appearanceStyle == .appIcons {
|
||||
vStackView.frame.size = NSSize(width: frame.width, height: appIcon.frame.height + Appearance.edgeInsetsSize * 2)
|
||||
hStackView.frame.size = NSSize(width: appIcon.frame.width, height: appIcon.frame.height)
|
||||
@@ -545,8 +519,10 @@ class ThumbnailView: FlippedView {
|
||||
return ((ThumbnailsPanel.maxThumbnailsHeight() - Appearance.interCellPadding) / Appearance.rowsCount - Appearance.interCellPadding).rounded()
|
||||
}
|
||||
|
||||
static func thumbnailSize(_ image: NSImage?, _ isWindowlessApp: Bool) -> NSSize {
|
||||
static func thumbnailSize(_ image: CGImage?, _ isWindowlessApp: Bool) -> NSSize {
|
||||
guard let image else { return NSSize(width: 0, height: 0) }
|
||||
let imageWidth = CGFloat(image.width)
|
||||
let imageHeight = CGFloat(image.height)
|
||||
let thumbnailHeightMax = ThumbnailView.maxThumbnailHeight()
|
||||
- Appearance.edgeInsetsSize * 2
|
||||
- Appearance.intraCellPadding
|
||||
@@ -554,26 +530,26 @@ class ThumbnailView: FlippedView {
|
||||
let thumbnailWidthMax = ThumbnailView.maxThumbnailWidth()
|
||||
- Appearance.edgeInsetsSize * 2
|
||||
// don't stretch very small windows; keep them 1:1 in the switcher
|
||||
if !isWindowlessApp && image.size.width < thumbnailWidthMax && image.size.height < thumbnailHeightMax {
|
||||
return image.size
|
||||
if !isWindowlessApp && imageWidth < thumbnailWidthMax && imageHeight < thumbnailHeightMax {
|
||||
return NSSize(width: imageWidth, height: imageHeight)
|
||||
}
|
||||
let thumbnailHeight = min(image.size.height, thumbnailHeightMax)
|
||||
let thumbnailWidth = min(image.size.width, thumbnailWidthMax)
|
||||
let imageRatio = image.size.width / image.size.height
|
||||
let thumbnailHeight = min(imageHeight, thumbnailHeightMax)
|
||||
let thumbnailWidth = min(imageWidth, thumbnailWidthMax)
|
||||
let imageRatio = imageWidth / imageHeight
|
||||
let thumbnailRatio = thumbnailWidth / thumbnailHeight
|
||||
var width: CGFloat
|
||||
var height: CGFloat
|
||||
if thumbnailRatio > imageRatio {
|
||||
// Keep the height and reduce the width
|
||||
width = image.size.width * thumbnailHeight / image.size.height
|
||||
width = imageWidth * thumbnailHeight / imageHeight
|
||||
height = thumbnailHeight
|
||||
} else if thumbnailRatio < imageRatio {
|
||||
// Keep the width and reduce the height
|
||||
width = thumbnailWidth
|
||||
height = image.size.height * thumbnailWidth / image.size.width
|
||||
height = imageHeight * thumbnailWidth / imageWidth
|
||||
} else {
|
||||
// Enlarge the height to the maximum height and enlarge the width
|
||||
width = thumbnailHeightMax / image.size.height * image.size.width
|
||||
width = thumbnailHeightMax / imageHeight * imageWidth
|
||||
height = thumbnailHeightMax
|
||||
}
|
||||
return NSSize(width: width.rounded(), height: height.rounded())
|
||||
|
||||
@@ -34,12 +34,14 @@ class PermissionsWindow: NSWindow {
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
let appIcon = NSImageView(image: NSImage.initResizedCopy("app", 80, 80))
|
||||
appIcon.imageScaling = .scaleNone
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(App.appIcon, NSSize(width: 80, height: 80), fixBitmapInfo: true)
|
||||
appIcon.fit(80, 80)
|
||||
let appText = TitleLabel(NSLocalizedString("AltTab needs some permissions", comment: ""))
|
||||
appText.preferredMaxLayoutWidth = 380
|
||||
appText.font = .systemFont(ofSize: 25, weight: .regular)
|
||||
let header = NSStackView(views: [appIcon, appText])
|
||||
header.translatesAutoresizingMaskIntoConstraints = false
|
||||
header.spacing = GridView.interPadding
|
||||
accessibilityView = PermissionView(
|
||||
"accessibility",
|
||||
|
||||
@@ -71,9 +71,10 @@ class PreferencesWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
// it seems that new instances of NSToolbarItems have their image set to a 1x1 image
|
||||
if item.image == nil || item.image!.size == .init(width: 1, height: 1) {
|
||||
item.image = NSImage.initTemplateCopy(id)
|
||||
item.maxSize = .init(width: 22, height: 22)
|
||||
if item.image == nil || item.image!.size == NSSize(width: 1, height: 1) {
|
||||
item.image = NSImage.initCopy(id)
|
||||
item.image!.isTemplate = true
|
||||
item.maxSize = NSSize(width: 22, height: 22)
|
||||
}
|
||||
item.target = self
|
||||
item.action = #selector(tabItemClicked)
|
||||
|
||||
@@ -2,8 +2,9 @@ import Cocoa
|
||||
|
||||
class AboutTab {
|
||||
static func initTab() -> NSView {
|
||||
let appIcon = NSImageView(image: NSImage.initResizedCopy("app", 256, 256))
|
||||
appIcon.imageScaling = .scaleNone
|
||||
let appIcon = LightImageView()
|
||||
appIcon.updateWithResizedCopy(App.appIcon, NSSize(width: 256, height: 256), fixBitmapInfo: true)
|
||||
appIcon.fit(256, 256)
|
||||
let appText = StackView([
|
||||
BoldLabel(App.name),
|
||||
NSTextField(wrappingLabelWithString: NSLocalizedString("Version", comment: "") + " " + App.version),
|
||||
@@ -14,7 +15,8 @@ class AboutTab {
|
||||
appText.spacing = GridView.interPadding / 2
|
||||
let rowToSeparate = 3
|
||||
appText.views[rowToSeparate].topAnchor.constraint(equalTo: appText.views[rowToSeparate - 1].bottomAnchor, constant: GridView.interPadding).isActive = true
|
||||
let appInfo = StackView([appIcon, appText])
|
||||
let appInfo = NSStackView(views: [appIcon, appText])
|
||||
appIcon.translatesAutoresizingMaskIntoConstraints = false
|
||||
appInfo.spacing = GridView.interPadding
|
||||
appInfo.alignment = .centerY
|
||||
let sendFeedback = NSButton(title: NSLocalizedString("Send feedback…", comment: ""), target: nil, action: #selector(App.app.showFeedbackPanel))
|
||||
|
||||
@@ -4,7 +4,7 @@ import Sparkle
|
||||
class BlacklistsTab {
|
||||
static func initTab() -> NSView {
|
||||
let blacklist = BlacklistView()
|
||||
let add = NSSegmentedControl.init(images: [NSImage(named: NSImage.addTemplateName)!, NSImage(named: NSImage.removeTemplateName)!], trackingMode: .momentary, target: nil, action: nil)
|
||||
let add = NSSegmentedControl(images: [NSImage(named: NSImage.addTemplateName)!, NSImage(named: NSImage.removeTemplateName)!], trackingMode: .momentary, target: nil, action: nil)
|
||||
add.onAction = {
|
||||
let tableView = blacklist.documentView as! TableView
|
||||
if ($0 as! NSSegmentedControl).selectedSegment == 0 {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//import XCTest
|
||||
//
|
||||
//final class ImageScalingTests: XCTestCase {
|
||||
// /// 4ms
|
||||
// func testCgImageResizedWithCoreGraphics() throws {
|
||||
// let image = load20MbImage().cgImage()
|
||||
// measure(options: options) {
|
||||
// let _ = image.resizedCopyWithCoreGraphics(NSSize(width: 200, height: 200))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// 4ms
|
||||
// func testNsImageResizedWithCoreGraphics() throws {
|
||||
// let image = load20MbImage()
|
||||
// measure(options: options) {
|
||||
// let _ = image.resizedWithCoreGraphics(NSSize(width: 200, height: 200))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// 9ms
|
||||
// func testResizedWithNSGraphicsContext() throws {
|
||||
// let image = load20MbImage()
|
||||
// measure(options: options) {
|
||||
// let _ = image.resizedWithNSGraphicsContext(NSSize(width: 200, height: 200))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// 42s
|
||||
// func testResizedWithCoreImage() throws {
|
||||
// let image = load20MbImage()
|
||||
// measure(options: options) {
|
||||
// let _ = image.resizedWithCoreImage(NSSize(width: 200, height: 200))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private var options: XCTMeasureOptions = {
|
||||
// let o = XCTMeasureOptions()
|
||||
// o.iterationCount = 10
|
||||
// return o
|
||||
// }()
|
||||
//
|
||||
// private func load20MbImage() -> NSImage {
|
||||
// let bundle = Bundle(for: type(of: self))
|
||||
// let imagePath = bundle.path(forResource: "20MB-photo.jpg", ofType: nil)!
|
||||
// return NSImage(contentsOfFile: imagePath)!
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//extension NSImage {
|
||||
// func resizedWithNSGraphicsContext(_ newSize: NSSize) -> NSImage {
|
||||
// let newImage = NSImage(size: newSize)
|
||||
// newImage.lockFocus()
|
||||
// NSGraphicsContext.current?.imageInterpolation = .high
|
||||
// self.draw(in: NSRect(origin: .zero, size: newSize), from: NSRect(origin: .zero, size: size),
|
||||
// operation: .copy, fraction: 1.0)
|
||||
// newImage.unlockFocus()
|
||||
// return newImage
|
||||
// }
|
||||
//
|
||||
// @available(macOS 10.15, *)
|
||||
// func resizedWithCoreImage(_ newSize: NSSize) -> NSImage {
|
||||
// let ciImage = CIImage(data: tiffRepresentation!)!
|
||||
// let filter = CIFilter.lanczosScaleTransform()
|
||||
// filter.inputImage = ciImage
|
||||
// let scaleX = newSize.width / size.width
|
||||
// let scaleY = newSize.height / size.height
|
||||
// let scale = min(scaleX, scaleY) // Preserve aspect ratio
|
||||
// filter.scale = Float(scale)
|
||||
// filter.aspectRatio = 1.0
|
||||
// let outputCIImage = filter.outputImage!
|
||||
// let context = CIContext(options: [CIContextOption.useSoftwareRenderer: false]) // Prefer GPU
|
||||
// let outputCGImage = context.createCGImage(outputCIImage, from: outputCIImage.extent)!
|
||||
// return NSImage(cgImage: outputCGImage, size: newSize)
|
||||
// }
|
||||
//
|
||||
// func resizedWithCoreGraphics(_ newSize: NSSize) -> NSImage {
|
||||
// let image = cgImage()
|
||||
// let context = CGContext(data: nil,
|
||||
// width: Int(newSize.width),
|
||||
// height: Int(newSize.height),
|
||||
// bitsPerComponent: image.bitsPerComponent,
|
||||
// bytesPerRow: 0,
|
||||
// space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
|
||||
// bitmapInfo: image.bitmapInfo.rawValue)!
|
||||
// context.interpolationQuality = .high
|
||||
// context.draw(image, in: CGRect(origin: .zero, size: newSize))
|
||||
// let scaledImage = context.makeImage()!
|
||||
// return NSImage(cgImage: scaledImage, size: newSize)
|
||||
// }
|
||||
//}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 MiB |
Reference in new issue
Block a user