├── 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 |
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 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
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 |
--------------------------------------------------------------------------------