OSDN Git Service

EntityProviderを準するexensionをファイルに分けた
[kcd/KCD.git] / KCD / TiledImageView.swift
1 //
2 //  TiledImageView.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2017/01/03.
6 //  Copyright © 2017年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10
11 private struct TitledImageCellInformation {
12     
13     let frame: NSRect
14     
15     init(with frame: NSRect) {
16         
17         self.frame = frame
18     }
19 }
20
21 final class TiledImageView: NSView {
22     
23     private static let privateDraggingUTI = "com.masakih.KCD.ScreenshotDDImte"
24     
25     private let imageCell = NSImageCell()
26     
27     required init?(coder: NSCoder) {
28         
29         imageCell.imageAlignment = .alignCenter
30         imageCell.imageScaling = .scaleProportionallyDown
31         
32         super.init(coder: coder)
33         
34         registerForDraggedTypes([NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)])
35     }
36     
37     override var frame: NSRect {
38         
39         didSet {
40             calcImagePosition()
41         }
42     }
43     
44     var image: NSImage? {
45         
46         return imageCell.image
47     }
48     
49     var images: [NSImage] = [] {
50         
51         didSet {
52             calcImagePosition()
53         }
54     }
55     
56     var columnCount: Int = 2 {
57         
58         didSet {
59             calcImagePosition()
60         }
61     }
62     
63     private var infos: [TitledImageCellInformation] = [] {
64         
65         didSet {
66             if inLiveResize { return }
67             if infos.count < 2 { currentSelection = nil }
68             setTrackingArea()
69         }
70     }
71     
72     private var currentSelection: TitledImageCellInformation?
73     
74     override func draw(_ dirtyRect: NSRect) {
75         
76         NSColor.controlBackgroundColor.setFill()
77         NSColor.black.setStroke()
78         NSBezierPath.fill(bounds)
79         NSBezierPath.defaultLineWidth = 1.0
80         NSBezierPath.stroke(bounds)
81         
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)
86         
87         if let rect = currentSelection?.frame {
88             
89             let forcusRing = rect.insetBy(dx: 1, dy: 1)
90             NSColor.keyboardFocusIndicatorColor.setStroke()
91             NSBezierPath.defaultLineWidth = 2.0
92             NSBezierPath.stroke(forcusRing)
93         }
94     }
95     
96     // 市松模様を描画
97     private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) {
98         
99         let size = image.size
100         
101         do {
102             
103             image.lockFocus()
104             defer { image.unlockFocus() }
105             
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))
111             
112             for i in 0..<colTileNum {
113                 
114                 for j in 0..<rowTileNum {
115                     
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()
119                 }
120             }
121         }
122     }
123     
124     private func calcImagePosition() {
125         
126         let imageCount = images.count
127         if imageCount == 0 {
128             
129             imageCell.image = nil
130             needsDisplay = true
131             
132             return
133         }
134         
135         let size = images[0].size
136         DispatchQueue(label: "makeTrimedImage queue").async {
137             
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)))
142             
143             self.drawCheckerIn(tiledImage)
144             
145             // 画像の描画
146             let offset = (0..<self.images.count).map {
147                 
148                 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
149                         y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
150             }
151             let imageRect = NSRect(origin: .zero, size: size)
152             
153             
154             tiledImage.lockFocus()
155             zip(self.images, offset).forEach {
156                 
157                 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
158             }
159             tiledImage.unlockFocus()
160             
161             
162             let newInfos = offset.map { TitledImageCellInformation(with: NSRect(origin: $0, size: size)) }
163             
164             DispatchQueue.main.sync {
165                 
166                 self.imageCell.image = tiledImage
167                 self.infos = self.calcurated(trackingAreaInfo: newInfos)
168                 self.needsDisplay = true
169             }
170         }
171     }
172     
173     private func calcurated(trackingAreaInfo originalInfos: [TitledImageCellInformation]) -> [TitledImageCellInformation] {
174         
175         guard let size = imageCell.image?.size else { return originalInfos }
176         
177         let bounds = self.bounds
178         let ratioX = bounds.height / size.height
179         let ratioY = bounds.width / size.width
180         let ratio: CGFloat
181         let offset: (x: CGFloat, y: CGFloat)
182         if ratioX > 1 && ratioY > 1 {
183             
184             ratio = 1.0
185             offset = (x: (bounds.width - size.width) / 2,
186                       y: (bounds.height - size.height) / 2)
187             
188         } else if ratioX > ratioY {
189             
190             ratio = ratioY
191             offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
192             
193         } else {
194             
195             ratio = ratioX
196             offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
197             
198         }
199         
200         return originalInfos.map {
201             
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)
206             }
207             .map { TitledImageCellInformation(with: $0) }
208     }
209     
210     private func removeAllTrackingAreas() {
211         
212         trackingAreas.forEach(removeTrackingArea)
213     }
214     
215     private func setTrackingArea() {
216         
217         removeAllTrackingAreas()
218         infos.forEach {
219             
220             let area = NSTrackingArea(rect: $0.frame,
221                                       options: [.mouseEnteredAndExited, .activeInKeyWindow],
222                                       owner: self,
223                                       userInfo: ["info": $0])
224             addTrackingArea(area)
225         }
226     }
227 }
228
229 extension TiledImageView {
230     
231     override func viewWillStartLiveResize() {
232         
233         removeAllTrackingAreas()
234     }
235     
236     override func viewDidEndLiveResize() {
237         
238         calcImagePosition()
239     }
240     
241     override func mouseEntered(with event: NSEvent) {
242         
243         guard let entered = event.trackingArea?.userInfo?["info"] as? TitledImageCellInformation else { return }
244         
245         currentSelection = entered
246         needsDisplay = true
247     }
248     
249     override func mouseExited(with event: NSEvent) {
250         
251         currentSelection = nil
252         needsDisplay = true
253     }
254     
255     override func mouseDown(with event: NSEvent) {
256         
257         let mouse = convert(event.locationInWindow, from: nil)
258         
259         infos.enumerated().forEach {
260             
261             if !NSMouseInRect(mouse, $0.element.frame, isFlipped) { return }
262             
263             guard let pItem = NSPasteboardItem(pasteboardPropertyList: $0.offset,
264                                                ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
265                     
266                     fatalError()
267             }
268             
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
274         }
275         // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
276         removeAllTrackingAreas()
277     }
278 }
279
280 extension TiledImageView: NSDraggingSource {
281     
282     func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
283         
284         return context == .withinApplication ? .move : []
285     }
286     
287     override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
288         
289         return draggingUpdated(sender)
290     }
291     
292     override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
293         
294         guard let types = sender.draggingPasteboard().types,
295             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
296                 
297                 return []
298         }
299         
300         if !sender.draggingSourceOperationMask().contains(.move) { return [] }
301         
302         let mouse = convert(sender.draggingLocation(), from: nil)
303         let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
304         if underMouse.count == 0 {
305             
306             currentSelection = nil
307             
308         } else {
309             
310             currentSelection = underMouse[0]
311         }
312         needsDisplay = true
313         
314         return .move
315     }
316     
317     override func draggingExited(_ sender: NSDraggingInfo?) {
318         
319         currentSelection = nil
320         needsDisplay = true
321     }
322     
323     override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
324         
325         guard let types = sender.draggingPasteboard().types,
326             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
327                 
328                 return false
329         }
330         
331         currentSelection = nil
332         needsDisplay = true
333         
334         return true
335     }
336     
337     override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
338         
339         guard let types = sender.draggingPasteboard().types,
340             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
341                 
342                 return false
343         }
344         
345         let pboard = sender.draggingPasteboard()
346         
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 {
350                 
351                 return false
352         }
353         
354         let mouse = convert(sender.draggingLocation(), from: nil)
355         
356         let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
357         
358         guard !underMouse.isEmpty else { return false }
359         
360         var newImages = images
361         let image = images[index]
362         newImages.remove(at: index)
363         newImages.insert(image, at: underMouse[0].offset)
364         images = newImages
365         
366         return true
367     }
368     
369     override func draggingEnded(_ sender: NSDraggingInfo) {
370         
371         setTrackingArea()
372     }
373 }