2 // TiledImageView.swift
5 // Created by Hori,Masaki on 2017/01/03.
6 // Copyright © 2017年 Hori,Masaki. All rights reserved.
11 private struct TiledImageCellInformation {
15 init(with frame: NSRect) {
21 final class TiledImageView: NSView {
23 private static let privateDraggingUTI = "com.masakih.KCD.ScreenshotDDImte"
25 private let imageCell = NSImageCell()
27 required init?(coder: NSCoder) {
29 imageCell.imageAlignment = .alignCenter
30 imageCell.imageScaling = .scaleProportionallyDown
32 super.init(coder: coder)
34 registerForDraggedTypes([NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)])
37 override var frame: NSRect {
46 return imageCell.image
49 var images: [NSImage] = [] {
56 var columnCount: Int = 2 {
63 private var infos: [TiledImageCellInformation] = [] {
66 if inLiveResize { return }
67 if infos.count < 2 { currentSelection = nil }
72 private var currentSelection: TiledImageCellInformation?
74 override func draw(_ dirtyRect: NSRect) {
76 NSColor.controlBackgroundColor.setFill()
77 NSColor.black.setStroke()
78 NSBezierPath.fill(bounds)
79 NSBezierPath.defaultLineWidth = 1.0
80 NSBezierPath.stroke(bounds)
82 let cellRect = bounds.insetBy(dx: 1, dy: 1)
83 NSBezierPath.clip(cellRect)
84 imageCell.draw(withFrame: cellRect, in: self)
85 imageCell.drawInterior(withFrame: cellRect, in: self)
87 if let rect = currentSelection?.frame {
89 let forcusRing = rect.insetBy(dx: 1, dy: 1)
90 NSColor.keyboardFocusIndicatorColor.setStroke()
91 NSBezierPath.defaultLineWidth = 2.0
92 NSBezierPath.stroke(forcusRing)
97 private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) {
104 defer { image.unlockFocus() }
106 NSColor.white.setFill()
107 NSRect(origin: .zero, size: size).fill()
108 NSColor.lightGray.setFill()
109 let colTileNum = Int(ceil(size.width / CGFloat(checkerSize)))
110 let rowTileNum = Int(ceil(size.height / CGFloat(checkerSize)))
112 for i in 0..<colTileNum {
114 for j in 0..<rowTileNum {
116 if (i + j) % 2 == 1 { continue }
117 NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
123 private func calcImagePosition() {
125 let imageCount = images.count
128 imageCell.image = nil
133 let size = images[0].size
134 DispatchQueue(label: "makeTrimedImage queue").async {
136 let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
137 let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
138 let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
139 height: size.height * CGFloat(numberOfRow)))
141 self.drawCheckerIn(tiledImage)
144 let offset = (0..<self.images.count).map {
146 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
147 y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
149 let imageRect = NSRect(origin: .zero, size: size)
151 tiledImage.lockFocus()
152 zip(self.images, offset).forEach {
154 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
156 tiledImage.unlockFocus()
158 let newInfos = offset.map { TiledImageCellInformation(with: NSRect(origin: $0, size: size)) }
160 DispatchQueue.main.sync {
162 self.imageCell.image = tiledImage
163 self.infos = self.calcurated(trackingAreaInfo: newInfos)
164 self.needsDisplay = true
169 private func calcurated(trackingAreaInfo originalInfos: [TiledImageCellInformation]) -> [TiledImageCellInformation] {
171 guard let size = imageCell.image?.size else { return originalInfos }
173 let bounds = self.bounds
174 let ratioX = bounds.height / size.height
175 let ratioY = bounds.width / size.width
177 let offset: (x: CGFloat, y: CGFloat)
178 if ratioX > 1 && ratioY > 1 {
181 offset = (x: (bounds.width - size.width) / 2,
182 y: (bounds.height - size.height) / 2)
184 } else if ratioX > ratioY {
187 offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
192 offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
195 return originalInfos.map {
197 NSRect(x: $0.frame.minX * ratio + offset.x,
198 y: $0.frame.minY * ratio + offset.y,
199 width: $0.frame.width * ratio,
200 height: $0.frame.height * ratio)
202 .map { TiledImageCellInformation(with: $0) }
205 private func removeAllTrackingAreas() {
207 trackingAreas.forEach(removeTrackingArea)
210 private func setTrackingArea() {
212 removeAllTrackingAreas()
215 let area = NSTrackingArea(rect: $0.frame,
216 options: [.mouseEnteredAndExited, .activeInKeyWindow],
218 userInfo: ["info": $0])
219 addTrackingArea(area)
224 extension TiledImageView {
226 override func viewWillStartLiveResize() {
228 removeAllTrackingAreas()
231 override func viewDidEndLiveResize() {
236 override func mouseEntered(with event: NSEvent) {
238 guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else { return }
240 currentSelection = entered
244 override func mouseExited(with event: NSEvent) {
246 currentSelection = nil
250 override func mouseDown(with event: NSEvent) {
252 let mouse = convert(event.locationInWindow, from: nil)
254 let items = infos.enumerated().compactMap { (offset, element) -> NSDraggingItem? in
256 if !NSMouseInRect(mouse, element.frame, isFlipped) { return nil }
258 guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset,
259 ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
264 let item = NSDraggingItem(pasteboardWriter: pItem)
265 item.setDraggingFrame(element.frame, contents: images[offset])
270 let session = beginDraggingSession(with: items, event: event, source: self)
271 session.animatesToStartingPositionsOnCancelOrFail = true
272 session.draggingFormation = .none
274 // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
275 removeAllTrackingAreas()
279 extension TiledImageView: NSDraggingSource {
281 func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
283 return context == .withinApplication ? .move : []
286 override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
288 return draggingUpdated(sender)
291 override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
293 guard let types = sender.draggingPasteboard().types,
294 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
299 if !sender.draggingSourceOperationMask().contains(.move) { return [] }
301 let mouse = convert(sender.draggingLocation(), from: nil)
302 let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
303 if underMouse.count == 0 {
305 currentSelection = nil
309 currentSelection = underMouse[0]
316 override func draggingExited(_ sender: NSDraggingInfo?) {
318 currentSelection = nil
322 override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
324 guard let types = sender.draggingPasteboard().types,
325 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
330 currentSelection = nil
336 override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
338 guard let types = sender.draggingPasteboard().types,
339 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
344 let pboard = sender.draggingPasteboard()
346 guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false }
347 guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
348 case 0..<images.count = index else {
353 let mouse = convert(sender.draggingLocation(), from: nil)
355 let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
357 guard !underMouse.isEmpty else { return false }
359 var newImages = images
360 let image = images[index]
361 newImages.remove(at: index)
362 newImages.insert(image, at: underMouse[0].offset)
368 override func draggingEnded(_ sender: NSDraggingInfo) {