|
7 | 7 | import datetime
|
8 | 8 | import plistlib
|
9 | 9 |
|
| 10 | +import objc |
10 | 11 | import Quartz
|
11 | 12 | import rumps
|
| 13 | +from AppKit import NSPasteboardTypeFileURL |
12 | 14 | from Foundation import (
|
| 15 | + NSURL, |
13 | 16 | NSLog,
|
14 | 17 | NSMetadataQuery,
|
15 | 18 | NSMetadataQueryDidFinishGatheringNotification,
|
16 | 19 | NSMetadataQueryDidStartGatheringNotification,
|
17 | 20 | NSMetadataQueryDidUpdateNotification,
|
18 | 21 | NSMetadataQueryGatheringProgressNotification,
|
19 | 22 | NSNotificationCenter,
|
| 23 | + NSObject, |
20 | 24 | NSPredicate,
|
| 25 | + NSString, |
| 26 | + NSUTF8StringEncoding, |
21 | 27 | )
|
22 | 28 |
|
23 | 29 | from loginitems import add_login_item, list_login_items, remove_login_item
|
24 | 30 | from macvision import (
|
25 | 31 | ciimage_from_file,
|
26 | 32 | detect_qrcodes_in_ciimage,
|
27 |
| - detect_qrcodes_in_file, |
28 | 33 | detect_text_in_ciimage,
|
29 |
| - detect_text_in_file, |
30 | 34 | get_supported_vision_languages,
|
31 | 35 | )
|
32 |
| -from pasteboard import Pasteboard, TIFF |
| 36 | +from pasteboard import TIFF, Pasteboard |
33 | 37 | from utils import get_app_path, verify_desktop_access
|
| 38 | +import typing as t |
34 | 39 |
|
35 | 40 | __version__ = "0.9.0"
|
36 | 41 |
|
@@ -142,6 +147,11 @@ def __init__(self, *args, **kwargs):
|
142 | 147 |
|
143 | 148 | self.log(__file__)
|
144 | 149 |
|
| 150 | + from AppKit import NSApplication |
| 151 | + |
| 152 | + self.service_provider = ServiceProvider.alloc().initWithApp_(self) |
| 153 | + NSApplication.sharedApplication().setServicesProvider_(self.service_provider) |
| 154 | + |
145 | 155 | # This will be used by clipboard_watcher() to detect changes to the pasteboard
|
146 | 156 | # (which everyone but Apple calls the clipboard)
|
147 | 157 | self.pasteboard = Pasteboard()
|
@@ -241,9 +251,6 @@ def on_toggle(self, sender):
|
241 | 251 | def on_clear_clipboard(self, sender):
|
242 | 252 | """Clear the clipboard"""
|
243 | 253 | self.pasteboard.clear()
|
244 |
| - # ZZZ replace pyperclip with a self.clipboard_clear, self.clipboard_copy, self.clipboard_paste |
245 |
| - # or Clipboard class with those methods |
246 |
| - # that auto update the pasteboard_count |
247 | 254 |
|
248 | 255 | def on_confidence(self, sender):
|
249 | 256 | """Change confidence threshold."""
|
@@ -434,7 +441,7 @@ def process_image(self, image: Quartz.CIImage) -> str:
|
434 | 441 | clipboard_text = (
|
435 | 442 | self.pasteboard.paste() if self.pasteboard.has_text() else ""
|
436 | 443 | )
|
437 |
| - clipboard_text = f"{clipboard_text}\n{text}" |
| 444 | + clipboard_text = f"{clipboard_text}\n{text}" if clipboard_text else text |
438 | 445 | else:
|
439 | 446 | clipboard_text = text
|
440 | 447 |
|
@@ -503,5 +510,54 @@ def process_clipboard_image(self):
|
503 | 510 | self.log("failed to get image data from pasteboard")
|
504 | 511 |
|
505 | 512 |
|
| 513 | +def serviceSelector(fn): |
| 514 | + # this is the signature of service selectors |
| 515 | + return objc.selector(fn, signature=b"v@:@@o^@") |
| 516 | + |
| 517 | + |
| 518 | +def ErrorValue(e): |
| 519 | + """Handler for errors returned by the service.""" |
| 520 | + NSLog(f"{APP_NAME} {__version__} error: {e}") |
| 521 | + return e |
| 522 | + |
| 523 | + |
| 524 | +class ServiceProvider(NSObject): |
| 525 | + """Service provider class to handle messages from the Services menu |
| 526 | +
|
| 527 | + Initialize with ServiceProvider.alloc().initWithApp_(app) |
| 528 | + """ |
| 529 | + |
| 530 | + app: t.Optional[Textinator] = None |
| 531 | + |
| 532 | + def initWithApp_(self, app: Textinator): |
| 533 | + self = objc.super(ServiceProvider, self).init() |
| 534 | + self.app = app |
| 535 | + return self |
| 536 | + |
| 537 | + @serviceSelector |
| 538 | + def detectTextInImage_userData_error_(self, pasteboard, userdata, error) -> None: |
| 539 | + """Detect text in an image on the clipboard.""" |
| 540 | + self.app.log("detectTextInImage_userData_error_ called via Services menu") |
| 541 | + |
| 542 | + if self.app._paused: |
| 543 | + self.app.log("skipping text detection because app is paused") |
| 544 | + |
| 545 | + try: |
| 546 | + for item in pasteboard.pasteboardItems(): |
| 547 | + pb_url_data = item.dataForType_(NSPasteboardTypeFileURL) |
| 548 | + pb_url = NSURL.URLWithString_( |
| 549 | + NSString.alloc().initWithData_encoding_( |
| 550 | + pb_url_data, NSUTF8StringEncoding |
| 551 | + ) |
| 552 | + ) |
| 553 | + self.app.log(f"processing file from Services menu: {pb_url.path()}") |
| 554 | + image = Quartz.CIImage.imageWithContentsOfURL_(pb_url) |
| 555 | + self.app.process_image(image) |
| 556 | + except Exception as e: |
| 557 | + return ErrorValue(e) |
| 558 | + |
| 559 | + return None |
| 560 | + |
| 561 | + |
506 | 562 | if __name__ == "__main__":
|
507 | 563 | Textinator(name=APP_NAME, quit_button=None).run()
|
0 commit comments