├── .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 | 
11 |
12 | 
13 |
14 | 
15 |
16 | 
17 |
18 | 
19 |
--------------------------------------------------------------------------------