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 required init?(coder: NSCoder) {
27 imageCell = NSImageCell()
28 imageCell.imageAlignment = .alignCenter
29 imageCell.imageScaling = .scaleProportionallyDown
31 super.init(coder: coder)
33 registerForDraggedTypes([NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)])
36 override var frame: NSRect {
45 return imageCell.image
48 var images: [NSImage] = [] {
55 var columnCount: Int = 2 {
62 private var infos: [TitledImageCellInformation] = [] {
65 if inLiveResize { return }
66 if infos.count < 2 { currentSelection = nil }
71 private var currentSelection: TitledImageCellInformation?
73 private var imageCell: NSImageCell
75 override func draw(_ dirtyRect: NSRect) {
77 NSColor.controlBackgroundColor.setFill()
78 NSColor.black.setStroke()
79 NSBezierPath.fill(bounds)
80 NSBezierPath.defaultLineWidth = 1.0
81 NSBezierPath.stroke(bounds)
83 let cellRect = bounds.insetBy(dx: 1, dy: 1)
84 NSBezierPath.clip(cellRect)
85 imageCell.draw(withFrame: cellRect, in: self)
86 imageCell.drawInterior(withFrame: cellRect, in: self)
88 if let rect = currentSelection?.frame {
90 let forcusRing = rect.insetBy(dx: 1, dy: 1)
91 NSColor.keyboardFocusIndicatorColor.setStroke()
92 NSBezierPath.defaultLineWidth = 2.0
93 NSBezierPath.stroke(forcusRing)
98 private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) {
100 let size = image.size
105 defer { image.unlockFocus() }
107 NSColor.white.setFill()
108 NSRect(origin: .zero, size: size).fill()
109 NSColor.lightGray.setFill()
110 let colTileNum: Int = Int(size.width / CGFloat(checkerSize))
111 let rowTileNum: Int = Int(size.height / CGFloat(checkerSize))
113 for i in 0..<colTileNum {
115 for j in 0..<rowTileNum {
117 if i % 2 == 0 && j % 2 == 1 { continue }
118 if i % 2 == 1 && j % 2 == 0 { continue }
119 NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
125 private func calcImagePosition() {
127 let imageCount = images.count
130 imageCell.image = nil
136 let size = images[0].size
137 DispatchQueue(label: "makeTrimedImage queue").async {
139 let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
140 let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
141 let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
142 height: size.height * CGFloat(numberOfRow)))
144 self.drawCheckerIn(tiledImage)
147 let offset = (0..<self.images.count).map {
149 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
150 y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
152 let imageRect = NSRect(origin: .zero, size: size)
155 tiledImage.lockFocus()
156 zip(self.images, offset).forEach {
158 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
160 tiledImage.unlockFocus()
163 let newInfos = offset.map { TitledImageCellInformation(with: NSRect(origin: $0, size: size)) }
165 DispatchQueue.main.sync {
167 self.imageCell.image = tiledImage
168 self.infos = self.calcurated(trackingAreaInfo: newInfos)
169 self.needsDisplay = true
173 // swiftlint:disable:next line_length
174 private func calcurated(trackingAreaInfo originalInfos: [TitledImageCellInformation]) -> [TitledImageCellInformation] {
176 guard let size = imageCell.image?.size else { return originalInfos }
178 let bounds = self.bounds
179 let ratioX = bounds.height / size.height
180 let ratioY = bounds.width / size.width
182 let offset: (x: CGFloat, y: CGFloat)
183 if ratioX > 1 && ratioY > 1 {
186 offset = (x: (bounds.width - size.width) / 2,
187 y: (bounds.height - size.height) / 2)
189 } else if ratioX > ratioY {
192 offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
197 offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
201 return originalInfos.map {
203 NSRect(x: $0.frame.minX * ratio + offset.x,
204 y: $0.frame.minY * ratio + offset.y,
205 width: $0.frame.width * ratio,
206 height: $0.frame.height * ratio)
208 .map { TitledImageCellInformation(with: $0) }
211 private func removeAllTrackingAreas() {
213 trackingAreas.forEach(removeTrackingArea)
216 private func setTrackingArea() {
218 removeAllTrackingAreas()
221 let area = NSTrackingArea(rect: $0.frame,
222 options: [.mouseEnteredAndExited, .activeInKeyWindow],
224 userInfo: ["info": $0])
225 addTrackingArea(area)
230 extension TiledImageView {
232 override func viewWillStartLiveResize() {
234 removeAllTrackingAreas()
237 override func viewDidEndLiveResize() {
242 override func mouseEntered(with event: NSEvent) {
244 guard let entered = event.trackingArea?.userInfo?["info"] as? TitledImageCellInformation else { return }
246 currentSelection = entered
250 override func mouseExited(with event: NSEvent) {
252 currentSelection = nil
256 override func mouseDown(with event: NSEvent) {
258 let mouse = convert(event.locationInWindow, from: nil)
260 infos.enumerated().forEach {
262 if !NSMouseInRect(mouse, $0.element.frame, isFlipped) { return }
264 guard let pItem = NSPasteboardItem(pasteboardPropertyList: $0.offset,
265 ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
270 let item = NSDraggingItem(pasteboardWriter: pItem)
271 item.setDraggingFrame($0.element.frame, contents: images[$0.offset])
272 let session = beginDraggingSession(with: [item], event: event, source: self)
273 session.animatesToStartingPositionsOnCancelOrFail = true
274 session.draggingFormation = .none
276 // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
277 removeAllTrackingAreas()
281 extension TiledImageView: NSDraggingSource {
283 func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
285 return context == .withinApplication ? .move : []
288 override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
290 return draggingUpdated(sender)
293 override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
295 guard let types = sender.draggingPasteboard().types,
296 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
301 if !sender.draggingSourceOperationMask().contains(.move) { return [] }
303 let mouse = convert(sender.draggingLocation(), from: nil)
304 let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
305 if underMouse.count == 0 {
307 currentSelection = nil
311 currentSelection = underMouse[0]
319 override func draggingExited(_ sender: NSDraggingInfo?) {
321 currentSelection = nil
325 override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
327 guard let types = sender.draggingPasteboard().types,
328 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
333 currentSelection = nil
339 override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
341 guard let types = sender.draggingPasteboard().types,
342 types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
347 let pboard = sender.draggingPasteboard()
349 guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false }
350 guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
351 case 0..<images.count = index else {
356 let mouse = convert(sender.draggingLocation(), from: nil)
358 let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
360 guard !underMouse.isEmpty else { return false }
362 var newImages = images
363 let image = images[index]
364 newImages.remove(at: index)
365 newImages.insert(image, at: underMouse[0].offset)
371 override func draggingEnded(_ sender: NSDraggingInfo) {