OSDN Git Service

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