OSDN Git Service

Doutakuを導入
[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 TiledImageCellInformation {
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: [TiledImageCellInformation] = [] {
64         
65         didSet {
66             if inLiveResize { return }
67             if infos.count < 2 { currentSelection = nil }
68             setTrackingArea()
69         }
70     }
71     
72     private var currentSelection: TiledImageCellInformation?
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(ceil(size.width / CGFloat(checkerSize)))
110             let rowTileNum = Int(ceil(size.height / CGFloat(checkerSize)))
111             
112             for i in 0..<colTileNum {
113                 
114                 for j in 0..<rowTileNum {
115                     
116                     if (i + j) % 2 == 1 { continue }
117                     NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
118                 }
119             }
120         }
121     }
122     
123     private func calcImagePosition() {
124         
125         let imageCount = images.count
126         if imageCount == 0 {
127             
128             imageCell.image = nil
129             needsDisplay = true
130             return
131         }
132         
133         let size = images[0].size
134         DispatchQueue(label: "makeTrimedImage queue").async {
135             
136             let numberOfCol = imageCount < self.columnCount ? imageCount : self.columnCount
137             let numberOfRow = imageCount / self.columnCount + ((imageCount % self.columnCount != 0) ? 1 : 0)
138             let tiledImage = NSImage(size: NSSize(width: size.width * CGFloat(numberOfCol),
139                                                   height: size.height * CGFloat(numberOfRow)))
140             
141             self.drawCheckerIn(tiledImage)
142             
143             // 画像の描画
144             let offset = (0..<self.images.count).map {
145                 
146                 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
147                         y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
148             }
149             let imageRect = NSRect(origin: .zero, size: size)
150             
151             tiledImage.lockFocus()
152             zip(self.images, offset).forEach {
153                 
154                 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
155             }
156             tiledImage.unlockFocus()
157             
158             let newInfos = offset.map { TiledImageCellInformation(with: NSRect(origin: $0, size: size)) }
159             
160             DispatchQueue.main.sync {
161                 
162                 self.imageCell.image = tiledImage
163                 self.infos = self.calcurated(trackingAreaInfo: newInfos)
164                 self.needsDisplay = true
165             }
166         }
167     }
168     
169     private func calcurated(trackingAreaInfo originalInfos: [TiledImageCellInformation]) -> [TiledImageCellInformation] {
170         
171         guard let size = imageCell.image?.size else { return originalInfos }
172         
173         let bounds = self.bounds
174         let ratioX = bounds.height / size.height
175         let ratioY = bounds.width / size.width
176         let ratio: CGFloat
177         let offset: (x: CGFloat, y: CGFloat)
178         if ratioX > 1 && ratioY > 1 {
179             
180             ratio = 1.0
181             offset = (x: (bounds.width - size.width) / 2,
182                       y: (bounds.height - size.height) / 2)
183             
184         } else if ratioX > ratioY {
185             
186             ratio = ratioY
187             offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
188             
189         } else {
190             
191             ratio = ratioX
192             offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
193         }
194         
195         return originalInfos.map {
196             
197             NSRect(x: $0.frame.minX * ratio + offset.x,
198                    y: $0.frame.minY * ratio + offset.y,
199                    width: $0.frame.width * ratio,
200                    height: $0.frame.height * ratio)
201             }
202             .map { TiledImageCellInformation(with: $0) }
203     }
204     
205     private func removeAllTrackingAreas() {
206         
207         trackingAreas.forEach(removeTrackingArea)
208     }
209     
210     private func setTrackingArea() {
211         
212         removeAllTrackingAreas()
213         infos.forEach {
214             
215             let area = NSTrackingArea(rect: $0.frame,
216                                       options: [.mouseEnteredAndExited, .activeInKeyWindow],
217                                       owner: self,
218                                       userInfo: ["info": $0])
219             addTrackingArea(area)
220         }
221     }
222 }
223
224 extension TiledImageView {
225     
226     override func viewWillStartLiveResize() {
227         
228         removeAllTrackingAreas()
229     }
230     
231     override func viewDidEndLiveResize() {
232         
233         calcImagePosition()
234     }
235     
236     override func mouseEntered(with event: NSEvent) {
237         
238         guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else { return }
239         
240         currentSelection = entered
241         needsDisplay = true
242     }
243     
244     override func mouseExited(with event: NSEvent) {
245         
246         currentSelection = nil
247         needsDisplay = true
248     }
249     
250     override func mouseDown(with event: NSEvent) {
251         
252         let mouse = convert(event.locationInWindow, from: nil)
253         
254         let items = infos.enumerated().compactMap { (offset, element) -> NSDraggingItem? in
255             
256             if !NSMouseInRect(mouse, element.frame, isFlipped) { return nil }
257             
258             guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset,
259                                                ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
260                     
261                     fatalError()
262             }
263             
264             let item = NSDraggingItem(pasteboardWriter: pItem)
265             item.setDraggingFrame(element.frame, contents: images[offset])
266             
267             return item
268         }
269         
270         let session = beginDraggingSession(with: items, event: event, source: self)
271         session.animatesToStartingPositionsOnCancelOrFail = true
272         session.draggingFormation = .none
273         
274         // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
275         removeAllTrackingAreas()
276     }
277 }
278
279 extension TiledImageView: NSDraggingSource {
280     
281     func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
282         
283         return context == .withinApplication ? .move : []
284     }
285     
286     override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
287         
288         return draggingUpdated(sender)
289     }
290     
291     override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
292         
293         guard let types = sender.draggingPasteboard().types,
294             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
295                 
296                 return []
297         }
298         
299         if !sender.draggingSourceOperationMask().contains(.move) { return [] }
300         
301         let mouse = convert(sender.draggingLocation(), from: nil)
302         let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
303         if underMouse.count == 0 {
304             
305             currentSelection = nil
306             
307         } else {
308             
309             currentSelection = underMouse[0]
310         }
311         needsDisplay = true
312         
313         return .move
314     }
315     
316     override func draggingExited(_ sender: NSDraggingInfo?) {
317         
318         currentSelection = nil
319         needsDisplay = true
320     }
321     
322     override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
323         
324         guard let types = sender.draggingPasteboard().types,
325             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
326                 
327                 return false
328         }
329         
330         currentSelection = nil
331         needsDisplay = true
332         
333         return true
334     }
335     
336     override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
337         
338         guard let types = sender.draggingPasteboard().types,
339             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
340                 
341                 return false
342         }
343         
344         let pboard = sender.draggingPasteboard()
345         
346         guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else { return false }
347         guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
348             case 0..<images.count = index else {
349                 
350                 return false
351         }
352         
353         let mouse = convert(sender.draggingLocation(), from: nil)
354         
355         let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
356         
357         guard !underMouse.isEmpty else { return false }
358         
359         var newImages = images
360         let image = images[index]
361         newImages.remove(at: index)
362         newImages.insert(image, at: underMouse[0].offset)
363         images = newImages
364         
365         return true
366     }
367     
368     override func draggingEnded(_ sender: NSDraggingInfo) {
369         
370         setTrackingArea()
371     }
372 }