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
134 let size = images[0].size
135 DispatchQueue(label: "makeTrimedImage queue").async {
137 let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
138 let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
139 let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
140 height: size.height * CGFloat(numberOfRow)))
142 self.drawCheckerIn(tiledImage)
145 let offset = (0..<self.images.count).map {
147 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
148 y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
150 let imageRect = NSRect(origin: .zero, size: size)
153 tiledImage.lockFocus()
154 zip(self.images, offset).forEach {
156 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
158 tiledImage.unlockFocus()
161 let newInfos = offset.map { TiledImageCellInformation(with: NSRect(origin: $0, size: size)) }
163 DispatchQueue.main.sync {
165 self.imageCell.image = tiledImage
166 self.infos = self.calcurated(trackingAreaInfo: newInfos)
167 self.needsDisplay = true
172 private func calcurated(trackingAreaInfo originalInfos: [TiledImageCellInformation]) -> [TiledImageCellInformation] {
174 guard let size = imageCell.image?.size else { return originalInfos }
176 let bounds = self.bounds
177 let ratioX = bounds.height / size.height
178 let ratioY = bounds.width / size.width
180 let offset: (x: CGFloat, y: CGFloat)
181 if ratioX > 1 && ratioY > 1 {
184 offset = (x: (bounds.width - size.width) / 2,
185 y: (bounds.height - size.height) / 2)
187 } else if ratioX > ratioY {
190 offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
195 offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
199 return originalInfos.map {
201 NSRect(x: $0.frame.minX * ratio + offset.x,
202 y: $0.frame.minY * ratio + offset.y,
203 width: $0.frame.width * ratio,
204 height: $0.frame.height * ratio)
206 .map { TiledImageCellInformation(with: $0) }
209 private func removeAllTrackingAreas() {
211 trackingAreas.forEach(removeTrackingArea)
214 private func setTrackingArea() {
216 removeAllTrackingAreas()
219 let area = NSTrackingArea(rect: $0.frame,
220 options: [.mouseEnteredAndExited, .activeInKeyWindow],
222 userInfo: ["info": $0])
223 addTrackingArea(area)
228 extension TiledImageView {
230 override func viewWillStartLiveResize() {
232 removeAllTrackingAreas()
235 override func viewDidEndLiveResize() {
240 override func mouseEntered(with event: NSEvent) {
242 guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else { return }
244 currentSelection = entered
248 override func mouseExited(with event: NSEvent) {
250 currentSelection = nil
254 override func mouseDown(with event: NSEvent) {
256 let mouse = convert(event.locationInWindow, from: nil)
258 let items = infos.enumerated().flatMap { (offset, element) -> NSDraggingItem? in
260 if !NSMouseInRect(mouse, element.frame, isFlipped) { return nil }
262 guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset,
263 ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
268 let item = NSDraggingItem(pasteboardWriter: pItem)
269 item.setDraggingFrame(element.frame, contents: images[offset])
274 let session = beginDraggingSession(with: items, event: event, source: self)
275 session.animatesToStartingPositionsOnCancelOrFail = true
276 session.draggingFormation = .none
278 // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
279 removeAllTrackingAreas()
283 extension TiledImageView: NSDraggingSource {
285 func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
287 return context == .withinApplication ? .move : []
290 override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
292 return draggingUpdated(sender)
295 override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
297 guard let types = sender.draggingPasteboard().types,
298 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
303 if !sender.draggingSourceOperationMask().contains(.move) { return [] }
305 let mouse = convert(sender.draggingLocation(), from: nil)
306 let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
307 if underMouse.count == 0 {
309 currentSelection = nil
313 currentSelection = underMouse[0]
320 override func draggingExited(_ sender: NSDraggingInfo?) {
322 currentSelection = nil
326 override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
328 guard let types = sender.draggingPasteboard().types,
329 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
334 currentSelection = nil
340 override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
342 guard let types = sender.draggingPasteboard().types,
343 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
348 let pboard = sender.draggingPasteboard()
350 guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false }
351 guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
352 case 0..<images.count = index else {
357 let mouse = convert(sender.draggingLocation(), from: nil)
359 let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
361 guard !underMouse.isEmpty else { return false }
363 var newImages = images
364 let image = images[index]
365 newImages.remove(at: index)
366 newImages.insert(image, at: underMouse[0].offset)
372 override func draggingEnded(_ sender: NSDraggingInfo) {