diff --git a/Color Picker.xcodeproj/project.pbxproj b/Color Picker.xcodeproj/project.pbxproj index 775c299..390a684 100644 --- a/Color Picker.xcodeproj/project.pbxproj +++ b/Color Picker.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 4A66F1971EBC274100D81A4A /* ColorWheelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A66F1961EBC274100D81A4A /* ColorWheelView.swift */; }; 4A66F19B1EBC6F0900D81A4A /* GradientSliderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A66F19A1EBC6F0900D81A4A /* GradientSliderCell.swift */; }; 4A6A7EF01F09D3E60048A92E /* ColorPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6A7EEF1F09D3E60048A92E /* ColorPickerView.swift */; }; + 4A6E05461F0F77D300A3C3AA /* ColorDragView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6E05451F0F77D300A3C3AA /* ColorDragView.swift */; }; 4A720F7C1EBD7E0000205678 /* NSColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A720F7B1EBD7E0000205678 /* NSColor+.swift */; }; 4A720F811EBE8B8000205678 /* Preferences.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A720F831EBE8B8000205678 /* Preferences.storyboard */; }; 4A720F841EBE8B8800205678 /* Palettes.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A720F861EBE8B8800205678 /* Palettes.storyboard */; }; @@ -48,6 +49,7 @@ 4A66F1961EBC274100D81A4A /* ColorWheelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorWheelView.swift; sourceTree = ""; }; 4A66F19A1EBC6F0900D81A4A /* GradientSliderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientSliderCell.swift; sourceTree = ""; }; 4A6A7EEF1F09D3E60048A92E /* ColorPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerView.swift; sourceTree = ""; }; + 4A6E05451F0F77D300A3C3AA /* ColorDragView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorDragView.swift; sourceTree = ""; }; 4A720F7B1EBD7E0000205678 /* NSColor+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSColor+.swift"; sourceTree = ""; }; 4A720F821EBE8B8000205678 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Preferences.storyboard; sourceTree = ""; }; 4A720F851EBE8B8800205678 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Palettes.storyboard; sourceTree = ""; }; @@ -97,6 +99,7 @@ 4A62A81B1EBED0FB003A02D2 /* PaletteCollectionViewItem.xib */, 4A66F1861EBC24E200D81A4A /* AppDelegate.swift */, 4AC3DDCA1ECA418200E08B8D /* ColorController.swift */, + 4A6E05451F0F77D300A3C3AA /* ColorDragView.swift */, 4A6A7EEF1F09D3E60048A92E /* ColorPickerView.swift */, 4A66F1881EBC24E200D81A4A /* ColorPickerViewController.swift */, 4A66F1961EBC274100D81A4A /* ColorWheelView.swift */, @@ -199,6 +202,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4A6E05461F0F77D300A3C3AA /* ColorDragView.swift in Sources */, 4A66F19B1EBC6F0900D81A4A /* GradientSliderCell.swift in Sources */, 4AC3DDCB1ECA418200E08B8D /* ColorController.swift in Sources */, 4A62A81D1EBED596003A02D2 /* PaletteCollectionViewItem.swift in Sources */, diff --git a/Color Picker/Assets.xcassets/Contents.json b/Color Picker/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Color Picker/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Color Picker/Assets.xcassets/Pasteboard.imageset/Contents.json b/Color Picker/Assets.xcassets/Pasteboard.imageset/Contents.json new file mode 100644 index 0000000..65af876 --- /dev/null +++ b/Color Picker/Assets.xcassets/Pasteboard.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "mac", + "filename" : "Pasteboard.png", + "scale" : "1x" + }, + { + "idiom" : "mac", + "filename" : "Pasteboard@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard.png b/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard.png new file mode 100644 index 0000000..4862295 Binary files /dev/null and b/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard.png differ diff --git a/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard@2x.png b/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard@2x.png new file mode 100644 index 0000000..6955d74 Binary files /dev/null and b/Color Picker/Assets.xcassets/Pasteboard.imageset/Pasteboard@2x.png differ diff --git a/Color Picker/Base.lproj/ColorPicker.storyboard b/Color Picker/Base.lproj/ColorPicker.storyboard index c04acd1..410dd2f 100644 --- a/Color Picker/Base.lproj/ColorPicker.storyboard +++ b/Color Picker/Base.lproj/ColorPicker.storyboard @@ -60,10 +60,16 @@ + + + + + + @@ -73,4 +79,7 @@ + + + diff --git a/Color Picker/ColorController.swift b/Color Picker/ColorController.swift index 2334470..4e65f69 100644 --- a/Color Picker/ColorController.swift +++ b/Color Picker/ColorController.swift @@ -43,6 +43,8 @@ class ColorController { saturation: color.saturationComponent, brightness: 1.0, alpha: 1.0) - colorPicker.updateSelectedColor() + colorPicker.updateColorWheel() + colorPicker.updateSlider() + colorPicker.updateLabel() } } diff --git a/Color Picker/ColorDragView.swift b/Color Picker/ColorDragView.swift new file mode 100644 index 0000000..b07ee1e --- /dev/null +++ b/Color Picker/ColorDragView.swift @@ -0,0 +1,42 @@ +// +// ColorDragView.swift +// Color Picker +// +// Created by David Wu on 7/7/17. +// Copyright © 2017 Gofake1. All rights reserved. +// + +import Cocoa + +/// Allows colors to be dragged out +class ColorDragView: NSImageView { + + override func mouseDown(with event: NSEvent) { + let pasteboardItem = NSPasteboardItem() + pasteboardItem.setDataProvider(self, forTypes: [.color]) + let draggingImage = NSImage(size: bounds.size) + draggingImage.lockFocus() + ColorController.shared.selectedColor.drawSwatch(in: bounds) + draggingImage.unlockFocus() + let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem) + draggingItem.setDraggingFrame(bounds, contents: draggingImage) + beginDraggingSession(with: [draggingItem], event: event, source: self) + } +} + +extension ColorDragView: NSDraggingSource { + func draggingSession(_ session: NSDraggingSession, + sourceOperationMaskFor context: NSDraggingContext) + -> NSDragOperation { + return .generic + } +} + +extension ColorDragView: NSPasteboardItemDataProvider { + func pasteboard(_ pasteboard: NSPasteboard?, + item: NSPasteboardItem, + provideDataForType type: NSPasteboard.PasteboardType) { + guard let pasteboard = pasteboard, type == .color else { return } + ColorController.shared.selectedColor.write(to: pasteboard) + } +} diff --git a/Color Picker/ColorPickerView.swift b/Color Picker/ColorPickerView.swift index 063c86d..71ae139 100644 --- a/Color Picker/ColorPickerView.swift +++ b/Color Picker/ColorPickerView.swift @@ -8,8 +8,40 @@ import Cocoa +/// `ColorPickerViewController` content view. Allows colors to be dragged in. class ColorPickerView: NSView { + override func awakeFromNib() { + registerForDraggedTypes([.color]) + } + + // MARK: - NSDraggingDestination + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + return .copy + } + + override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pasteboard = sender.draggingPasteboard() + guard let colors = pasteboard.readObjects(forClasses: [NSColor.self], options: nil) as? [NSColor], + colors.count > 0 + else { return false } + // Cancel if dragged color is the same as the current color + return ColorController.shared.selectedColor != colors[0] + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pasteboard = sender.draggingPasteboard() + guard let colors = pasteboard.readObjects(forClasses: [NSColor.self], options: nil) as? [NSColor], + colors.count > 0 + else { return false } + ColorController.shared.setColor(colors[0]) + return true + } + + // MARK: - + + // Allows mouse click to lose `ColorPickerViewController`'s text field's focus override func mouseDown(with event: NSEvent) { window?.makeFirstResponder(self) } diff --git a/Color Picker/ColorPickerViewController.swift b/Color Picker/ColorPickerViewController.swift index 2b4b452..72f0f81 100644 --- a/Color Picker/ColorPickerViewController.swift +++ b/Color Picker/ColorPickerViewController.swift @@ -11,41 +11,34 @@ import Cocoa class ColorPickerViewController: NSViewController { @IBOutlet weak var brightnessSlider: NSSlider! - @IBOutlet weak var colorLabel: NSTextField! - @IBOutlet weak var colorWheelView: ColorWheelView! + @IBOutlet weak var colorLabel: NSTextField! + @IBOutlet weak var colorWheelView: ColorWheelView! + @IBOutlet weak var colorDragView: ColorDragView! - override func viewDidLoad() { + override func awakeFromNib() { ColorController.shared.colorPicker = self colorWheelView.delegate = self } - /// - postcondition: Mutates `colorController.selectedColor` - override func controlTextDidEndEditing(_ obj: Notification) { - let string = (obj.userInfo?["NSFieldEditor"] as! NSTextView).textStorage!.string - let color = NSColor(hexString: string) - ColorController.shared.setColor(color) - view.window?.makeFirstResponder(view) - } - - /// Should only be called by `colorController` - func updateSelectedColor() { - updateColorWheel() - updateLabel() - updateSlider() - } - - /// - postcondition: Mutates `ColorController.brightness` + /// - postcondition: Mutates `ColorController.brightness`, redraws views @IBAction func setBrightness(_ sender: NSSlider) { ColorController.shared.brightness = CGFloat((sender.maxValue-sender.doubleValue) / sender.maxValue) updateColorWheel(redrawCrosshair: false) updateLabel() } - private func updateColorWheel(redrawCrosshair: Bool = true) { + /// - postcondition: Mutates `colorController.selectedColor`, redraws views + @IBAction func setColor(_ sender: NSTextField) { + let color = NSColor(hexString: sender.stringValue) + ColorController.shared.setColor(color) + view.window?.makeFirstResponder(view) + } + + func updateColorWheel(redrawCrosshair: Bool = true) { colorWheelView.setColor(ColorController.shared.selectedColor, redrawCrosshair) } - private func updateLabel() { + func updateLabel() { colorLabel.backgroundColor = ColorController.shared.selectedColor colorLabel.stringValue = "#"+ColorController.shared.selectedColor.rgbHexString if ColorController.shared.selectedColor.scaledBrightness < 0.5 { @@ -55,7 +48,7 @@ class ColorPickerViewController: NSViewController { } } - private func updateSlider() { + func updateSlider() { guard let sliderCell = brightnessSlider.cell as? GradientSliderCell else { fatalError() } sliderCell.colorA = ColorController.shared.masterColor brightnessSlider.drawCell(sliderCell) @@ -80,12 +73,12 @@ extension ColorPickerViewController: NSControlTextEditingDelegate { // or 7 characters if the first character is '#' switch string.characters.count { case 6: - guard string.containsOnlyHexCharacters() else { return false } + guard string.containsOnlyHexCharacters else { return false } return true case 7: guard string.hasPrefix("#") else { return false } var trimmed = string; trimmed.remove(at: trimmed.startIndex) - guard trimmed.containsOnlyHexCharacters() else { return false } + guard trimmed.containsOnlyHexCharacters else { return false } return true default: return false @@ -94,7 +87,7 @@ extension ColorPickerViewController: NSControlTextEditingDelegate { func control(_ control: NSControl, isValidObject obj: Any?) -> Bool { if control == colorLabel { - guard let _obj = obj, let string = _obj as? String else { return false } + guard let string = obj as? String else { return false } return validateControlString(string) } return false @@ -102,7 +95,7 @@ extension ColorPickerViewController: NSControlTextEditingDelegate { } extension String { - func containsOnlyHexCharacters() -> Bool { + var containsOnlyHexCharacters: Bool { return !characters.contains { switch $0 { case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "a", "b", diff --git a/Color Picker/Info.plist b/Color Picker/Info.plist index cd3369c..0a9cb30 100644 --- a/Color Picker/Info.plist +++ b/Color Picker/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.1 + 1.1 CFBundleVersion - 1.0.1 + 1.1 LSApplicationCategoryType public.app-category.graphics-design LSMinimumSystemVersion diff --git a/Color Picker/PaletteCollectionViewItem.swift b/Color Picker/PaletteCollectionViewItem.swift index 8ecb47e..750a78d 100644 --- a/Color Picker/PaletteCollectionViewItem.swift +++ b/Color Picker/PaletteCollectionViewItem.swift @@ -53,7 +53,7 @@ class PaletteCollectionViewItem: NSCollectionViewItem { @IBAction func edit(_ sender: NSMenuItem) { isEditing = true - paletteColorsView.isEditing = true + paletteColorsView.state = .isEditing } @IBAction func delete(_ sender: NSMenuItem) { @@ -63,7 +63,7 @@ class PaletteCollectionViewItem: NSCollectionViewItem { @IBAction func finishEditing(_ sender: NSButton) { isEditing = false - paletteColorsView.isEditing = false + paletteColorsView.state = .normal } } @@ -73,6 +73,12 @@ extension PaletteCollectionViewItem: PaletteColorsViewDelegate { ColorController.shared.setColor(color) } + func addColor(_ color: NSColor) { + if let palette = representedObject as? Palette { + palette.addColor(color) + } + } + func removeColor(_ color: NSColor) { if let palette = representedObject as? Palette { palette.removeColor(color) diff --git a/Color Picker/PaletteColorsView.swift b/Color Picker/PaletteColorsView.swift index 4c37444..f736e07 100644 --- a/Color Picker/PaletteColorsView.swift +++ b/Color Picker/PaletteColorsView.swift @@ -10,23 +10,37 @@ import Cocoa protocol PaletteColorsViewDelegate: class { func selectColor(_ color: NSColor) + func addColor(_ color: NSColor) func removeColor(_ color: NSColor) } +/// Allows colors to be dragged in and out class PaletteColorsView: NSView { + enum State { + case isDraggingOut + case isEditing + case normal + } + @objc dynamic var colors: Set! { didSet { sortedColors = colors.sorted(by: >) needsDisplay = true } } - var isEditing = false { + weak var delegate: PaletteColorsViewDelegate? + var state = State.normal { didSet { - needsDisplay = true + switch state { + case .isEditing: + needsDisplay = true + default: + break + } } } - weak var delegate: PaletteColorsViewDelegate? + private var draggedOutColor: NSColor? private var selectedIndex: Int? { didSet { if oldValue != selectedIndex { @@ -36,6 +50,10 @@ class PaletteColorsView: NSView { } private var sortedColors: [NSColor]! + override func awakeFromNib() { + registerForDraggedTypes([.color]) + } + override func draw(_ dirtyRect: NSRect) { guard let context = NSGraphicsContext.current?.cgContext, let colors = sortedColors else { return } context.setLineWidth(1) @@ -67,7 +85,8 @@ class PaletteColorsView: NSView { context.setStrokeColor(CGColor(red: 0.1, green: 0.1, blue: 1.0, alpha: 1.0)) context.stroke(CGRect(x: selectedIndex*20+1, y: 1, width: 18, height: 18)) - if isEditing && selectedIndex < colors.count { + // Draw 'X' on selected color swatch if in editing mode + if state == .isEditing && selectedIndex < colors.count { context.setStrokeColor(colors[selectedIndex].scaledBrightness > 0.5 ? CGColor.black : CGColor.white) context.setLineWidth(2) context.addLines(between: [CGPoint(x: selectedIndex*20+4, y: 16), @@ -79,6 +98,7 @@ class PaletteColorsView: NSView { } private func index(for point: CGPoint) -> Int? { + guard bounds.contains(point) else { return nil } switch point.x { case 0..<20: return 0 case 20..<40: return 1 @@ -94,16 +114,61 @@ class PaletteColorsView: NSView { } } + // MARK: - NSDraggingDestination + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + guard state == .normal else { return [] } + // Ignore drags from self + if let paletteColorsView = sender.draggingSource() as? PaletteColorsView, + paletteColorsView == self { + return [] + } + return .copy + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pasteboard = sender.draggingPasteboard() + guard let colors = pasteboard.readObjects(forClasses: [NSColor.self], options: nil) as? [NSColor], + colors.count > 0 + else { return false } + delegate?.addColor(colors[0]) + return true + } + // MARK: - Mouse - override func mouseDown(with event: NSEvent) { + override func mouseDragged(with event: NSEvent) { + guard state == .normal, + let index = selectedIndex, + sortedColors.count != 0, + sortedColors.count > index + else { return } + state = .isDraggingOut + draggedOutColor = sortedColors[index] + let pasteboardItem = NSPasteboardItem() + pasteboardItem.setDataProvider(self, forTypes: [.color]) + let draggingImage = NSImage(size: NSSize(width: 18, height: 18)) + draggingImage.lockFocus() + sortedColors[index].drawSwatch(in: NSRect(x: 0, y: 0, width: 18, height: 18)) + draggingImage.unlockFocus() + let dragPoint = convert(event.locationInWindow, from: nil) + let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem) + draggingItem.setDraggingFrame(NSRect(x: dragPoint.x-11, y: dragPoint.y-6, width: 18, height: 18), + contents: draggingImage) + beginDraggingSession(with: [draggingItem], event: event, source: self) + } + + override func mouseUp(with event: NSEvent) { guard let index = selectedIndex, sortedColors.count != 0, sortedColors.count > index else { return } - if isEditing { + switch state { + case .isDraggingOut: + return + case .isEditing: delegate?.removeColor(sortedColors[index]) - } else { + case .normal: delegate?.selectColor(sortedColors[index]) } } @@ -127,3 +192,30 @@ class PaletteColorsView: NSView { userInfo: nil)) } } + +extension PaletteColorsView: NSDraggingSource { + func draggingSession(_ session: NSDraggingSession, + sourceOperationMaskFor context: NSDraggingContext) + -> NSDragOperation { + return .generic + } + + func draggingSession(_ session: NSDraggingSession, + endedAt screenPoint: NSPoint, + operation: NSDragOperation) { + state = .normal + draggedOutColor = nil + } +} + +extension PaletteColorsView: NSPasteboardItemDataProvider { + func pasteboard(_ pasteboard: NSPasteboard?, + item: NSPasteboardItem, + provideDataForType type: NSPasteboard.PasteboardType) { + guard let pasteboard = pasteboard, + let color = draggedOutColor, + type == .color + else { return } + color.write(to: pasteboard) + } +} diff --git a/Color Picker/PaletteViewController.swift b/Color Picker/PaletteViewController.swift index aca7199..f02d85a 100644 --- a/Color Picker/PaletteViewController.swift +++ b/Color Picker/PaletteViewController.swift @@ -17,7 +17,7 @@ class PaletteViewController: NSViewController { /// Cached index paths for dragged items in the current drag session private var draggedIndexPaths = Set() - override func viewDidLoad() { + override func awakeFromNib() { collectionView.collectionController = collectionController collectionView.register(PaletteCollectionViewItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "palette")) @@ -47,7 +47,8 @@ class PaletteViewController: NSViewController { extension PaletteViewController: NSCollectionViewDelegate { func collectionView(_ collectionView: NSCollectionView, - pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { + pasteboardWriterForItemAt indexPath: IndexPath) + -> NSPasteboardWriting? { let writer = NSPasteboardItem() let data = NSKeyedArchiver.archivedData(withRootObject: paletteCollection.palettes[indexPath.item]) writer.setData(data, forType: NSPasteboard.PasteboardType(rawValue: "net.gofake1.Color-Picker.palette")) @@ -73,13 +74,14 @@ extension PaletteViewController: NSCollectionViewDelegate { proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { - return .move + return .move } func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, - dropOperation: NSCollectionView.DropOperation) -> Bool { + dropOperation: NSCollectionView.DropOperation) + -> Bool { for fromIndexPath in draggedIndexPaths { let temp = paletteCollection.palettes.remove(at: fromIndexPath.item) paletteCollection.palettes.insert(temp, at: (indexPath.item <= fromIndexPath.item)