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] = [] {
57 var columnCount: Int = 2 {
65 private var infos: [TiledImageCellInformation] = [] {
75 currentSelection = nil
81 private var currentSelection: TiledImageCellInformation?
83 override func draw(_ dirtyRect: NSRect) {
85 NSColor.controlBackgroundColor.setFill()
86 NSColor.black.setStroke()
87 NSBezierPath.fill(bounds)
88 NSBezierPath.defaultLineWidth = 1.0
89 NSBezierPath.stroke(bounds)
91 let cellRect = bounds.insetBy(dx: 1, dy: 1)
92 NSBezierPath.clip(cellRect)
93 imageCell.draw(withFrame: cellRect, in: self)
94 imageCell.drawInterior(withFrame: cellRect, in: self)
96 if let rect = currentSelection?.frame {
98 let forcusRing = rect.insetBy(dx: 1, dy: 1)
99 NSColor.keyboardFocusIndicatorColor.setStroke()
100 NSBezierPath.defaultLineWidth = 2.0
101 NSBezierPath.stroke(forcusRing)
106 private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) {
108 let size = image.size
113 defer { image.unlockFocus() }
115 NSColor.white.setFill()
116 NSRect(origin: .zero, size: size).fill()
117 NSColor.lightGray.setFill()
118 let colTileNum = Int(ceil(size.width / CGFloat(checkerSize)))
119 let rowTileNum = Int(ceil(size.height / CGFloat(checkerSize)))
121 for i in 0..<colTileNum {
123 for j in 0..<rowTileNum {
125 if (i + j) % 2 == 1 {
129 NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
135 private func calcImagePosition() {
137 let imageCount = images.count
140 imageCell.image = nil
146 let size = images[0].size
147 DispatchQueue(label: "makeTrimedImage queue").async {
149 let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
150 let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
151 let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
152 height: size.height * CGFloat(numberOfRow)))
154 self.drawCheckerIn(tiledImage)
157 let offset = (0..<self.images.count).map {
159 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
160 y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
162 let imageRect = NSRect(origin: .zero, size: size)
164 tiledImage.lockFocus()
165 zip(self.images, offset).forEach {
167 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
169 tiledImage.unlockFocus()
171 let newInfos = offset.map { TiledImageCellInformation(with: NSRect(origin: $0, size: size)) }
173 DispatchQueue.main.sync {
175 self.imageCell.image = tiledImage
176 self.infos = self.calcurated(trackingAreaInfo: newInfos)
177 self.needsDisplay = true
182 private func calcurated(trackingAreaInfo originalInfos: [TiledImageCellInformation]) -> [TiledImageCellInformation] {
184 guard let size = imageCell.image?.size else {
189 let bounds = self.bounds
190 let ratioX = bounds.height / size.height
191 let ratioY = bounds.width / size.width
193 let offset: (x: CGFloat, y: CGFloat)
194 if ratioX > 1 && ratioY > 1 {
197 offset = (x: (bounds.width - size.width) / 2,
198 y: (bounds.height - size.height) / 2)
200 } else if ratioX > ratioY {
203 offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
208 offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
211 return originalInfos.map {
213 NSRect(x: $0.frame.minX * ratio + offset.x,
214 y: $0.frame.minY * ratio + offset.y,
215 width: $0.frame.width * ratio,
216 height: $0.frame.height * ratio)
218 .map { TiledImageCellInformation(with: $0) }
221 private func removeAllTrackingAreas() {
223 trackingAreas.forEach(removeTrackingArea)
226 private func setTrackingArea() {
228 removeAllTrackingAreas()
231 let area = NSTrackingArea(rect: $0.frame,
232 options: [.mouseEnteredAndExited, .activeInKeyWindow],
234 userInfo: ["info": $0])
235 addTrackingArea(area)
240 extension TiledImageView {
242 override func viewWillStartLiveResize() {
244 removeAllTrackingAreas()
247 override func viewDidEndLiveResize() {
252 override func mouseEntered(with event: NSEvent) {
254 guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else {
259 currentSelection = entered
263 override func mouseExited(with event: NSEvent) {
265 currentSelection = nil
269 override func mouseDown(with event: NSEvent) {
271 let mouse = convert(event.locationInWindow, from: nil)
273 let items = infos.enumerated().compactMap { (offset, element) -> NSDraggingItem? in
275 if !NSMouseInRect(mouse, element.frame, isFlipped) {
280 guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset,
281 ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
286 let item = NSDraggingItem(pasteboardWriter: pItem)
287 item.setDraggingFrame(element.frame, contents: images[offset])
292 let session = beginDraggingSession(with: items, event: event, source: self)
293 session.animatesToStartingPositionsOnCancelOrFail = true
294 session.draggingFormation = .none
296 // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
297 removeAllTrackingAreas()
301 extension TiledImageView: NSDraggingSource {
303 func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
305 return context == .withinApplication ? .move : []
308 override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
310 return draggingUpdated(sender)
313 override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
315 guard let types = sender.draggingPasteboard().types,
316 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
321 if !sender.draggingSourceOperationMask().contains(.move) {
326 let mouse = convert(sender.draggingLocation(), from: nil)
327 let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
328 if underMouse.count == 0 {
330 currentSelection = nil
334 currentSelection = underMouse[0]
341 override func draggingExited(_ sender: NSDraggingInfo?) {
343 currentSelection = nil
347 override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
349 guard let types = sender.draggingPasteboard().types,
350 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
355 currentSelection = nil
361 override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
363 guard let types = sender.draggingPasteboard().types,
364 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
369 let pboard = sender.draggingPasteboard()
371 guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else {
375 guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
376 case 0..<images.count = index else {
381 let mouse = convert(sender.draggingLocation(), from: nil)
383 let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
385 guard !underMouse.isEmpty else {
390 var newImages = images
391 let image = images[index]
392 newImages.remove(at: index)
393 newImages.insert(image, at: underMouse[0].offset)
399 override func draggingEnded(_ sender: NSDraggingInfo) {