├── .gitignore ├── Code ├── GIF │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ ├── Contents.json │ │ ├── Left.imageset │ │ │ ├── Contents.json │ │ │ └── Left.png │ │ ├── Right.imageset │ │ │ ├── Contents.json │ │ │ └── Right.png │ │ ├── edit.imageset │ │ │ ├── Contents.json │ │ │ └── edit.png │ │ └── trash.imageset │ │ │ ├── Contents.json │ │ │ └── trash.png │ ├── Base.lproj │ │ └── Main.storyboard │ ├── ColoredBezierPath.swift │ ├── Constants.swift │ ├── Document.swift │ ├── DragNotificationImageView.swift │ ├── DrawingOptionsHandler.swift │ ├── EditViewController.swift │ ├── FancyAlert.swift │ ├── FancyButton.swift │ ├── FancyButtonCell.swift │ ├── FancyTextFieldCell.swift │ ├── FrameCollectionViewItem.swift │ ├── FrameCollectionViewItem.xib │ ├── GIF.entitlements │ ├── GIFFrame.swift │ ├── GIFHandler.swift │ ├── IAPHelper.swift │ ├── Info.plist │ ├── LoadingView.swift │ ├── MainViewController+FrameCollectionViewItemDelegate.swift │ ├── MainViewController+NSCollectionView.swift │ ├── MainViewController.swift │ ├── NSBezierPathExtension.swift │ ├── NSColorExtension.swift │ ├── NSImageExtension.swift │ ├── NSViewExtension.swift │ ├── PixelImageView.swift │ ├── PreviewViewController.swift │ ├── Products.swift │ ├── SmartTextField.swift │ ├── ZoomView.swift │ └── loading.gif └── Smart GIF Maker.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── Christian.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ └── Christian.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── GIF.xcscheme │ └── xcschememanagement.plist ├── Misc ├── Icon.png ├── Icon.pxm ├── Resources │ ├── clock.png │ ├── clock.pxm │ ├── edit.png │ ├── edit.pxm │ └── trash.png ├── Screenshots │ ├── ss1.png │ ├── ss2.png │ ├── ss3.png │ ├── ss4.png │ └── ss5.png ├── test.gif ├── testing │ ├── f1.png │ ├── f2.png │ ├── f3.png │ ├── f4.png │ ├── f5.png │ ├── f6.png │ ├── f7.png │ ├── f8.png │ ├── f9.png │ └── loading.gif └── version.rtf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | GIF/GIF.xcodeproj/project.xcworkspace/xcuserdata/Christian.xcuserdatad/UserInterfaceState.xcuserstate 3 | 4 | iap accounts.txt 5 | -------------------------------------------------------------------------------- /Code/GIF/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GIF 4 | // 5 | // Created by Christian Lundtofte on 14/03/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import StoreKit 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | // IAP 16 | var products = [SKProduct]() 17 | 18 | 19 | // MARK: Setup 20 | func applicationDidFinishLaunching(_ aNotification: Notification) { 21 | // In app purchase setup 22 | NotificationCenter.default.addObserver(self, 23 | selector: #selector(AppDelegate.productsLoaded), 24 | name: IAPHelper.IAPLoadedNotificationName, 25 | object: nil) 26 | NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.handlePurchaseNotification(_:)), 27 | name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), 28 | object: nil) 29 | NotificationCenter.default.addObserver(self, 30 | selector: #selector(AppDelegate.failedPurchase), 31 | name: NSNotification.Name(rawValue: IAPHelper.IAPPurchaseFailed), 32 | object: nil) 33 | loadProducts() 34 | } 35 | 36 | func applicationWillTerminate(_ aNotification: Notification) { 37 | } 38 | 39 | 40 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 41 | return true 42 | } 43 | 44 | 45 | // MARK: Pro / watermark removal 46 | // Should this really be in AppDelegate? 47 | func createPurchaseMenu() { 48 | guard let menu = NSApplication.shared().mainMenu else { return } 49 | let newItem = NSMenuItem(title: "UnlockPro", action: nil, keyEquivalent: "") 50 | let newMenu = NSMenu(title: "Remove watermarks") 51 | 52 | let unlockItem = NSMenuItem(title: "Remove watermarks", action: #selector(AppDelegate.unlockProButtonClicked), keyEquivalent: "") 53 | let unlockedItem = NSMenuItem(title: "Previously purchased", action: #selector(AppDelegate.unlockedButtonClicked), keyEquivalent: "") 54 | 55 | newMenu.addItem(unlockItem) 56 | newMenu.addItem(unlockedItem) 57 | 58 | newItem.submenu = newMenu 59 | menu.insertItem(newItem, at: menu.items.count-1) 60 | } 61 | 62 | func removePurchaseMenu() { 63 | guard let menu = NSApplication.shared().mainMenu, 64 | let item = menu.item(withTitle: "UnlockPro") else { return } 65 | menu.removeItem(item) 66 | } 67 | 68 | // Unlock button clicked 69 | func unlockProButtonClicked() { 70 | Products.store.buyProduct(products[0]) 71 | } 72 | 73 | // Previously unlocked button clicked 74 | func unlockedButtonClicked() { 75 | 76 | let alert = FancyAlert() 77 | alert.messageText = "Unlocking.." 78 | alert.informativeText = "Attempting to unlock. This might take a while." 79 | alert.alertStyle = .critical 80 | alert.addButton(withTitle: "OK") 81 | 82 | if let window = NSApplication.shared().mainWindow { 83 | alert.beginSheetModal(for: window, completionHandler: nil) 84 | } 85 | else { 86 | alert.runModal() 87 | } 88 | 89 | Products.store.restorePurchases() 90 | } 91 | 92 | 93 | // MARK: In app purchase 94 | func loadProducts() { 95 | products = [] 96 | Products.store.requestProducts{success, products in 97 | if success { 98 | self.products = products! 99 | } 100 | } 101 | } 102 | 103 | func productsLoaded() { 104 | if !Products.store.isProductPurchased(Products.Pro) { 105 | createPurchaseMenu() 106 | } 107 | } 108 | 109 | // Purchase did not succeed 110 | func failedPurchase() { 111 | 112 | if !Products.store.isProductPurchased(Products.Pro) { 113 | let alert = FancyAlert() 114 | alert.messageText = "An error occurred" 115 | alert.informativeText = "An error occurred during purchase. If you are trying to restore the purchase, make sure that you purchased it previously." 116 | alert.alertStyle = .critical 117 | alert.addButton(withTitle: "OK") 118 | 119 | if let window = NSApplication.shared().mainWindow { 120 | alert.beginSheetModal(for: window, completionHandler: nil) 121 | } 122 | else { 123 | alert.runModal() 124 | } 125 | } 126 | } 127 | 128 | func handlePurchaseNotification(_ notification: Notification) { 129 | 130 | if Products.store.isProductPurchased(Products.Pro) { 131 | removePurchaseMenu() 132 | 133 | let alert = FancyAlert() 134 | alert.messageText = "Watermarks removed" 135 | alert.informativeText = "Watermarks are now removed!" 136 | alert.alertStyle = .critical 137 | alert.addButton(withTitle: "OK") 138 | 139 | if let window = NSApplication.shared().mainWindow { 140 | alert.beginSheetModal(for: window, completionHandler: nil) 141 | } 142 | else { 143 | alert.runModal() 144 | } 145 | } 146 | else { // Error 147 | let alert = FancyAlert() 148 | alert.messageText = "An error occurred" 149 | alert.informativeText = "An error occurred during purchase. If you are trying to restore the purchase, make sure that you purchased it previously." 150 | alert.alertStyle = .critical 151 | alert.addButton(withTitle: "OK") 152 | 153 | if let window = NSApplication.shared().mainWindow { 154 | alert.beginSheetModal(for: window, completionHandler: nil) 155 | } 156 | else { 157 | alert.runModal() 158 | } 159 | } 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/Left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Left.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/Left.imageset/Left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/Left.imageset/Left.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/Right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Right.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/Right.imageset/Right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/Right.imageset/Right.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/edit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "edit.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/edit.imageset/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/edit.imageset/edit.png -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "trash.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Code/GIF/Assets.xcassets/trash.imageset/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/Assets.xcassets/trash.imageset/trash.png -------------------------------------------------------------------------------- /Code/GIF/ColoredBezierPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColoredBezierPath.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 18/08/2018. 6 | // Copyright © 2018 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | class ColoredBezierPath : NSBezierPath { 13 | var strokeColor : NSColor! 14 | } 15 | -------------------------------------------------------------------------------- /Code/GIF/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 16/08/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class Constants { 13 | static let darkBackgroundColor:NSColor = NSColor(red: 50.0/255.0, green: 50.0/255.0, blue: 50.0/255.0, alpha: 1.0) 14 | } 15 | -------------------------------------------------------------------------------- /Code/GIF/Document.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Document.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 28/06/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class Document: NSDocument { 12 | 13 | /* 14 | override var windowNibName: String? { 15 | // Override returning the nib file name of the document 16 | // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead. 17 | return "Document" 18 | } 19 | */ 20 | 21 | override func windowControllerDidLoadNib(_ aController: NSWindowController) { 22 | super.windowControllerDidLoadNib(aController) 23 | } 24 | 25 | override func data(ofType typeName: String) throws -> Data { 26 | // Insert code here to write your document to data of the specified type. If outError != NULL, ensure that you create and set an appropriate error when returning nil. 27 | // You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead. 28 | throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) 29 | } 30 | 31 | override func read(from data: Data, ofType typeName: String) throws { 32 | 33 | // Load NSImage from data, fetch info, and send 34 | if let gif = NSImage(data: data) { 35 | GIFHandler.loadGIF(with: gif, onFinish: { repr in 36 | let userInfo = ["info":repr] 37 | NotificationCenter.default.post(name: MainViewController.loadedDocumentFramesNotificationName, object: self, userInfo: userInfo) 38 | }) 39 | } 40 | } 41 | 42 | override class func autosavesInPlace() -> Bool { 43 | return false 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Code/GIF/DragNotificationImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragNotificationImageView.swift 3 | // SwiftImageResizer 4 | // 5 | // Created by Christian on 07/01/2016. 6 | // Copyright © 2016 Christian Lundtofte Sørensen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol DragNotificationImageViewDelegate { 12 | func imageClicked(imageView: DragNotificationImageView) 13 | func imageDragged(imageView: DragNotificationImageView) 14 | } 15 | 16 | class DragNotificationImageView: NSImageView { 17 | var delegate:DragNotificationImageViewDelegate? 18 | var gifFrame: GIFFrame? 19 | 20 | // MARK: Setup 21 | override func awakeFromNib() { 22 | super.awakeFromNib() 23 | self.register(forDraggedTypes: NSImage.imageTypes()) 24 | } 25 | 26 | override func draw(_ dirtyRect: NSRect) { 27 | super.draw(dirtyRect) 28 | } 29 | 30 | 31 | // MARK: Drag 32 | override func draggingEnded(_ sender: NSDraggingInfo?) { 33 | self.delegate?.imageDragged(imageView: self) 34 | } 35 | 36 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 37 | return NSDragOperation.copy 38 | } 39 | 40 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { 41 | return true 42 | } 43 | 44 | 45 | // MARK: Mouse 46 | override func mouseDown(with event: NSEvent) { 47 | } 48 | 49 | override func mouseUp(with event: NSEvent) { 50 | self.delegate?.imageClicked(imageView: self) 51 | } 52 | 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Code/GIF/DrawingOptionsHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrawingOptionsHandler.swift 3 | // ImageFun 4 | // 5 | // Created by Christian Lundtofte on 26/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class DrawingOptionsHandler { 13 | static let colorChangedNotificationName = Notification.Name(rawValue: "ColorChangedOutside") 14 | static let backgroundColorChangedNotificationName = Notification.Name(rawValue: "BackgroundColorChanged") 15 | static let usedEyeDropperNotificationName = Notification.Name(rawValue: "UsedEyeDropper") 16 | 17 | static let shared = DrawingOptionsHandler() 18 | 19 | var drawingColorPtr : [Int] = NSColor.blue.getRGBAr() 20 | 21 | private var _drawingColor = NSColor.blue; 22 | var drawingColor : NSColor { 23 | get { 24 | return _drawingColor 25 | } 26 | set { 27 | _drawingColor = newValue 28 | drawingColorPtr = _drawingColor.getRGBAr() 29 | } 30 | } 31 | var imageBackgroundColor:NSColor = NSColor.lightGray 32 | 33 | var isPickingColor = false 34 | 35 | var brushSize:Int = 1 36 | } 37 | -------------------------------------------------------------------------------- /Code/GIF/EditViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditViewController.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 13/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class EditViewController: NSViewController, ZoomViewDelegate, NSWindowDelegate { 12 | 13 | // MARK: Fields 14 | // UI 15 | @IBOutlet var imageScrollView:NSScrollView! 16 | 17 | @IBOutlet var imageBackgroundView:ZoomView! 18 | @IBOutlet var frameNumberLabel:NSTextField! 19 | @IBOutlet var currentFrameImageView:PixelImageView! 20 | @IBOutlet var previousFrameButton:NSButton! 21 | @IBOutlet var nextFrameButton:NSButton! 22 | @IBOutlet var brushSizeField:NSTextField! 23 | 24 | @IBOutlet var eyedropperButtonCell:FancyButtonCell! 25 | 26 | @IBOutlet var colorPicker:NSColorWell! 27 | @IBOutlet var backgroundColorPicker:NSColorWell! 28 | 29 | @IBOutlet var undoButton:NSButton! 30 | @IBOutlet var redoButton:NSButton! 31 | 32 | var drawingOptionsWindowController:NSWindowController? 33 | 34 | // Frames and frame count 35 | var frames:[GIFFrame] = [] 36 | var currentFrameNumber:Int = 0 37 | var initialFrameNumber:Int? 38 | 39 | 40 | // MARK: ViewController stuff 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | self.addEditorMenu() 45 | self.allowColorPanelAlpha() 46 | 47 | // Event listeners (Color changes and window resizes) 48 | NotificationCenter.default.addObserver(self, 49 | selector: #selector(EditViewController.windowResized), 50 | name: NSNotification.Name.NSWindowDidResize, 51 | object: nil) 52 | 53 | NotificationCenter.default.addObserver(self, selector: #selector(EditViewController.imageBackgroundColorUpdated), 54 | name: DrawingOptionsHandler.backgroundColorChangedNotificationName, 55 | object: nil) 56 | NotificationCenter.default.addObserver(self, 57 | selector: #selector(EditViewController.colorChangedOutside), 58 | name: DrawingOptionsHandler.colorChangedNotificationName, 59 | object: nil) 60 | 61 | NotificationCenter.default.addObserver(self, selector: #selector(EditViewController.usedEyeDropper), 62 | name: DrawingOptionsHandler.usedEyeDropperNotificationName, 63 | object: nil) 64 | 65 | self.colorPicker.addObserver(self, forKeyPath: "color", options: .new, context: nil) 66 | self.backgroundColorPicker.addObserver(self, forKeyPath: "color", options: .new, context: nil) 67 | 68 | 69 | 70 | self.view.wantsLayer = true 71 | } 72 | 73 | override func viewWillAppear() { 74 | super.viewWillAppear() 75 | 76 | // Sets up UI controls 77 | self.imageBackgroundView.backgroundColor = Constants.darkBackgroundColor 78 | self.currentFrameImageView.backgroundColor = DrawingOptionsHandler.shared.imageBackgroundColor 79 | self.brushSizeField.stringValue = String(DrawingOptionsHandler.shared.brushSize) 80 | 81 | self.colorPicker.color = DrawingOptionsHandler.shared.drawingColor 82 | self.backgroundColorPicker.color = DrawingOptionsHandler.shared.imageBackgroundColor 83 | 84 | imageBackgroundView.zoomView = currentFrameImageView 85 | imageBackgroundView.delegate = self 86 | imageScrollView.backgroundColor = Constants.darkBackgroundColor 87 | 88 | self.view.backgroundColor = Constants.darkBackgroundColor 89 | 90 | // Sets up window border 91 | self.view.window?.titlebarAppearsTransparent = true 92 | self.view.window?.isMovableByWindowBackground = true 93 | self.view.window?.titleVisibility = NSWindowTitleVisibility.hidden 94 | self.view.window?.backgroundColor = Constants.darkBackgroundColor 95 | self.view.window?.acceptsMouseMovedEvents = true 96 | self.view.window?.delegate = self 97 | 98 | 99 | 100 | // Show specific frame if chosen from main window 101 | if let frameIndex = self.initialFrameNumber { 102 | self.currentFrameNumber = frameIndex 103 | self.showFrame(frame: self.frames[frameIndex]) 104 | self.updateFrameLabel() 105 | } 106 | } 107 | 108 | override var representedObject: Any? { 109 | didSet { 110 | } 111 | } 112 | 113 | // Forces image update in main view 114 | func windowWillClose(_ notification: Notification) { 115 | if NSColorPanel.sharedColorPanelExists() { 116 | let panel = NSColorPanel.shared() 117 | panel.close() 118 | } 119 | 120 | NotificationCenter.default.post(name: MainViewController.editingEndedNotificationName, object: nil) 121 | 122 | self.removeEditorMenu() 123 | } 124 | 125 | 126 | // MARK: Values changing 127 | // Observe changes (Used for color wells) 128 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 129 | if keyPath == "color" { 130 | guard let object = object as? NSColorWell else { return } 131 | 132 | if object == colorPicker { 133 | DrawingOptionsHandler.shared.drawingColor = colorPicker.color 134 | } 135 | else if object == backgroundColorPicker { 136 | DrawingOptionsHandler.shared.imageBackgroundColor = backgroundColorPicker.color 137 | NotificationCenter.default.post(name: DrawingOptionsHandler.backgroundColorChangedNotificationName, object: nil) 138 | } 139 | } 140 | } 141 | 142 | // Called when something changes the color from outside this viewcontroller 143 | func colorChangedOutside() { 144 | self.colorPicker.color = DrawingOptionsHandler.shared.drawingColor 145 | } 146 | 147 | // Called when eyedropper tool selects color 148 | func usedEyeDropper() { 149 | DrawingOptionsHandler.shared.isPickingColor = false 150 | 151 | self.eyedropperButtonCell.showBackground = DrawingOptionsHandler.shared.isPickingColor 152 | self.eyedropperButtonCell.redraw() 153 | } 154 | 155 | // When window resizes, make sure image is center 156 | func windowResized() { 157 | handleCenterImage() 158 | } 159 | 160 | // MARK: ZoomViewDelegate 161 | func zoomChanged(magnification: CGFloat) { 162 | updateScrollViewSize() 163 | 164 | self.currentFrameImageView.center(inView: imageBackgroundView) 165 | } 166 | 167 | 168 | 169 | // MARK: Buttons 170 | // Eraser 171 | @IBAction func eraserButtonClicked(sender: AnyObject?) { 172 | self.colorPicker.color = NSColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) 173 | } 174 | 175 | // Eyedropper 176 | @IBAction func eyedropperButtonClicked(sender: AnyObject?) { 177 | DrawingOptionsHandler.shared.isPickingColor = !DrawingOptionsHandler.shared.isPickingColor 178 | 179 | self.eyedropperButtonCell.showBackground = DrawingOptionsHandler.shared.isPickingColor 180 | self.eyedropperButtonCell.redraw() 181 | } 182 | 183 | // Undo 184 | @IBAction func undoButtonClicked(sender: AnyObject?) { 185 | // self.currentFrameImageView.undo() 186 | } 187 | 188 | // Redo 189 | @IBAction func redoButtonClicked(sender: AnyObject?) { 190 | // self.currentFrameImageView.redo() 191 | } 192 | 193 | // Next frame 194 | @IBAction func nextFrameButtonClicked(sender: AnyObject?) { 195 | if self.currentFrameNumber+1 > self.frames.count-1 { 196 | self.currentFrameNumber = 0 197 | } 198 | else { 199 | self.currentFrameNumber += 1 200 | } 201 | 202 | self.showFrame(frame: self.frames[self.currentFrameNumber]) 203 | self.updateFrameLabel() 204 | } 205 | 206 | // Previous frame 207 | @IBAction func previousFrameButtonClicked(sender: AnyObject?) { 208 | if self.currentFrameNumber-1 < 0 { 209 | self.currentFrameNumber = self.frames.count-1 210 | } 211 | else { 212 | self.currentFrameNumber -= 1 213 | } 214 | 215 | self.showFrame(frame: self.frames[self.currentFrameNumber]) 216 | self.updateFrameLabel() 217 | } 218 | 219 | // Buttons on top of color wells 220 | // Drawing color 221 | @IBAction func drawingColorWellClicked(sender: AnyObject?) { 222 | colorPicker.performClick(sender) 223 | self.allowColorPanelAlpha() 224 | } 225 | 226 | // Background color 227 | @IBAction func backgroundColorWellClicked(sender: AnyObject?) { 228 | backgroundColorPicker.performClick(sender) 229 | self.allowColorPanelAlpha() 230 | } 231 | 232 | 233 | // MARK: UI 234 | // Creates a menu with editor related items 235 | func addEditorMenu() { 236 | guard let menu = NSApplication.shared().mainMenu else { return } 237 | let newItem = NSMenuItem(title: "Editor", action: nil, keyEquivalent: "") 238 | let newMenu = NSMenu(title: "Editor") 239 | 240 | let undoItem = NSMenuItem(title: "Undo", action: #selector(EditViewController.undoButtonClicked(sender:)), keyEquivalent: "") 241 | undoItem.keyEquivalent = "z" 242 | undoItem.keyEquivalentModifierMask = .command 243 | 244 | let redoItem = NSMenuItem(title: "Redo", action: #selector(EditViewController.redoButtonClicked(sender:)), keyEquivalent: "") 245 | redoItem.keyEquivalent = "z" 246 | redoItem.keyEquivalentModifierMask = [.command, .shift] 247 | 248 | let eraserItem = NSMenuItem(title: "Eraser", action: #selector(EditViewController.eraserButtonClicked(sender:)), keyEquivalent: "") 249 | let eyedropperItem = NSMenuItem(title: "Eyedropper", action: #selector(EditViewController.eyedropperButtonClicked(sender:)), keyEquivalent: "") 250 | 251 | let closeItem = NSMenuItem(title: "Close", action: #selector(EditViewController.closeWindow), keyEquivalent: "") 252 | closeItem.keyEquivalent = "w" 253 | closeItem.keyEquivalentModifierMask = .command 254 | 255 | newMenu.addItem(undoItem) 256 | newMenu.addItem(redoItem) 257 | newMenu.addItem(NSMenuItem.separator()) 258 | newMenu.addItem(eraserItem) 259 | newMenu.addItem(eyedropperItem) 260 | newMenu.addItem(NSMenuItem.separator()) 261 | newMenu.addItem(closeItem) 262 | 263 | newItem.submenu = newMenu 264 | menu.insertItem(newItem, at: 2) 265 | } 266 | 267 | // Removes the editor menu item 268 | func removeEditorMenu() { 269 | guard let menu = NSApplication.shared().mainMenu else { return } 270 | if let item = menu.item(withTitle: "Editor") { 271 | menu.removeItem(item) 272 | } 273 | } 274 | 275 | // Closes the window 276 | func closeWindow() { 277 | self.view.window?.close() 278 | } 279 | 280 | // Updates the frame counter label 281 | func updateFrameLabel() { 282 | self.frameNumberLabel.stringValue = "\(currentFrameNumber+1)/\(frames.count)" 283 | 284 | self.previousFrameButton.isHidden = false 285 | self.nextFrameButton.isHidden = false 286 | self.frameNumberLabel.isHidden = false 287 | } 288 | 289 | // Shows a given GIFFrame 290 | func showFrame(frame: GIFFrame) { 291 | guard let image = frame.image else { return } 292 | 293 | self.currentFrameImageView.resetUndoRedo() 294 | 295 | let maxWidth = imageBackgroundView.bounds.width 296 | let maxHeight = imageBackgroundView.bounds.height 297 | let width = image.size.width 298 | let height = image.size.height 299 | 300 | let tmp = calculateAspectRatioFit(srcWidth: width, srcHeight: height, maxWidth: maxWidth, maxHeight: maxHeight) 301 | 302 | self.currentFrameImageView.frame = NSRect(x: 0, y: 0, width: tmp.width, height: tmp.height) 303 | self.currentFrameImageView.image = image 304 | self.currentFrameImageView.isHidden = false 305 | 306 | NotificationCenter.default.post(name: PixelImageView.imageChangedNotificationName, object: nil) 307 | 308 | handleCenterImage() 309 | } 310 | 311 | // Updates the scrollview contentView size, if necessary 312 | func updateScrollViewSize() { 313 | let imgWidth = self.currentFrameImageView.frame.width 314 | let imgHeight = self.currentFrameImageView.frame.height 315 | 316 | var newSize = NSMakeSize(imageScrollView.frame.width, imageScrollView.frame.height) 317 | 318 | if imgHeight > newSize.height { 319 | newSize.height = imgHeight+20 320 | } 321 | 322 | if imgWidth > newSize.width { 323 | newSize.width = imgWidth+20 324 | } 325 | 326 | self.imageBackgroundView.setFrameSize(newSize) 327 | } 328 | 329 | // Centers the image and redo the zoom 330 | func handleCenterImage() { 331 | if self.imageBackgroundView.previousZoomSize != nil { 332 | self.imageBackgroundView.redoZoom() 333 | } 334 | else { 335 | self.currentFrameImageView.center(inView: imageBackgroundView) 336 | } 337 | } 338 | 339 | // Called when the user changes the background color of the image 340 | func imageBackgroundColorUpdated() { 341 | self.currentFrameImageView.backgroundColor = DrawingOptionsHandler.shared.imageBackgroundColor 342 | } 343 | 344 | // Textfield changed 345 | override func controlTextDidChange(_ obj: Notification) { 346 | if let field = obj.object as? NSTextField { 347 | if field == self.brushSizeField { // Set brush size 348 | if let size = Int(self.brushSizeField.stringValue) { 349 | DrawingOptionsHandler.shared.brushSize = size 350 | } 351 | } 352 | } 353 | } 354 | 355 | // MARK: Helpers 356 | func allowColorPanelAlpha() { 357 | if NSColorPanel.sharedColorPanelExists() { 358 | let panel = NSColorPanel.shared() 359 | panel.showsAlpha = true 360 | } 361 | } 362 | 363 | // Based on http://stackoverflow.com/a/14731922 364 | func calculateAspectRatioFit(srcWidth: CGFloat, srcHeight: CGFloat, maxWidth: CGFloat, maxHeight: CGFloat) -> (width: CGFloat, height: CGFloat) { 365 | if srcWidth < maxWidth && srcHeight < maxHeight { 366 | return (width: srcWidth, height: srcHeight) 367 | } 368 | 369 | let ratio = min(maxWidth/srcWidth, maxHeight/srcHeight) 370 | return (width: srcWidth*ratio, height: srcHeight*ratio) 371 | } 372 | 373 | // Setter for frames 374 | func setFrames(frames: [GIFFrame]) { 375 | self.frames = frames 376 | self.currentFrameNumber = 0 377 | 378 | self.updateFrameLabel() 379 | 380 | self.showFrame(frame: self.frames[self.currentFrameNumber]) 381 | } 382 | } 383 | 384 | -------------------------------------------------------------------------------- /Code/GIF/FancyAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyAlert.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 17/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class FancyAlert: NSAlert { 12 | override func awakeFromNib() { 13 | if let contentView = self.window.contentView { 14 | self.window.contentView?.backgroundColor = Constants.darkBackgroundColor 15 | 16 | // Modify or find subviews to be changed 17 | contentView.subviews.forEach({ (subview) in 18 | if subview is NSTextField { 19 | (subview as! NSTextField).textColor = NSColor.white 20 | } 21 | }) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Code/GIF/FancyButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyButton.swift 3 | // ImageFun 4 | // 5 | // Created by Christian Lundtofte on 15/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class FancyButton: NSButton { 12 | let textColor = NSColor.white 13 | 14 | override func draw(_ dirtyRect: NSRect) { 15 | super.draw(dirtyRect) 16 | } 17 | 18 | override func awakeFromNib() { 19 | let style = NSMutableParagraphStyle() 20 | style.alignment = .center 21 | let attrs = [NSForegroundColorAttributeName: self.textColor, NSParagraphStyleAttributeName: style] 22 | let attrString = NSAttributedString(string: self.title, attributes: attrs) 23 | self.attributedTitle = attrString 24 | 25 | self.focusRingType = .none 26 | 27 | // Mouse in / out 28 | let area = NSTrackingArea.init(rect: self.bounds, options: [NSTrackingAreaOptions.mouseEnteredAndExited, NSTrackingAreaOptions.activeAlways], owner: self, userInfo: nil) 29 | self.addTrackingArea(area) 30 | } 31 | 32 | override func mouseEntered(with event: NSEvent) { 33 | if let cell = self.cell as? FancyButtonCell { 34 | cell.mouseOver = true 35 | cell.redraw() 36 | } 37 | } 38 | 39 | override func mouseExited(with event: NSEvent) { 40 | if let cell = self.cell as? FancyButtonCell { 41 | cell.mouseOver = false 42 | cell.redraw() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Code/GIF/FancyButtonCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyButton.swift 3 | // ImageFun 4 | // 5 | // Created by Christian Lundtofte on 15/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class FancyButtonCell: NSButtonCell { 12 | fileprivate var intLineColor:NSColor = NSColor(red: 0.0, green: 139.0/255.0, blue: 1.0, alpha: 1.0) 13 | fileprivate var intPushLineColor:NSColor = NSColor(red: 0.0, green: 194.0/255.0, blue: 1.0, alpha: 1.0) 14 | var mouseOver:Bool = false 15 | var showBackground:Bool = false 16 | 17 | @IBInspectable var lineColor: NSColor { 18 | get { 19 | return intLineColor 20 | } 21 | set { 22 | intLineColor = newValue 23 | } 24 | } 25 | 26 | @IBInspectable var pushedLineColor: NSColor { 27 | get { 28 | return intPushLineColor 29 | } 30 | set { 31 | intPushLineColor = newValue 32 | } 33 | } 34 | 35 | override func awakeFromNib() { 36 | self.setButtonType(.momentaryChange) 37 | } 38 | 39 | func redraw() { 40 | self.backgroundColor = self.backgroundColor // lol 41 | } 42 | 43 | override func drawBezel(withFrame frame: NSRect, in controlView: NSView) { 44 | 45 | let path = NSBezierPath(roundedRect: frame.insetBy(dx: 0.5, dy: 0.5), xRadius: 3, yRadius: 3) 46 | path.lineWidth = 2 47 | 48 | if !self.mouseOver && !self.showBackground { // Normal or selected 49 | if self.isHighlighted { 50 | intPushLineColor.setStroke() 51 | } 52 | else { 53 | intLineColor.setStroke() 54 | } 55 | 56 | path.stroke() 57 | } 58 | else { // Mouse over 59 | intLineColor.setFill() 60 | intLineColor.setStroke() 61 | path.fill() 62 | path.stroke() 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Code/GIF/FancyTextFieldCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FancyTextFieldCell.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 15/08/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | // From https://stackoverflow.com/a/40065608 12 | class FancyTextFieldCell: NSTextFieldCell { 13 | @IBInspectable var borderColor: NSColor = .clear 14 | @IBInspectable var cornerRadius: CGFloat = 3 15 | 16 | override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { 17 | 18 | let bounds = NSBezierPath(roundedRect: cellFrame, xRadius: cornerRadius, yRadius: cornerRadius) 19 | bounds.addClip() 20 | 21 | super.draw(withFrame: cellFrame, in: controlView) 22 | 23 | if borderColor != .clear { 24 | bounds.lineWidth = 2 25 | borderColor.setStroke() 26 | bounds.stroke() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Code/GIF/FrameCollectionViewItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameCollectionView.swift 3 | // GIF 4 | // 5 | // Created by Christian Lundtofte on 15/03/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol FrameCollectionViewItemDelegate { 12 | func removeFrame(item: FrameCollectionViewItem) 13 | func editFrame(item: FrameCollectionViewItem) 14 | 15 | func frameImageChanged(item: FrameCollectionViewItem) 16 | func frameImageClicked(item: FrameCollectionViewItem) 17 | func frameDurationChanged(item: FrameCollectionViewItem) 18 | } 19 | 20 | class FrameCollectionViewItem: NSCollectionViewItem, DragNotificationImageViewDelegate { 21 | var itemIndex = -1 22 | var delegate:FrameCollectionViewItemDelegate? 23 | 24 | @IBOutlet var durationTextField:SmartTextField! 25 | 26 | // MARK: NSCollectionViewItem init 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | view.wantsLayer = true 31 | 32 | view.layer?.backgroundColor = NSColor.clear.cgColor 33 | view.layer?.cornerRadius = 10 34 | view.layer?.borderColor = NSColor.selectedControlColor.cgColor 35 | 36 | self.durationTextField.stringValue = String(format: "%.3lf", GIFHandler.defaultFrameDuration) 37 | 38 | if let imgView = self.imageView as? DragNotificationImageView { 39 | imgView.delegate = self 40 | } 41 | } 42 | 43 | override func viewWillAppear() { 44 | super.viewWillAppear() 45 | 46 | // Set duration if known 47 | if let imgView = self.imageView as? DragNotificationImageView, 48 | let frame = imgView.gifFrame { 49 | self.durationTextField.stringValue = String(format: "%.3lf", frame.duration) 50 | } 51 | } 52 | 53 | func setHighlight(selected: Bool) { 54 | if selected { 55 | view.layer?.borderWidth = 4.0 56 | } 57 | else { 58 | view.layer?.borderWidth = 0.0 59 | } 60 | } 61 | 62 | // MARK: UI 63 | override func controlTextDidChange(_ obj: Notification) { 64 | if let field = obj.object as? NSTextField { 65 | if field == self.durationTextField { 66 | if var val = Double(self.durationTextField.stringValue) { 67 | if val < GIFHandler.minFrameDuration { 68 | val = GIFHandler.minFrameDuration 69 | } 70 | 71 | self.delegate?.frameDurationChanged(item: self) 72 | } 73 | else { 74 | self.durationTextField.stringValue = String(format: "%.3lf", GIFHandler.defaultFrameDuration) 75 | } 76 | } 77 | } 78 | } 79 | 80 | override func controlTextDidEndEditing(_ obj: Notification) { 81 | if let field = obj.object as? NSTextField { 82 | if field == self.durationTextField { 83 | if var val = Double(self.durationTextField.stringValue) { 84 | if val < GIFHandler.minFrameDuration { 85 | val = GIFHandler.minFrameDuration 86 | } 87 | 88 | self.delegate?.frameDurationChanged(item: self) 89 | self.durationTextField.stringValue = String(format: "%.3lf", val) 90 | } 91 | else { 92 | self.durationTextField.stringValue = String(format: "%.3lf", GIFHandler.defaultFrameDuration) 93 | } 94 | } 95 | } 96 | } 97 | 98 | // Sets the frame number 99 | func setFrameNumber(_ n: Int) { 100 | self.textField?.stringValue = "Frame "+String(n) 101 | } 102 | 103 | // Removes me 104 | @IBAction func removeMe(sender: AnyObject?) { 105 | self.delegate?.removeFrame(item: self) 106 | } 107 | 108 | // Edits me 109 | @IBAction func editMe(sender: AnyObject?) { 110 | self.delegate?.editFrame(item: self) 111 | } 112 | 113 | // MARK: Image handling 114 | // Sets an image 115 | func setImage(_ img: NSImage) { 116 | if let imgView = self.imageView as? DragNotificationImageView { 117 | imgView.image = img 118 | } 119 | } 120 | 121 | // Resets(removes) an image 122 | func resetImage() { 123 | if let imgView = self.imageView as? DragNotificationImageView { 124 | imgView.image = nil 125 | } 126 | } 127 | 128 | // MARK: DragNotificationImageViewDelegate 129 | // Image was dragged on to imageview 130 | func imageDragged(imageView: DragNotificationImageView) { 131 | self.delegate?.frameImageChanged(item: self) 132 | } 133 | 134 | // User clicked image view 135 | func imageClicked(imageView: DragNotificationImageView) { 136 | self.delegate?.frameImageClicked(item: self) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Code/GIF/FrameCollectionViewItem.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /Code/GIF/GIF.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Code/GIF/GIFFrame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GIFFrame.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 07/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | class GIFFrame { 13 | var image:NSImage? 14 | var duration:Double = GIFHandler.defaultFrameDuration 15 | 16 | static let emptyFrame:GIFFrame = GIFFrame() 17 | 18 | init(image: NSImage, duration: Double = GIFHandler.defaultFrameDuration) { 19 | self.image = image 20 | self.duration = duration 21 | } 22 | 23 | init() { } 24 | } 25 | -------------------------------------------------------------------------------- /Code/GIF/GIFHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GIFHandler.swift 3 | // GIF 4 | // 5 | // Created by Christian Lundtofte on 14/03/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AVFoundation 11 | import Cocoa 12 | 13 | // Replaces the old '(frames: [GIFFrame], loops:Int, secondsPrFrame: Float)' with a type 14 | // Describes the data necessary to show a gif. Frames, loops, and duration 15 | class GIFRepresentation { 16 | var frames:[GIFFrame] = [GIFFrame.emptyFrame] 17 | var loops:Int = GIFHandler.defaultLoops 18 | 19 | init(frames: [GIFFrame], loops:Int) { 20 | self.frames = frames 21 | self.loops = loops 22 | } 23 | 24 | init() {} 25 | } 26 | 27 | // Creates and loads gifs 28 | class GIFHandler { 29 | 30 | // MARK: Constants 31 | static let errorNotificationName = NSNotification.Name(rawValue: "GIFError") 32 | static let defaultLoops:Int = 0 33 | static let defaultFrameDuration:Double = 0.2 34 | static let minFrameDuration:Double = 0.06 35 | 36 | // MARK: Loading gifs 37 | static func loadGIF(with image: NSImage, onFinish: ((GIFRepresentation) -> ())) { 38 | 39 | // Attempt to fetch the number of frames, frame duration, and loop count from the .gif 40 | guard let bitmapRep = image.representations[0] as? NSBitmapImageRep, 41 | let frameCount = (bitmapRep.value(forProperty: NSImageFrameCount) as? NSNumber)?.intValue, 42 | let loopCount = (bitmapRep.value(forProperty: NSImageLoopCount) as? NSNumber)?.intValue else { 43 | 44 | NotificationCenter.default.post(name: errorNotificationName, object: self, userInfo: ["Error":"Could not load gif. The file does not contain the metadata required for a gif."]) 45 | onFinish(GIFRepresentation()) 46 | return 47 | } 48 | 49 | 50 | var retFrames:[GIFFrame] = [] 51 | 52 | // Iterate the frames, set the current frame on the bitmapRep and add this to 'retImages' 53 | for n in 0 ..< frameCount { 54 | bitmapRep.setProperty(NSImageCurrentFrame, withValue: NSNumber(value: n)) 55 | 56 | if let data = bitmapRep.representation(using: .GIF, properties: [:]), 57 | let img = NSImage(data: data) { 58 | let frame = GIFFrame(image: img) 59 | 60 | if let frameDuration = (bitmapRep.value(forProperty: NSImageCurrentFrameDuration) as? NSNumber)?.doubleValue { 61 | frame.duration = frameDuration 62 | } 63 | 64 | retFrames.append(frame) 65 | } 66 | } 67 | 68 | onFinish(GIFRepresentation(frames: retFrames, loops: loopCount)) 69 | } 70 | 71 | // MARK: Loading video files 72 | static func loadVideo(with path: URL, withFPS: Float64 = 4, onFinish: ((GIFRepresentation) -> ())) { 73 | let videoRepresentation = GIFRepresentation(frames: [], loops: 0) 74 | 75 | // Read video 76 | let asset = AVURLAsset(url: path) 77 | let generator = AVAssetImageGenerator(asset: asset) 78 | generator.requestedTimeToleranceAfter = kCMTimeZero 79 | generator.requestedTimeToleranceBefore = kCMTimeZero 80 | 81 | // Find frames and setup variables 82 | var curFrame:Float64 = 0 83 | let duration = CMTimeGetSeconds(asset.duration) 84 | let frames = duration * withFPS 85 | while curFrame < frames { // Find images 86 | let time = CMTimeMake(Int64(curFrame), Int32(withFPS)) 87 | var actualTime:CMTime = CMTime() 88 | 89 | do { 90 | let cgimg = try generator.copyCGImage(at: time, actualTime: &actualTime) 91 | let tmpImg = NSImage(cgImage: cgimg, size: NSSize(width: cgimg.width, height: cgimg.height)) 92 | 93 | // Remove representations, and add NSBitmapImageRep (Used for modifying when editing) 94 | let img = NSImage() 95 | img.addRepresentation(tmpImg.unscaledBitmapImageRep()) 96 | 97 | videoRepresentation.frames.append(GIFFrame(image: img, duration: (duration/withFPS)/100)) 98 | } 99 | catch { 100 | print("Exception during video load.") 101 | NotificationCenter.default.post(name: errorNotificationName, object: self, userInfo: ["Error":"Error loading frame in video."]) 102 | onFinish(videoRepresentation) 103 | return 104 | } 105 | 106 | curFrame += 1 107 | } 108 | 109 | onFinish(videoRepresentation) 110 | } 111 | 112 | 113 | // MARK: Making gifs from frames 114 | // Creates and saves a gif 115 | static func createAndSaveGIF(with frames: [GIFFrame], savePath: URL, loops: Int = GIFHandler.defaultLoops, watermark: Bool = true) { 116 | // Get and save data at 'savePath' 117 | let data = GIFHandler.createGIFData(with: frames, loops: loops, watermark: watermark) 118 | 119 | do { 120 | try data.write(to: savePath) 121 | } 122 | catch { 123 | NotificationCenter.default.post(name: errorNotificationName, object: self, userInfo: ["Error":"Could not save file: "+error.localizedDescription]) 124 | print("Error: \(error)") 125 | } 126 | } 127 | 128 | // Creates and returns an NSImage from given images 129 | static func createGIF(with frames: [GIFFrame], loops: Int = GIFHandler.defaultLoops, watermark: Bool = true) -> NSImage? { 130 | // Get data and convert to image 131 | let data = GIFHandler.createGIFData(with: frames, loops: loops, watermark: watermark) 132 | let img = NSImage(data: data) 133 | return img 134 | } 135 | 136 | // Creates NSData from given images 137 | static func createGIFData(with frames: [GIFFrame], loops: Int = GIFHandler.defaultLoops, watermark: Bool) -> Data { 138 | // Loop count 139 | let loopCountDic = NSDictionary(dictionary: [kCGImagePropertyGIFDictionary:NSDictionary(dictionary: [kCGImagePropertyGIFLoopCount: loops])]) 140 | 141 | // Number of frames 142 | let imageCount = frames.filter { (frame) -> Bool in 143 | return frame.image != nil 144 | }.count 145 | print("Num frames: \(imageCount)") 146 | 147 | // Destination (Data object) 148 | guard let dataObj = CFDataCreateMutable(nil, 0), 149 | let dst = CGImageDestinationCreateWithData(dataObj, kUTTypeGIF, imageCount, nil) else { fatalError("Can't create gif") } 150 | CGImageDestinationSetProperties(dst, loopCountDic as CFDictionary) // Set loop count on object 151 | 152 | // Add images to destination 153 | frames.forEach { (frame) in 154 | guard var image = frame.image else { print("No image!"); return } 155 | if watermark && !Products.store.isProductPurchased(Products.Pro) { 156 | // Watermark 157 | image = GIFHandler.addWatermark(image: image, watermark: "Smart GIF Maker") 158 | } 159 | 160 | if let imageRef = image.CGImage { 161 | // Frame duration 162 | let frameDurationDic = NSDictionary(dictionary: [kCGImagePropertyGIFDictionary:NSDictionary(dictionary: [kCGImagePropertyGIFDelayTime: frame.duration])]) 163 | 164 | // Add image 165 | CGImageDestinationAddImage(dst, imageRef, frameDurationDic as CFDictionary) 166 | } 167 | else { 168 | print("Error getting cgImage") 169 | } 170 | } 171 | 172 | 173 | // Close, cast as data and return 174 | let success = CGImageDestinationFinalize(dst) 175 | if !success { 176 | print("Failed finalizing the gif!") 177 | } 178 | 179 | let retData = dataObj as Data 180 | return retData 181 | } 182 | 183 | 184 | // MARK: Helper functions for gifs 185 | // Naive method for determining whether something is an animated gif 186 | static func isAnimatedGIF(_ image: NSImage) -> Bool { 187 | // Attempt to fetch the number of frames, frame duration, and loop count from the .gif 188 | guard let bitmapRep = image.representations[0] as? NSBitmapImageRep, 189 | let frameCount = (bitmapRep.value(forProperty: NSImageFrameCount) as? NSNumber)?.intValue, 190 | let _ = (bitmapRep.value(forProperty: NSImageLoopCount) as? NSNumber)?.intValue, 191 | let _ = (bitmapRep.value(forProperty: NSImageCurrentFrameDuration) as? NSNumber)?.floatValue else { 192 | return false 193 | } 194 | 195 | return frameCount > 1 // We have loops, duration and everything, and there's more than 1 frame, it's probably a gif 196 | } 197 | 198 | 199 | // Adds a watermark to an image 200 | static func addWatermark(image: NSImage, watermark: String) -> NSImage { 201 | guard let font = NSFont(name: "Helvetica", size: 14) else { return image } 202 | 203 | let attrs:[String:Any] = [NSForegroundColorAttributeName: NSColor.white, NSFontAttributeName: font, NSStrokeWidthAttributeName: -3, NSStrokeColorAttributeName: NSColor.black] 204 | 205 | // We need to create a 'copy' of the imagerep, as we need 'isPlanar' to be false in order to draw on it 206 | // Thanks http://stackoverflow.com/a/13617013 and https://gist.github.com/randomsequence/b9f4462b005d0ced9a6c 207 | let tmpRep = NSBitmapImageRep(data: image.tiffRepresentation!)! 208 | guard let imgRep = NSBitmapImageRep(bitmapDataPlanes: nil, 209 | pixelsWide: tmpRep.pixelsWide, 210 | pixelsHigh: tmpRep.pixelsHigh, 211 | bitsPerSample: 8, 212 | samplesPerPixel: 4, 213 | hasAlpha: true, 214 | isPlanar: false, 215 | colorSpaceName: NSCalibratedRGBColorSpace, 216 | bytesPerRow: 0, 217 | bitsPerPixel: 0) else { print("Error image"); return image } 218 | 219 | NSGraphicsContext.saveGraphicsState() 220 | NSGraphicsContext.setCurrent(NSGraphicsContext.init(bitmapImageRep: imgRep)) 221 | 222 | // Draw image and string 223 | image.draw(at: NSPoint.zero, from: NSZeroRect, operation: .copy, fraction: 1.0) 224 | watermark.draw(at: NSPoint(x: 5, y: 5), withAttributes: attrs) 225 | 226 | NSGraphicsContext.restoreGraphicsState() 227 | 228 | let data = imgRep.representation(using: .GIF, properties: [:]) 229 | if let newImg = NSImage(data: data!) { 230 | return newImg 231 | } 232 | 233 | return image 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Code/GIF/IAPHelper.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Razeware LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | /* 24 | https://www.raywenderlich.com/122144/in-app-purchase-tutorial 25 | */ 26 | 27 | import StoreKit 28 | 29 | public typealias ProductIdentifier = String 30 | public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> () 31 | 32 | open class IAPHelper : NSObject { 33 | 34 | fileprivate let productIdentifiers: Set 35 | fileprivate var purchasedProductIdentifiers = Set() 36 | 37 | fileprivate var productsRequest: SKProductsRequest? 38 | fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler? 39 | 40 | static let IAPLoadedNotificationName = NSNotification.Name(rawValue: "PurchasesReloaded") 41 | static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification" 42 | static let IAPPurchaseFailed = "IAPPurchaseFailed" 43 | 44 | public init(productIds: Set) { 45 | self.productIdentifiers = productIds 46 | 47 | for productIdentifier in productIds { 48 | let purchased = UserDefaults.standard.bool(forKey: productIdentifier) 49 | if purchased { 50 | purchasedProductIdentifiers.insert(productIdentifier) 51 | print("Previously purchased: \(productIdentifier)") 52 | } else { 53 | print("Not purchased: \(productIdentifier)") 54 | } 55 | } 56 | 57 | super.init() 58 | 59 | SKPaymentQueue.default().add(self) 60 | } 61 | } 62 | 63 | // MARK: - StoreKit API 64 | 65 | extension IAPHelper { 66 | 67 | public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) { 68 | productsRequest?.cancel() 69 | productsRequestCompletionHandler = completionHandler 70 | 71 | productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) 72 | productsRequest!.delegate = self 73 | productsRequest!.start() 74 | } 75 | 76 | public func buyProduct(_ product: SKProduct) { 77 | print("Buying \(product.productIdentifier)...") 78 | let payment = SKPayment(product: product) 79 | SKPaymentQueue.default().add(payment) 80 | } 81 | 82 | public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool { 83 | return purchasedProductIdentifiers.contains(productIdentifier) 84 | } 85 | 86 | public class func canMakePayments() -> Bool { 87 | return SKPaymentQueue.canMakePayments() 88 | } 89 | 90 | public func restorePurchases() { 91 | print("Forsøger at restore!") 92 | SKPaymentQueue.default().restoreCompletedTransactions() 93 | } 94 | } 95 | 96 | // MARK: - SKProductsRequestDelegate 97 | 98 | extension IAPHelper: SKProductsRequestDelegate { 99 | public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { 100 | print("Loaded list of products...") 101 | let products = response.products 102 | productsRequestCompletionHandler?(true, products) 103 | clearRequestAndHandler() 104 | 105 | for p in products { 106 | print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)") 107 | } 108 | 109 | NotificationCenter.default.post(name: IAPHelper.IAPLoadedNotificationName, object: nil) 110 | } 111 | 112 | public func request(_ request: SKRequest, didFailWithError error: Error) { 113 | print("Failed to load list of products.") 114 | print("Error: \(error.localizedDescription)") 115 | productsRequestCompletionHandler?(false, nil) 116 | clearRequestAndHandler() 117 | } 118 | 119 | fileprivate func clearRequestAndHandler() { 120 | productsRequest = nil 121 | productsRequestCompletionHandler = nil 122 | } 123 | } 124 | 125 | // MARK: - SKPaymentTransactionObserver 126 | 127 | extension IAPHelper: SKPaymentTransactionObserver { 128 | 129 | public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { 130 | print("Transaction 1") 131 | for transaction in transactions { 132 | switch (transaction.transactionState) { 133 | case .purchased: 134 | completeTransaction(transaction) 135 | break 136 | case .failed: 137 | failedTransaction(transaction) 138 | break 139 | case .restored: 140 | restoreTransaction(transaction) 141 | break 142 | case .deferred: 143 | break 144 | case .purchasing: 145 | break 146 | } 147 | } 148 | } 149 | 150 | public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { 151 | NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelper.IAPPurchaseFailed), object: nil) 152 | } 153 | 154 | fileprivate func completeTransaction(_ transaction: SKPaymentTransaction) { 155 | print("completeTransaction...") 156 | deliverPurchaseNotificatioForIdentifier(transaction.payment.productIdentifier) 157 | SKPaymentQueue.default().finishTransaction(transaction) 158 | } 159 | 160 | fileprivate func restoreTransaction(_ transaction: SKPaymentTransaction) { 161 | print("Transaction 2") 162 | guard let productIdentifier = transaction.original?.payment.productIdentifier else { return } 163 | 164 | print("restoreTransaction... \(productIdentifier)") 165 | deliverPurchaseNotificatioForIdentifier(productIdentifier) 166 | SKPaymentQueue.default().finishTransaction(transaction) 167 | } 168 | 169 | fileprivate func failedTransaction(_ transaction: SKPaymentTransaction) { 170 | print("failedTransaction...") 171 | 172 | 173 | /*if transaction.error!.code != SKError.paymentCancelled.rawValue { 174 | print("Transaction Error: \(transaction.error?.localizedDescription)") 175 | }*/ 176 | NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelper.IAPPurchaseFailed), object: nil) 177 | SKPaymentQueue.default().finishTransaction(transaction) 178 | } 179 | 180 | fileprivate func deliverPurchaseNotificatioForIdentifier(_ identifier: String?) { 181 | print("Transaction 3") 182 | guard let identifier = identifier else { return } 183 | 184 | purchasedProductIdentifiers.insert(identifier) 185 | UserDefaults.standard.set(true, forKey: identifier) 186 | UserDefaults.standard.synchronize() 187 | NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Code/GIF/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | gif 13 | 14 | CFBundleTypeMIMETypes 15 | 16 | image/gif 17 | 18 | CFBundleTypeName 19 | GIF 20 | CFBundleTypeRole 21 | Editor 22 | LSItemContentTypes 23 | 24 | com.compuserve.gif 25 | 26 | NSDocumentClass 27 | $(PRODUCT_MODULE_NAME).Document 28 | 29 | 30 | CFBundleExecutable 31 | $(EXECUTABLE_NAME) 32 | CFBundleIconFile 33 | 34 | CFBundleIdentifier 35 | $(PRODUCT_BUNDLE_IDENTIFIER) 36 | CFBundleInfoDictionaryVersion 37 | 6.0 38 | CFBundleName 39 | $(PRODUCT_NAME) 40 | CFBundlePackageType 41 | APPL 42 | CFBundleShortVersionString 43 | 2.1.2 44 | CFBundleVersion 45 | 10 46 | LSApplicationCategoryType 47 | public.app-category.graphics-design 48 | LSMinimumSystemVersion 49 | $(MACOSX_DEPLOYMENT_TARGET) 50 | NSHumanReadableCopyright 51 | Copyright © 2017 Christian Lundtofte. All rights reserved. 52 | NSMainStoryboardFile 53 | Main 54 | NSPrincipalClass 55 | NSApplication 56 | 57 | 58 | -------------------------------------------------------------------------------- /Code/GIF/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 16/08/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class LoadingView: NSView { 12 | 13 | @IBOutlet var imageView:NSImageView! 14 | 15 | override func draw(_ dirtyRect: NSRect) { 16 | 17 | // Image 18 | let img = NSImage(named: "loading.gif") 19 | self.imageView.canDrawSubviewsIntoLayer = true 20 | self.imageView.animates = true 21 | self.imageView.image = img 22 | 23 | self.wantsLayer = true 24 | 25 | super.draw(dirtyRect) // Draw 26 | 27 | // Background 28 | // self.alphaValue = 0.7 29 | // let col:CGFloat = 85.0/255.0 30 | Constants.darkBackgroundColor.set() 31 | NSRectFill(self.bounds) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Code/GIF/MainViewController+FrameCollectionViewItemDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController+FrameCollectionViewItemDelegate.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 17/08/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension MainViewController: FrameCollectionViewItemDelegate { 13 | 14 | // MARK: FrameCollectionViewItemDelegate 15 | func removeFrame(item: FrameCollectionViewItem) { 16 | 17 | // If there's one frame, reset it 18 | if currentFrames.count == 1 { 19 | currentFrames[0] = GIFFrame.emptyFrame 20 | 21 | } 22 | else { 23 | // Remove the index and reload everything 24 | let index = item.itemIndex 25 | currentFrames.remove(at: index) 26 | } 27 | 28 | deselectAll() 29 | imageCollectionView.reloadData() 30 | } 31 | 32 | func editFrame(item: FrameCollectionViewItem) { 33 | let index = item.itemIndex 34 | showEditing(withIndex: index) 35 | } 36 | 37 | // An image was dragged onto the DragNotificationImageView 38 | func frameImageChanged(item: FrameCollectionViewItem) { 39 | guard let imgView = item.imageView as? DragNotificationImageView else { return } 40 | guard let img = imgView.image else { return } 41 | 42 | if GIFHandler.isAnimatedGIF(img) { // Dragged GIF 43 | // Import? 44 | let alert = self.createAskImportAlert() 45 | 46 | alert.beginSheetModal(for: self.view.window!, completionHandler: { (resp) in 47 | if resp == NSAlertFirstButtonReturn { // Replace 48 | self.loadAndSetGIF(image: img) 49 | } 50 | else { // TODO: Load first image, I guess 51 | } 52 | }) 53 | } 54 | else { // Dragged regular image 55 | let newFrame = GIFFrame(image: img) 56 | if let frame = imgView.gifFrame { 57 | newFrame.duration = frame.duration 58 | } 59 | 60 | currentFrames[item.itemIndex] = newFrame 61 | } 62 | 63 | self.selectedRow = nil 64 | self.imageCollectionView.reloadData() 65 | } 66 | 67 | // User clicked DragNotificationImageView 68 | func frameImageClicked(item: FrameCollectionViewItem) { 69 | guard let imgView = item.imageView as? DragNotificationImageView else { return } 70 | 71 | // Show panel 72 | let panel = NSOpenPanel() 73 | panel.allowsMultipleSelection = false 74 | panel.canChooseDirectories = false 75 | panel.canChooseFiles = true 76 | panel.allowedFileTypes = ["png", "jpg", "jpeg", "gif", "tiff", "bmp"] 77 | panel.beginSheetModal(for: self.view.window!) { (response) -> Void in 78 | 79 | imgView.resignFirstResponder() 80 | self.addFrameButton.becomeFirstResponder() 81 | 82 | if response != NSFileHandlingPanelOKButton { 83 | return 84 | } 85 | 86 | // Insert image into imageview and 'currentImages' and reload 87 | guard let URL = panel.url else { return } 88 | guard let image = NSImage(contentsOf: URL) else { self.showError("Could not load image."); return } 89 | 90 | if GIFHandler.isAnimatedGIF(image) { // Gif 91 | // Import? 92 | let alert = self.createAskImportAlert() 93 | 94 | alert.beginSheetModal(for: self.view.window!, completionHandler: { (resp) in 95 | if resp == NSAlertFirstButtonReturn { // Replace 96 | self.importGIF(from: URL) 97 | } 98 | else { // TODO: Load first image, I guess 99 | } 100 | }) 101 | 102 | } 103 | else { // Single image 104 | let newFrame = GIFFrame(image: image) 105 | if let frame = imgView.gifFrame { 106 | newFrame.duration = frame.duration 107 | } 108 | 109 | self.currentFrames[item.itemIndex] = newFrame 110 | } 111 | 112 | self.imageCollectionView.reloadData() 113 | } 114 | } 115 | 116 | // Frame duration changed 117 | func frameDurationChanged(item: FrameCollectionViewItem) { 118 | guard let imgView = item.imageView as? DragNotificationImageView, 119 | let frame = imgView.gifFrame, 120 | let newDuration = Double(item.durationTextField.stringValue) else { return } 121 | frame.duration = newDuration 122 | } 123 | 124 | 125 | // MARK: Helpers 126 | func createAskImportAlert() -> FancyAlert { 127 | let alert = FancyAlert() 128 | alert.messageText = "GIF Found" 129 | alert.informativeText = "Do you want to import it and replace all frames with the contents of this GIF?" 130 | 131 | alert.addButton(withTitle: "Yes") 132 | alert.addButton(withTitle: "No") 133 | 134 | return alert 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Code/GIF/MainViewController+NSCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController+NSCollectionView.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 25/06/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | 13 | extension MainViewController: NSCollectionViewDelegate, NSCollectionViewDataSource { 14 | 15 | 16 | // MARK: NSCollectionView 17 | // Sets up the collection view variables (Could probably be done in IB), and allows drag'n'drop 18 | // https://www.raywenderlich.com/145978/nscollectionview-tutorial 19 | // https://www.raywenderlich.com/132268/advanced-collection-views-os-x-tutorial 20 | func configureCollectionView() { 21 | // Layout 22 | // MARK: Size of cells here! 23 | let flowLayout = NSCollectionViewFlowLayout() 24 | flowLayout.itemSize = NSSize(width: 200.0, height: 240.0) 25 | flowLayout.sectionInset = EdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) 26 | flowLayout.minimumInteritemSpacing = 20.0 27 | flowLayout.minimumLineSpacing = 20.0 28 | imageCollectionView.collectionViewLayout = flowLayout 29 | 30 | view.wantsLayer = true 31 | 32 | // Drag 33 | var dragTypes = NSImage.imageTypes() 34 | dragTypes.append(NSURLPboardType) 35 | imageCollectionView.register(forDraggedTypes: dragTypes) 36 | imageCollectionView.setDraggingSourceOperationMask(NSDragOperation.every, forLocal: true) 37 | imageCollectionView.setDraggingSourceOperationMask(NSDragOperation.every, forLocal: false) 38 | } 39 | 40 | // Deselects all items 41 | func deselectAll() { 42 | let paths = imageCollectionView.indexPathsForVisibleItems() 43 | paths.forEach { (path) in 44 | if let item = imageCollectionView.item(at: path) as? FrameCollectionViewItem { 45 | item.setHighlight(selected: false) 46 | } 47 | } 48 | 49 | selectedRow = nil 50 | } 51 | 52 | // MARK: General delegate / datasource (num items and items themselves) 53 | 54 | // Creates item(frame) in collection view 55 | public func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { 56 | let item = collectionView.makeItem(withIdentifier: "FrameCollectionViewItem", for: indexPath) 57 | 58 | // Cast to FrameCollectionView, set index and reset image (To remove old index image) 59 | guard let frameCollectionViewItem = item as? FrameCollectionViewItem else { return item } 60 | 61 | frameCollectionViewItem.delegate = self 62 | frameCollectionViewItem.setFrameNumber(indexPath.item+1) 63 | frameCollectionViewItem.itemIndex = indexPath.item 64 | frameCollectionViewItem.resetImage() // Remove current image 65 | frameCollectionViewItem.setHighlight(selected: false) 66 | 67 | if selectedRow != nil && selectedRow!.item == indexPath.item { 68 | frameCollectionViewItem.setHighlight(selected: true) 69 | } 70 | 71 | let frame = currentFrames[indexPath.item] 72 | 73 | // Set GIFFrame 74 | if let imgView = frameCollectionViewItem.imageView as? DragNotificationImageView { 75 | imgView.gifFrame = frame 76 | frameCollectionViewItem.durationTextField.stringValue = String(format: "%.3lf", frame.duration) 77 | } 78 | 79 | // If we have an image, insert it here 80 | if let img = frame.image { 81 | frameCollectionViewItem.setImage(img) 82 | } 83 | 84 | return item 85 | } 86 | 87 | // Number of items in section (Number of frames) 88 | public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { 89 | return currentFrames.count 90 | } 91 | 92 | 93 | // Selection of items 94 | func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { 95 | deselectAll() 96 | 97 | for indexPath in indexPaths { 98 | guard let item = collectionView.item(at: indexPath) as? FrameCollectionViewItem else {continue} 99 | 100 | selectedRow = indexPath 101 | item.setHighlight(selected: true) 102 | 103 | break 104 | } 105 | } 106 | 107 | func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { 108 | selectedRow = nil 109 | deselectAll() 110 | } 111 | 112 | 113 | // MARK: Drag and drop 114 | private func collectionView(collectionView: NSCollectionView, canDragItemsAtIndexes indexes: NSIndexSet, withEvent event: NSEvent) -> Bool { 115 | return true 116 | } 117 | 118 | // Add the image from the cell to the drag'n'drop handler 119 | func collectionView(_ collectionView: NSCollectionView, pasteboardWriterForItemAt indexPath: IndexPath) -> NSPasteboardWriting? { 120 | guard let item = collectionView.item(at: indexPath) as? FrameCollectionViewItem, 121 | let imgView = item.imageView, 122 | let img = imgView.image else { 123 | return nil 124 | } 125 | 126 | return img 127 | } 128 | 129 | 130 | // When dragging starts 131 | func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forItemsAt indexPaths: Set) { 132 | indexPathsOfItemsBeingDragged = indexPaths 133 | } 134 | 135 | // Dragging ends, reset drag variables 136 | func collectionView(_ collectionView: NSCollectionView, draggingSession session: NSDraggingSession, endedAt screenPoint: NSPoint, dragOperation operation: NSDragOperation) { 137 | indexPathsOfItemsBeingDragged = nil 138 | } 139 | 140 | // Is the drag allowed (And if so, what type of event is needed?) 141 | func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer, dropOperation proposedDropOperation: UnsafeMutablePointer) -> NSDragOperation { 142 | 143 | if proposedDropOperation.pointee == NSCollectionViewDropOperation.on { 144 | proposedDropOperation.pointee = NSCollectionViewDropOperation.before 145 | } 146 | 147 | if indexPathsOfItemsBeingDragged == nil { 148 | return NSDragOperation.copy 149 | } else { 150 | return NSDragOperation.move 151 | } 152 | } 153 | 154 | // On drag complete (Frames inserted or moved here) 155 | func collectionView(_ collectionView: NSCollectionView, acceptDrop draggingInfo: NSDraggingInfo, indexPath: IndexPath, dropOperation: NSCollectionViewDropOperation) -> Bool { 156 | // From outside (Finder, or whatever) 157 | if indexPathsOfItemsBeingDragged == nil { 158 | handleOutsideDrag(draggingInfo: draggingInfo, indexPath: indexPath) 159 | } 160 | else { // Moving frames inside the app 161 | handleInsideDrag(indexPath: indexPath) 162 | } 163 | 164 | imageCollectionView.reloadData() 165 | deselectAll() 166 | 167 | return true 168 | } 169 | 170 | 171 | // Inserts frames that were dragged from outside the app 172 | func handleOutsideDrag(draggingInfo: NSDraggingInfo, indexPath: IndexPath) { 173 | 174 | // Enumerate URLs and load images 175 | var droppedImages:[NSImage] = [] 176 | draggingInfo.enumerateDraggingItems(options: NSDraggingItemEnumerationOptions.concurrent, 177 | for: imageCollectionView, 178 | classes: [NSURL.self], 179 | searchOptions: [NSPasteboardURLReadingFileURLsOnlyKey : NSNumber(value: true)]) { (draggingItem, idx, stop) in 180 | if let url = draggingItem.item as? URL, 181 | let image = NSImage(contentsOf: url) { 182 | droppedImages.append(image) 183 | } 184 | } 185 | 186 | 187 | // Any gifs? 188 | let hasGifs = droppedImages.index { (img) -> Bool in 189 | return GIFHandler.isAnimatedGIF(img) 190 | } 191 | 192 | if let gifIndex = hasGifs { // A gif was dragged 193 | let alert = self.createAskImportAlert() 194 | alert.beginSheetModal(for: self.view.window!, completionHandler: { (resp) in 195 | if resp == NSAlertFirstButtonReturn { // Replace all with gif 196 | let gif = droppedImages[gifIndex] 197 | self.loadAndSetGIF(image: gif) 198 | } 199 | else { // No clicked. Remove gif, and insert frames 200 | droppedImages.remove(at: gifIndex) 201 | self.insertImages(images: droppedImages, at: indexPath) 202 | } 203 | }) 204 | } 205 | else { 206 | // Insert frames 207 | self.insertImages(images: droppedImages, at: indexPath) 208 | } 209 | 210 | } 211 | 212 | 213 | // MARK: Helpers 214 | // Inserts the given images 215 | func insertImages(images: [NSImage], at indexPath: IndexPath) { 216 | var frameAr:[GIFFrame] = [] 217 | images.forEach { (image) in 218 | let frame = GIFFrame(image: image) 219 | frameAr.append(frame) 220 | } 221 | 222 | if images.count == 0 { // No reason to change anything 223 | return 224 | } 225 | 226 | // One empty frame, remove this and insert new images 227 | if currentFrames.count == 1 && currentFrames[0].image == nil { 228 | currentFrames.removeAll() 229 | currentFrames = frameAr 230 | } 231 | else { // Append to frames already in view 232 | for n in 0 ..< frameAr.count { 233 | currentFrames.insert(frameAr[n], at: indexPath.item+n) 234 | } 235 | } 236 | 237 | self.selectedRow = nil 238 | self.imageCollectionView.reloadData() 239 | } 240 | 241 | // Moves frames that were dragged inside the app 242 | func handleInsideDrag(indexPath: IndexPath) { 243 | // From inside the collectionview 244 | let indexPathOfFirstItemBeingDragged = indexPathsOfItemsBeingDragged.first! 245 | var toIndexPath: IndexPath 246 | if indexPathOfFirstItemBeingDragged.compare(indexPath) == .orderedAscending { 247 | toIndexPath = IndexPath(item: indexPath.item-1, section: indexPath.section) 248 | } 249 | else { 250 | toIndexPath = IndexPath(item: indexPath.item, section: indexPath.section) 251 | } 252 | 253 | // The index we're moving, the image, and the destination 254 | let dragItem = indexPathOfFirstItemBeingDragged.item 255 | let curFrame = currentFrames[dragItem] 256 | let newItem = toIndexPath.item 257 | currentFrames.remove(at: dragItem) 258 | currentFrames.insert(curFrame, at: newItem) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Code/GIF/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // GIF 4 | // 5 | // Created by Christian Lundtofte on 14/03/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class MainViewController: NSViewController { 12 | // MARK: Fields 13 | 14 | // Constants 15 | 16 | // TODO: Create a delegate or something instead of this mess 17 | static let editingEndedNotificationName = NSNotification.Name(rawValue: "EditingEnded") 18 | static let loadedDocumentFramesNotificationName = NSNotification.Name(rawValue: "DocumentFrames") 19 | 20 | // UI elements 21 | @IBOutlet var imageCollectionView:NSCollectionView! 22 | @IBOutlet var addFrameButton:NSButton! 23 | @IBOutlet var loopsTextField:NSTextField! 24 | @IBOutlet var loadingView:LoadingView! 25 | 26 | // Fields used in UI handling 27 | var currentFrames:[GIFFrame] = [GIFFrame.emptyFrame] // Default is 1 empty image, to show something in UI 28 | var selectedRow:IndexPath? = nil // Needed for inserting and removing item 29 | var indexPathsOfItemsBeingDragged: Set! // Paths of items being dragged (If dragging inside the app) 30 | var editingWindowController:NSWindowController? 31 | 32 | // Preview variables 33 | var previewImagePath:URL? 34 | 35 | 36 | // MARK: View setup 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | // Close color panel if open. 41 | if NSColorPanel.sharedColorPanelExists() { 42 | let panel = NSColorPanel.shared() 43 | panel.close() 44 | } 45 | 46 | self.configureCollectionView() 47 | self.setupNotificationListeners() 48 | } 49 | 50 | override func viewDidAppear() { 51 | super.viewDidAppear() 52 | 53 | self.view.backgroundColor = Constants.darkBackgroundColor 54 | 55 | // Sets up window border 56 | self.view.window?.titlebarAppearsTransparent = true 57 | self.view.window?.isMovableByWindowBackground = true 58 | self.view.window?.titleVisibility = NSWindowTitleVisibility.hidden 59 | self.view.window?.backgroundColor = Constants.darkBackgroundColor 60 | 61 | self.imageCollectionView.backgroundView?.backgroundColor = Constants.darkBackgroundColor 62 | self.imageCollectionView.backgroundColor = Constants.darkBackgroundColor 63 | 64 | addFrameButton.becomeFirstResponder() 65 | 66 | // If menu items does not exist, create them 67 | if let menu = NSApplication.shared().menu { 68 | if menu.item(withTitle: "Actions") == nil { 69 | self.setupMenuItems() 70 | } 71 | } 72 | } 73 | 74 | override var representedObject: Any? { 75 | didSet { 76 | } 77 | } 78 | 79 | // View changing 80 | override func prepare(for segue: NSStoryboardSegue, sender: Any?) { 81 | if segue.identifier == "ShowPreview" { 82 | if let viewController = segue.destinationController as? PreviewViewController, 83 | let preview = self.previewImagePath { 84 | viewController.previewImagePath = preview 85 | } 86 | } 87 | } 88 | 89 | 90 | // MARK: UI 91 | override func controlTextDidChange(_ obj: Notification) { 92 | if let field = obj.object as? NSTextField { 93 | if field == loopsTextField { 94 | if let _ = Int(loopsTextField.stringValue) { } 95 | else { 96 | showError("Loop count must be an integer!") 97 | loopsTextField.stringValue = "0" 98 | } 99 | } 100 | } 101 | 102 | } 103 | 104 | func reloadImages() { 105 | imageCollectionView.reloadData() 106 | } 107 | 108 | // Creates menu item 109 | func setupMenuItems() { 110 | guard let menu = NSApplication.shared().mainMenu else { return } 111 | let newItem = NSMenuItem(title: "Actions", action: nil, keyEquivalent: "") 112 | let newMenu = NSMenu(title: "Actions") 113 | 114 | /* 115 | Import .GIF (CMD+O) 116 | Export .GIF (CMD+S) 117 | - 118 | Add frame (CMD+F) 119 | Reverse frames 120 | Set all frame durations 121 | - 122 | Preview (CMD+P) 123 | Edit (CMD+E) 124 | Reset (CMD+R) 125 | */ 126 | 127 | let importItem = NSMenuItem(title: "Import", action: #selector(MainViewController.importButtonClicked(sender:)), keyEquivalent: "") 128 | importItem.keyEquivalentModifierMask = .command 129 | importItem.keyEquivalent = "o" 130 | 131 | let exportItem = NSMenuItem(title: "Export .GIF", action: #selector(MainViewController.exportGIFButtonClicked(sender:)), keyEquivalent: "") 132 | exportItem.keyEquivalentModifierMask = .command 133 | exportItem.keyEquivalent = "s" 134 | 135 | let addFrameItem = NSMenuItem(title: "Add frame", action: #selector(MainViewController.addFrameButtonClicked(sender:)), keyEquivalent: "") 136 | addFrameItem.keyEquivalent = "f" 137 | addFrameItem.keyEquivalentModifierMask = .command 138 | 139 | let reverseItem = NSMenuItem(title: "Reverse frames", action: #selector(MainViewController.reverseFrames), keyEquivalent: "") 140 | let changeDuration = NSMenuItem(title: "Set all frame durations", action: #selector(MainViewController.setAllFrameDurations), keyEquivalent: "") 141 | 142 | let previewItem = NSMenuItem(title: "Preview", action: #selector(MainViewController.previewButtonClicked(sender:)), keyEquivalent: "") 143 | previewItem.keyEquivalentModifierMask = .command 144 | previewItem.keyEquivalent = "p" 145 | 146 | let editItem = NSMenuItem(title: "Edit", action: #selector(MainViewController.editButtonClicked(sender:)), keyEquivalent: "") 147 | editItem.keyEquivalentModifierMask = .command 148 | editItem.keyEquivalent = "e" 149 | 150 | let resetItem = NSMenuItem(title: "Reset", action: #selector(MainViewController.resetButtonClicked(sender:)), keyEquivalent: "") 151 | resetItem.keyEquivalent = "r" 152 | resetItem.keyEquivalentModifierMask = .command 153 | 154 | newMenu.addItem(importItem) 155 | newMenu.addItem(exportItem) 156 | newMenu.addItem(NSMenuItem.separator()) 157 | newMenu.addItem(addFrameItem) 158 | newMenu.addItem(reverseItem) 159 | newMenu.addItem(changeDuration) 160 | newMenu.addItem(NSMenuItem.separator()) 161 | newMenu.addItem(previewItem) 162 | newMenu.addItem(editItem) 163 | newMenu.addItem(resetItem) 164 | 165 | newItem.submenu = newMenu 166 | menu.insertItem(newItem, at: 1) 167 | } 168 | 169 | // Shows an error 170 | func showError(_ error: String) { 171 | let alert = FancyAlert() 172 | alert.messageText = "An error occurred" 173 | alert.informativeText = error 174 | alert.alertStyle = .critical 175 | alert.addButton(withTitle: "OK") 176 | alert.beginSheetModal(for: self.view.window!, completionHandler: nil) 177 | } 178 | 179 | // Shows informational alert 180 | func showAlert(title: String, msg: String) { 181 | let alert = FancyAlert() 182 | alert.messageText = title 183 | alert.informativeText = msg 184 | alert.alertStyle = .informational 185 | alert.addButton(withTitle: "OK") 186 | alert.beginSheetModal(for: self.view.window!, completionHandler: nil) 187 | } 188 | 189 | // Shows import error 190 | func importError() { 191 | self.showError("Could not open file. It might be in a format that Smart GIF Maker does not understand.") 192 | } 193 | 194 | 195 | // MARK: Buttons 196 | // Adds a new frame 197 | @IBAction func addFrameButtonClicked(sender: AnyObject?) { 198 | if let indexPath = selectedRow { // Add after selectedRow 199 | let selectedFrame = self.currentFrames[indexPath.item] 200 | let newFrame = GIFFrame.emptyFrame 201 | newFrame.duration = selectedFrame.duration 202 | 203 | currentFrames.insert(newFrame, at: indexPath.item+1) 204 | selectedRow = IndexPath(item: indexPath.item+1, section: 0) 205 | } 206 | else { // Add empty frame 207 | currentFrames.append(GIFFrame.emptyFrame) 208 | } 209 | 210 | self.imageCollectionView.reloadData() 211 | } 212 | 213 | // Edit button clicked 214 | @IBAction func editButtonClicked(sender: AnyObject?) { 215 | var startIndex:Int? = nil 216 | 217 | if let indexPath = imageCollectionView.selectionIndexPaths.first { 218 | startIndex = indexPath.item 219 | } 220 | 221 | showEditing(withIndex: startIndex) 222 | } 223 | 224 | // Export a gif 225 | @IBAction func exportGIFButtonClicked(sender: AnyObject?) { 226 | let validate = self.findAndValidateUIValues() 227 | 228 | if validate.error { 229 | return 230 | } 231 | 232 | let panel = NSSavePanel() 233 | panel.allowedFileTypes = ["gif"] 234 | panel.begin { (res) in 235 | if res == NSFileHandlingPanelOKButton { 236 | if let url = panel.url { 237 | let rep = validate.gif 238 | self.exportGIF(to: url, gif: rep) 239 | } 240 | } 241 | } 242 | 243 | } 244 | 245 | // Load a gif from a file 246 | @IBAction func importButtonClicked(sender: AnyObject?) { 247 | 248 | // Show file panel 249 | let panel = NSOpenPanel() 250 | 251 | // TODO: Allow other types of imports? 252 | // No reason to now allow single images to be selected this way 253 | 254 | panel.allowedFileTypes = ["gif", "mp4", "mov"] 255 | panel.allowsMultipleSelection = false 256 | panel.allowsOtherFileTypes = false 257 | panel.canChooseDirectories = false 258 | panel.begin { (res) in 259 | if res == NSFileHandlingPanelOKButton { 260 | // Load image from file 261 | if let url = panel.url { 262 | let fileExtension = url.pathExtension 263 | 264 | if fileExtension == "gif" { 265 | self.importGIF(from: url) 266 | } 267 | else if fileExtension == "mp4" || fileExtension == "mov" { 268 | self.importVideo(from: url) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | // Reset everything 276 | @IBAction func resetButtonClicked(sender: AnyObject?) { 277 | let alert = FancyAlert() 278 | alert.alertStyle = .warning 279 | alert.informativeText = "This will remove everything, are you sure?" 280 | alert.messageText = "Are you sure?" 281 | alert.addButton(withTitle: "Yes") 282 | alert.addButton(withTitle: "No") 283 | 284 | alert.beginSheetModal(for: self.view.window!) { (resp) in 285 | if resp == NSAlertFirstButtonReturn { // Yes clicked, reset 286 | self.currentFrames = [GIFFrame.emptyFrame] 287 | self.loopsTextField.stringValue = String(GIFHandler.defaultLoops) 288 | 289 | self.imageCollectionView.reloadData() 290 | self.deselectAll() 291 | } 292 | } 293 | } 294 | 295 | // Preview 296 | @IBAction func previewButtonClicked(sender: AnyObject?) { 297 | let validate = self.findAndValidateUIValues() 298 | 299 | if validate.error { 300 | return 301 | } 302 | 303 | self.loadingView.isHidden = false 304 | 305 | DispatchQueue.global(qos: .utility).async { 306 | let directory = NSTemporaryDirectory() 307 | let fileName = NSUUID().uuidString+".gif" 308 | if let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName]) { 309 | GIFHandler.createAndSaveGIF(with: validate.gif.frames, savePath: fullURL, loops: validate.gif.loops, watermark: false) 310 | self.previewImagePath = fullURL 311 | } 312 | 313 | DispatchQueue.main.async { 314 | self.loadingView.isHidden = true 315 | self.performSegue(withIdentifier: "ShowPreview", sender: self) 316 | } 317 | } 318 | } 319 | 320 | 321 | // MARK: Helpers 322 | // Validates values from UI and returns them 323 | func findAndValidateUIValues() -> (error: Bool, gif: GIFRepresentation) { 324 | 325 | let empRep = GIFRepresentation() 326 | let errorReturn = (error: true, gif: empRep) 327 | 328 | guard let loops = Int(loopsTextField.stringValue) else { 329 | showError("Invalid value for loop count (Zero or positive integer allowed)") 330 | return errorReturn 331 | } 332 | 333 | // Remove empty images 334 | let tmpFrames:[GIFFrame] = currentFrames.filter({ (frame) -> Bool in 335 | return frame.image != nil 336 | }) 337 | 338 | if tmpFrames.count == 0 { 339 | showError("No frames to export.") 340 | return errorReturn 341 | } 342 | 343 | // Success! 344 | return (error: false, gif: GIFRepresentation(frames: tmpFrames, loops: loops)) 345 | } 346 | 347 | // Imports a gif from a given location 348 | func importGIF(from: URL) { 349 | if let image = NSImage(contentsOf: from) { 350 | DispatchQueue.global(qos: .utility).async { // Perform in background to not break UI 351 | // Set values from the .GIF 352 | self.loadAndSetGIF(image: image) 353 | } 354 | } 355 | else { 356 | self.importError() 357 | } 358 | } 359 | 360 | // Loads gif from NSImage and sets variables in UI 361 | func loadAndSetGIF(image: NSImage) { 362 | GIFHandler.loadGIF(with: image, onFinish: { rep in 363 | self.currentFrames = rep.frames 364 | self.loopsTextField.stringValue = String(rep.loops) 365 | 366 | DispatchQueue.main.async { // Update UI in main 367 | self.selectedRow = nil 368 | self.imageCollectionView.reloadData() 369 | } 370 | }) 371 | } 372 | 373 | // Imports MP4 from given location 374 | func importVideo(from: URL) { 375 | 376 | showAlert(title: "Importing video", msg: "This might take a while.") 377 | self.loadingView.isHidden = false 378 | 379 | DispatchQueue.global(qos: .utility).async { 380 | GIFHandler.loadVideo(with: from, withFPS: 5, onFinish: { representation in 381 | if representation.frames.count < 1 { 382 | DispatchQueue.main.async { 383 | self.importError() 384 | } 385 | return 386 | } 387 | 388 | DispatchQueue.main.async { // Set variables in main thread 389 | self.currentFrames = representation.frames 390 | self.loopsTextField.stringValue = String(representation.loops) 391 | 392 | self.selectedRow = nil 393 | self.imageCollectionView.reloadData() 394 | self.loadingView.isHidden = true 395 | } 396 | }) 397 | } 398 | } 399 | 400 | // Exports given gif to given url 401 | func exportGIF(to: URL, gif: GIFRepresentation) { 402 | self.loadingView.isHidden = false 403 | 404 | DispatchQueue.global(qos: .utility).async { 405 | // GIFHandler.createGIF(with: preview.frames, loops: preview.loops) 406 | GIFHandler.createAndSaveGIF(with: gif.frames, savePath: to, loops: gif.loops) 407 | NSWorkspace.shared().activateFileViewerSelecting([to]) 408 | 409 | DispatchQueue.main.async { 410 | self.loadingView.isHidden = true 411 | } 412 | } 413 | } 414 | 415 | // Adds NotificationCenter listeners 416 | func setupNotificationListeners() { 417 | // Listeners for events regarding frames and images 418 | NotificationCenter.default.addObserver(self, selector: #selector(MainViewController.reloadImages), 419 | name: MainViewController.editingEndedNotificationName, object: nil) 420 | NotificationCenter.default.addObserver(self, selector: #selector(MainViewController.documentFramesLoaded(notification:)), 421 | name: MainViewController.loadedDocumentFramesNotificationName, object: nil) 422 | 423 | // GIFHandler events 424 | NotificationCenter.default.addObserver(self, selector: #selector(MainViewController.gifError(sender:)), 425 | name: GIFHandler.errorNotificationName, object: nil) 426 | } 427 | 428 | // Frames loaded using 'Open with...' menu 429 | func documentFramesLoaded(notification: NSNotification) { 430 | if let values = notification.userInfo?["info"] as? GIFRepresentation { 431 | self.currentFrames = values.frames 432 | self.loopsTextField.stringValue = String(values.loops) 433 | 434 | self.selectedRow = nil 435 | self.imageCollectionView.reloadData() 436 | } 437 | } 438 | 439 | // Shows editing window with given image index 440 | func showEditing(withIndex: Int? = nil) { 441 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 442 | editingWindowController = storyboard.instantiateController(withIdentifier: "EditingWindow") as? NSWindowController 443 | 444 | if let contentViewController = editingWindowController?.contentViewController as? EditViewController { 445 | contentViewController.setFrames(frames: self.currentFrames) 446 | contentViewController.initialFrameNumber = withIndex 447 | } 448 | 449 | editingWindowController?.showWindow(self) 450 | } 451 | 452 | // Notification when an error occurs in GIFHandler 453 | func gifError(sender: NSNotification) { 454 | guard let userInfo = sender.userInfo, 455 | let error = userInfo["Error"] as? String else { 456 | return 457 | } 458 | 459 | showError(error) 460 | } 461 | 462 | // MARK: Actions 463 | // Reverses alle frames 464 | func reverseFrames() { 465 | self.currentFrames.reverse() 466 | self.reloadImages() 467 | } 468 | 469 | func setAllFrameDurations() { 470 | let alert = FancyAlert() 471 | alert.messageText = "Set frame duration" 472 | alert.informativeText = "What frame duration do you want to set?" 473 | alert.alertStyle = .informational 474 | alert.addButton(withTitle: "Set frame durations") 475 | alert.addButton(withTitle: "Cancel") 476 | 477 | let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) 478 | input.stringValue = String(GIFHandler.defaultFrameDuration) 479 | 480 | alert.accessoryView = input 481 | 482 | alert.beginSheetModal(for: self.view.window!) { (resp) in 483 | if resp == NSAlertFirstButtonReturn { 484 | guard var duration = Double(input.stringValue) else { // Could not parse as Double 485 | self.showError("Frame duration must be a number!") 486 | return 487 | } 488 | 489 | if duration < GIFHandler.minFrameDuration { // Limit 490 | duration = GIFHandler.minFrameDuration 491 | } 492 | 493 | self.currentFrames.forEach({ (frame) in 494 | frame.duration = duration 495 | }) 496 | self.reloadImages() 497 | } 498 | else { // Cancel 499 | } 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /Code/GIF/NSBezierPathExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSBezierPathExtension.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 18/08/2018. 6 | // Copyright © 2018 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | extension NSBezierPath 13 | { 14 | 15 | // func scaleAroundCenter(factor: CGFloat) { 16 | // let beforeCenter = CGPoint(x: NSMidX(self.bounds), y: NSMidY(self.bounds)) 17 | // 18 | // // SCALE path by factor 19 | // let scaleTransform = AffineTransform(scaleByX: factor, byY: factor) 20 | // self.transform(using: scaleTransform) 21 | // 22 | // let afterCenter = CGPoint(x: NSMidX(self.bounds), y: NSMidY(self.bounds)) 23 | // let diff = CGPoint( 24 | // x: beforeCenter.x - afterCenter.x, 25 | // y: beforeCenter.y - afterCenter.y) 26 | // 27 | // let translateTransform = AffineTransform(scaleByX: diff.x, byY: diff.y) 28 | // self.transform(using: translateTransform) 29 | // } 30 | // 31 | // func scale(fromSize: NSSize, toSize: NSSize) { 32 | // if fromSize.width == 0 || fromSize.height == 0 { 33 | // Swift.print("Should not happen") 34 | // return 35 | // } 36 | // 37 | // let scaledWidth = toSize.width / fromSize.width 38 | // let scaledHeight = toSize.height / fromSize.height 39 | // print("Scale: \(scaledWidth), \(scaledHeight)") 40 | // } 41 | // 42 | func scaleBy(_ mag: CGFloat) { 43 | // Scale 44 | let trans2 = AffineTransform(scaleByX: mag, byY: mag) 45 | self.transform(using: trans2) 46 | 47 | // Move 48 | let frame = self.bounds 49 | let curPos = frame.origin //CGPoint(x: NSMidX(frame), y: NSMidY(frame)) 50 | let newPos = CGPoint(x: curPos.x * mag, y: curPos.y * mag) 51 | let xMove = newPos.x - curPos.x 52 | let yMove = newPos.y - curPos.y 53 | 54 | let trans = AffineTransform(translationByX: xMove, byY: yMove) 55 | self.transform(using: trans) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Code/GIF/NSColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColorExtension.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 12/08/2018. 6 | // Copyright © 2018 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSColor { 13 | func getRGBAr() -> [Int] { 14 | return [Int(self.redComponent * 255.99999), Int(self.greenComponent * 255.99999), Int(self.blueComponent * 255.99999), Int(self.alphaComponent * 255.99999)] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Code/GIF/NSImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImageExtension.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 16/08/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSImage { 13 | 14 | // Used for converting anything else to NSBitmapImageRep 15 | // Based on https://stackoverflow.com/a/34555866 16 | func unscaledBitmapImageRep() -> NSBitmapImageRep { 17 | guard let rep = NSBitmapImageRep( 18 | bitmapDataPlanes: nil, 19 | pixelsWide: Int(self.size.width), 20 | pixelsHigh: Int(self.size.height), 21 | bitsPerSample: 8, 22 | samplesPerPixel: 4, 23 | hasAlpha: true, 24 | isPlanar: false, 25 | colorSpaceName: NSCalibratedRGBColorSpace, 26 | bytesPerRow: 0, 27 | bitsPerPixel: 0 28 | ) else { 29 | preconditionFailure() 30 | } 31 | 32 | NSGraphicsContext.saveGraphicsState() 33 | NSGraphicsContext.setCurrent(NSGraphicsContext(bitmapImageRep: rep)) 34 | self.draw(at: NSZeroPoint, from: NSZeroRect, operation: .sourceOver, fraction: 1.0) 35 | NSGraphicsContext.restoreGraphicsState() 36 | 37 | return rep 38 | } 39 | 40 | func getBitmapRep() -> NSBitmapImageRep? { 41 | for rep in self.representations { 42 | if let tmpRep = rep as? NSBitmapImageRep { 43 | return tmpRep 44 | } 45 | } 46 | 47 | return self.unscaledBitmapImageRep() 48 | } 49 | 50 | var CGImage: CGImage? { 51 | get { 52 | if let imageData = self.tiffRepresentation, 53 | let source = CGImageSourceCreateWithData((imageData as CFData), nil), 54 | let maskRef = CGImageSourceCreateImageAtIndex(source, Int(0), nil) { 55 | return maskRef 56 | } 57 | if let cgimg = self.cgImage(forProposedRect: nil, context: nil, hints: nil) { 58 | return cgimg 59 | } 60 | return nil 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Code/GIF/NSViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ImageFun 4 | // 5 | // Created by Christian Lundtofte on 13/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension NSView { 13 | 14 | // http://stackoverflow.com/a/31461380 15 | var backgroundColor: NSColor? { 16 | get { 17 | if let colorRef = self.layer?.backgroundColor { 18 | return NSColor(cgColor: colorRef) 19 | } 20 | else { 21 | return nil 22 | } 23 | } 24 | set { 25 | self.wantsLayer = true 26 | self.layer?.backgroundColor = newValue?.cgColor 27 | } 28 | } 29 | 30 | func center(inView: NSView) { 31 | self.setFrameOrigin(NSMakePoint( 32 | (NSWidth(inView.bounds) - NSWidth(self.frame)) / 2, 33 | (NSHeight(inView.bounds) - NSHeight(self.frame)) / 2 34 | )); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Code/GIF/PixelImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PixelImageView.swift 3 | // ImageFun 4 | // 5 | // Created by Christian Lundtofte on 25/05/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | fileprivate struct PixelChange { 12 | var location: (x: Int, y: Int)! 13 | var oldColor: NSColor! 14 | var newColor: NSColor! 15 | var size: Int! 16 | } 17 | 18 | fileprivate class UndoOperation { 19 | var changes:[PixelChange] = [] 20 | } 21 | 22 | class PixelImageView: NSImageView { 23 | 24 | static let imageChangedNotificationName = Notification.Name(rawValue: "ColorChangedOutside") 25 | 26 | // Drawing variables 27 | fileprivate var currentPath : ColoredBezierPath? 28 | fileprivate var paths : [ColoredBezierPath] = [] 29 | fileprivate var drawing = false 30 | fileprivate var previousDrawingPosition:(x: Int, y: Int)? 31 | 32 | var overlayImageView : NSImageView? 33 | 34 | // Undo / redo variables 35 | fileprivate var undoOperations:[UndoOperation] = [] 36 | fileprivate var redoOperations:[UndoOperation] = [] 37 | fileprivate var currentUndoOperation:UndoOperation? 38 | 39 | fileprivate static let maxUndoRedoCount = 30 40 | 41 | override func awakeFromNib() { 42 | super.awakeFromNib() 43 | self.postsFrameChangedNotifications = true 44 | NotificationCenter.default.addObserver(self, 45 | selector: #selector(PixelImageView.imageChanged), 46 | name: PixelImageView.imageChangedNotificationName, 47 | object: nil) 48 | // NotificationCenter.default.addObserver(self, 49 | // selector: #selector(PixelImageView.frameChanged(_:)), 50 | // name: NSNotification.Name.NSViewFrameDidChange, 51 | // object: nil) 52 | } 53 | 54 | // Disables antialiasing (No smoothing, clean pixels, makes sense when creating gifs.) 55 | // Furthermore draws the current line being drawn, if any. 56 | override func draw(_ dirtyRect: NSRect) { 57 | NSGraphicsContext.saveGraphicsState() 58 | NSGraphicsContext.current()?.imageInterpolation = .none 59 | super.draw(dirtyRect) 60 | 61 | if let path = self.currentPath { 62 | DrawingOptionsHandler.shared.drawingColor.set() 63 | path.stroke() 64 | } 65 | 66 | NSGraphicsContext.restoreGraphicsState() 67 | } 68 | 69 | 70 | // MARK: Notifications 71 | @objc func imageChanged() { 72 | guard let _ = self.image else { 73 | return 74 | } 75 | 76 | if self.overlayImageView != nil { 77 | self.overlayImageView?.removeFromSuperview() 78 | self.overlayImageView = nil 79 | } 80 | 81 | // Overlay used for drawing 82 | self.overlayImageView = NSImageView(frame: NSRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)) 83 | self.addSubview(overlayImageView!) 84 | } 85 | 86 | // @objc func frameChanged(_ notif: Notification) { 87 | // guard let obj = notif.object as? PixelImageView else { 88 | // return 89 | // } 90 | // if obj != self { 91 | // return 92 | // } 93 | // 94 | // if let overlay = self.overlayImageView { 95 | // overlay.setFrameSize(<#T##newSize: NSSize##NSSize#>) 96 | // } 97 | // } 98 | 99 | // MARK: Mouse actions 100 | // Mouse down 101 | // override func mouseDown(with event: NSEvent) { 102 | // let windowLoc = event.locationInWindow 103 | // let pixelLoc = self.convertWindowToPixels(windowLoc: windowLoc) 104 | // 105 | // // User is using eyedropper tool 106 | // if DrawingOptionsHandler.shared.isPickingColor { 107 | // 108 | // // Set color 109 | // if let color = self.getPixelColor(x: pixelLoc.x, y: pixelLoc.y) { 110 | // DrawingOptionsHandler.shared.drawingColor = color 111 | // } 112 | // 113 | // // Disable eyedropper and send notifications 114 | // DrawingOptionsHandler.shared.isPickingColor = false 115 | // NotificationCenter.default.post(name: DrawingOptionsHandler.colorChangedNotificationName, object: nil) 116 | // NotificationCenter.default.post(name: DrawingOptionsHandler.usedEyeDropperNotificationName, object: nil) 117 | // 118 | // return 119 | // } 120 | // 121 | // // Normal drawing procedure 122 | // self.drawing = true 123 | // self.previousDrawingPosition = pixelLoc 124 | // 125 | // self.currentUndoOperation = UndoOperation() 126 | // self.setPixelColor(color: &DrawingOptionsHandler.shared.drawingColorPtr, 127 | // x: pixelLoc.x, 128 | // y: pixelLoc.y, 129 | // canUndo: true, 130 | // size: DrawingOptionsHandler.shared.brushSize) 131 | // } 132 | // 133 | // // Mouse drag / mouse moved while mouse down 134 | // // If we're drawing, draw new pixels 135 | // override func mouseDragged(with event: NSEvent) { 136 | // if !drawing { 137 | // return 138 | // } 139 | // 140 | // let windowLoc = event.locationInWindow 141 | // let pixelLoc = self.convertWindowToPixels(windowLoc: windowLoc) 142 | // 143 | // if self.previousDrawingPosition == nil { 144 | // self.setPixelColor(color: &DrawingOptionsHandler.shared.drawingColorPtr, 145 | // x: pixelLoc.x, 146 | // y: pixelLoc.y, 147 | // canUndo: true, 148 | // size: DrawingOptionsHandler.shared.brushSize) 149 | // self.previousDrawingPosition = pixelLoc 150 | // return 151 | // } 152 | // 153 | // // Only draw on changed pixel, no reason to draw more than necessary 154 | // if pixelLoc.x != previousDrawingPosition!.x || pixelLoc.y != previousDrawingPosition!.y { 155 | // self.setPixelColor(color: &DrawingOptionsHandler.shared.drawingColorPtr, 156 | // x: pixelLoc.x, 157 | // y: pixelLoc.y, 158 | // canUndo: true, 159 | // size: DrawingOptionsHandler.shared.brushSize) 160 | // self.previousDrawingPosition = pixelLoc 161 | // } 162 | // } 163 | // 164 | // // Mouse up 165 | // override func mouseUp(with event: NSEvent) { 166 | // drawing = false 167 | // 168 | // // Add current undo to list 169 | // self.closeCurrentUndo() 170 | // } 171 | // 172 | 173 | // MARK: Undo and Redo 174 | // func undo() { 175 | // if let undoOp = self.undoOperations.last { 176 | // undoOp.changes.reversed().forEach({ (change) in 177 | // var tmps:[Int] = change.oldColor.getRGBAr() 178 | // self.setPixelColor(color: &tmps, x: change.location.x, y: change.location.y, canUndo: false) 179 | // }) 180 | // self.undoOperations.removeLast() 181 | // self.redoOperations.append(undoOp) 182 | // 183 | // if self.redoOperations.count > PixelImageView.maxUndoRedoCount { 184 | // self.redoOperations.removeFirst() 185 | // } 186 | // } 187 | // } 188 | // 189 | // func redo() { 190 | // if let redoOp = self.redoOperations.last { 191 | // redoOp.changes.reversed().forEach({ (change) in 192 | // var tmps:[Int] = change.newColor.getRGBAr() 193 | // self.setPixelColor(color: &tmps, x: change.location.x, y: change.location.y, canUndo: false) 194 | // }) 195 | // self.redoOperations.removeLast() 196 | // self.undoOperations.append(redoOp) 197 | // } 198 | // } 199 | // 200 | func resetUndoRedo() { 201 | self.redoOperations.removeAll() 202 | self.undoOperations.removeAll() 203 | } 204 | 205 | func closeCurrentUndo() { 206 | if let op = self.currentUndoOperation { 207 | self.undoOperations.append(op) 208 | 209 | if self.undoOperations.count > PixelImageView.maxUndoRedoCount { 210 | self.undoOperations.removeFirst() 211 | } 212 | 213 | 214 | self.currentUndoOperation = nil 215 | } 216 | } 217 | 218 | func createUndoOperation(x: Int, y: Int, newColor: NSColor, size: Int) { 219 | // Create undo operation 220 | guard let curColor = getPixelColor(x: x, y: y) else { 221 | // 222 | // if !drawing { 223 | // return 224 | // } 225 | // 226 | // // Dragged outside window 227 | // self.closeCurrentUndo() 228 | // 229 | // drawing = false 230 | // 231 | return 232 | } 233 | 234 | self.currentUndoOperation?.changes.append(PixelChange(location: (x: x, y: y), oldColor: curColor, newColor: newColor, size: size)) 235 | } 236 | 237 | 238 | // MARK: Helpers 239 | // Images and NSViews have flipped Y coordinates, this turns them 240 | // func pointInFlippedRect(inPoint: NSPoint, aRect: NSRect) -> NSPoint { 241 | // return NSMakePoint(inPoint.x, NSHeight(aRect) - inPoint.y) 242 | // } 243 | // 244 | // // Converts window event position go pixel coordinates 245 | // func convertWindowToPixels(windowLoc: NSPoint) -> (x: Int, y: Int) { 246 | // guard let imgRep = self.imageBitmapRep else { return (x: 0, y: 0) } 247 | // 248 | // let localLoc = self.pointInFlippedRect(inPoint: self.convert(windowLoc, from: nil), aRect: self.frame) 249 | // 250 | // let height = self.frame.height 251 | // let width = self.frame.width 252 | // let pixelHeight = CGFloat(imgRep.pixelsHigh) 253 | // let pixelWidth = CGFloat(imgRep.pixelsWide) 254 | // 255 | // let clickX = Int(ceil((pixelWidth/width)*localLoc.x))-1 256 | // let clickY = Int(ceil((pixelHeight/height)*localLoc.y))-1 257 | // 258 | // return (x: clickX, y: clickY) 259 | // } 260 | // 261 | // 262 | // // MARK: Drawing 263 | // // Sets a color at a given coordinate 264 | // func setPixelColor(color: UnsafeMutablePointer, x: Int, y: Int, canUndo: Bool, size: Int = 1) { 265 | // guard let imgRep = self.imageBitmapRep else { Swift.print("Error"); return } 266 | // 267 | // self.image?.lockFocus() 268 | // if canUndo { 269 | // self.createUndoOperation(x: x, y: y, newColor: DrawingOptionsHandler.shared.drawingColor, size: size) 270 | // } 271 | // 272 | // 273 | // } 274 | 275 | // Returns NSColor at given coordinates 276 | func getPixelColor(x: Int, y: Int) -> NSColor? { 277 | guard let image = self.image else { return nil } 278 | guard let imgRep = image.getBitmapRep() else { return nil } 279 | 280 | return imgRep.colorAt(x:x, y:y) 281 | } 282 | 283 | override func mouseDown(with event: NSEvent) { 284 | 285 | self.currentPath = ColoredBezierPath() 286 | self.currentPath?.strokeColor = DrawingOptionsHandler.shared.drawingColor 287 | self.currentPath?.lineWidth = CGFloat(DrawingOptionsHandler.shared.brushSize) 288 | 289 | self.currentPath?.lineJoinStyle = .miterLineJoinStyle 290 | self.currentPath?.lineCapStyle = .squareLineCapStyle 291 | 292 | self.currentPath?.move(to: convert(event.locationInWindow, from: nil)) 293 | self.paths.append(currentPath!) 294 | 295 | self.needsDisplay = true 296 | } 297 | 298 | override func mouseDragged(with event: NSEvent) { 299 | guard let path = self.currentPath else { 300 | return 301 | } 302 | 303 | path.line(to: convert(event.locationInWindow, from: nil)) 304 | self.needsDisplay = true 305 | } 306 | 307 | override func mouseUp(with event: NSEvent) { 308 | self.currentPath = nil 309 | 310 | let img = NSImage(size: self.overlayImageView!.frame.size, flipped: false) { (rect) -> Bool in 311 | 312 | for path in self.paths { 313 | path.strokeColor.set() 314 | path.stroke() 315 | } 316 | return true 317 | } 318 | self.overlayImageView?.image = img 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /Code/GIF/PreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewViewController.swift 3 | // Smart GIF Maker 4 | // 5 | // Created by Christian Lundtofte on 16/03/2017. 6 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class PreviewViewController: NSViewController { 12 | @IBOutlet var previewImageView:NSImageView! 13 | var previewImagePath:URL? 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | self.view.wantsLayer = true 19 | self.view.backgroundColor = Constants.darkBackgroundColor 20 | 21 | // Load image or dismiss preview 22 | if let previewImagePath = self.previewImagePath { 23 | self.previewImageView.canDrawSubviewsIntoLayer = true 24 | // self.previewImageView.imageScaling = .scaleProportionallyDown 25 | self.previewImageView.animates = true 26 | self.previewImageView.image = NSImage(contentsOf: previewImagePath) 27 | } 28 | else { 29 | self.dismiss(self) 30 | } 31 | } 32 | 33 | override func viewDidAppear() { 34 | super.viewDidAppear() 35 | 36 | self.view.wantsLayer = true 37 | self.view.backgroundColor = Constants.darkBackgroundColor 38 | 39 | 40 | } 41 | 42 | @IBAction func closeButtonClicked(sender: AnyObject?) { 43 | self.dismiss(self) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Code/GIF/Products.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPStuff.swift 3 | // LiveMap 4 | // 5 | // Created by Christian Lundtofte on 09/08/2016. 6 | // Copyright © 2016 Christian Lundtofte. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Products { 12 | 13 | fileprivate static let Prefix = "com.imakezappz.GIF." 14 | 15 | public static let Pro = Prefix + "pro" 16 | 17 | fileprivate static let productIdentifiers: Set = [Products.Pro] 18 | 19 | public static let store = IAPHelper(productIds: Products.productIdentifiers) 20 | } 21 | -------------------------------------------------------------------------------- /Code/GIF/SmartTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLTextField.swift 3 | // JSON Viewer 4 | // 5 | // Created by Christian on 15/01/2016. 6 | // Copyright © 2016 Christian Lundtofte Sørensen. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SmartTextField: NSTextField { 12 | 13 | override func draw(_ dirtyRect: NSRect) { 14 | super.draw(dirtyRect) 15 | } 16 | 17 | override func performKeyEquivalent(with theEvent: NSEvent) -> Bool { 18 | if theEvent.type == .keyDown && theEvent.modifierFlags.contains(NSEventModifierFlags.command) { 19 | let responder = self.window?.firstResponder 20 | 21 | if responder != nil && responder is NSTextView { 22 | 23 | let textView = responder as! NSTextView 24 | let range = textView.selectedRange 25 | let bHasSelectedTexts = (range.length > 0) 26 | 27 | let keyCode = theEvent.keyCode 28 | var bHandled = false 29 | 30 | //6 = Z, 7 = X, 8 = C, 9 = V, A = 0 31 | if keyCode == 6 { 32 | if theEvent.modifierFlags.contains(NSEventModifierFlags.shift) { 33 | if ((textView.undoManager?.canRedo) != nil) { 34 | textView.undoManager?.redo() 35 | bHandled = true 36 | } 37 | } 38 | else { 39 | if ((textView.undoManager?.canUndo) != nil) { 40 | textView.undoManager?.undo() 41 | bHandled = true 42 | } 43 | } 44 | } 45 | else if keyCode == 7 && bHasSelectedTexts { 46 | textView.cut(self) 47 | bHandled = true 48 | } 49 | else if keyCode == 8 && bHasSelectedTexts { 50 | textView.copy(self) 51 | bHandled = true 52 | } 53 | else if keyCode == 9 { 54 | textView.paste(self) 55 | bHandled = true 56 | } 57 | else if keyCode == 0 { 58 | textView.selectAll(self) 59 | bHandled = true 60 | } 61 | 62 | return bHandled 63 | } 64 | } 65 | 66 | return false 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Code/GIF/ZoomView.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // ZoomView.swift 4 | // ImageFun 5 | // 6 | // Created by Christian Lundtofte on 16/05/2017. 7 | // Copyright © 2017 Christian Lundtofte. All rights reserved. 8 | // 9 | 10 | import Cocoa 11 | 12 | protocol ZoomViewDelegate { 13 | func zoomChanged(magnification: CGFloat) 14 | } 15 | 16 | class ZoomView: NSView { 17 | 18 | var delegate:ZoomViewDelegate? 19 | var zoomView:NSView? 20 | var previousZoomSize:NSSize? 21 | 22 | override func draw(_ dirtyRect: NSRect) { 23 | super.draw(dirtyRect) 24 | } 25 | 26 | override func magnify(with event: NSEvent) { 27 | super.magnify(with: event) 28 | 29 | guard let zoomView = self.zoomView else { return } 30 | 31 | if(event.phase == .changed) { 32 | var newSize = NSMakeSize(0, 0) 33 | newSize.width = zoomView.frame.size.width * (event.magnification + 1.0) 34 | newSize.height = zoomView.frame.size.height * (event.magnification + 1.0) 35 | 36 | zoomView.setFrameSize(newSize) 37 | 38 | if zoomView is PixelImageView { 39 | (zoomView as! PixelImageView).overlayImageView?.setFrameSize(newSize) 40 | } 41 | 42 | zoomView.needsDisplay = true 43 | 44 | self.previousZoomSize = newSize 45 | 46 | self.delegate?.zoomChanged(magnification: event.magnification) 47 | } 48 | } 49 | 50 | func redoZoom() { 51 | if let zoom = previousZoomSize { 52 | zoomView?.setFrameSize(zoom) 53 | self.delegate?.zoomChanged(magnification: 0.0) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Code/GIF/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/GIF/loading.gif -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AA17D4A21ECB710600E260A7 /* NSViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA17D4A11ECB710600E260A7 /* NSViewExtension.swift */; }; 11 | AA17D4A51ECB713400E260A7 /* FancyButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA17D4A31ECB713400E260A7 /* FancyButtonCell.swift */; }; 12 | AA17D4A61ECB713400E260A7 /* FancyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA17D4A41ECB713400E260A7 /* FancyButton.swift */; }; 13 | AA1F32C521281EFD0043D1B3 /* NSBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1F32C421281EFD0043D1B3 /* NSBezierPathExtension.swift */; }; 14 | AA1F32C8212832590043D1B3 /* ColoredBezierPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1F32C7212832590043D1B3 /* ColoredBezierPath.swift */; }; 15 | AA33867B1EBF3BEA0056354F /* GIFFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA33867A1EBF3BEA0056354F /* GIFFrame.swift */; }; 16 | AA33867E1EBF45970056354F /* SmartTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA33867D1EBF45970056354F /* SmartTextField.swift */; }; 17 | AA3E1E5E1F0000830030E636 /* MainViewController+NSCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3E1E5D1F0000830030E636 /* MainViewController+NSCollectionView.swift */; }; 18 | AA45A5491ED8603100D508BC /* ZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA45A5471ED8603100D508BC /* ZoomView.swift */; }; 19 | AA45A54A1ED8603100D508BC /* PixelImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA45A5481ED8603100D508BC /* PixelImageView.swift */; }; 20 | AA45A54E1ED8604100D508BC /* EditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA45A54B1ED8604100D508BC /* EditViewController.swift */; }; 21 | AA45A5501ED8604100D508BC /* DrawingOptionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA45A54D1ED8604100D508BC /* DrawingOptionsHandler.swift */; }; 22 | AA6992A51F4478B7008F7409 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6992A41F4478B7008F7409 /* LoadingView.swift */; }; 23 | AA6992A71F447A0C008F7409 /* loading.gif in Resources */ = {isa = PBXBuildFile; fileRef = AA6992A61F447A0C008F7409 /* loading.gif */; }; 24 | AA6992A91F447CF8008F7409 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6992A81F447CF8008F7409 /* Constants.swift */; }; 25 | AA6992AD1F448487008F7409 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6992AC1F448487008F7409 /* NSImageExtension.swift */; }; 26 | AA6D4BB81E7B3DEA00F1B642 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6D4BB71E7B3DEA00F1B642 /* PreviewViewController.swift */; }; 27 | AA8F63961E787DE2000965B5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8F63951E787DE2000965B5 /* AppDelegate.swift */; }; 28 | AA8F63981E787DE2000965B5 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8F63971E787DE2000965B5 /* MainViewController.swift */; }; 29 | AA8F639A1E787DE2000965B5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA8F63991E787DE2000965B5 /* Assets.xcassets */; }; 30 | AA8F639D1E787DE2000965B5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA8F639B1E787DE2000965B5 /* Main.storyboard */; }; 31 | AA8F63A71E788BEE000965B5 /* GIFHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8F63A61E788BEE000965B5 /* GIFHandler.swift */; }; 32 | AAA8F5061F461ECC007EC2CB /* MainViewController+FrameCollectionViewItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA8F5051F461ECC007EC2CB /* MainViewController+FrameCollectionViewItemDelegate.swift */; }; 33 | AAA9C4CF1ECCA01A007EC0D4 /* FancyAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA9C4CE1ECCA01A007EC0D4 /* FancyAlert.swift */; }; 34 | AABC063C1F431D25002DFEE6 /* FancyTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABC063B1F431D25002DFEE6 /* FancyTextFieldCell.swift */; }; 35 | AAC1DF5B21204CA6000BA24A /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC1DF5A21204CA6000BA24A /* NSColorExtension.swift */; }; 36 | AACB80041E795D5D00A3F195 /* FrameCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACB80021E795D5D00A3F195 /* FrameCollectionViewItem.swift */; }; 37 | AACB80051E795D5D00A3F195 /* FrameCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AACB80031E795D5D00A3F195 /* FrameCollectionViewItem.xib */; }; 38 | AACB80081E796A7400A3F195 /* DragNotificationImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACB80071E796A7400A3F195 /* DragNotificationImageView.swift */; }; 39 | AAEC81CF1F41C58B00B71D5D /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAEC81CE1F41C58B00B71D5D /* StoreKit.framework */; }; 40 | AAEC81D11F41C5CE00B71D5D /* IAPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC81D01F41C5CE00B71D5D /* IAPHelper.swift */; }; 41 | AAEC81D41F41C93C00B71D5D /* Products.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC81D31F41C93C00B71D5D /* Products.swift */; }; 42 | AAEF89DA1F04432D00658D9F /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF89D91F04432D00658D9F /* Document.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | AA17D4A11ECB710600E260A7 /* NSViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSViewExtension.swift; sourceTree = ""; }; 47 | AA17D4A31ECB713400E260A7 /* FancyButtonCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FancyButtonCell.swift; sourceTree = ""; }; 48 | AA17D4A41ECB713400E260A7 /* FancyButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FancyButton.swift; sourceTree = ""; }; 49 | AA1F32C421281EFD0043D1B3 /* NSBezierPathExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSBezierPathExtension.swift; sourceTree = ""; }; 50 | AA1F32C7212832590043D1B3 /* ColoredBezierPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredBezierPath.swift; sourceTree = ""; }; 51 | AA33867A1EBF3BEA0056354F /* GIFFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFFrame.swift; sourceTree = ""; }; 52 | AA33867D1EBF45970056354F /* SmartTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SmartTextField.swift; sourceTree = ""; }; 53 | AA3E1E5D1F0000830030E636 /* MainViewController+NSCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainViewController+NSCollectionView.swift"; sourceTree = ""; }; 54 | AA45A5471ED8603100D508BC /* ZoomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZoomView.swift; sourceTree = ""; }; 55 | AA45A5481ED8603100D508BC /* PixelImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PixelImageView.swift; sourceTree = ""; }; 56 | AA45A54B1ED8604100D508BC /* EditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditViewController.swift; sourceTree = ""; }; 57 | AA45A54D1ED8604100D508BC /* DrawingOptionsHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawingOptionsHandler.swift; sourceTree = ""; }; 58 | AA6992A41F4478B7008F7409 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 59 | AA6992A61F447A0C008F7409 /* loading.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = loading.gif; sourceTree = ""; }; 60 | AA6992A81F447CF8008F7409 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 61 | AA6992AC1F448487008F7409 /* NSImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = ""; }; 62 | AA6D4BB71E7B3DEA00F1B642 /* PreviewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 63 | AA8F63921E787DE2000965B5 /* Smart GIF Maker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Smart GIF Maker.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | AA8F63951E787DE2000965B5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 65 | AA8F63971E787DE2000965B5 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 66 | AA8F63991E787DE2000965B5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 67 | AA8F639C1E787DE2000965B5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 68 | AA8F639E1E787DE2000965B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | AA8F63A61E788BEE000965B5 /* GIFHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFHandler.swift; sourceTree = ""; }; 70 | AAA8F5051F461ECC007EC2CB /* MainViewController+FrameCollectionViewItemDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainViewController+FrameCollectionViewItemDelegate.swift"; sourceTree = ""; }; 71 | AAA9C4CE1ECCA01A007EC0D4 /* FancyAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FancyAlert.swift; sourceTree = ""; }; 72 | AABC063B1F431D25002DFEE6 /* FancyTextFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FancyTextFieldCell.swift; sourceTree = ""; }; 73 | AAC1DF5A21204CA6000BA24A /* NSColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSColorExtension.swift; sourceTree = ""; }; 74 | AAC9B9B21E799DD70096A497 /* GIF.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GIF.entitlements; sourceTree = ""; }; 75 | AACB80021E795D5D00A3F195 /* FrameCollectionViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameCollectionViewItem.swift; sourceTree = ""; }; 76 | AACB80031E795D5D00A3F195 /* FrameCollectionViewItem.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FrameCollectionViewItem.xib; sourceTree = ""; }; 77 | AACB80071E796A7400A3F195 /* DragNotificationImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragNotificationImageView.swift; sourceTree = ""; }; 78 | AAEC81CE1F41C58B00B71D5D /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 79 | AAEC81D01F41C5CE00B71D5D /* IAPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAPHelper.swift; sourceTree = ""; }; 80 | AAEC81D31F41C93C00B71D5D /* Products.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Products.swift; sourceTree = ""; }; 81 | AAEF89D91F04432D00658D9F /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | AA8F638F1E787DE2000965B5 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | AAEC81CF1F41C58B00B71D5D /* StoreKit.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | AA33867C1EBF3BF10056354F /* GIF classes */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | AA8F63A61E788BEE000965B5 /* GIFHandler.swift */, 100 | AA33867A1EBF3BEA0056354F /* GIFFrame.swift */, 101 | ); 102 | name = "GIF classes"; 103 | sourceTree = ""; 104 | }; 105 | AA3E1E5C1F00004E0030E636 /* Main Window */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | AA8F63971E787DE2000965B5 /* MainViewController.swift */, 109 | AA3E1E5D1F0000830030E636 /* MainViewController+NSCollectionView.swift */, 110 | AAA8F5051F461ECC007EC2CB /* MainViewController+FrameCollectionViewItemDelegate.swift */, 111 | ); 112 | name = "Main Window"; 113 | sourceTree = ""; 114 | }; 115 | AA3E1E5F1F0000B40030E636 /* Preview */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | AA6D4BB71E7B3DEA00F1B642 /* PreviewViewController.swift */, 119 | ); 120 | name = Preview; 121 | sourceTree = ""; 122 | }; 123 | AA3E1E601F000F8C0030E636 /* Buttons */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | AA17D4A31ECB713400E260A7 /* FancyButtonCell.swift */, 127 | AA17D4A41ECB713400E260A7 /* FancyButton.swift */, 128 | ); 129 | name = Buttons; 130 | sourceTree = ""; 131 | }; 132 | AA3E1E611F000FA60030E636 /* GIFFrames in UI */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | AACB80071E796A7400A3F195 /* DragNotificationImageView.swift */, 136 | AACB80021E795D5D00A3F195 /* FrameCollectionViewItem.swift */, 137 | AACB80031E795D5D00A3F195 /* FrameCollectionViewItem.xib */, 138 | ); 139 | name = "GIFFrames in UI"; 140 | sourceTree = ""; 141 | }; 142 | AA45A5511ED8604400D508BC /* ViewControllers */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | AA3E1E5C1F00004E0030E636 /* Main Window */, 146 | AA3E1E5F1F0000B40030E636 /* Preview */, 147 | AA45A5521ED867A100D508BC /* Edit */, 148 | ); 149 | name = ViewControllers; 150 | sourceTree = ""; 151 | }; 152 | AA45A5521ED867A100D508BC /* Edit */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | AA45A54B1ED8604100D508BC /* EditViewController.swift */, 156 | AA45A54D1ED8604100D508BC /* DrawingOptionsHandler.swift */, 157 | ); 158 | name = Edit; 159 | sourceTree = ""; 160 | }; 161 | AA6992AA1F448109008F7409 /* Loading view */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | AA6992A61F447A0C008F7409 /* loading.gif */, 165 | AA6992A41F4478B7008F7409 /* LoadingView.swift */, 166 | ); 167 | name = "Loading view"; 168 | sourceTree = ""; 169 | }; 170 | AA6992AB1F448467008F7409 /* IAP */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | AAEC81D01F41C5CE00B71D5D /* IAPHelper.swift */, 174 | AAEC81D31F41C93C00B71D5D /* Products.swift */, 175 | ); 176 | name = IAP; 177 | sourceTree = ""; 178 | }; 179 | AA6992AE1F448781008F7409 /* Extensions */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | AA6992AC1F448487008F7409 /* NSImageExtension.swift */, 183 | AAC1DF5A21204CA6000BA24A /* NSColorExtension.swift */, 184 | AA1F32C421281EFD0043D1B3 /* NSBezierPathExtension.swift */, 185 | ); 186 | name = Extensions; 187 | sourceTree = ""; 188 | }; 189 | AA6992AF1F44BB53008F7409 /* TextFields */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | AABC063B1F431D25002DFEE6 /* FancyTextFieldCell.swift */, 193 | AA33867D1EBF45970056354F /* SmartTextField.swift */, 194 | ); 195 | name = TextFields; 196 | sourceTree = ""; 197 | }; 198 | AA8F63891E787DE2000965B5 = { 199 | isa = PBXGroup; 200 | children = ( 201 | AA8F63941E787DE2000965B5 /* GIF */, 202 | AA8F63931E787DE2000965B5 /* Products */, 203 | AAEC81CD1F41C58B00B71D5D /* Frameworks */, 204 | ); 205 | sourceTree = ""; 206 | }; 207 | AA8F63931E787DE2000965B5 /* Products */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | AA8F63921E787DE2000965B5 /* Smart GIF Maker.app */, 211 | ); 212 | name = Products; 213 | sourceTree = ""; 214 | }; 215 | AA8F63941E787DE2000965B5 /* GIF */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | AAC9B9B21E799DD70096A497 /* GIF.entitlements */, 219 | AA8F63991E787DE2000965B5 /* Assets.xcassets */, 220 | AA8F639B1E787DE2000965B5 /* Main.storyboard */, 221 | AA8F639E1E787DE2000965B5 /* Info.plist */, 222 | AAEC81D21F41C67A00B71D5D /* Misc classes */, 223 | AA45A5511ED8604400D508BC /* ViewControllers */, 224 | AA33867C1EBF3BF10056354F /* GIF classes */, 225 | AACB80061E795D7200A3F195 /* UI */, 226 | ); 227 | path = GIF; 228 | sourceTree = ""; 229 | }; 230 | AACB80061E795D7200A3F195 /* UI */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | AA6992AA1F448109008F7409 /* Loading view */, 234 | AA45A5471ED8603100D508BC /* ZoomView.swift */, 235 | AA45A5481ED8603100D508BC /* PixelImageView.swift */, 236 | AAA9C4CE1ECCA01A007EC0D4 /* FancyAlert.swift */, 237 | AA3E1E601F000F8C0030E636 /* Buttons */, 238 | AA17D4A11ECB710600E260A7 /* NSViewExtension.swift */, 239 | AA6992AF1F44BB53008F7409 /* TextFields */, 240 | AA3E1E611F000FA60030E636 /* GIFFrames in UI */, 241 | ); 242 | name = UI; 243 | sourceTree = ""; 244 | }; 245 | AAEC81CD1F41C58B00B71D5D /* Frameworks */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | AAEC81CE1F41C58B00B71D5D /* StoreKit.framework */, 249 | ); 250 | name = Frameworks; 251 | sourceTree = ""; 252 | }; 253 | AAEC81D21F41C67A00B71D5D /* Misc classes */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | AA6992AB1F448467008F7409 /* IAP */, 257 | AA6992A81F447CF8008F7409 /* Constants.swift */, 258 | AA8F63951E787DE2000965B5 /* AppDelegate.swift */, 259 | AAEF89D91F04432D00658D9F /* Document.swift */, 260 | AA6992AE1F448781008F7409 /* Extensions */, 261 | AA1F32C7212832590043D1B3 /* ColoredBezierPath.swift */, 262 | ); 263 | name = "Misc classes"; 264 | sourceTree = ""; 265 | }; 266 | /* End PBXGroup section */ 267 | 268 | /* Begin PBXNativeTarget section */ 269 | AA8F63911E787DE2000965B5 /* Smart GIF Maker */ = { 270 | isa = PBXNativeTarget; 271 | buildConfigurationList = AA8F63A11E787DE2000965B5 /* Build configuration list for PBXNativeTarget "Smart GIF Maker" */; 272 | buildPhases = ( 273 | AA8F638E1E787DE2000965B5 /* Sources */, 274 | AA8F638F1E787DE2000965B5 /* Frameworks */, 275 | AA8F63901E787DE2000965B5 /* Resources */, 276 | ); 277 | buildRules = ( 278 | ); 279 | dependencies = ( 280 | ); 281 | name = "Smart GIF Maker"; 282 | productName = GIF; 283 | productReference = AA8F63921E787DE2000965B5 /* Smart GIF Maker.app */; 284 | productType = "com.apple.product-type.application"; 285 | }; 286 | /* End PBXNativeTarget section */ 287 | 288 | /* Begin PBXProject section */ 289 | AA8F638A1E787DE2000965B5 /* Project object */ = { 290 | isa = PBXProject; 291 | attributes = { 292 | LastSwiftUpdateCheck = 0820; 293 | LastUpgradeCheck = 0940; 294 | ORGANIZATIONNAME = "Christian Lundtofte"; 295 | TargetAttributes = { 296 | AA8F63911E787DE2000965B5 = { 297 | CreatedOnToolsVersion = 8.2.1; 298 | DevelopmentTeam = ZRKANV2HV4; 299 | ProvisioningStyle = Automatic; 300 | SystemCapabilities = { 301 | com.apple.InAppPurchase = { 302 | enabled = 1; 303 | }; 304 | com.apple.Sandbox = { 305 | enabled = 1; 306 | }; 307 | }; 308 | }; 309 | }; 310 | }; 311 | buildConfigurationList = AA8F638D1E787DE2000965B5 /* Build configuration list for PBXProject "Smart GIF Maker" */; 312 | compatibilityVersion = "Xcode 3.2"; 313 | developmentRegion = English; 314 | hasScannedForEncodings = 0; 315 | knownRegions = ( 316 | en, 317 | Base, 318 | ); 319 | mainGroup = AA8F63891E787DE2000965B5; 320 | productRefGroup = AA8F63931E787DE2000965B5 /* Products */; 321 | projectDirPath = ""; 322 | projectRoot = ""; 323 | targets = ( 324 | AA8F63911E787DE2000965B5 /* Smart GIF Maker */, 325 | ); 326 | }; 327 | /* End PBXProject section */ 328 | 329 | /* Begin PBXResourcesBuildPhase section */ 330 | AA8F63901E787DE2000965B5 /* Resources */ = { 331 | isa = PBXResourcesBuildPhase; 332 | buildActionMask = 2147483647; 333 | files = ( 334 | AACB80051E795D5D00A3F195 /* FrameCollectionViewItem.xib in Resources */, 335 | AA8F639A1E787DE2000965B5 /* Assets.xcassets in Resources */, 336 | AA6992A71F447A0C008F7409 /* loading.gif in Resources */, 337 | AA8F639D1E787DE2000965B5 /* Main.storyboard in Resources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | /* End PBXResourcesBuildPhase section */ 342 | 343 | /* Begin PBXSourcesBuildPhase section */ 344 | AA8F638E1E787DE2000965B5 /* Sources */ = { 345 | isa = PBXSourcesBuildPhase; 346 | buildActionMask = 2147483647; 347 | files = ( 348 | AA1F32C8212832590043D1B3 /* ColoredBezierPath.swift in Sources */, 349 | AA1F32C521281EFD0043D1B3 /* NSBezierPathExtension.swift in Sources */, 350 | AAC1DF5B21204CA6000BA24A /* NSColorExtension.swift in Sources */, 351 | AAA9C4CF1ECCA01A007EC0D4 /* FancyAlert.swift in Sources */, 352 | AA8F63981E787DE2000965B5 /* MainViewController.swift in Sources */, 353 | AA6992AD1F448487008F7409 /* NSImageExtension.swift in Sources */, 354 | AA45A5491ED8603100D508BC /* ZoomView.swift in Sources */, 355 | AA6992A51F4478B7008F7409 /* LoadingView.swift in Sources */, 356 | AACB80081E796A7400A3F195 /* DragNotificationImageView.swift in Sources */, 357 | AAA8F5061F461ECC007EC2CB /* MainViewController+FrameCollectionViewItemDelegate.swift in Sources */, 358 | AA17D4A21ECB710600E260A7 /* NSViewExtension.swift in Sources */, 359 | AA6992A91F447CF8008F7409 /* Constants.swift in Sources */, 360 | AA45A54E1ED8604100D508BC /* EditViewController.swift in Sources */, 361 | AA45A54A1ED8603100D508BC /* PixelImageView.swift in Sources */, 362 | AA8F63961E787DE2000965B5 /* AppDelegate.swift in Sources */, 363 | AA17D4A61ECB713400E260A7 /* FancyButton.swift in Sources */, 364 | AA33867B1EBF3BEA0056354F /* GIFFrame.swift in Sources */, 365 | AACB80041E795D5D00A3F195 /* FrameCollectionViewItem.swift in Sources */, 366 | AAEC81D11F41C5CE00B71D5D /* IAPHelper.swift in Sources */, 367 | AA8F63A71E788BEE000965B5 /* GIFHandler.swift in Sources */, 368 | AA45A5501ED8604100D508BC /* DrawingOptionsHandler.swift in Sources */, 369 | AA6D4BB81E7B3DEA00F1B642 /* PreviewViewController.swift in Sources */, 370 | AAEF89DA1F04432D00658D9F /* Document.swift in Sources */, 371 | AA33867E1EBF45970056354F /* SmartTextField.swift in Sources */, 372 | AAEC81D41F41C93C00B71D5D /* Products.swift in Sources */, 373 | AA3E1E5E1F0000830030E636 /* MainViewController+NSCollectionView.swift in Sources */, 374 | AABC063C1F431D25002DFEE6 /* FancyTextFieldCell.swift in Sources */, 375 | AA17D4A51ECB713400E260A7 /* FancyButtonCell.swift in Sources */, 376 | ); 377 | runOnlyForDeploymentPostprocessing = 0; 378 | }; 379 | /* End PBXSourcesBuildPhase section */ 380 | 381 | /* Begin PBXVariantGroup section */ 382 | AA8F639B1E787DE2000965B5 /* Main.storyboard */ = { 383 | isa = PBXVariantGroup; 384 | children = ( 385 | AA8F639C1E787DE2000965B5 /* Base */, 386 | ); 387 | name = Main.storyboard; 388 | sourceTree = ""; 389 | }; 390 | /* End PBXVariantGroup section */ 391 | 392 | /* Begin XCBuildConfiguration section */ 393 | AA8F639F1E787DE2000965B5 /* Debug */ = { 394 | isa = XCBuildConfiguration; 395 | buildSettings = { 396 | ALWAYS_SEARCH_USER_PATHS = NO; 397 | CLANG_ANALYZER_NONNULL = YES; 398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 399 | CLANG_CXX_LIBRARY = "libc++"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 403 | CLANG_WARN_BOOL_CONVERSION = YES; 404 | CLANG_WARN_COMMA = YES; 405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNREACHABLE_CODE = YES; 421 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 422 | CODE_SIGN_IDENTITY = "-"; 423 | COPY_PHASE_STRIP = NO; 424 | DEBUG_INFORMATION_FORMAT = dwarf; 425 | ENABLE_STRICT_OBJC_MSGSEND = YES; 426 | ENABLE_TESTABILITY = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu99; 428 | GCC_DYNAMIC_NO_PIC = NO; 429 | GCC_NO_COMMON_BLOCKS = YES; 430 | GCC_OPTIMIZATION_LEVEL = 0; 431 | GCC_PREPROCESSOR_DEFINITIONS = ( 432 | "DEBUG=1", 433 | "$(inherited)", 434 | ); 435 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 436 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 437 | GCC_WARN_UNDECLARED_SELECTOR = YES; 438 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 439 | GCC_WARN_UNUSED_FUNCTION = YES; 440 | GCC_WARN_UNUSED_VARIABLE = YES; 441 | MACOSX_DEPLOYMENT_TARGET = 10.12; 442 | MTL_ENABLE_DEBUG_INFO = YES; 443 | ONLY_ACTIVE_ARCH = YES; 444 | SDKROOT = macosx; 445 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 446 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 447 | }; 448 | name = Debug; 449 | }; 450 | AA8F63A01E787DE2000965B5 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ALWAYS_SEARCH_USER_PATHS = NO; 454 | CLANG_ANALYZER_NONNULL = YES; 455 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 456 | CLANG_CXX_LIBRARY = "libc++"; 457 | CLANG_ENABLE_MODULES = YES; 458 | CLANG_ENABLE_OBJC_ARC = YES; 459 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 460 | CLANG_WARN_BOOL_CONVERSION = YES; 461 | CLANG_WARN_COMMA = YES; 462 | CLANG_WARN_CONSTANT_CONVERSION = YES; 463 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 464 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 465 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 466 | CLANG_WARN_EMPTY_BODY = YES; 467 | CLANG_WARN_ENUM_CONVERSION = YES; 468 | CLANG_WARN_INFINITE_RECURSION = YES; 469 | CLANG_WARN_INT_CONVERSION = YES; 470 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 471 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 472 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 474 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 475 | CLANG_WARN_STRICT_PROTOTYPES = YES; 476 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 477 | CLANG_WARN_UNREACHABLE_CODE = YES; 478 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 479 | CODE_SIGN_IDENTITY = "-"; 480 | COPY_PHASE_STRIP = NO; 481 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 482 | ENABLE_NS_ASSERTIONS = NO; 483 | ENABLE_STRICT_OBJC_MSGSEND = YES; 484 | GCC_C_LANGUAGE_STANDARD = gnu99; 485 | GCC_NO_COMMON_BLOCKS = YES; 486 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 487 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 488 | GCC_WARN_UNDECLARED_SELECTOR = YES; 489 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 490 | GCC_WARN_UNUSED_FUNCTION = YES; 491 | GCC_WARN_UNUSED_VARIABLE = YES; 492 | MACOSX_DEPLOYMENT_TARGET = 10.12; 493 | MTL_ENABLE_DEBUG_INFO = NO; 494 | SDKROOT = macosx; 495 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 496 | }; 497 | name = Release; 498 | }; 499 | AA8F63A21E787DE2000965B5 /* Debug */ = { 500 | isa = XCBuildConfiguration; 501 | buildSettings = { 502 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 503 | CODE_SIGN_ENTITLEMENTS = GIF/GIF.entitlements; 504 | CODE_SIGN_IDENTITY = "Mac Developer"; 505 | COMBINE_HIDPI_IMAGES = YES; 506 | DEVELOPMENT_TEAM = ""; 507 | INFOPLIST_FILE = GIF/Info.plist; 508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 509 | PRODUCT_BUNDLE_IDENTIFIER = com.iMakezAppz.GIF; 510 | PRODUCT_NAME = "$(TARGET_NAME)"; 511 | PROVISIONING_PROFILE_SPECIFIER = ""; 512 | SWIFT_VERSION = 3.0; 513 | }; 514 | name = Debug; 515 | }; 516 | AA8F63A31E787DE2000965B5 /* Release */ = { 517 | isa = XCBuildConfiguration; 518 | buildSettings = { 519 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 520 | CODE_SIGN_ENTITLEMENTS = GIF/GIF.entitlements; 521 | CODE_SIGN_IDENTITY = "Mac Developer"; 522 | COMBINE_HIDPI_IMAGES = YES; 523 | DEVELOPMENT_TEAM = ""; 524 | INFOPLIST_FILE = GIF/Info.plist; 525 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 526 | PRODUCT_BUNDLE_IDENTIFIER = com.iMakezAppz.GIF; 527 | PRODUCT_NAME = "$(TARGET_NAME)"; 528 | PROVISIONING_PROFILE_SPECIFIER = ""; 529 | SWIFT_VERSION = 3.0; 530 | }; 531 | name = Release; 532 | }; 533 | /* End XCBuildConfiguration section */ 534 | 535 | /* Begin XCConfigurationList section */ 536 | AA8F638D1E787DE2000965B5 /* Build configuration list for PBXProject "Smart GIF Maker" */ = { 537 | isa = XCConfigurationList; 538 | buildConfigurations = ( 539 | AA8F639F1E787DE2000965B5 /* Debug */, 540 | AA8F63A01E787DE2000965B5 /* Release */, 541 | ); 542 | defaultConfigurationIsVisible = 0; 543 | defaultConfigurationName = Release; 544 | }; 545 | AA8F63A11E787DE2000965B5 /* Build configuration list for PBXNativeTarget "Smart GIF Maker" */ = { 546 | isa = XCConfigurationList; 547 | buildConfigurations = ( 548 | AA8F63A21E787DE2000965B5 /* Debug */, 549 | AA8F63A31E787DE2000965B5 /* Release */, 550 | ); 551 | defaultConfigurationIsVisible = 0; 552 | defaultConfigurationName = Release; 553 | }; 554 | /* End XCConfigurationList section */ 555 | }; 556 | rootObject = AA8F638A1E787DE2000965B5 /* Project object */; 557 | } 558 | -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/project.xcworkspace/xcuserdata/Christian.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Code/Smart GIF Maker.xcodeproj/project.xcworkspace/xcuserdata/Christian.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/xcuserdata/Christian.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 24 | 36 | 37 | 38 | 40 | 52 | 53 | 54 | 56 | 68 | 69 | 70 | 72 | 84 | 85 | 86 | 88 | 100 | 101 | 102 | 104 | 116 | 117 | 118 | 120 | 132 | 133 | 134 | 136 | 148 | 149 | 150 | 152 | 164 | 165 | 166 | 168 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/xcuserdata/Christian.xcuserdatad/xcschemes/GIF.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Code/Smart GIF Maker.xcodeproj/xcuserdata/Christian.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | GIF.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | AA8F63911E787DE2000965B5 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Misc/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Icon.png -------------------------------------------------------------------------------- /Misc/Icon.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Icon.pxm -------------------------------------------------------------------------------- /Misc/Resources/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Resources/clock.png -------------------------------------------------------------------------------- /Misc/Resources/clock.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Resources/clock.pxm -------------------------------------------------------------------------------- /Misc/Resources/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Resources/edit.png -------------------------------------------------------------------------------- /Misc/Resources/edit.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Resources/edit.pxm -------------------------------------------------------------------------------- /Misc/Resources/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Resources/trash.png -------------------------------------------------------------------------------- /Misc/Screenshots/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Screenshots/ss1.png -------------------------------------------------------------------------------- /Misc/Screenshots/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Screenshots/ss2.png -------------------------------------------------------------------------------- /Misc/Screenshots/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Screenshots/ss3.png -------------------------------------------------------------------------------- /Misc/Screenshots/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Screenshots/ss4.png -------------------------------------------------------------------------------- /Misc/Screenshots/ss5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/Screenshots/ss5.png -------------------------------------------------------------------------------- /Misc/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/test.gif -------------------------------------------------------------------------------- /Misc/testing/f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f1.png -------------------------------------------------------------------------------- /Misc/testing/f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f2.png -------------------------------------------------------------------------------- /Misc/testing/f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f3.png -------------------------------------------------------------------------------- /Misc/testing/f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f4.png -------------------------------------------------------------------------------- /Misc/testing/f5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f5.png -------------------------------------------------------------------------------- /Misc/testing/f6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f6.png -------------------------------------------------------------------------------- /Misc/testing/f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f7.png -------------------------------------------------------------------------------- /Misc/testing/f8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f8.png -------------------------------------------------------------------------------- /Misc/testing/f9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/f9.png -------------------------------------------------------------------------------- /Misc/testing/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Krillere/GIF-Maker/610d7a08b367a4a59c25ddf8a30b3d0676102eb3/Misc/testing/loading.gif -------------------------------------------------------------------------------- /Misc/version.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf830 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw11900\paperh16840\margl1440\margr1440\vieww9100\viewh5620\viewkind0 6 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 \ 9 | 2.1.2:\ 10 | - Tegn p\'e5 flere samtidig i editor\ 11 | - Stop import knap (Hvis muligt)\ 12 | - Eksporter bestemt st\'f8rrelse\ 13 | - Importer video rigtigt (FPS cap)\ 14 | - Tekst p\'e5 GIFs} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GIF-Maker 2 | Small application that allows the user to modify or create gifs. Drag and drop images to the empty frames, and drag and drop to change the order of frames. 3 | Select a frame and 'Add frame' to add the frame next to the selected. Click the image view and a file panel will open, allowing the user to select an image to insert. Click the trashbin to remove the frame. Drag one or more images from outside the app to populate the view. 4 | 5 | The current release can be downloaded from the App Store: https://itunes.apple.com/us/app/smart-gif-maker/id1216650837?l=da&ls=1&mt=12 6 | 7 | This will often be significantly older than the version on Github, but obviously be more stable than the development version on here. :-) 8 | 9 | Screenshots: 10 | ![New window](Misc/Screenshots/ss1.png) 11 | 12 | ![Loaded gif](Misc/Screenshots/ss2.png) 13 | 14 | ![New frame added to gif](Misc/Screenshots/ss3.png) 15 | 16 | ![Preview of gif being shown](Misc/Screenshots/ss4.png) 17 | 18 | ![Editing a frame](Misc/Screenshots/ss5.png) 19 | --------------------------------------------------------------------------------