class RemoteRelayNode: # websocket is an instance from the websockets library # # see https://websockets.readthedocs.io/en/stable/api.html def __init__(self, websocket): self.new_message_signal = AsyncSignal() self._websocket = websocket self._progress_signaler = ProgressSignaler(signals.outgoing_transfer) async def accept_relayed_message(self, message): self._progress_signaler.begin_transfer(message) async for chunk in message.chunks: await self._websocket.send(pickle.dumps(chunk)) self._progress_signaler.on_chunk_transferred() def start_relaying_changes(self): asyncio.ensure_future(self._process_messages()) async def _process_messages(self): async for message in self._chunked_message_receiver.received_messages: self.new_message_signal.send(message) @property def _chunked_message_receiver(self): return ChunkedMessageReceiver(self._depickled_socket_messages) @property async def _depickled_socket_messages(self): async for msg in self._websocket: yield pickle.loads(msg) def __repr__(self): return (f'<{type(self).__name__}: ' f'{self._websocket.host}:{self._websocket.port}>')
class AppSettings: on_change = AsyncSignal() @classproperty def get(cls): qsettings = cls._qsettings return Settings(is_server_enabled=qsettings.value('is_server_enabled', type=bool), server_listen_ip=qsettings.value('server_listen_ip', type=str), server_listen_port=qsettings.value( 'server_listen_port', type=str), is_client_enabled=qsettings.value('is_client_enabled', type=bool), client_ws_url=qsettings.value('client_ws_url', type=str)) @classmethod def set(cls, settings): qsettings = cls._qsettings qsettings.setValue('is_server_enabled', settings.is_server_enabled) qsettings.setValue('server_listen_ip', settings.server_listen_ip) qsettings.setValue('server_listen_port', settings.server_listen_port) qsettings.setValue('is_client_enabled', settings.is_client_enabled) qsettings.setValue('client_ws_url', settings.client_ws_url) cls.on_change.send() _qsettings = QSettings('sumeet', 'clipshare')
class LinuxClipboard: @classmethod def new(cls, qt_app): return cls(qt_app.clipboard()) def __init__(self, qt_clipboard): self.new_clipboard_contents_signal = AsyncSignal() self._qt_clipboard = qt_clipboard def set(self, clipboard_contents): with self._stop_receiving_clipboard_updates(): change_tiff_to_png(clipboard_contents) qmimedata_to_set = QMimeDataSerializer.deserialize(clipboard_contents) self._qt_clipboard.setMimeData(qmimedata_to_set) def clear(self): with self._stop_receiving_clipboard_updates(): self._qt_clipboard.clear() def start_listening_for_changes(self): self._qt_clipboard.dataChanged.connect(self._grab_and_signal_clipboard_data) # temporarily stop listening to the clipboard while we set it, because we # don't want to detect our own updates @contextlib.contextmanager def _stop_receiving_clipboard_updates(self): try: self._qt_clipboard.dataChanged.disconnect(self._grab_and_signal_clipboard_data) yield finally: self.start_listening_for_changes() def _grab_and_signal_clipboard_data(self): mime_data = self._qt_clipboard.mimeData() clipboard_contents = QMimeDataSerializer.serialize(mime_data) logger.debug(f'detected change {log.format_obj(clipboard_contents)}') if self._contains_data(clipboard_contents): self.new_clipboard_contents_signal.send(clipboard_contents) else: logger.debug('nothing to update, backing out') def _contains_data(self, clipboard_contents): return any(clipboard_contents.values()) def __repr__(self): return f'<{type(self).__name__}>'
class ClientRelayNode: def __init__(self, clipboard): self.new_message_signal = AsyncSignal() self._clipboard = clipboard self._progress_signaler = ProgressSignaler(signals.incoming_transfer) async def accept_relayed_message(self, message): # we receive the message here after receiving the first chunk fully. up # to this point, we haven't gotten the entire message. # # there's a delay between when we first hear that there's a new message # and when we're ready to set clipboard contents, because we have to # wait for several chunks to download. transmitting an image may take # a few seconds to transfer, and in the meantime, if you try to paste, # you'll end up the previous item on the clipboard. # clearing the clipboard in anticipation of new contents being # downloaded will prevent accidental pasting. logger.debug('got wind of a message, clearing the clipboard') self._clipboard.clear() logger.debug('actually setting the clipboard') ensure_future(self._broadcast_incoming_transfer_progress(message)) self._clipboard.set(await message.full_payload) def start_relaying_changes(self): self._clipboard.new_clipboard_contents_signal.connect( self._handle_new_clipboard_contents_signal) self._clipboard.start_listening_for_changes() def _handle_new_clipboard_contents_signal(self, clipboard_contents): message = Message(payload=clipboard_contents, split_size=BYTES_PER_SPLIT) self.new_message_signal.send(message) def __repr__(self): return f'<{type(self).__name__}: {type(self._clipboard).__name__}>' async def _broadcast_incoming_transfer_progress(self, message): self._progress_signaler.begin_transfer(message) async for chunk in message.chunks: self._progress_signaler.on_chunk_transferred()
def __init__(self, ns_pasteboard): self.new_clipboard_contents_signal = AsyncSignal() self._ns_pasteboard = ns_pasteboard self._poller = Poller(self._ns_pasteboard)
class MacClipboard: @classmethod def new(cls, qt_app): # we don't use qt to read for the mac clipboard, for now return cls(NSPasteboard.generalPasteboard()) def __init__(self, ns_pasteboard): self.new_clipboard_contents_signal = AsyncSignal() self._ns_pasteboard = ns_pasteboard self._poller = Poller(self._ns_pasteboard) def set(self, clipboard_contents): object_to_set = self._extract_settable_nsobject(clipboard_contents) if not object_to_set: all_types = repr(clipboard_contents.keys()) logger.debug('unsupported clipboard payload ' + log.format_obj(clipboard_contents)) return try: # pause the poller while we write to it, so we don't detect our own # changes self._poller.pause_polling() # for some reason you've gotta clear before writing or the write doesn't # have any effect self._ns_pasteboard.clearContents() self._ns_pasteboard.writeObjects_( NSArray.arrayWithObject_(object_to_set)) # tell the poller to ignore the change we just made, self._poller.ignore_change_count(self._poller.current_change_count) finally: # and then when it resumes, it won't detect that change self._poller.resume_polling() def clear(self): logger.debug('clearing the clipboard') self._ns_pasteboard.clearContents() def start_listening_for_changes(self): asyncio.ensure_future(self._poll_forever()) async def _poll_forever(self): while True: t = time.time() clipboard_contents = await self._poller.poll_for_new_clipboard_contents( ) if clipboard_contents: logger.debug( f'detected change {self._poller.current_change_count} ' + log.format_obj(clipboard_contents)) self.new_clipboard_contents_signal.send(clipboard_contents) await asyncio.sleep(CLIPBOARD_POLL_INTERVAL_SECONDS) def _extract_settable_nsobject(self, clipboard_contents): image_type = self._find_image_type(clipboard_contents) if image_type: image_data = clipboard_contents[image_type] return NSImage.alloc().initWithData_(image_data) elif 'text/plain' in clipboard_contents: bytes = clipboard_contents['text/plain'] return NSString.alloc().initWithUTF8String_(bytes) # TODO: rich text, if i ever feel like i need it else: return None def _find_image_type(self, clipboard_contents): image_types = (t for t in clipboard_contents if t.startswith('image')) return next(image_types, None) def __repr__(self): return f'<{type(self).__name__}>'
def __init__(self, clipboard): self.new_message_signal = AsyncSignal() self._clipboard = clipboard self._progress_signaler = ProgressSignaler(signals.incoming_transfer)
def __init__(self, qt_clipboard): self.new_clipboard_contents_signal = AsyncSignal() self._qt_clipboard = qt_clipboard
def __init__(self, websocket): self.new_message_signal = AsyncSignal() self._websocket = websocket self._progress_signaler = ProgressSignaler(signals.outgoing_transfer)