├── Quick Paste ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── document.png │ │ └── Contents.json │ ├── paste.imageset │ │ ├── quick-paste.png │ │ └── Contents.json │ └── stackoverflow.imageset │ │ ├── stackoverflow-white-30.png │ │ └── Contents.json ├── Quick Paste.entitlements ├── CopiedDataSource.swift ├── GlobalEventMonitor.swift ├── LocalEventMonitor.swift ├── Quick Paste.xcdatamodeld │ └── Quick Paste.xcdatamodel │ │ └── contents ├── Info.plist ├── NSFRCChangeConsolidator.swift ├── SplitViewController.swift ├── Logger.swift ├── DetailViewController.swift ├── CopiedTableViewDelegate.swift ├── DataController.swift ├── AppDelegate.swift ├── ViewController.swift └── Base.lproj │ └── Main.storyboard ├── Quick Paste.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── zhangyichi.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── Quick Paste.xcscheme └── project.pbxproj ├── .gitignore ├── README.md └── devlog.md /Quick Paste/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/AppIcon.appiconset/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardoudou/quick-paste/HEAD/Quick Paste/Assets.xcassets/AppIcon.appiconset/document.png -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/paste.imageset/quick-paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardoudou/quick-paste/HEAD/Quick Paste/Assets.xcassets/paste.imageset/quick-paste.png -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/stackoverflow.imageset/stackoverflow-white-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardoudou/quick-paste/HEAD/Quick Paste/Assets.xcassets/stackoverflow.imageset/stackoverflow-white-30.png -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Quick\ Paste.xcodeproj/project.xcworkspace/xcuserdata/zhangyichi.xcuserdatad/UserInterfaceState.xcuserstate 3 | Quick\ Paste.xcodeproj/project.xcworkspace/xcuserdata/* 4 | Quick\ Paste.xcodeproj/xcshareddata/* 5 | !Quick\ Paste.xcodeproj/xcshareddata/xcschemes -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/stackoverflow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "mac", 9 | "filename" : "stackoverflow-white-30.png", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/paste.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "mac", 9 | "filename" : "quick-paste.png", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | }, 17 | "properties" : { 18 | "template-rendering-intent" : "template" 19 | } 20 | } -------------------------------------------------------------------------------- /Quick Paste/Quick Paste.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.downloads.read-write 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/xcuserdata/zhangyichi.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Quick Paste.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3B80A20021D1C2EF003C0156 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Quick Paste/CopiedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CopiedDataSource.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 12/22/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class CopiedDataSource: NSObject, NSTableViewDataSource { 12 | var fetchedResultsController: NSFetchedResultsController! 13 | // 1/2 have to implement function to show core data in table view 14 | func numberOfRows(in tableView: NSTableView) -> Int { 15 | let count = fetchedResultsController.fetchedObjects?.count 16 | logger.log(category: .data, message: "Number of Rows: \(String(describing: count))") 17 | return count ?? 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Paste 2 | #### Manage you frequently copied items for easy access. 3 | 4 | # Usage 5 | 0. CTRL (⌃) + SHIFT (⇧) + SPACE to bring up Quick Paste or click icon in the menu bar. 6 | 1. Regular copy or generate screenshot to clipboard(CTRL (⌃) + COMMAND (⌘) + SHIFT (⇧) + 3/4/5) will be record. 7 | 2. Search record by typing any content of text or time of screenshot(HH:MM) 8 | 3. Navigate and see detail of results using UP (▴) or DOWN (▾) 9 | 4. To copy record, hit RETURN/ENTER (⏎) when record is focused or click the record, or use COMMAND (⌘) + 1~6 shortcut. 10 | 5. To delete the record, press DELETE (⌫) when record is focused, click Clear will delete all the records. 11 | -------------------------------------------------------------------------------- /Quick Paste/GlobalEventMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalEventMonitor.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 10/21/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | public class GlobalEventMonitor{ 13 | private let mask: NSEvent.EventTypeMask 14 | private let handler : (NSEvent?)->() 15 | private var monitor: Any? 16 | 17 | public init(mask: NSEvent.EventTypeMask, handler: @escaping(NSEvent?)->()){ 18 | self.mask = mask 19 | self.handler = handler 20 | } 21 | deinit { 22 | stop() 23 | } 24 | public func start(){ 25 | monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) 26 | } 27 | public func stop(){ 28 | if monitor != nil{ 29 | NSEvent.removeMonitor(monitor!) 30 | monitor = nil 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Quick Paste/LocalEventMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventMonitor.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 10/19/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | public class LocalEventMonitor{ 13 | private var monitor: Any? 14 | private let mask: NSEvent.EventTypeMask 15 | private let handler: (NSEvent?) -> NSEvent 16 | 17 | public init (mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> NSEvent){ 18 | self.mask = mask 19 | self.handler = handler 20 | } 21 | 22 | deinit { 23 | stop() 24 | } 25 | 26 | public func start(){ 27 | monitor = NSEvent.addLocalMonitorForEvents(matching: mask, handler: handler) 28 | } 29 | 30 | public func stop(){ 31 | if monitor != nil { 32 | NSEvent.removeMonitor(monitor!) 33 | monitor = nil 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Quick Paste/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "size" : "512x512", 45 | "idiom" : "mac", 46 | "filename" : "document.png", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "idiom" : "mac", 51 | "size" : "512x512", 52 | "scale" : "2x" 53 | } 54 | ], 55 | "info" : { 56 | "version" : 1, 57 | "author" : "xcode" 58 | } 59 | } -------------------------------------------------------------------------------- /Quick Paste/Quick Paste.xcdatamodeld/Quick Paste.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Quick Paste/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSApplicationCategoryType 22 | public.app-category.utilities 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSHumanReadableCopyright 28 | Copyright © 2018 张壹弛. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | CFBundleURLTypes 34 | 35 | 36 | CFBundleTypeRole 37 | Viewer 38 | CFBundleURLName 39 | com.wolfPack.Quick-Paste 40 | CFBundleURLSchemes 41 | 42 | readlog 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Quick Paste/NSFRCChangeConsolidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSFRCChangeConsolidator.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 12/4/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | class NSFRCChangeConsolidator: NSObject { 13 | private var rowDeletes: [IndexPath] = [] 14 | private var rowInserts: [IndexPath] = [] 15 | private var rowUpdates: [IndexPath] = [] 16 | 17 | func ingestItemChange(ofType changeType: NSFetchedResultsChangeType, oldIndexPath: IndexPath?, newIndexPath: IndexPath?) { 18 | switch changeType { 19 | case .insert: 20 | self.rowInserts.append(newIndexPath!) 21 | case .delete: 22 | self.rowDeletes.append(oldIndexPath!) 23 | case .move: 24 | self.rowDeletes.append(oldIndexPath!) 25 | self.rowInserts.append(newIndexPath!) 26 | case .update: 27 | self.rowUpdates.append(newIndexPath!) 28 | @unknown default: 29 | logger.log(category: .app, message: "Unknown change type") 30 | fatalError("Unknown change type") 31 | } 32 | } 33 | 34 | // Reverse-sorted row deletes, suitable for feeding into table views. 35 | func sortedRowDeletes() -> [IndexPath] { 36 | return rowDeletes.sorted { $0.item > $1.item } 37 | } 38 | 39 | // Sorted row inserts, suitable for feeding into table views. 40 | func sortedRowInserts() -> [IndexPath] { 41 | return rowInserts.sorted { $0.item < $1.item } 42 | } 43 | 44 | // Sorted row updates, suitable for feeding into table views. 45 | func sortedRowUpdates() -> [IndexPath] { 46 | return rowUpdates.sorted { $0.item < $1.item } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Quick Paste/SplitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitViewController.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 1/2/20. 6 | // Copyright © 2020 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SplitViewController: NSSplitViewController { 12 | @IBOutlet weak var viewItem: NSSplitViewItem! 13 | @IBOutlet weak var detailViewItem: NSSplitViewItem! 14 | override func viewDidLoad() { 15 | logger.log(category: .ui, message: "inside viewDidLoad of SplitViewController:\(self)") 16 | super.viewDidLoad() 17 | logger.log(category: .ui, message: "super is \(super.className)") 18 | logger.log(category: .ui, message: "after SplitViewController super.viewDidLoad") 19 | logger.log(category: .ui, message: "children are \(self.children)") 20 | // Do view setup here. 21 | if let viewController = viewItem.viewController as? ViewController{ 22 | if let detailViewController = detailViewItem.viewController as? DetailViewController { 23 | viewController.detailViewController = detailViewController 24 | detailViewController.viewController = viewController 25 | //instead of create direct reference to tableViewDelegate, indirectly use viewController's property as tableViewDelegate is instantiated in viewController 26 | viewController.tableViewDelegate.detailViewController = detailViewController 27 | logger.log(category: .app, message: "viewController.tableViewDelegate.detailViewController: \(String(describing: viewController.tableViewDelegate.detailViewController))") 28 | } 29 | } 30 | } 31 | 32 | } 33 | extension NSSplitViewController { 34 | // MARK: Storyboard instantiation 35 | static func freshController() -> SplitViewController { 36 | //1. 37 | logger.log(category: .app, message: "instantiating Main storyboard") 38 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 39 | //2. 40 | logger.log(category: .app, message: "instantiating SplitViewController") 41 | let identifier = NSStoryboard.SceneIdentifier("SplitViewController") 42 | //3. 43 | guard let splitViewController = storyboard.instantiateController(withIdentifier: identifier) as? SplitViewController else { 44 | logger.log(category: .app, message: "Why cant i find SplitViewController? - Check Main.storyboard", type: .error) 45 | fatalError("Why cant i find SplitViewController? - Check Main.storyboard") 46 | } 47 | logger.log(category: .app, message: "\(splitViewController) is instantiated") 48 | return splitViewController 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Quick Paste/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 2/28/20. 6 | // Copyright © 2020 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import os 11 | 12 | class Logger: NSObject { 13 | 14 | enum Category: String{ 15 | case app 16 | case event 17 | case ui 18 | case data 19 | } 20 | enum AccessLevel{ 21 | //to use reserved word as identifier, wrap it with backtick 22 | case `private` 23 | case `public` 24 | } 25 | private func createOSLog(category: Category) -> OSLog{ 26 | return OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-" , category: category.rawValue) 27 | } 28 | /// Returns current thread name 29 | private var currentThread: String { 30 | if Thread.isMainThread { 31 | return "main" 32 | } else { 33 | if let threadName = Thread.current.name, !threadName.isEmpty { 34 | return"\(threadName)" 35 | } else if let queueName = String(validatingUTF8: __dispatch_queue_get_label(nil)), !queueName.isEmpty { 36 | return"\(queueName)" 37 | } else { 38 | return String(format: "%p", Thread.current) 39 | } 40 | } 41 | } 42 | func log(category: Logger.Category, message: String, access: Logger.AccessLevel = .private, type: OSLogType = .debug, file: String = #file, function: String = #function, line: Int = #line){ 43 | //default file parameter to #file.basename in signature would let all log file portion showing as Looger.swift [confuse] 44 | let file = file.basename 45 | let line = String(line) 46 | switch access{ 47 | case .private: 48 | os_log("[%{private}@:%{private}@ %{private}@ %{private}@] | %{private}@", log: createOSLog(category: category), type: type, currentThread, file, function, line, message) 49 | case .public: 50 | os_log("[%{public}@:%{private}@ %{public}@ %{public}@] | %{public}@", log: createOSLog(category: category), type: type, currentThread, file, function, line, message) 51 | } 52 | } 53 | 54 | } 55 | extension String{ 56 | var fileURL: URL { 57 | return URL(fileURLWithPath: self) 58 | } 59 | var basename: String{ 60 | return fileURL.lastPathComponent 61 | } 62 | public var fourCharCodeValue: Int { 63 | var result: Int = 0 64 | if let data = self.data(using: String.Encoding.macOSRoman) { 65 | data.withUnsafeBytes({ (rawBytes) in 66 | let bytes = rawBytes.bindMemory(to: UInt8.self) 67 | for i in 0 ..< data.count { 68 | result = result << 8 + Int(bytes[i]) 69 | } 70 | }) 71 | } 72 | return result 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/xcshareddata/xcschemes/Quick Paste.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /devlog.md: -------------------------------------------------------------------------------- 1 | # Quick Paste 2 | #### Manage you frequently copied items for easy access. 3 | 4 | ## Current 5 | 1. copy history 6 | 2. url scheme support 7 | 3. filter history by name 8 | 4. record screenshot copy 9 | 5. shortcut paste top 6 record 10 | 6. delete single record 11 | 7. show either image or text 12 | 8. shortcut launch 13 | 9. copy when return on focus item 14 | 10. clear all 15 | 11. detail view of text and screenshot 16 | 17 | ## To Do 18 | - [x] key binding, menu seperator 19 | - [x] support file 20 | - [x] fix custom url scheme crash 21 | - [x] clean file representation 22 | - [x] binded item searchable 23 | - [x] file icon 24 | - [x] click outside window hide 25 | - [x] onPasteboardChange() catch all the copy activity 26 | - [x] table should update automatically after persistent container update 27 | - [x] support in-app naive search 28 | - [x] entry on select could be copied(reuse copyIt) 29 | - [x] retrieve object based on tableview.index, especially when after apply search(filtered history row index has nothing to do with id, temp copied array) 30 | - [ ] move clear and quit to menubar 31 | - [ ] show six record 32 | - [ ] align image in middle 33 | - [ ] resize menu to show at least six items 34 | - [ ] fix file display 35 | 36 | ## Project Structure 37 | - [x] naive refactor ViewController 38 | - [x] use core data managed object and corresponding class(currently no subclass) 39 | - [x] show history in popover and table instead of status bar menu 40 | ## Feature 41 | - [x] remove copied activity record from history 42 | - [x] record screenshot stored to clipboard.(Default is cmd+shift+3+ctrl and cmd+shift+4+ctrl) 43 | - [x] local shortcut paste using cmd+1~6 44 | - [x] show detail preview on side 45 | ## Issue 46 | - [x] avoid save most recent copied since last close, which end up duplicate search history. (use property firstTime check) 47 | - [x] copy it should not add new record, avoid this type of changeCount being trigger (set lastChangeCount same to changeCount) 48 | - [x] right after insert new history record click the history, not the same history record when paste it.(after each update, the tableview should be consistent with copieds) 49 | - [x] if text copied, even default image is set to so icon, but the image won't showup until certain type of searching being performed or relaunch(a real old bug) 50 | - [x] funk sound when hit global shortcut 51 | ## Dev history 52 | 1. The initial purpose resume parsing, menu would look good only in this [resume](https://www.dropbox.com/s/8r6wm7d8t45pmsc/2019_Resume_Yichi_Zhang.pdf?dl=0) format. 53 | 2. Key equavilant support from 1 to 9 54 | 3. Support custom url scheme, run `open "readlog://textwanttosend"`in terminal would copy the text to menu. 55 | 4. Support local file and folder 56 | 5. Log copy from mac and ios devices 57 | 6. Support search binded item in spotlight 58 | 7. Show file icon in menu(not that useful since most file has extension shown, mainly for further image related feature) 59 | 8. Add data controller which working with core data persistent container to memorize previous history after relaunch app 60 | 9. Use popover and table view instead of status bar menu for furtuher layout customization 61 | 10. Use NSFetchedResultsController along with NSTableViewDelegate and NSTableViewDataSource showing data in table view 62 | 11. Able to track most copy activities 63 | 12. Change copy->bindIt(create object) logic to copy(automatically create object) 64 | 13. Support naive search in search field with NSSearchFieldDelegate(predicate update, fetch and reload) 65 | 14. On click copy to pasteboard 66 | 15. Record screenshots directly into clipboard 67 | 16. Support local shortcut paste top 6 shown item 68 | 17. Show either image or text, not both. 69 | 18. Dynamic height of text 70 | 19. shortcut launch 71 | 20. copy when return on focus item 72 | 21. clear all record 73 | 22. source placeholder 74 | 23. separate data source and tableView delegate from viewcontroller 75 | 24. enhance focus behavior(focus on same record when reopen popover & focus remain nearby after deletion) 76 | 25. little visual improvement(enlarge visual assets, exchange column order) 77 | 26. set up splitview(view present) 78 | 27. connecet between viewcontroller and detailViewController 79 | 28. fix deletion crash after connecting(27) 80 | 29. fix copyOnEnter without menu showing up 81 | 30. showing content in detailViewController 82 | 31. mute funk sound 83 | ## Inspiration (Why yet another pasteboard manager) 84 | Copy paste repeatedly is tedious. It's even less interesting when you forget what you copy. 85 | Paid pasteboard manager app provide really complicated use scenario, which I barely used. Among all the open source alternatives, none of them support screenshot, which heavily involved in my daily documentation and note-taking. -------------------------------------------------------------------------------- /Quick Paste/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 1/2/20. 6 | // Copyright © 2020 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DetailViewController: NSViewController { 12 | //both viewController and copied need to be reference outside the class, so can't be private 13 | var viewController: ViewController! 14 | var copied: Copied! 15 | private var scrollView: NSScrollView! 16 | private var imageView:NSImageView! 17 | private var textView:NSTextView! 18 | 19 | override func viewDidLoad() { 20 | logger.log(category: .ui, message: "inside viewDidLoad of DetailViewController:\(self)") 21 | super.viewDidLoad() 22 | logger.log(category: .ui, message: "super is \(super.className)") 23 | logger.log(category: .ui, message: "after DetailViewController super.viewDidLoad") 24 | logger.log(category: .ui, message: "children are \(self.children)") 25 | // Do view setup here. 26 | imageView = NSImageView() 27 | textView = NSTextView() 28 | scrollView = NSScrollView() 29 | textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 30 | textView.autoresizingMask = .width 31 | textView.isVerticallyResizable = true 32 | textView.textContainer?.widthTracksTextView = true 33 | 34 | let DefaultAttribute = 35 | [NSAttributedString.Key.foregroundColor: NSColor.textColor] as [NSAttributedString.Key: Any] 36 | let attributeString = NSAttributedString(string: "hello world", attributes: DefaultAttribute) 37 | // textView.textStorage?.append(attributeString) 38 | textView.textStorage?.setAttributedString(attributeString) 39 | 40 | view.addSubview(scrollView) 41 | scrollView.translatesAutoresizingMaskIntoConstraints = false 42 | scrollView.documentView = textView 43 | 44 | NSLayoutConstraint.activate([ 45 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 46 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 47 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 48 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 49 | ]) 50 | logger.log(category: .ui, message: "view:\(view.subviews)") 51 | let testScrollView: NSScrollView = view.subviews[0] as! NSScrollView 52 | logger.log(category: .ui, message: "testScrollView.documentView: \(String(describing: testScrollView.documentView))") 53 | } 54 | // static func freshController() -> DetailViewController { 55 | // //1. 56 | // let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 57 | // print("inside detailviewcontroller freshcontroller") 58 | // //2. 59 | // let identifier = NSStoryboard.SceneIdentifier("DetailViewController") 60 | // //3.use this controller class as casting type, not ViewController !! 61 | // guard let detailviewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? DetailViewController else { 62 | // fatalError("Why cant i find detailViewController? - Check Main.storyboard") 63 | // } 64 | // print("detailviewcontroller:\(detailviewcontroller)") 65 | // return detailviewcontroller 66 | // } 67 | 68 | func getCopiedFromLeft(){ 69 | logger.log(category: .app, message: "inside getCopiedFromLeft():") 70 | logger.log(category: .app, message: "copied: \(String(describing: copied))") 71 | } 72 | 73 | func showImageDetail(){ 74 | if(copied != nil){ 75 | var imageRect: NSRect 76 | imageView.image = NSImage(data: copied.thumbnail!) 77 | //imageView's FrameSize should be it superView size, then scale could work 78 | imageRect = NSMakeRect(0.0, 0.0, scrollView.frame.size.width, scrollView.frame.size.height) 79 | imageView.setFrameSize(CGSize(width: imageRect.width, height: imageRect.height)) 80 | imageView.imageScaling = NSImageScaling.scaleProportionallyDown 81 | scrollView.allowsMagnification = true 82 | scrollView.documentView = imageView 83 | } 84 | } 85 | 86 | func showTextDetail(){ 87 | if(copied != nil){ 88 | // textView = NSTextView(frame: <#T##NSRect#>, textContainer: <#T##NSTextContainer?#>) 89 | let DefaultAttribute = 90 | [NSAttributedString.Key.foregroundColor: NSColor.textColor] as [NSAttributedString.Key: Any] 91 | let attributeString = NSAttributedString(string: copied.name!, attributes: DefaultAttribute) 92 | // textView.textStorage?.append(attributeString) 93 | // textView.string = copied.name! 94 | textView.textStorage?.setAttributedString(attributeString) 95 | //no need set again 96 | scrollView.documentView = textView 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Quick Paste/CopiedTableViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CopiedTableViewDelegate.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 12/24/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class CopiedTableViewDelegate: NSObject, NSTableViewDelegate { 12 | var fetchedResultsController: NSFetchedResultsController! 13 | //both tableView and detailViewController reference are have-to have to allow detailViewVC work, 14 | //and yet both refence instance are instantiated in other file, so timing to point to those reference is critical. 15 | //eg: tableView is realy when ViewController's view is really, so reference to tableView could done in viewDidLoad of ViewController, 16 | //while detailViewController won't be ready until detialViewController is instantiated, so safe place to referencing is in detailViewController's viewDidLoad or splitViewController viewDidLoad 17 | //as splitViewController's view is parent view of detailVC'view and viewController's view, so it's view won't be ready until its children view ready 18 | var tableView: NSTableView! 19 | var detailViewController: DetailViewController! 20 | func tableViewSelectionDidChange(_ notification: Notification) { 21 | let scrollView: NSScrollView = detailViewController.view.subviews[0] as! NSScrollView 22 | scrollView.magnification = 1.0 23 | logger.log(category: .app, message: "inside tableViewSelectionDidChange") 24 | logger.log(category: .ui, message: "tableView.selectedRow: \(tableView.selectedRow)") 25 | logger.log(category: .app , message: "detailViewController: \(String(describing: detailViewController))") 26 | detailViewController.getCopiedFromLeft() 27 | logger.log(category: .app, message: "before passing data to detailViewController") 28 | if(tableView.selectedRow == -1){return} 29 | if let copied = fetchedResultsController.fetchedObjects![tableView.selectedRow] as? Copied{ 30 | detailViewController.copied = copied 31 | if(copied.thumbnail == nil){ 32 | detailViewController.showTextDetail() 33 | }else{ 34 | detailViewController.showImageDetail() 35 | } 36 | } 37 | detailViewController.getCopiedFromLeft() 38 | } 39 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 40 | // if let copied: Copied = fetchedResultsController.fetchedObjects![row] as? Copied{ 41 | // if let thumbnail = copied.thumbnail as NSData?{ 42 | // // let tv: NSImageView = NSImageView(image: NSImage(data: copied.thumbnail!)!) 43 | // // let someWidth: CGFloat = tableView.frame.size.width 44 | // // let frame: NSRect = NSMakeRect(0, 0, someWidth, CGFloat.greatestFiniteMagnitude) 45 | // // let tv: NSImageView = NSImageView(frame: frame) 46 | // // print("Before sizeToFit\(tv.frame.size.height)") 47 | // // tv.sizeToFit() 48 | // // print("After sizeToFit\(tv.frame.size.height)") 49 | // return 70 50 | // } 51 | // if let string: String = copied.name{ 52 | // let someWidth: CGFloat = tableView.frame.size.width 53 | // let stringAttributes = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12)] //change to font/size u are using 54 | // let attrString: NSAttributedString = NSAttributedString(string: string, attributes: stringAttributes) 55 | // let frame: NSRect = NSMakeRect(0, 0, someWidth, CGFloat.greatestFiniteMagnitude) 56 | // let tv: NSTextView = NSTextView(frame: frame) 57 | // tv.textStorage?.setAttributedString(attrString) 58 | // tv.isHorizontallyResizable = false 59 | // tv.sizeToFit() 60 | // let height: CGFloat = tv.frame.size.height + 17 // + other objects... 61 | // return height 62 | // } 63 | // } 64 | return 17 65 | } 66 | // 2/2 have to implement function to show core data in table view 67 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 68 | var cellIdentifier: String = "" 69 | // var cell: NSTableCellView! 70 | //probably should guard 71 | let copied :Copied = fetchedResultsController.fetchedObjects![row] 72 | let column = tableView.tableColumns.firstIndex(of: tableColumn!)! 73 | switch column{ 74 | case 0: 75 | cellIdentifier = "DeviceCellId" 76 | case 1: 77 | cellIdentifier = "NameCellId" 78 | case 2: 79 | cellIdentifier = "TimeCellId" 80 | default: 81 | return nil 82 | } 83 | if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView{ 84 | configureCell(cell: cell, row: row, column: column) 85 | //pure text hide image 86 | if(copied.thumbnail == nil){ 87 | if(column == 1){ 88 | cell.imageView?.isHidden = true 89 | cell.textField?.isHidden = false 90 | } 91 | //if have image type data, hide text 92 | }else{ 93 | if(column == 1){ 94 | cell.textField?.isHidden = false 95 | cell.imageView?.isHidden = true 96 | } 97 | } 98 | return cell 99 | } 100 | return nil 101 | } 102 | func configureCell(cell: NSTableCellView, row: Int, column: Int){ 103 | var image: NSImage? 104 | var name: String? 105 | var time: String? 106 | var device: String? 107 | var text: String? 108 | let dateFormatter = DateFormatter() 109 | let copied = fetchedResultsController.fetchedObjects![row] 110 | switch column { 111 | case 0: 112 | device = String(copied.id) 113 | if let deviceNonOptional = copied.device{ 114 | device = deviceNonOptional == "mac" ? "🖥" : "📱" 115 | } 116 | text = device 117 | case 1: 118 | image = copied.thumbnail != nil ? NSImage(data: copied.thumbnail!) : NSImage(named: "stackoverflow") 119 | name = copied.name 120 | text = name 121 | case 2: 122 | dateFormatter.timeStyle = .medium 123 | time = copied.timestamp != nil ? dateFormatter.string(from: copied.timestamp!) : "notime" 124 | text = time 125 | default: 126 | break 127 | } 128 | cell.textField?.stringValue = text != nil ? text! : "default" 129 | cell.imageView?.image = image 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Quick Paste/DataController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateController.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 10/5/19. 6 | // Copyright © 2019 张壹弛. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import Cocoa 12 | 13 | public class DataController: NSObject{ 14 | // MARK: - Core Data stack 15 | 16 | lazy var persistentContainer: NSPersistentContainer = { 17 | /* 18 | The persistent container for the application. This implementation 19 | creates and returns a container, having loaded the store for the 20 | application to it. This property is optional since there are legitimate 21 | error conditions that could cause the creation of the store to fail. 22 | */ 23 | let container = NSPersistentContainer(name: "Quick Paste") 24 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 25 | if let error = error { 26 | // Replace this implementation with code to handle the error appropriately. 27 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 28 | 29 | /* 30 | Typical reasons for an error here include: 31 | * The parent directory does not exist, cannot be created, or disallows writing. 32 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 33 | * The device is out of space. 34 | * The store could not be migrated to the current model version. 35 | Check the error message to determine what the actual problem was. 36 | */ 37 | logger.log(category: .data, message: "Unresolved error \(error)") 38 | fatalError("Unresolved error \(error)") 39 | } 40 | }) 41 | return container 42 | }() 43 | //this stored property doesn't make sense, it make persistentContainer declared as lazy var no used 44 | var context: NSManagedObjectContext { 45 | return persistentContainer.viewContext 46 | } 47 | // MARK: - Core Data Saving support 48 | func saveContext () { 49 | let context = persistentContainer.viewContext 50 | if !context.commitEditing() { 51 | logger.log(category: .data, message: "\(NSStringFromClass(type(of: self))) unable to commit editing before saving") 52 | } 53 | if context.hasChanges { 54 | do { 55 | try context.save() 56 | } catch { 57 | // Customize this code block to include application-specific recovery steps. 58 | let nserror = error as NSError 59 | NSApplication.shared.presentError(nserror) 60 | print(nserror.userInfo) 61 | } 62 | } 63 | } 64 | //path is default param(could be omit), data is optional 65 | public func createCopied(id: Int, title: String, path: String = "", type: String, data: Data? = nil, timestamp: Date, device: String = "mac"){ 66 | let copied = NSEntityDescription.insertNewObject(forEntityName: "Copied", into: context) as! Copied 67 | copied.id = Int64(id) 68 | copied.name = title 69 | copied.path = path 70 | copied.thumbnail = data 71 | copied.timestamp = timestamp 72 | copied.type = type 73 | copied.device = device 74 | logger.log(category: .data, message: "copied object \(copied.id) is set") 75 | do{ 76 | try context.save() 77 | logger.log(category: .data, message: "✅ Copied saved successfully") 78 | let defaults = UserDefaults.standard 79 | defaults.set(String(id+1), forKey: "maxId") 80 | }catch let error{ 81 | logger.log(category: .data, message: "❌ Failed to create Copied \(error.localizedDescription) ", type: .error) 82 | } 83 | } 84 | public func removeCopied(item: Copied){ 85 | do{ 86 | context.delete(item) 87 | logger.log(category: .data, message: "✅ Copied \(item.id) removed successfully") 88 | }catch let error{ 89 | logger.log(category: .data, message: "❌ Failed to remove Copied \(error.localizedDescription) ") 90 | } 91 | } 92 | public func deleteAll(){ 93 | logger.log(category: .app, message: "-------- begin delete All --------") 94 | let fetchRequest = NSFetchRequest(entityName: "Copied") 95 | fetchRequest.predicate = NSPredicate(format: "id>%lld", Int64(-1)) 96 | let batchRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 97 | batchRequest.resultType = NSBatchDeleteRequestResultType.resultTypeObjectIDs 98 | let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 99 | privateMOC.parent = context 100 | privateMOC.perform(){ 101 | do{ 102 | let res = try privateMOC.execute(batchRequest) as? NSBatchDeleteResult 103 | let objectIDArray = res?.result as? [NSManagedObjectID] 104 | // dump(objectIDArray) 105 | let changes = [NSDeletedObjectsKey : objectIDArray] 106 | // dump(changes) 107 | // try privateMOC.save() 108 | //avoid starting with non-zero id after clear, set maxId to "" right after batchDelete execute 109 | let defaults = UserDefaults.standard 110 | defaults.set("", forKey: "maxId") 111 | self.context.performAndWait { 112 | do{ 113 | NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self.context]) 114 | try self.context.save() 115 | }catch let error{ 116 | logger.log(category: .data, message: "❌ Failed to merge changes or context on main queue fail to save: \(error)") 117 | } 118 | } 119 | }catch let error{ 120 | logger.log(category: .data, message: "❌ Failed to batch delete or private queue fail to save: \(error)") 121 | } 122 | } 123 | logger.log(category: .app, message: "-------- finish delete All --------") 124 | } 125 | 126 | public func fetch(id: Int)->Copied?{ 127 | // construct fetchRequest 128 | let fetchRequest = NSFetchRequest(entityName: "Copied") 129 | // use predicate filter fetchRequest 130 | fetchRequest.predicate = NSPredicate(format: "id==%lld", Int64(id) ) 131 | var res:Copied? 132 | do{ 133 | //even if no record fetched the result set is not nil 134 | let copieds = try context.fetch(fetchRequest) 135 | guard copieds.count > 0 else{ 136 | return res 137 | } 138 | print("\(copieds[0].id)") 139 | print("\(copieds[0].name ?? "NAME")") 140 | print("\(copieds[0].path ?? "PATH")") 141 | print("\(copieds[0].timestamp ?? Date.init(timeIntervalSince1970: 1))") 142 | print("\(copieds[0].type ?? "TYPE" ) ") 143 | res = copieds[0] 144 | }catch let error{ 145 | logger.log(category: .data, message: "❌ Failed to fetch Copied: \(error)") 146 | } 147 | return res 148 | } 149 | 150 | } 151 | 152 | 153 | -------------------------------------------------------------------------------- /Quick Paste/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 12/24/18. 6 | // Copyright © 2018 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import CoreSpotlight 11 | import Carbon 12 | 13 | let logger: Logger = { 14 | return Logger() 15 | }() 16 | 17 | @NSApplicationMain 18 | class AppDelegate: NSObject, NSApplicationDelegate { 19 | 20 | var item : NSStatusItem? = nil 21 | let menu = NSMenu() 22 | var index = 1 23 | var firstTime = true 24 | var firstParenthesisEntry = true 25 | var maxCharacterSize = 255 26 | let preferTypes: [NSPasteboard.PasteboardType] = [NSPasteboard.PasteboardType.init("public.file-url"),NSPasteboard.PasteboardType.init("public.utf8-plain-text"),NSPasteboard.PasteboardType.init("public.png")] 27 | let mobileTypes: [NSPasteboard.PasteboardType] = [NSPasteboard.PasteboardType.init("iOS rich content paste pasteboard type"), NSPasteboard.PasteboardType.init("com.apple.mobilemail.attachment-ids"),NSPasteboard.PasteboardType.init("com.apple.is-remote-clipboard")] 28 | var timer: Timer! 29 | var lastChangeCount: Int = 0 30 | let pasteboard = NSPasteboard.general 31 | var dataController: DataController! 32 | let popover = NSPopover() 33 | let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) 34 | var splitViewController: SplitViewController! = SplitViewController.freshController() 35 | var viewController: ViewController! 36 | lazy var localEventMonitor : LocalEventMonitor = LocalEventMonitor(mask: .keyDown){[weak self] 37 | event in 38 | if let strongSelf = self { 39 | logger.log(category: .event, message: "localEvent") 40 | strongSelf.viewController.keyDown(with: event!) 41 | logger.log(category: .event, message: "localEvent") 42 | } 43 | return event! 44 | } 45 | lazy var globalEventMonitor: GlobalEventMonitor = GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown]){ [weak self] 46 | event in 47 | if let strongSelf = self, strongSelf.popover.isShown { 48 | logger.log(category: .event, message: "globalEvent") 49 | strongSelf.closePopover(sender: event) 50 | logger.log(category: .event, message: "globalEvent") 51 | } 52 | } 53 | 54 | func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 { 55 | let flags = cocoaFlags.rawValue 56 | var newFlags: Int = 0 57 | if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) { 58 | newFlags |= controlKey 59 | } 60 | if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) { 61 | newFlags |= cmdKey 62 | } 63 | if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) { 64 | newFlags |= shiftKey; 65 | } 66 | if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) { 67 | newFlags |= optionKey 68 | } 69 | if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) { 70 | newFlags |= alphaLock 71 | } 72 | return UInt32(newFlags); 73 | } 74 | 75 | func register() { 76 | var hotKeyRef: EventHotKeyRef? 77 | let modifierFlags: UInt32 = getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.init(rawValue: NSEvent.ModifierFlags.shift.rawValue + NSEvent.ModifierFlags.control.rawValue)) 78 | let keyCode = kVK_Space 79 | var gMyHotKeyID = EventHotKeyID() 80 | 81 | gMyHotKeyID.id = UInt32(keyCode) 82 | 83 | // Not sure what "swat" vs "htk1" do. 84 | gMyHotKeyID.signature = OSType("swat".fourCharCodeValue) 85 | // gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue) 86 | 87 | var eventType = EventTypeSpec() 88 | eventType.eventClass = OSType(kEventClassKeyboard) 89 | eventType.eventKind = OSType(kEventHotKeyReleased) 90 | 91 | let observer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) 92 | // Install handler. 93 | InstallEventHandler(GetApplicationEventTarget(), { 94 | (nextHanlder, theEvent, observer) -> OSStatus in 95 | // var hkCom = EventHotKeyID() 96 | let mySelf = Unmanaged.fromOpaque(observer!).takeUnretainedValue() 97 | mySelf.togglePopover(mySelf.statusItem.button) 98 | // GetEventParameter(theEvent, 99 | // EventParamName(kEventParamDirectObject), 100 | // EventParamType(typeEventHotKeyID), 101 | // nil, 102 | // MemoryLayout.size, 103 | // nil, 104 | // &hkCom) 105 | 106 | // print("Shift + space Released!") 107 | logger.log(category: .event, message: "Shift + Control + space Released!") 108 | return noErr 109 | /// Check that hkCom in indeed your hotkey ID and handle it. 110 | }, 1, &eventType, observer, nil) 111 | 112 | // Register hotkey. 113 | let status = RegisterEventHotKey(UInt32(keyCode), 114 | modifierFlags, 115 | gMyHotKeyID, 116 | GetApplicationEventTarget(), 117 | 0, 118 | &hotKeyRef) 119 | assert(status == noErr) 120 | } 121 | 122 | func applicationDidFinishLaunching(_ aNotification: Notification) { 123 | // Insert code here to initialize your application 124 | // buildMenu() 125 | if let button = statusItem.button { 126 | button.image = NSImage(named:"paste") 127 | button.action = #selector(togglePopover) 128 | } 129 | logger.log(category: .app, message: "Appdelegate property has been initialized. SplitViewItems:\(splitViewController.splitViewItems)") 130 | viewController = splitViewController.viewItem.viewController as? ViewController 131 | logger.log(category: .app, message: "initializing app's datacontroller") 132 | dataController = DataController() 133 | logger.log(category: .app, message: "App's datacontroller: \(String(describing: dataController)) has been initialized") 134 | //grab the view controller and pass a Persistent Container Reference to a View Controller 135 | viewController.dataController = self.dataController 136 | logger.log(category: .app, message: "ViewController's datacontroller is setting to \(String(describing: viewController.dataController))") 137 | //bind popover's contentViewController to splitViewController 138 | popover.contentViewController = splitViewController 139 | //url scheme 140 | NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(self.handleAppleEvent(event:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) 141 | //add pasteboard observer/listener to notification center, center will post onPasteboardChanged when be notified 142 | NotificationCenter.default.addObserver(self, selector: #selector(onPasteboardChanged(_:)), name: .NSPasteBoardDidChange, object: nil) 143 | //the interval 0.05 0.1 doesn't help, system unlikey trigger it at precisely 0.05 0.1 second intervals, system reserve the right 144 | //not sure if using selector would help, so far 1 sec is somehow robust to record all the copy 145 | timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (t) in 146 | //print("\(self.lastChangeCount) vs \(self.pasteboard.changeCount)") 147 | if self.lastChangeCount != self.pasteboard.changeCount { 148 | self.lastChangeCount = self.pasteboard.changeCount 149 | //no sure when changeCount would be reset to 0 by system, better not use lastChangeCount as check 150 | if !self.firstTime{ 151 | NotificationCenter.default.post(name: .NSPasteBoardDidChange, object: self.pasteboard) 152 | } 153 | if self.firstTime{ 154 | self.firstTime = false 155 | } 156 | } 157 | } 158 | let defaults = UserDefaults.standard 159 | let defaultValue = ["maxId" : ""] 160 | defaults.register(defaults: defaultValue) 161 | register() 162 | togglePopover(statusItem.button) 163 | } 164 | 165 | @objc func togglePopover(_ sender: Any?) { 166 | if popover.isShown { 167 | closePopover(sender: sender) 168 | } else { 169 | showPopover(sender: sender) 170 | } 171 | } 172 | 173 | func showPopover(sender: Any?) { 174 | if let button = statusItem.button { 175 | logger.log(category: .app, message: "-------- initializing popover --------") 176 | logger.log(category: .app, message: "preparing popover contentviecontroller's view and its children views") 177 | //before actually execute popover.show, first need to make sure popover.contentViewController's view didLoad, which also involve child view's didLoad 178 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 179 | } 180 | logger.log(category: .app, message: "-------- popover is ready --------") 181 | localEventMonitor.start() 182 | globalEventMonitor.start() 183 | } 184 | 185 | func closePopover(sender: Any?) { 186 | popover.performClose(sender) 187 | localEventMonitor.stop() 188 | globalEventMonitor.stop() 189 | } 190 | 191 | func applicationWillTerminate(_ aNotification: Notification) { 192 | // Insert code here to tear down your application 193 | timer.invalidate() 194 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 195 | // Saves changes in the application's managed object context before the application terminates. 196 | dataController.saveContext() 197 | } 198 | 199 | 200 | 201 | //paste board changed handler 202 | @objc func onPasteboardChanged(_ notification: Notification){ 203 | guard let pb = notification.object as? NSPasteboard else { return } 204 | guard let items = pb.pasteboardItems else { return } 205 | //only copy screenshot text and file, fix unsupport copy type unwrap nil crash 206 | if let preferType = items.first?.availableType(from: preferTypes){ 207 | logger.log(category: .app, message: "NSPasteBoardDidChange with incoming record type of '\(preferType)'") 208 | //index copy event 209 | bindIt() 210 | }else{ 211 | return 212 | } 213 | } 214 | 215 | 216 | //status bar menu 217 | @objc func bindIt(){ 218 | logger.log(category: .app, message: "-------- start binding --------") 219 | printPasteBoard() 220 | let items = NSPasteboard.general.pasteboardItems! 221 | if items.count == 0{ 222 | return 223 | } 224 | if(firstTime){ 225 | firstTime = false 226 | } 227 | var path: String 228 | var data: Data 229 | var title: String 230 | for item in items{ 231 | //retrieve id from UserDefault which persistent after relaunch, avoid fetch multiple object associated with same id 232 | let defaults = UserDefaults.standard 233 | let id = defaults.string(forKey: "maxId") == "" ? 0 : Int(defaults.string(forKey: "maxId")!)! 234 | logger.log(category: .app, message: "try binding to id: \(id)") 235 | let preferType = item.availableType(from: preferTypes)! 236 | var isMobile = false 237 | if let mobileType = item.availableType(from: mobileTypes){ 238 | isMobile = true 239 | } 240 | logger.log(category: .app, message: "Prefer type is: \(preferType)") 241 | logger.log(category: .app, message: "isMobile: \(isMobile)") 242 | if preferType.rawValue == "public.utf8-plain-text"{ 243 | title = item.string(forType: preferType) ?? "NoText" 244 | //NSPasteboard.general.clearContents() 245 | logger.log(category: .app, message: "plaintext is: \(title)") 246 | dataController.createCopied(id: id, title: title, type: preferType.rawValue, timestamp:Date(), device: isMobile == true ? "mobile" : "mac" ) 247 | } 248 | else if preferType.rawValue == "public.file-url"{ 249 | path = item.string(forType: preferType) ?? "NoPath" 250 | data = item.data(forType: NSPasteboard.PasteboardType.init("com.apple.icns")) ?? Data() 251 | title = item.string(forType: NSPasteboard.PasteboardType.init("public.utf8-plain-text")) ?? "NoFileName" 252 | //NSPasteboard.general.clearContents() 253 | logger.log(category: .app, message: "path is: \(path)") 254 | dataController.createCopied(id: id, title: title, path: path, type: preferType.rawValue, data: data, timestamp:Date(), device: isMobile == true ? "mobile" : "mac") 255 | } 256 | else if preferType.rawValue == "public.png"{ 257 | data = item.data(forType: NSPasteboard.PasteboardType.init("public.png")) ?? Data() 258 | let date = Date() 259 | let dateFormatter = DateFormatter() 260 | dateFormatter.timeStyle = .medium 261 | let timesstamp = "\(dateFormatter.string(from: date))" 262 | title = "Screen Shot at \(timesstamp)" 263 | dataController.createCopied(id: id, title: title, type: preferType.rawValue, data: data, timestamp: date, device: isMobile == true ? "mobile" : "mac") 264 | } 265 | else{ 266 | // TODO 267 | logger.log(category: .app, message: "Prefer type is: \(preferType)") 268 | } 269 | } 270 | logger.log(category: .app, message: "-------- binding finished --------") 271 | } 272 | 273 | 274 | @objc func printPasteBoard(){ 275 | logger.log(category: .app, message: "-------- checking current pasteboard --------") 276 | //it is possible no copy at all, so it need to be optional 277 | if let items = NSPasteboard.general.pasteboardItems{ 278 | for item in items{ 279 | for type in item.types{ 280 | logger.log(category: .app, message: "Type: \(type)") 281 | logger.log(category: .app, message: "String: \(String(describing: item.string(forType: type)))") 282 | } 283 | } 284 | } 285 | logger.log(category: .app, message: "-------- checking finished --------") 286 | } 287 | 288 | //url scheme event handler 289 | @objc func handleAppleEvent(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { 290 | if let text = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue?.removingPercentEncoding { 291 | if text.contains("readlog://") { 292 | if let indexOfSemiColon = text.firstIndex(of: ":") as String.Index?{ 293 | if(firstTime){ 294 | menu.insertItem(NSMenuItem.separator(), at: 1) 295 | firstTime = false 296 | } 297 | let defaults = UserDefaults.standard 298 | let id = defaults.string(forKey: "maxId") == "" ? 0 : Int(defaults.string(forKey: "maxId")!)! 299 | logger.log(category: .app, message: "id in appdelegate handleAppleEvent: \(id)") 300 | let start = text.index(indexOfSemiColon, offsetBy: 3) 301 | let end = text.endIndex 302 | let paramFromCommandLine = String(text[start.. NSImage?{ 335 | if sourceImage.isValid == false { 336 | return nil 337 | } 338 | let representation = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0) 339 | representation?.size = newSize 340 | 341 | NSGraphicsContext.saveGraphicsState() 342 | NSGraphicsContext.current = NSGraphicsContext.init(bitmapImageRep: representation!) 343 | sourceImage.draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: NSZeroRect, operation: .copy, fraction: 1.0) 344 | NSGraphicsContext.restoreGraphicsState() 345 | 346 | let newImage = NSImage(size: newSize) 347 | newImage.addRepresentation(representation!) 348 | 349 | return newImage 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /Quick Paste.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3B014F60235C057600838648 /* LocalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B014F5F235C057600838648 /* LocalEventMonitor.swift */; }; 11 | 3B0F8CF821F68B0F001CF701 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3B0F8CF721F68B0F001CF701 /* Assets.xcassets */; }; 12 | 3B10BAD023490CF50029AED3 /* Quick Paste.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3B10BACE23490CF50029AED3 /* Quick Paste.xcdatamodeld */; }; 13 | 3B10BAD223491C7F0029AED3 /* DataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B10BAD123491C7F0029AED3 /* DataController.swift */; }; 14 | 3B3DE77D23B009AC00F248D3 /* CopiedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3DE77C23B009AC00F248D3 /* CopiedDataSource.swift */; }; 15 | 3B510D3223BEE99C00A7F7F2 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B510D3123BEE99C00A7F7F2 /* DetailViewController.swift */; }; 16 | 3B510D3423BEEA9500A7F7F2 /* SplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B510D3323BEEA9500A7F7F2 /* SplitViewController.swift */; }; 17 | 3B56842323978AF700338B07 /* NSFRCChangeConsolidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B56842223978AF700338B07 /* NSFRCChangeConsolidator.swift */; }; 18 | 3B80A20521D1C2EF003C0156 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80A20421D1C2EF003C0156 /* AppDelegate.swift */; }; 19 | 3B80A20721D1C2EF003C0156 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80A20621D1C2EF003C0156 /* ViewController.swift */; }; 20 | 3B80A20C21D1C2F0003C0156 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3B80A20A21D1C2F0003C0156 /* Main.storyboard */; }; 21 | 3B88C3E6240A230700B68764 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B88C3E5240A230700B68764 /* Logger.swift */; }; 22 | 3BD1CE48235EB5F700F4E18A /* GlobalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD1CE47235EB5F700F4E18A /* GlobalEventMonitor.swift */; }; 23 | 3BF303FA23B2BAC000657550 /* CopiedTableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF303F923B2BAC000657550 /* CopiedTableViewDelegate.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 3B014F5F235C057600838648 /* LocalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalEventMonitor.swift; sourceTree = ""; }; 28 | 3B0F8CF721F68B0F001CF701 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 3B10BACF23490CF50029AED3 /* Quick Paste.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Quick Paste.xcdatamodel"; sourceTree = ""; }; 30 | 3B10BAD123491C7F0029AED3 /* DataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataController.swift; sourceTree = ""; }; 31 | 3B3DE77C23B009AC00F248D3 /* CopiedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedDataSource.swift; sourceTree = ""; }; 32 | 3B510D3123BEE99C00A7F7F2 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 33 | 3B510D3323BEEA9500A7F7F2 /* SplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = ""; }; 34 | 3B56842223978AF700338B07 /* NSFRCChangeConsolidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFRCChangeConsolidator.swift; sourceTree = ""; }; 35 | 3B80A20121D1C2EF003C0156 /* Quick Paste.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Quick Paste.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 3B80A20421D1C2EF003C0156 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 3B80A20621D1C2EF003C0156 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 38 | 3B80A20B21D1C2F0003C0156 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 39 | 3B80A20D21D1C2F0003C0156 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 3B88C3E5240A230700B68764 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 41 | 3BC3C7E82340522900FE92E8 /* Quick Paste.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Quick Paste.entitlements"; sourceTree = ""; }; 42 | 3BD1CE47235EB5F700F4E18A /* GlobalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventMonitor.swift; sourceTree = ""; }; 43 | 3BF303F923B2BAC000657550 /* CopiedTableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedTableViewDelegate.swift; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | 3B80A1FE21D1C2EF003C0156 /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 3B80A1F821D1C2EF003C0156 = { 58 | isa = PBXGroup; 59 | children = ( 60 | 3B80A20321D1C2EF003C0156 /* Quick Paste */, 61 | 3B80A20221D1C2EF003C0156 /* Products */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 3B80A20221D1C2EF003C0156 /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 3B80A20121D1C2EF003C0156 /* Quick Paste.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 3B80A20321D1C2EF003C0156 /* Quick Paste */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 3B10BACE23490CF50029AED3 /* Quick Paste.xcdatamodeld */, 77 | 3BC3C7E82340522900FE92E8 /* Quick Paste.entitlements */, 78 | 3B0F8CF721F68B0F001CF701 /* Assets.xcassets */, 79 | 3B510D3323BEEA9500A7F7F2 /* SplitViewController.swift */, 80 | 3B80A20421D1C2EF003C0156 /* AppDelegate.swift */, 81 | 3B80A20621D1C2EF003C0156 /* ViewController.swift */, 82 | 3B510D3123BEE99C00A7F7F2 /* DetailViewController.swift */, 83 | 3B80A20A21D1C2F0003C0156 /* Main.storyboard */, 84 | 3BF303F923B2BAC000657550 /* CopiedTableViewDelegate.swift */, 85 | 3B56842223978AF700338B07 /* NSFRCChangeConsolidator.swift */, 86 | 3B10BAD123491C7F0029AED3 /* DataController.swift */, 87 | 3B88C3E5240A230700B68764 /* Logger.swift */, 88 | 3B3DE77C23B009AC00F248D3 /* CopiedDataSource.swift */, 89 | 3BD1CE47235EB5F700F4E18A /* GlobalEventMonitor.swift */, 90 | 3B014F5F235C057600838648 /* LocalEventMonitor.swift */, 91 | 3B80A20D21D1C2F0003C0156 /* Info.plist */, 92 | ); 93 | path = "Quick Paste"; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | 3B80A20021D1C2EF003C0156 /* Quick Paste */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = 3B80A21121D1C2F0003C0156 /* Build configuration list for PBXNativeTarget "Quick Paste" */; 102 | buildPhases = ( 103 | 3B80A1FE21D1C2EF003C0156 /* Frameworks */, 104 | 3B80A1FD21D1C2EF003C0156 /* Sources */, 105 | 3B80A1FF21D1C2EF003C0156 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = "Quick Paste"; 112 | productName = "Link It"; 113 | productReference = 3B80A20121D1C2EF003C0156 /* Quick Paste.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 3B80A1F921D1C2EF003C0156 /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | LastSwiftUpdateCheck = 1010; 123 | LastUpgradeCheck = 1010; 124 | ORGANIZATIONNAME = "张壹弛"; 125 | TargetAttributes = { 126 | 3B80A20021D1C2EF003C0156 = { 127 | CreatedOnToolsVersion = 10.1; 128 | SystemCapabilities = { 129 | com.apple.Sandbox = { 130 | enabled = 1; 131 | }; 132 | }; 133 | }; 134 | }; 135 | }; 136 | buildConfigurationList = 3B80A1FC21D1C2EF003C0156 /* Build configuration list for PBXProject "Quick Paste" */; 137 | compatibilityVersion = "Xcode 9.3"; 138 | developmentRegion = en; 139 | hasScannedForEncodings = 0; 140 | knownRegions = ( 141 | en, 142 | Base, 143 | ); 144 | mainGroup = 3B80A1F821D1C2EF003C0156; 145 | productRefGroup = 3B80A20221D1C2EF003C0156 /* Products */; 146 | projectDirPath = ""; 147 | projectRoot = ""; 148 | targets = ( 149 | 3B80A20021D1C2EF003C0156 /* Quick Paste */, 150 | ); 151 | }; 152 | /* End PBXProject section */ 153 | 154 | /* Begin PBXResourcesBuildPhase section */ 155 | 3B80A1FF21D1C2EF003C0156 /* Resources */ = { 156 | isa = PBXResourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 3B0F8CF821F68B0F001CF701 /* Assets.xcassets in Resources */, 160 | 3B80A20C21D1C2F0003C0156 /* Main.storyboard in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 3B80A1FD21D1C2EF003C0156 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 3B3DE77D23B009AC00F248D3 /* CopiedDataSource.swift in Sources */, 172 | 3BF303FA23B2BAC000657550 /* CopiedTableViewDelegate.swift in Sources */, 173 | 3B510D3423BEEA9500A7F7F2 /* SplitViewController.swift in Sources */, 174 | 3B10BAD023490CF50029AED3 /* Quick Paste.xcdatamodeld in Sources */, 175 | 3B014F60235C057600838648 /* LocalEventMonitor.swift in Sources */, 176 | 3BD1CE48235EB5F700F4E18A /* GlobalEventMonitor.swift in Sources */, 177 | 3B10BAD223491C7F0029AED3 /* DataController.swift in Sources */, 178 | 3B88C3E6240A230700B68764 /* Logger.swift in Sources */, 179 | 3B80A20721D1C2EF003C0156 /* ViewController.swift in Sources */, 180 | 3B510D3223BEE99C00A7F7F2 /* DetailViewController.swift in Sources */, 181 | 3B56842323978AF700338B07 /* NSFRCChangeConsolidator.swift in Sources */, 182 | 3B80A20521D1C2EF003C0156 /* AppDelegate.swift in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 3B80A20A21D1C2F0003C0156 /* Main.storyboard */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 3B80A20B21D1C2F0003C0156 /* Base */, 193 | ); 194 | name = Main.storyboard; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXVariantGroup section */ 198 | 199 | /* Begin XCBuildConfiguration section */ 200 | 3B80A20F21D1C2F0003C0156 /* Debug */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | CLANG_ANALYZER_NONNULL = YES; 205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_ENABLE_OBJC_WEAK = YES; 211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 212 | CLANG_WARN_BOOL_CONVERSION = YES; 213 | CLANG_WARN_COMMA = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 226 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 227 | CLANG_WARN_STRICT_PROTOTYPES = YES; 228 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 229 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 230 | CLANG_WARN_UNREACHABLE_CODE = YES; 231 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 232 | CODE_SIGN_IDENTITY = "Mac Developer"; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = dwarf; 235 | ENABLE_STRICT_OBJC_MSGSEND = YES; 236 | ENABLE_TESTABILITY = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu11; 238 | GCC_DYNAMIC_NO_PIC = NO; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_OPTIMIZATION_LEVEL = 0; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "DEBUG=1", 243 | "$(inherited)", 244 | ); 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | MACOSX_DEPLOYMENT_TARGET = 10.14; 252 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 253 | MTL_FAST_MATH = YES; 254 | ONLY_ACTIVE_ARCH = YES; 255 | SDKROOT = macosx; 256 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 257 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 258 | }; 259 | name = Debug; 260 | }; 261 | 3B80A21021D1C2F0003C0156 /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 268 | CLANG_CXX_LIBRARY = "libc++"; 269 | CLANG_ENABLE_MODULES = YES; 270 | CLANG_ENABLE_OBJC_ARC = YES; 271 | CLANG_ENABLE_OBJC_WEAK = YES; 272 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_COMMA = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 277 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 278 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INFINITE_RECURSION = YES; 282 | CLANG_WARN_INT_CONVERSION = YES; 283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 285 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 286 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 288 | CLANG_WARN_STRICT_PROTOTYPES = YES; 289 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | CODE_SIGN_IDENTITY = "Mac Developer"; 294 | COPY_PHASE_STRIP = NO; 295 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 296 | ENABLE_NS_ASSERTIONS = NO; 297 | ENABLE_STRICT_OBJC_MSGSEND = YES; 298 | GCC_C_LANGUAGE_STANDARD = gnu11; 299 | GCC_NO_COMMON_BLOCKS = YES; 300 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 301 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 302 | GCC_WARN_UNDECLARED_SELECTOR = YES; 303 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 304 | GCC_WARN_UNUSED_FUNCTION = YES; 305 | GCC_WARN_UNUSED_VARIABLE = YES; 306 | MACOSX_DEPLOYMENT_TARGET = 10.14; 307 | MTL_ENABLE_DEBUG_INFO = NO; 308 | MTL_FAST_MATH = YES; 309 | SDKROOT = macosx; 310 | SWIFT_COMPILATION_MODE = wholemodule; 311 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 312 | }; 313 | name = Release; 314 | }; 315 | 3B80A21221D1C2F0003C0156 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_ENTITLEMENTS = "Quick Paste/Quick Paste.entitlements"; 320 | CODE_SIGN_IDENTITY = "Mac Developer"; 321 | CODE_SIGN_STYLE = Automatic; 322 | COMBINE_HIDPI_IMAGES = YES; 323 | DEVELOPMENT_TEAM = W6G7AYP4H7; 324 | INFOPLIST_FILE = "$(SRCROOT)/Quick Paste/Info.plist"; 325 | LD_RUNPATH_SEARCH_PATHS = ( 326 | "$(inherited)", 327 | "@executable_path/../Frameworks", 328 | ); 329 | PRODUCT_BUNDLE_IDENTIFIER = "com.wolfPack.Quick-Paste"; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | PROVISIONING_PROFILE_SPECIFIER = ""; 332 | SWIFT_VERSION = 4.2; 333 | }; 334 | name = Debug; 335 | }; 336 | 3B80A21321D1C2F0003C0156 /* Release */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | CODE_SIGN_ENTITLEMENTS = "Quick Paste/Quick Paste.entitlements"; 341 | CODE_SIGN_IDENTITY = "Mac Developer"; 342 | CODE_SIGN_STYLE = Automatic; 343 | COMBINE_HIDPI_IMAGES = YES; 344 | DEVELOPMENT_TEAM = W6G7AYP4H7; 345 | INFOPLIST_FILE = "$(SRCROOT)/Quick Paste/Info.plist"; 346 | LD_RUNPATH_SEARCH_PATHS = ( 347 | "$(inherited)", 348 | "@executable_path/../Frameworks", 349 | ); 350 | PRODUCT_BUNDLE_IDENTIFIER = "com.wolfPack.Quick-Paste"; 351 | PRODUCT_NAME = "$(TARGET_NAME)"; 352 | PROVISIONING_PROFILE_SPECIFIER = ""; 353 | SWIFT_VERSION = 4.2; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | 3B80A1FC21D1C2EF003C0156 /* Build configuration list for PBXProject "Quick Paste" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 3B80A20F21D1C2F0003C0156 /* Debug */, 364 | 3B80A21021D1C2F0003C0156 /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | 3B80A21121D1C2F0003C0156 /* Build configuration list for PBXNativeTarget "Quick Paste" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 3B80A21221D1C2F0003C0156 /* Debug */, 373 | 3B80A21321D1C2F0003C0156 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | 380 | /* Begin XCVersionGroup section */ 381 | 3B10BACE23490CF50029AED3 /* Quick Paste.xcdatamodeld */ = { 382 | isa = XCVersionGroup; 383 | children = ( 384 | 3B10BACF23490CF50029AED3 /* Quick Paste.xcdatamodel */, 385 | ); 386 | currentVersion = 3B10BACF23490CF50029AED3 /* Quick Paste.xcdatamodel */; 387 | path = "Quick Paste.xcdatamodeld"; 388 | sourceTree = ""; 389 | versionGroupType = wrapper.xcdatamodel; 390 | }; 391 | /* End XCVersionGroup section */ 392 | }; 393 | rootObject = 3B80A1F921D1C2EF003C0156 /* Project object */; 394 | } 395 | -------------------------------------------------------------------------------- /Quick Paste/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Quick Paste 4 | // 5 | // Created by 张壹弛 on 12/24/18. 6 | // Copyright © 2018 张壹弛. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import CoreData 11 | 12 | class ViewController: NSViewController { 13 | 14 | @IBOutlet weak var searchField: NSSearchField! 15 | @IBOutlet weak var tableView: NSTableView! 16 | var detailViewController: DetailViewController! 17 | var consolidator : NSFRCChangeConsolidator? 18 | var rowToBeFocusedIndex: NSIndexSet! 19 | // var container: NSPersistentContainer! 20 | //store managedObject array of current view 21 | var copieds : [Copied]? 22 | var fetchPredicate : NSPredicate? { 23 | didSet { 24 | fetchedResultsController.fetchRequest.predicate = fetchPredicate 25 | logger.log(category: .data, message: "FetchResultsController.fecthRequest.predicate changed from \(String(describing: oldValue)) to \(String(describing: fetchPredicate))") 26 | } 27 | } 28 | var appDelegate : AppDelegate! 29 | var dataController : DataController! 30 | private var dataSource: CopiedDataSource! 31 | //to make this property visible from outside, can't be private 32 | var tableViewDelegate: CopiedTableViewDelegate! 33 | private lazy var fetchedResultsController: NSFetchedResultsController = { 34 | // let context = container.viewContext 35 | let context = dataController.context 36 | let fetchRequest = NSFetchRequest(entityName: "Copied") 37 | let nameSort = NSSortDescriptor(key: "timestamp", ascending: false) 38 | fetchRequest.sortDescriptors = [nameSort] 39 | fetchRequest.predicate = fetchPredicate 40 | //here should refer directly from place where persistent initialize rather detour from appdelegate, but for now just leave it as it was 41 | //let context = dataController.persistentContainer 42 | let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) 43 | //important step, but don't know exactly why 44 | controller.delegate = self 45 | return controller 46 | }() 47 | override func viewDidAppear() { 48 | super.viewDidAppear() 49 | logger.log(category: .ui, message: "after super viewDidAppear") 50 | //after using CustomView searchfield is not auto focused anymore 51 | if(tableView.selectedRow == -1){ 52 | searchField.window?.makeFirstResponder(searchField) 53 | } 54 | } 55 | override func viewDidLoad() { 56 | super.viewDidLoad() 57 | logger.log(category: .ui, message: "after ViewController super.viewDidLoad") 58 | logger.log(category: .ui, message: "children are \(self.children)") 59 | appDelegate = NSApplication.shared.delegate as! AppDelegate 60 | //make sure datacontroller reference of viewController is consistent with one initially created in appDelegate 61 | logger.log(category: .ui, message: "datacontroller of viewcontroller: \(String(describing: dataController))") 62 | logger.log(category: .ui, message: "dataController of appDelegate: \(String(describing: appDelegate.dataController))") 63 | guard dataController.persistentContainer != nil else{ 64 | logger.log(category: .ui, message: "This view need a persistent container", type: .error) 65 | fatalError("This view need a persistent container") 66 | } 67 | // Do any additional setup after loading the view. 68 | do{ 69 | try fetchedResultsController.performFetch() 70 | }catch{ 71 | logger.log(category: .ui, message: "Failed to fecth entites: \(error)", type: .error) 72 | fatalError("Failed to fecth entites: \(error)") 73 | } 74 | copieds = fetchedResultsController.fetchedObjects 75 | dataSource = CopiedDataSource() 76 | dataSource.fetchedResultsController = fetchedResultsController 77 | tableViewDelegate = CopiedTableViewDelegate() 78 | tableViewDelegate.fetchedResultsController = fetchedResultsController 79 | //tableViewDelegate need access selectedRow property of tableView, so this reference is necessary 80 | tableViewDelegate.tableView = tableView 81 | //tableViewDelegate would call detailViewController method when selection change, so this property is necessary, but at this point, this statement is useless, see comment one line after 82 | tableViewDelegate.detailViewController = detailViewController 83 | //here detailViewController will be nil, cause viewController's detailViewController hasn't been initialize due to splitViewController viewDidLoad hasn't finished yet. At this point, all lines after super.viewDidLoad hasn't executed yet as children view hasn't loaded yet(corresponding viewcontroller hasn't instantiated either) 84 | //this print just give a sense that what correct initialize sequence is 85 | logger.log(category: .ui, message: "tableViewDelegate.detailViewController: \(String(describing: detailViewController))") 86 | tableView.dataSource = dataSource 87 | tableView.delegate = tableViewDelegate 88 | searchField.delegate = self 89 | tableView.action = #selector(copyOnSelect) 90 | logger.log(category: .ui, message: "AXIsProcessTrusted(): \(AXIsProcessTrusted())") 91 | 92 | } 93 | 94 | override var representedObject: Any? { 95 | didSet { 96 | // Update the view, if already loaded. 97 | } 98 | } 99 | //table view copy on select 100 | @objc func copyOnSelect(sender: NSTableView){ 101 | logger.log(category: .event, message: "------- copyOnSelect --------") 102 | // guard tableView.selectedRow >= 0, 103 | if tableView.selectedRow < 0 { 104 | return 105 | } 106 | let item: Copied = copieds![tableView.selectedRow] 107 | // else { 108 | // return 109 | // } 110 | let cellView = tableView.view(atColumn: 1, row: tableView.selectedRow, makeIfNecessary: false) 111 | logger.log(category: .data, message: "cellView\(String(describing: cellView?.subviews[0]))") 112 | let imageView = cellView?.subviews[0] as! NSImageView 113 | logger.log(category: .data, message: "imageView hide status: \(imageView.isHidden)") 114 | let textField = cellView?.subviews[1] as! NSTextField 115 | logger.log(category: .data, message: "stringValue: \(textField.stringValue)") 116 | logger.log(category: .data, message: "thumbnail == nil ? : \(item.thumbnail == nil)") 117 | copyIt(item: item) 118 | } 119 | //table view copy on shortcut 120 | @objc func copyOnNumber(numberKey: Int){ 121 | logger.log(category: .event, message: "-------- copyOnNumber --------") 122 | logger.log(category: .data, message: "available copied count in current view: \(copieds!.count)") 123 | // guard numberKey <= copieds!.count, 124 | if numberKey > copieds!.count { 125 | return 126 | } 127 | let item: Copied = copieds![numberKey-1] 128 | // else { 129 | // return 130 | // } 131 | copyIt(item: item) 132 | } 133 | private func copyOnReturn(currentRow: Int){ 134 | logger.log(category: .event, message: "--------copyOnReturn --------") 135 | logger.log(category: .data, message: "available copied count in current view: \(copieds!.count)") 136 | // guard currentRow <= copieds!.count, 137 | if currentRow > copieds!.count{ 138 | return 139 | } 140 | let item: Copied = copieds![currentRow] 141 | // else { 142 | // return 143 | // } 144 | copyIt(item: item) 145 | } 146 | //base copy 147 | private func copyIt(item: Copied){ 148 | //important step 149 | NSPasteboard.general.clearContents() 150 | //avoid post to notification center post to .NSPasteBoardDidChange, since clearContents would increment pasteboard changeCount 151 | appDelegate.lastChangeCount = NSPasteboard.general.changeCount 152 | logger.log(category: .ui, message: "Copied \(item.id): \(String(describing: item.type))") 153 | if(item.type == "public.png"){ 154 | NSPasteboard.general.setData(item.thumbnail!, forType: NSPasteboard.PasteboardType.init("public.png")) 155 | }else{ 156 | //data error may lead to unwrap failure, though now it seems not possible, so using if let to safely unwrap 157 | if let path = item.path, let type = item.type, let name = item.name { 158 | NSPasteboard.general.setString(path, forType: NSPasteboard.PasteboardType.init(type)) 159 | NSPasteboard.general.setString(name, forType: NSPasteboard.PasteboardType.init("public.utf8-plain-text")) 160 | } 161 | } 162 | logger.log(category: .ui, message: "we copy entry content to pasteboard") 163 | appDelegate.printPasteBoard() 164 | } 165 | //base delete 166 | private func deleteIt(){ 167 | logger.log(category: .ui, message: "------- deleteIt --------") 168 | let item: Copied = copieds![tableView.selectedRow] 169 | dataController.removeCopied(item: item) 170 | } 171 | //monitor for keydown event 172 | override func keyDown(with event: NSEvent) { 173 | //avoid crash caused by cmd+backspace 174 | if(event.keyCode>=18 && event.keyCode<=23){ 175 | logger.log(category: .event, message: "detect numeric key 1~6") 176 | switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) { 177 | //not using event.keycode is because keyCode of key 5 key 6 is reverse sequence 178 | case [.command] where Int(event.characters!)! <= 6 && Int(event.characters!)! >= 1 : 179 | logger.log(category: .ui, message: "we copy entry content to pasteboard") 180 | copyOnNumber(numberKey: Int(event.characters!)!) 181 | //other modifier keys 182 | default: 183 | break 184 | } 185 | } 186 | if event.keyCode == 51{ 187 | logger.log(category: .event, message: "detected deletetable. View.selectedRow:\(tableView.selectedRow), event.timestamp: \(event.timestamp)") 188 | var popOverWindow: NSWindow? 189 | NSApplication.shared.windows.forEach{window in 190 | // print(window.className) 191 | if(window.className.contains("Popover")){ 192 | popOverWindow = window 193 | // print(popOverWindow) 194 | } 195 | } 196 | if popOverWindow!.firstResponder?.isKind(of: NSTableView.self) == true && popOverWindow!.isKeyWindow { 197 | //focus change to searchfield only if no entry left 198 | logger.log(category: .ui, message: "deleting row: \(tableView.selectedRow)") 199 | //if deleting row is last row, focus on prev index instead of sticking to same index of deleting row 200 | let rowToBeFocused = tableView.selectedRow == copieds!.count-1 ? tableView.selectedRow-1: tableView.selectedRow 201 | logger.log(category: .ui, message: "rowToBeFocused: \(rowToBeFocused)") 202 | //even if you selectRowIndexes here, it won't work at this point even put it after deleteIt(core data processing is done), since tableview UI hasn't start yet. 203 | rowToBeFocusedIndex = NSIndexSet(index: rowToBeFocused) 204 | let changeFocusToSearchBar = copieds!.count == 1 ? true : false 205 | //currently only support delete on record 206 | if tableView.selectedRow >= 0 { 207 | deleteIt() 208 | if changeFocusToSearchBar == true{ 209 | popOverWindow!.makeFirstResponder(searchField) 210 | searchField.currentEditor()?.moveToEndOfLine(nil) 211 | searchField.moveToEndOfDocument(nil) 212 | } 213 | else{ 214 | popOverWindow!.makeFirstResponder(tableView) 215 | } 216 | } 217 | } 218 | } 219 | if event.keyCode == 125{ 220 | logger.log(category: .event, message: "detected arrow down") 221 | logger.log(category: .ui, message: "tableView.selectedRow:\(tableView.selectedRow), event.timestamp: \(event.timestamp)") 222 | var popOverWindow: NSWindow? 223 | NSApplication.shared.windows.forEach{window in 224 | if(window.className.contains("Popover")){ 225 | popOverWindow = window; 226 | logger.log(category: .ui, message: "\(String(describing: popOverWindow))") 227 | } 228 | } 229 | //when focus on searchbar, arrow down would bring focus to tableview and highlight first row 230 | if popOverWindow!.firstResponder?.isKind(of: NSTextView.self) == true{ 231 | //tackle click back to search bar, remaining rows selected 232 | logger.log(category: .ui, message: "tableView.selectedRowIndexes.count\(tableView.selectedRowIndexes.count)") 233 | //only right after launch directly go to table will not go inside this condition 234 | if tableView.selectedRowIndexes.count > 0{ 235 | tableView.deselectAll(tableView.selectedRowIndexes) 236 | } 237 | //move focus only if there is result present 238 | if(copieds!.count>0){ 239 | popOverWindow!.makeFirstResponder(tableView) 240 | } 241 | } 242 | } 243 | if event.keyCode == 126{ 244 | logger.log(category: .event, message: "detected arrow up") 245 | logger.log(category: .ui, message: "tableView.selectedRow:\(tableView.selectedRow), event.timestamp: \(event.timestamp)") 246 | var popOverWindow: NSWindow? 247 | NSApplication.shared.windows.forEach{window in 248 | logger.log(category: .ui, message: "window.className") 249 | if(window.className.contains("Popover")){ 250 | popOverWindow = window; print(popOverWindow) 251 | } 252 | } 253 | if popOverWindow!.firstResponder?.isKind(of: NSTableView.self) == true{ 254 | logger.log(category: .data, message: "copieds!.count\(copieds!.count)") 255 | if tableView.selectedRow == 0{ 256 | popOverWindow!.makeFirstResponder(searchField) 257 | //remove this line if you want text being selected 258 | searchField.currentEditor()?.moveToEndOfLine(nil) 259 | searchField.moveToEndOfDocument(nil) 260 | } 261 | } 262 | } 263 | if event.keyCode == 36{ 264 | logger.log(category: .event, message: "return key detected,View.selectedRow:\(tableView.selectedRow), event.timestamp: \(event.timestamp)") 265 | var popOverWindow: NSWindow? 266 | NSApplication.shared.windows.forEach{window in 267 | print(window.className) 268 | if(window.className.contains("Popover")){ 269 | popOverWindow = window; 270 | logger.log(category: .ui, message: "popOverWindow: \(String(describing: popOverWindow))") 271 | } 272 | } 273 | if popOverWindow?.firstResponder?.isKind(of: NSTableView.self) == true{ 274 | logger.log(category: .ui, message: "current selected row:\(tableView.selectedRow)") 275 | copyOnReturn(currentRow: tableView.selectedRow) 276 | } 277 | } 278 | 279 | if event.keyCode == 3{ 280 | switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) { 281 | case [.command]: 282 | logger.log(category: .event, message: "command + f detectd.") 283 | searchField.window?.makeFirstResponder(searchField) 284 | default: 285 | break 286 | } 287 | } 288 | } 289 | } 290 | 291 | extension ViewController { 292 | // MARK: Storyboard instantiation 293 | // static func freshController() -> ViewController { 294 | // //1. 295 | // let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 296 | // print("inside viewcontroller freshcontroller") 297 | // //2. 298 | // let identifier = NSStoryboard.SceneIdentifier("ViewController") 299 | // //3. 300 | // guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? ViewController else { 301 | // fatalError("Why cant i find ViewController? - Check Main.storyboard") 302 | // } 303 | // print("viewcontroller:\(viewcontroller)") 304 | // return viewcontroller 305 | // } 306 | } 307 | 308 | extension ViewController{ 309 | @IBAction func Quit(_ sender: Any) { 310 | NSApplication.shared.terminate(self) 311 | } 312 | @IBAction func clear(_ sender: NSButton) { 313 | logger.log(category: .event, message: "trigger clear button") 314 | dataController.deleteAll() 315 | // tableView.reloadData() 316 | } 317 | } 318 | extension ViewController: NSSearchFieldDelegate{ 319 | func controlTextDidChange(_ notification: Notification) { 320 | // var timer: Timer? = nil 321 | // //wait for timeInterval to do selector task, but causing execute search for each character you type, which is still unacceptable 322 | // timer = Timer.init(timeInterval: 0.5, target: self, selector: #selector(ViewController.search(timer:)), userInfo: notification, repeats: false) 323 | // RunLoop.main.add(timer!, forMode: .default) 324 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.search(_:)), object: notification) 325 | perform(#selector(self.search(_:)), with: notification, afterDelay: 0.382) 326 | } 327 | 328 | // @objc func search(timer: Timer){ 329 | // let notification: Notification = timer.userInfo as! Notification 330 | @objc func search(_ notification: Notification){ 331 | if let field = notification.object as? NSSearchField { 332 | let query = field.stringValue 333 | if query.isEmpty { 334 | fetchPredicate = nil 335 | } else { 336 | fetchPredicate = NSPredicate(format: "name contains[cd] %@", query) 337 | } 338 | requestData(with: fetchPredicate) 339 | } else { 340 | //not sure how to call super's method, gives me a error now 341 | //super.controlTextDidChange(notification) 342 | } 343 | } 344 | 345 | func requestData(with predicate : NSPredicate? = nil) { 346 | //instead of modify the predicate of fetchRequest of NSFetchedResultsController, simply change property fetchPredicate value, add observer on the property, so everytime fetchPredicate change, the predicate of fetchRequest of NSFetchedResultsController change as well 347 | //fetchedResultsController.fetchRequest.predicate = predicate 348 | let t1 = Date() 349 | logger.log(category: .ui, message: "start fetch") 350 | DispatchQueue.global(qos: .userInteractive).async { 351 | do{ 352 | try self.fetchedResultsController.performFetch() 353 | //when you search, you dont actually add any new object, instead you change the fetchedResultsController's predicate, since this repointing, the context doesn't observer any changes. you need reload to refresh the view manually 354 | logger.log(category: .ui, message: "fetch done, total performFetch() took \(Date().timeIntervalSince(t1)) ") 355 | DispatchQueue.main.async { [x = self.fetchedResultsController] in 356 | let t2 = Date() 357 | logger.log(category: .ui, message: "start reloading data after fetch") 358 | self.tableView.reloadData() 359 | logger.log(category: .ui, message: "end of reload Dada, total reloadData() took \(Date().timeIntervalSince(t2)) ") 360 | self.copieds = x.fetchedObjects 361 | } 362 | }catch let error{ 363 | logger.log(category: .data, message: "\(error.localizedDescription) Fetched \(String(describing: self.fetchedResultsController.fetchedObjects?.count)) objects", type: .error) 364 | } 365 | } 366 | } 367 | } 368 | 369 | //this extension is important, but don't know the difference from class ViewController: NSViewController, NSFetchedResultsControllerDelegate 370 | extension ViewController: NSFetchedResultsControllerDelegate { 371 | func controllerWillChangeContent(_ controller: NSFetchedResultsController) { 372 | logger.log(category: .ui, message: "-------- tableViewBeginUpdates -------- ") 373 | logger.log(category: .ui, message: "tableView select row before tableview UI update: \(tableView.selectedRow)") 374 | consolidator = NSFRCChangeConsolidator() 375 | tableView.beginUpdates() 376 | } 377 | func controllerDidChangeContent(_ controller: NSFetchedResultsController){ 378 | if let rowDeletes = consolidator?.sortedRowDeletes(){ 379 | if(rowDeletes.count > 0){ 380 | for indexPath in rowDeletes{ 381 | tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade) 382 | } 383 | logger.log(category: .ui, message: "tableView select row before manual set: \(tableView.selectedRow)") 384 | logger.log(category: .ui, message: "rowToBeFocusedIndex: \(String(describing: rowToBeFocusedIndex))") 385 | //if rowToBeFocus is -1, rowToBeFocusIndex would be nil, that would lead to stmt below unwrap nil(crash) 386 | if rowToBeFocusedIndex != nil{ 387 | tableView.selectRowIndexes(rowToBeFocusedIndex as IndexSet, byExtendingSelection: false) 388 | } 389 | logger.log(category: .ui, message: "tableView select row after manual set: \(tableView.selectedRow)") 390 | } 391 | } 392 | if let rowInserts = consolidator?.sortedRowInserts(){ 393 | if(rowInserts.count > 0){ 394 | for indexPath in rowInserts{ 395 | tableView.insertRows(at: [indexPath.item], withAnimation: .effectFade) 396 | } 397 | } 398 | } 399 | tableView.endUpdates() 400 | //deallocate consolidator 401 | consolidator = nil 402 | logger.log(category: .ui, message: "-------- tableViewEndUpdates --------") 403 | } 404 | func controller(_ controller: NSFetchedResultsController,didChange anObject: Any, at indexPath: IndexPath?,for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?){ 405 | consolidator?.ingestItemChange(ofType: type, oldIndexPath: indexPath, newIndexPath: newIndexPath) 406 | logger.log(category: .ui, message: "tableView.selectedRow:\(tableView.selectedRow)") 407 | logger.log(category: .ui, message: "Change type \(type) for indexPath \(String(describing: indexPath)), newIndexPath \(String(describing: newIndexPath)). Changed object: \(anObject). FRC by this moment has \(String(describing: self.fetchedResultsController.fetchedObjects?.count)) objects, tableView has \(self.tableView.numberOfRows) rows") 408 | switch type { 409 | case .insert: 410 | if let newIndexPath = newIndexPath { 411 | // tableView.insertRows(at: [newIndexPath.item], withAnimation: .effectFade) 412 | } 413 | case .delete: 414 | if let indexPath = indexPath{ 415 | // tableView.removeRows(at: [indexPath.item], withAnimation: .effectFade) 416 | } 417 | case .update: 418 | //post about sequence on 12.4 log says we shouldn't care about oldindexpath 419 | if let indexPath = indexPath{ 420 | let row = indexPath.item 421 | for column in 0.. 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 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 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | Default 531 | 532 | 533 | 534 | 535 | 536 | 537 | Left to Right 538 | 539 | 540 | 541 | 542 | 543 | 544 | Right to Left 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | Default 556 | 557 | 558 | 559 | 560 | 561 | 562 | Left to Right 563 | 564 | 565 | 566 | 567 | 568 | 569 | Right to Left 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 840 | 844 | 845 | 855 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | --------------------------------------------------------------------------------