// // TiledImageView.swift // KCD // // Created by Hori,Masaki on 2017/01/03. // Copyright © 2017年 Hori,Masaki. All rights reserved. // import Cocoa private struct TiledImageCellInformation { let frame: NSRect init(with frame: NSRect) { self.frame = frame } } final class TiledImageView: NSView { private static let privateDraggingUTI = "com.masakih.KCD.ScreenshotDDImte" private let imageCell = NSImageCell() required init?(coder: NSCoder) { imageCell.imageAlignment = .alignCenter imageCell.imageScaling = .scaleProportionallyDown super.init(coder: coder) registerForDraggedTypes([NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)]) } override var frame: NSRect { didSet { calcImagePosition() } } var image: NSImage? { return imageCell.image } var images: [NSImage] = [] { didSet { calcImagePosition() } } var columnCount: Int = 2 { didSet { calcImagePosition() } } private var infos: [TiledImageCellInformation] = [] { didSet { if inLiveResize { return } if infos.count < 2 { currentSelection = nil } setTrackingArea() } } private var currentSelection: TiledImageCellInformation? override func draw(_ dirtyRect: NSRect) { NSColor.controlBackgroundColor.setFill() NSColor.black.setStroke() NSBezierPath.fill(bounds) NSBezierPath.defaultLineWidth = 1.0 NSBezierPath.stroke(bounds) let cellRect = bounds.insetBy(dx: 1, dy: 1) NSBezierPath.clip(cellRect) imageCell.draw(withFrame: cellRect, in: self) imageCell.drawInterior(withFrame: cellRect, in: self) if let rect = currentSelection?.frame { let forcusRing = rect.insetBy(dx: 1, dy: 1) NSColor.keyboardFocusIndicatorColor.setStroke() NSBezierPath.defaultLineWidth = 2.0 NSBezierPath.stroke(forcusRing) } } // 市松模様を描画 private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) { let size = image.size do { image.lockFocus() defer { image.unlockFocus() } NSColor.white.setFill() NSRect(origin: .zero, size: size).fill() NSColor.lightGray.setFill() let colTileNum = Int(ceil(size.width / CGFloat(checkerSize))) let rowTileNum = Int(ceil(size.height / CGFloat(checkerSize))) for i in 0.. [TiledImageCellInformation] { guard let size = imageCell.image?.size else { return originalInfos } let bounds = self.bounds let ratioX = bounds.height / size.height let ratioY = bounds.width / size.width let ratio: CGFloat let offset: (x: CGFloat, y: CGFloat) if ratioX > 1 && ratioY > 1 { ratio = 1.0 offset = (x: (bounds.width - size.width) / 2, y: (bounds.height - size.height) / 2) } else if ratioX > ratioY { ratio = ratioY offset = (x: 0, y: (bounds.height - size.height * ratio) / 2) } else { ratio = ratioX offset = (x: (bounds.width - size.width * ratio) / 2, y: 0) } return originalInfos.map { NSRect(x: $0.frame.minX * ratio + offset.x, y: $0.frame.minY * ratio + offset.y, width: $0.frame.width * ratio, height: $0.frame.height * ratio) } .map { TiledImageCellInformation(with: $0) } } private func removeAllTrackingAreas() { trackingAreas.forEach(removeTrackingArea) } private func setTrackingArea() { removeAllTrackingAreas() infos.forEach { let area = NSTrackingArea(rect: $0.frame, options: [.mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: ["info": $0]) addTrackingArea(area) } } } extension TiledImageView { override func viewWillStartLiveResize() { removeAllTrackingAreas() } override func viewDidEndLiveResize() { calcImagePosition() } override func mouseEntered(with event: NSEvent) { guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else { return } currentSelection = entered needsDisplay = true } override func mouseExited(with event: NSEvent) { currentSelection = nil needsDisplay = true } override func mouseDown(with event: NSEvent) { let mouse = convert(event.locationInWindow, from: nil) let items = infos.enumerated().compactMap { (offset, element) -> NSDraggingItem? in if !NSMouseInRect(mouse, element.frame, isFlipped) { return nil } guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset, ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else { fatalError() } let item = NSDraggingItem(pasteboardWriter: pItem) item.setDraggingFrame(element.frame, contents: images[offset]) return item } let session = beginDraggingSession(with: items, event: event, source: self) session.animatesToStartingPositionsOnCancelOrFail = true session.draggingFormation = .none // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化 removeAllTrackingAreas() } } extension TiledImageView: NSDraggingSource { func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { return context == .withinApplication ? .move : [] } override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { return draggingUpdated(sender) } override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { guard let types = sender.draggingPasteboard().types, types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else { return [] } if !sender.draggingSourceOperationMask().contains(.move) { return [] } let mouse = convert(sender.draggingLocation(), from: nil) let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) } if underMouse.count == 0 { currentSelection = nil } else { currentSelection = underMouse[0] } needsDisplay = true return .move } override func draggingExited(_ sender: NSDraggingInfo?) { currentSelection = nil needsDisplay = true } override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { guard let types = sender.draggingPasteboard().types, types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else { return false } currentSelection = nil needsDisplay = true return true } override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { guard let types = sender.draggingPasteboard().types, types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else { return false } let pboard = sender.draggingPasteboard() guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false } guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int, case 0..