diff options
Diffstat (limited to 'Hotline/macOS/FilePreviewView.swift')
| -rw-r--r-- | Hotline/macOS/FilePreviewView.swift | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/Hotline/macOS/FilePreviewView.swift b/Hotline/macOS/FilePreviewView.swift new file mode 100644 index 0000000..d170dd7 --- /dev/null +++ b/Hotline/macOS/FilePreviewView.swift @@ -0,0 +1,333 @@ +import SwiftUI + +class FilePreviewWindowController: NSWindowController, NSWindowDelegate { + init(info: PreviewFileInfo) { + let window = PreviewWindow( + contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), + styleMask: [.titled, .unifiedTitleAndToolbar, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, defer: false) + + super.init(window: window) + + window.toolbarStyle = .unified + window.delegate = self + window.title = info.name + window.acceptsMouseMovedEvents = true + window.appearance = NSAppearance(named: .darkAqua) + window.collectionBehavior = .fullScreenAuxiliary + + window.standardWindowButton(.closeButton)?.isHidden = false + window.standardWindowButton(.miniaturizeButton)?.isHidden = false + window.standardWindowButton(.zoomButton)?.isHidden = false + + window.center() + + let rootView = PreviewWindowView(frame: NSRect(x: 0, y: 0, width: 480, height: 300)) + rootView.autoresizingMask = [.width, .height] + + let hostingView = NSHostingView(rootView: FilePreviewView(info: info)) + hostingView.frame = rootView.bounds + hostingView.autoresizingMask = [.width, .height] + rootView.addSubview(hostingView) + + window.contentView = rootView + +// let toolbar = NSToolbar() +// toolbar.allowsUserCustomization = false +// toolbar.displayMode = .iconOnly +// +// window.toolbar = toolbar + + self.showWindow(nil) + window.makeFirstResponder(nil) + } + + deinit { + print("FilePreviewWindowController: dealloc") + } + + required init?(coder: NSCoder) { + return nil + } +} + +struct FilePreviewToolbar: View { + var body: some View { +// ToolbarItem(placement: .primaryAction) { + Button { + + } label: { + Image(systemName: "plus") + } + .buttonStyle(.accessoryBar) +// } + } +} + +struct FilePreviewView: View { + enum FilePreviewFocus: Hashable { + case window + } + + @Environment(\.controlActiveState) private var controlActiveState + @Environment(\.colorScheme) private var colorScheme + + let info: PreviewFileInfo + + @State var preview: FilePreview? = nil + @Namespace var mainNamespace + @FocusState private var focusField: FilePreviewFocus? + + var body: some View { + Group { + if preview?.state != .loaded { + ProgressView(value: max(0.0, min(1.0, preview?.progress ?? 0.0))) + .focusable(false) + .padding() + .accentColor(colorScheme == .dark ? .white : .black) + .frame(maxWidth: 250) + .focusEffectDisabled() + } + else { + if let img = preview?.image { + AnimatedImageView(image: img) + .focusable(false) + .scaledToFit() + .focusEffectDisabled() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + else { + Text("UNSUPPORTED FORMAT:") + } + } + } + .focusable() + .focusEffectDisabled() + .focused($focusField, equals: .window) +// .prefersDefaultFocus(in: mainNamespace) +// .focusScope(mainNamespace) +// .focusSection() + + .ignoresSafeArea() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.black) + .overlay(DraggableWindowView()) + .task { + preview = FilePreview(info: info) + preview?.download() + } + .onAppear { + focusField = .window + } + .onDisappear { + preview?.cancel() + } + } +} + +fileprivate struct AnimatedImageView: NSViewRepresentable { + var image: NSImage? + + func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { + let sourceAspectRatio = sourceSize.width / sourceSize.height + + var fitSize: CGSize = sourceSize + + if fitSize.width > boundingSize.width { + fitSize.width = boundingSize.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height > boundingSize.height { + fitSize.height = boundingSize.height + fitSize.width = fitSize.height * sourceAspectRatio + } + + if let m = minSize { + if fitSize.width < m.width { + fitSize.width = m.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height < m.height { + fitSize.height = m.height + fitSize.width = fitSize.height * sourceAspectRatio + } + } + + return fitSize + } + + func makeNSView(context: Context) -> NSImageView { + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.animates = true + imageView.isEditable = false + imageView.allowsCutCopyPaste = true + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + if let img = self.image { + imageView.image = img + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if let window = imageView.window { + window.aspectRatio = img.size + + var windowRect = window.frame + let centerPoint = CGPoint(x: windowRect.midX, y: windowRect.midY) + + windowRect.size = img.size + + if let screen = window.screen { + var paddedScreenSize = screen.frame.size + paddedScreenSize.width *= 0.5 + paddedScreenSize.height *= 0.5 + + window.minSize = aspectFit(source: windowRect.size, bounds: CGSize(width: 400, height: 400)) + windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: window.minSize) + } + + windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 + windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 + + window.setFrame(windowRect, display: true, animate: true) + } + } + } + + return imageView + } + + func updateNSView(_ nsView: NSImageView, context: Context) { + nsView.image = self.image + } +} + +fileprivate struct DraggableWindowView: NSViewRepresentable { + func makeNSView(context: Context) -> NSView { + return DraggableWindowNSView() + } + + func updateNSView(_ nsView: NSView, context: Context) { } +} + +fileprivate class DraggableWindowNSView: NSView { + override public func mouseDown(with event: NSEvent) { + window?.performDrag(with: event) + } +} + +fileprivate class PreviewWindow: NSWindow { + override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { + super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) + self.minSize = NSSize(width: 200, height: 200) + self.isReleasedWhenClosed = true + self.titlebarSeparatorStyle = .line + self.acceptsMouseMovedEvents = true + } + + deinit { + print("FilePreviewWindow: dealloc") + } + + override var canBecomeMain: Bool { return true } + override var canBecomeKey: Bool { return true } +} + +fileprivate class PreviewWindowView: NSView { + private var trackingArea: NSTrackingArea? = nil + private var windowIsFullscreen: Bool = false + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + self.updateTrackingAreas() + } + + required init?(coder: NSCoder) { + return nil + } + + deinit { + print("FilePreviewWindowView: dealloc") + } + + override func updateTrackingAreas() { + if let t = self.trackingArea { + self.removeTrackingArea(t) + self.trackingArea = nil + } + + let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] + self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) + self.addTrackingArea(self.trackingArea!) + } + +// override func viewDidMoveToWindow() { +// print("VIEW MOVED TO WINDOW?") +// if let w = self.window { +//// w.titlebarAppearsTransparent = false +// let toolbarButtons = NSHostingView(rootView: FilePreviewToolbar()) +// toolbarButtons.frame.size = toolbarButtons.fittingSize +// +// let titlebarAccessory = NSTitlebarAccessoryViewController() +// titlebarAccessory.automaticallyAdjustsSize = true +// titlebarAccessory.view = toolbarButtons +// titlebarAccessory.layoutAttribute = .trailing +// w.addTitlebarAccessoryViewController(titlebarAccessory) +// } +// } + + override func mouseEntered(with event: NSEvent) { + if self.windowIsFullscreen { + return + } + + if let w = self.window { + w.titlebarAppearsTransparent = false + w.titleVisibility = .visible + w.standardWindowButton(.closeButton)?.isHidden = false + w.standardWindowButton(.miniaturizeButton)?.isHidden = false + w.standardWindowButton(.zoomButton)?.isHidden = false + w.toolbar?.isVisible = true + } + } + + override func mouseExited(with event: NSEvent) { + if self.windowIsFullscreen { + return + } + + if let w = self.window { + w.titlebarAppearsTransparent = true + w.titleVisibility = .hidden + w.standardWindowButton(.closeButton)?.isHidden = true + w.standardWindowButton(.miniaturizeButton)?.isHidden = true + w.standardWindowButton(.zoomButton)?.isHidden = true + w.toolbar?.isVisible = false + } + } + + func setWindowIsFullscreen(_ isFullscreen: Bool) { + self.windowIsFullscreen = isFullscreen + + if let w = self.window, isFullscreen { + w.titlebarAppearsTransparent = false + w.titleVisibility = .visible + w.standardWindowButton(.closeButton)?.isHidden = false + w.standardWindowButton(.miniaturizeButton)?.isHidden = false + w.standardWindowButton(.zoomButton)?.isHidden = false + } + } + + override var acceptsFirstResponder: Bool { return true } + + override func keyDown(with event: NSEvent) { + switch event.charactersIgnoringModifiers?.first { + case " ": + self.window?.performClose(nil) + default: + break + } + } +} |