2
0

UTMPasteboard.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. //
  2. // Copyright © 2020 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. #if canImport(UIKit)
  17. import UIKit
  18. import MobileCoreServices
  19. typealias SystemPasteboard = UIPasteboard
  20. typealias SystemPasteboardType = String
  21. #elseif canImport(AppKit)
  22. import AppKit
  23. typealias SystemPasteboard = NSPasteboard
  24. typealias SystemPasteboardType = NSPasteboard.PasteboardType
  25. #else
  26. #error("Neither UIKit nor AppKit found!")
  27. #endif
  28. #if !WITH_USB
  29. import CocoaSpiceNoUsb
  30. #else
  31. import CocoaSpice
  32. #endif
  33. @objc class UTMPasteboard: NSObject, CSPasteboardDelegate {
  34. @objc(generalPasteboard)
  35. static let general = UTMPasteboard(for: SystemPasteboard.general)
  36. private(set) var changeCount: Int
  37. fileprivate let systemPasteboard: SystemPasteboard
  38. private var timer: Timer?
  39. private var listeners = Set<AnyHashable>()
  40. private init(for systemPasteboard: SystemPasteboard) {
  41. self.systemPasteboard = systemPasteboard
  42. self.changeCount = systemPasteboard.changeCount
  43. }
  44. @nonobjc func requestPollingMode<T>(forHashable hashable: T) where T: Hashable {
  45. if listeners.isEmpty {
  46. timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
  47. self.onTimerTick()
  48. }
  49. }
  50. _ = listeners.insert(hashable)
  51. }
  52. @objc func requestPollingMode(forObject object: AnyHashable) {
  53. requestPollingMode(forHashable: object)
  54. }
  55. @nonobjc func releasePollingMode<T>(forHashable hashable: T) where T: Hashable {
  56. _ = listeners.remove(hashable)
  57. if listeners.isEmpty {
  58. timer?.invalidate()
  59. timer = nil
  60. }
  61. }
  62. @objc func releasePollingMode(forObject object: AnyHashable) {
  63. releasePollingMode(forHashable: object)
  64. }
  65. private func onTimerTick() {
  66. let newCount = systemPasteboard.changeCount
  67. if newCount > changeCount {
  68. if hasContents() {
  69. NotificationCenter.default.post(name: .csPasteboardChanged, object: self)
  70. } else {
  71. NotificationCenter.default.post(name: .csPasteboardRemoved, object: self)
  72. }
  73. }
  74. changeCount = newCount
  75. }
  76. }
  77. #if canImport(UIKit)
  78. extension CSPasteboardType {
  79. init?(rawValue: String) {
  80. let cfValue = rawValue as CFString
  81. switch cfValue {
  82. case kUTTypeURL: self = .URL
  83. case kUTTypeBMP: self = .bmp
  84. case kUTTypeFileURL: self = .fileURL
  85. case kUTTypeFont: self = .font
  86. case kUTTypeHTML: self = .html
  87. case kUTTypeJPEG: self = .jpg
  88. case kUTTypeJPEG2000: self = .jpg
  89. case kUTTypePDF: self = .pdf
  90. case kUTTypePNG: self = .png
  91. case kUTTypeRTF: self = .rtf
  92. case kUTTypeRTFD: self = .rtfd
  93. case kUTTypeFlatRTFD: self = .rtfd
  94. case kUTTypeWaveformAudio: self = .sound
  95. case kUTTypePlainText: self = .string
  96. case kUTTypeUTF8PlainText: self = .string
  97. case kUTTypeTabSeparatedText: self = .tabularText
  98. case kUTTypeUTF8TabSeparatedText: self = .tabularText
  99. case kUTTypeTIFF: self = .tiff
  100. default: return nil
  101. }
  102. }
  103. var rawValue: String {
  104. switch self {
  105. case .URL: return kUTTypeURL as String
  106. case .bmp: return kUTTypeBMP as String
  107. case .fileURL: return kUTTypeFileURL as String
  108. case .font: return kUTTypeFont as String
  109. case .html: return kUTTypeHTML as String
  110. case .jpg: return kUTTypeJPEG as String
  111. case .pdf: return kUTTypePDF as String
  112. case .png: return kUTTypePNG as String
  113. case .rtf: return kUTTypeRTF as String
  114. case .rtfd: return kUTTypeRTFD as String
  115. case .sound: return kUTTypeWaveformAudio as String
  116. case .string: return kUTTypeUTF8PlainText as String
  117. case .tabularText: return kUTTypeUTF8TabSeparatedText as String
  118. case .tiff: return kUTTypeTIFF as String
  119. @unknown default:
  120. return kUTTypeUTF8PlainText as String
  121. }
  122. }
  123. }
  124. @objc extension UTMPasteboard {
  125. func hasContents() -> Bool {
  126. return systemPasteboard.types.count > 0
  127. }
  128. func clearContents() {
  129. changeCount += 1
  130. systemPasteboard.items = []
  131. }
  132. func setData(_ data: Data, for type: CSPasteboardType) {
  133. clearContents()
  134. changeCount += 1
  135. systemPasteboard.setData(data, forPasteboardType: type.rawValue)
  136. }
  137. func data(for type: CSPasteboardType) -> Data? {
  138. return systemPasteboard.data(forPasteboardType: type.rawValue)
  139. }
  140. func setString(_ string: String, for type: CSPasteboardType) {
  141. clearContents()
  142. changeCount += 1
  143. systemPasteboard.setValue(string, forPasteboardType: type.rawValue)
  144. }
  145. func setString(_ string: String) {
  146. setString(string, for: .string)
  147. }
  148. func string(for type: CSPasteboardType) -> String? {
  149. return systemPasteboard.value(forPasteboardType: type.rawValue) as? String
  150. }
  151. func string() -> String? {
  152. return string(for: .string)
  153. }
  154. func canReadItem(for type: CSPasteboardType) -> Bool {
  155. let types = systemPasteboard.types
  156. return types.contains(type.rawValue)
  157. }
  158. }
  159. #elseif canImport(AppKit)
  160. extension CSPasteboardType {
  161. init?(rawValue: NSPasteboard.PasteboardType) {
  162. switch rawValue {
  163. case .URL: self = .URL
  164. case .fileURL: self = .fileURL
  165. case .font: self = .font
  166. case .html: self = .html
  167. case .pdf: self = .pdf
  168. case .png: self = .png
  169. case .rtf: self = .rtf
  170. case .rtfd: self = .rtfd
  171. case .sound: self = .sound
  172. case .string: self = .string
  173. case .tabularText: self = .tabularText
  174. case .tiff: self = .tiff
  175. default: return nil
  176. }
  177. }
  178. var rawValue: NSPasteboard.PasteboardType {
  179. switch self {
  180. case .URL: return .URL
  181. case .bmp: return .fileURL
  182. case .fileURL: return .fileURL
  183. case .font: return .font
  184. case .html: return .html
  185. case .jpg: return .fileURL
  186. case .pdf: return .pdf
  187. case .png: return .png
  188. case .rtf: return .rtf
  189. case .rtfd: return .rtfd
  190. case .sound: return .sound
  191. case .string: return .string
  192. case .tabularText: return .tabularText
  193. case .tiff: return .tiff
  194. @unknown default:
  195. return .string
  196. }
  197. }
  198. }
  199. @objc extension UTMPasteboard {
  200. func hasContents() -> Bool {
  201. if let items = systemPasteboard.pasteboardItems {
  202. return items.count > 0
  203. } else {
  204. return false
  205. }
  206. }
  207. func clearContents() {
  208. changeCount += 1
  209. _ = systemPasteboard.clearContents()
  210. }
  211. func setData(_ data: Data, for type: CSPasteboardType) {
  212. clearContents()
  213. changeCount += 1
  214. _ = systemPasteboard.setData(data, forType: type.rawValue)
  215. }
  216. func data(for type: CSPasteboardType) -> Data? {
  217. return systemPasteboard.data(forType: type.rawValue)
  218. }
  219. func setString(_ string: String, for type: CSPasteboardType) {
  220. clearContents()
  221. changeCount += 1
  222. _ = systemPasteboard.setString(string, forType: type.rawValue)
  223. }
  224. func setString(_ string: String) {
  225. setString(string, for: .string)
  226. }
  227. func string(for type: CSPasteboardType) -> String? {
  228. return systemPasteboard.string(forType: type.rawValue)
  229. }
  230. func string() -> String? {
  231. return string(for: .string)
  232. }
  233. func canReadItem(for type: CSPasteboardType) -> Bool {
  234. systemPasteboard.availableType(from: [type.rawValue]) != nil
  235. }
  236. }
  237. #endif