OSDN Git Service

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