OSDN Git Service

Timerをクロージャ方式にした
[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             
53             calcImagePosition()
54         }
55     }
56     
57     var columnCount: Int = 2 {
58         
59         didSet {
60             
61             calcImagePosition()
62         }
63     }
64     
65     private var infos: [TiledImageCellInformation] = [] {
66         
67         didSet {
68             
69             if inLiveResize {
70                 
71                 return
72             }
73             if infos.count < 2 {
74                 
75                 currentSelection = nil
76             }
77             setTrackingArea()
78         }
79     }
80     
81     private var currentSelection: TiledImageCellInformation?
82     
83     override func draw(_ dirtyRect: NSRect) {
84         
85         NSColor.controlBackgroundColor.setFill()
86         NSColor.black.setStroke()
87         NSBezierPath.fill(bounds)
88         NSBezierPath.defaultLineWidth = 1.0
89         NSBezierPath.stroke(bounds)
90         
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)
95         
96         if let rect = currentSelection?.frame {
97             
98             let forcusRing = rect.insetBy(dx: 1, dy: 1)
99             NSColor.keyboardFocusIndicatorColor.setStroke()
100             NSBezierPath.defaultLineWidth = 2.0
101             NSBezierPath.stroke(forcusRing)
102         }
103     }
104     
105     // 市松模様を描画
106     private func drawCheckerIn(_ image: NSImage, checkerSize: Int = 10) {
107         
108         let size = image.size
109         
110         do {
111             
112             image.lockFocus()
113             defer { image.unlockFocus() }
114             
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)))
120             
121             for i in 0..<colTileNum {
122                 
123                 for j in 0..<rowTileNum {
124                     
125                     if (i + j) % 2 == 1 {
126                         
127                         continue
128                     }
129                     NSRect(x: i * checkerSize, y: j * checkerSize, width: checkerSize, height: checkerSize).fill()
130                 }
131             }
132         }
133     }
134     
135     private func calcImagePosition() {
136         
137         let imageCount = images.count
138         if imageCount == 0 {
139             
140             imageCell.image = nil
141             needsDisplay = true
142             
143             return
144         }
145         
146         let size = images[0].size
147         DispatchQueue(label: "makeTrimedImage queue").async {
148             
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)))
153             
154             self.drawCheckerIn(tiledImage)
155             
156             // 画像の描画
157             let offset = (0..<self.images.count).map {
158                 
159                 NSPoint(x: CGFloat($0 % self.columnCount) * size.width,
160                         y: size.height * CGFloat(numberOfRow) - CGFloat($0 / self.columnCount + 1) * size.height)
161             }
162             let imageRect = NSRect(origin: .zero, size: size)
163             
164             tiledImage.lockFocus()
165             zip(self.images, offset).forEach {
166                 
167                 $0.0.draw(at: $0.1, from: imageRect, operation: .copy, fraction: 1.0)
168             }
169             tiledImage.unlockFocus()
170             
171             let newInfos = offset.map { TiledImageCellInformation(with: NSRect(origin: $0, size: size)) }
172             
173             DispatchQueue.main.sync {
174                 
175                 self.imageCell.image = tiledImage
176                 self.infos = self.calcurated(trackingAreaInfo: newInfos)
177                 self.needsDisplay = true
178             }
179         }
180     }
181     
182     private func calcurated(trackingAreaInfo originalInfos: [TiledImageCellInformation]) -> [TiledImageCellInformation] {
183         
184         guard let size = imageCell.image?.size else {
185             
186             return originalInfos
187         }
188         
189         let bounds = self.bounds
190         let ratioX = bounds.height / size.height
191         let ratioY = bounds.width / size.width
192         let ratio: CGFloat
193         let offset: (x: CGFloat, y: CGFloat)
194         if ratioX > 1 && ratioY > 1 {
195             
196             ratio = 1.0
197             offset = (x: (bounds.width - size.width) / 2,
198                       y: (bounds.height - size.height) / 2)
199             
200         } else if ratioX > ratioY {
201             
202             ratio = ratioY
203             offset = (x: 0, y: (bounds.height - size.height * ratio) / 2)
204             
205         } else {
206             
207             ratio = ratioX
208             offset = (x: (bounds.width - size.width * ratio) / 2, y: 0)
209         }
210         
211         return originalInfos.map {
212             
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)
217             }
218             .map { TiledImageCellInformation(with: $0) }
219     }
220     
221     private func removeAllTrackingAreas() {
222         
223         trackingAreas.forEach(removeTrackingArea)
224     }
225     
226     private func setTrackingArea() {
227         
228         removeAllTrackingAreas()
229         infos.forEach {
230             
231             let area = NSTrackingArea(rect: $0.frame,
232                                       options: [.mouseEnteredAndExited, .activeInKeyWindow],
233                                       owner: self,
234                                       userInfo: ["info": $0])
235             addTrackingArea(area)
236         }
237     }
238 }
239
240 extension TiledImageView {
241     
242     override func viewWillStartLiveResize() {
243         
244         removeAllTrackingAreas()
245     }
246     
247     override func viewDidEndLiveResize() {
248         
249         calcImagePosition()
250     }
251     
252     override func mouseEntered(with event: NSEvent) {
253         
254         guard let entered = event.trackingArea?.userInfo?["info"] as? TiledImageCellInformation else {
255             
256             return
257         }
258         
259         currentSelection = entered
260         needsDisplay = true
261     }
262     
263     override func mouseExited(with event: NSEvent) {
264         
265         currentSelection = nil
266         needsDisplay = true
267     }
268     
269     override func mouseDown(with event: NSEvent) {
270         
271         let mouse = convert(event.locationInWindow, from: nil)
272         
273         let items = infos.enumerated().compactMap { (offset, element) -> NSDraggingItem? in
274             
275             if !NSMouseInRect(mouse, element.frame, isFlipped) {
276                 
277                 return nil
278             }
279             
280             guard let pItem = NSPasteboardItem(pasteboardPropertyList: offset,
281                                                ofType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
282                     
283                     fatalError()
284             }
285             
286             let item = NSDraggingItem(pasteboardWriter: pItem)
287             item.setDraggingFrame(element.frame, contents: images[offset])
288             
289             return item
290         }
291         
292         let session = beginDraggingSession(with: items, event: event, source: self)
293         session.animatesToStartingPositionsOnCancelOrFail = true
294         session.draggingFormation = .none
295         
296         // ドラッグ中の全てのmouseEnterイベントがドラッグ後に一気にくるため一時的にTrackingを無効化
297         removeAllTrackingAreas()
298     }
299 }
300
301 extension TiledImageView: NSDraggingSource {
302     
303     func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
304         
305         return context == .withinApplication ? .move : []
306     }
307     
308     override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
309         
310         return draggingUpdated(sender)
311     }
312     
313     override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
314         
315         guard let types = sender.draggingPasteboard().types,
316             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
317                 
318                 return []
319         }
320         
321         if !sender.draggingSourceOperationMask().contains(.move) {
322             
323             return []
324         }
325         
326         let mouse = convert(sender.draggingLocation(), from: nil)
327         let underMouse = infos.filter { NSMouseInRect(mouse, $0.frame, isFlipped) }
328         if underMouse.count == 0 {
329             
330             currentSelection = nil
331             
332         } else {
333             
334             currentSelection = underMouse[0]
335         }
336         needsDisplay = true
337         
338         return .move
339     }
340     
341     override func draggingExited(_ sender: NSDraggingInfo?) {
342         
343         currentSelection = nil
344         needsDisplay = true
345     }
346     
347     override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
348         
349         guard let types = sender.draggingPasteboard().types,
350             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
351                 
352                 return false
353         }
354         
355         currentSelection = nil
356         needsDisplay = true
357         
358         return true
359     }
360     
361     override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
362         
363         guard let types = sender.draggingPasteboard().types,
364             types.contains(NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) else {
365                 
366                 return false
367         }
368         
369         let pboard = sender.draggingPasteboard()
370         
371         guard let pbItems = pboard.pasteboardItems, !pbItems.isEmpty else {
372             
373             return false
374         }
375         guard let index = pbItems.first?.propertyList(forType: NSPasteboard.PasteboardType(TiledImageView.privateDraggingUTI)) as? Int,
376             case 0..<images.count = index else {
377                 
378                 return false
379         }
380         
381         let mouse = convert(sender.draggingLocation(), from: nil)
382         
383         let underMouse = infos.enumerated().filter { NSMouseInRect(mouse, $0.element.frame, isFlipped) }
384         
385         guard !underMouse.isEmpty else {
386             
387             return false
388         }
389         
390         var newImages = images
391         let image = images[index]
392         newImages.remove(at: index)
393         newImages.insert(image, at: underMouse[0].offset)
394         images = newImages
395         
396         return true
397     }
398     
399     override func draggingEnded(_ sender: NSDraggingInfo) {
400         
401         setTrackingArea()
402     }
403 }