2 // TiledImageView.swift
5 // Created by Hori,Masaki on 2017/01/03.
6 // Copyright © 2017年 Hori,Masaki. All rights reserved.
11 private struct TitledImageCellInformation {
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: [TitledImageCellInformation] = [] {
66 if inLiveResize { return }
67 if infos.count < 2 { currentSelection = nil }
72 private var currentSelection: TitledImageCellInformation?
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 = Int(size.width / CGFloat(checkerSize))
110 let rowTileNum: Int = Int(size.height / CGFloat(checkerSize))
112 for i in 0..<colTileNum {
114 for j in 0..<rowTileNum {
116 if i % 2 == 0 && j % 2 == 1 { continue }
117 if i % 2 == 1 && j % 2 == 0 { continue }
118 NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
124 private func calcImagePosition() {
126 let imageCount = images.count
129 imageCell.image = nil
135 let size = images[0].size
136 DispatchQueue(label: "makeTrimedImage queue").async {
138 let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
139 let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
140 let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
141 height: size.height * CGFloat(numberOfRow)))
143 self.drawCheckerIn(tiledImage)
146 let offset = (0..<self.images.count).map {
148 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
149 y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
151 let imageRect = NSRect(origin: .zero, size: size)
154 tiledImage.lockFocus()
155 zip(self.images, offset).forEach {
157 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
159 tiledImage.unlockFocus()
162 let newInfos = offset.map { TitledImageCellInformation(with: NSRect(origin: $0, size: size)) }
164 DispatchQueue.main.sync {
166 self.imageCell.image = tiledImage
167 self.infos = self.calcurated(trackingAreaInfo: newInfos)
168 self.needsDisplay = true
173 private func calcurated(trackingAreaInfo originalInfos: [TitledImageCellInformation]) -> [TitledImageCellInformation] {
175 guard let size = imageCell.image?.size else { return originalInfos }
177 let bounds = self.bounds
178 let ratioX = bounds.height / size.height
179 let ratioY = bounds.width / size.width
181 let offset: (x: CGFloat, y: CGFloat)
182 if ratioX > 1 && ratioY > 1 {
185 offset = (x: (bounds.width - size.width) / 2,
186 y: (bounds.height - size.height) / 2)
188 } else if ratioX > ratioY {
191 offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
196 offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
200 return originalInfos.map {
202 NSRect(x: $0.frame.minX * ratio + offset.x,
203 y: $0.frame.minY * ratio + offset.y,
204 width: $0.frame.width * ratio,
205 height: $0.frame.height * ratio)
207 .map { TitledImageCellInformation(with: $0) }
210 private func removeAllTrackingAreas() {
212 trackingAreas.forEach(removeTrackingArea)
215 private func setTrackingArea() {
217 removeAllTrackingAreas()
220 let area = NSTrackingArea(rect: $0.frame,
221 options: [.mouseEnteredAndExited, .activeInKeyWindow],
223 userInfo: ["info": $0])
224 addTrackingArea(area)
229 extension TiledImageView {
231 override func viewWillStartLiveResize() {
233 removeAllTrackingAreas()
236 override func viewDidEndLiveResize() {
241 override func mouseEntered(with event: NSEvent) {
243 guard let entered = event.trackingArea?.userInfo?["info"] as? TitledImageCellInformation else { return }
245 currentSelection = entered
249 override func mouseExited(with event: NSEvent) {
251 currentSelection = nil
255 override func mouseDown(with event: NSEvent) {
257 let mouse = convert(event.locationInWindow, from: nil)
259 infos.enumerated().forEach {
261 if !NSMouseInRect(mouse, $0.element.frame, isFlipped) { return }
263 guard let pItem = NSPasteboardItem(pasteboardPropertyList: $0.offset,
264 ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
269 let item = NSDraggingItem(pasteboardWriter: pItem)
270 item.setDraggingFrame($0.element.frame, contents: images[$0.offset])
271 let session = beginDraggingSession(with: [item], event: event, source: self)
272 session.animatesToStartingPositionsOnCancelOrFail = true
273 session.draggingFormation = .none
275 // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
276 removeAllTrackingAreas()
280 extension TiledImageView: NSDraggingSource {
282 func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
284 return context == .withinApplication ? .move : []
287 override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
289 return draggingUpdated(sender)
292 override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
294 guard let types = sender.draggingPasteboard().types,
295 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
300 if !sender.draggingSourceOperationMask().contains(.move) { return [] }
302 let mouse = convert(sender.draggingLocation(), from: nil)
303 let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
304 if underMouse.count == 0 {
306 currentSelection = nil
310 currentSelection = underMouse[0]
317 override func draggingExited(_ sender: NSDraggingInfo?) {
319 currentSelection = nil
323 override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
325 guard let types = sender.draggingPasteboard().types,
326 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
331 currentSelection = nil
337 override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
339 guard let types = sender.draggingPasteboard().types,
340 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
345 let pboard = sender.draggingPasteboard()
347 guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false }
348 guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
349 case 0..<images.count = index else {
354 let mouse = convert(sender.draggingLocation(), from: nil)
356 let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
358 guard !underMouse.isEmpty else { return false }
360 var newImages = images
361 let image = images[index]
362 newImages.remove(at: index)
363 newImages.insert(image, at: underMouse[0].offset)
369 override func draggingEnded(_ sender: NSDraggingInfo) {