diff --git a/resources/main-window.ui b/resources/main-window.ui index f849e8eff..b2717c01a 100644 --- a/resources/main-window.ui +++ b/resources/main-window.ui @@ -592,6 +592,26 @@ 1 + + + True + True + False + + + True + False + Send message + + + + + True + True + end + 2 + + diff --git a/resources/op-item.ui b/resources/op-item.ui index d245f74a5..368858638 100644 --- a/resources/op-item.ui +++ b/resources/op-item.ui @@ -51,6 +51,12 @@ center list-remove-symbolic + + True + False + center + edit-copy-symbolic + True False @@ -139,8 +145,10 @@ False Awaiting approval True + end 30 30 + 4 0 @@ -386,6 +394,21 @@ 8 + + + True + True + True + Copy message + center + image11 + + + False + False + 9 + + diff --git a/src/notifications.py b/src/notifications.py index 193b53ed2..9b580a07e 100644 --- a/src/notifications.py +++ b/src/notifications.py @@ -224,3 +224,34 @@ def _notification_response(self, action, variant, op): app = Gio.Application.get_default() app.lookup_action("notification-response").disconnect_by_func(self._notification_response) + +class TextMessageNotification(): + def __init__(self, op): + self.op = op + self.send_notification() + + @misc._idle + def send_notification(self): + if prefs.get_show_notifications(): + notification = Gio.Notification.new(_("New message from %s") % self.op.sender_name) + notification.set_body(self.op.message) + notification.set_icon(Gio.ThemedIcon(name="org.x.Warpinator-symbolic")) + notification.set_priority(Gio.NotificationPriority.URGENT) + + notification.add_button(_("Copy"), "app.notification-response::copy") + notification.set_default_action("app.notification-response::focus") + + app = Gio.Application.get_default() + app.lookup_action("notification-response").connect("activate", self._notification_response, self.op) + app.send_notification(self.op.sender, notification) + + def _notification_response(self, action, variant, op): + response = variant.unpack() + + if response == "copy": + op.copy_message() + else: + op.focus() + + app = Gio.Application.get_default() + app.lookup_action("notification-response").disconnect_by_func(self._notification_response) diff --git a/src/ops.py b/src/ops.py index 43ac62e49..22b7e0786 100644 --- a/src/ops.py +++ b/src/ops.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from gi.repository import GObject, GLib, Gio +from gi.repository import GObject, GLib, Gio, Gtk, Gdk import grpc @@ -283,3 +283,23 @@ def stop_transfer(self): def remove_transfer(self): self.emit("op-command", OpCommand.REMOVE_TRANSFER) +class TextMessageOp(CommonOp): + message = None + + def __init__(self, direction, sender): + super(TextMessageOp, self).__init__(direction, sender) + self.gicon = Gio.ThemedIcon.new("mail-message-new-symbolic") + self.description = _("Text message") + + def copy_message(self): + cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + cb.set_text(self.message, -1) + + def send_notification(self): + notifications.TextMessageNotification(self) + + def remove_transfer(self): + self.emit("op-command", OpCommand.REMOVE_TRANSFER) + + def retry_transfer(self): + self.emit("op-command", OpCommand.RETRY_TRANSFER) diff --git a/src/remote.py b/src/remote.py index b94a82acd..1523d79c7 100644 --- a/src/remote.py +++ b/src/remote.py @@ -17,7 +17,7 @@ import misc import transfers import auth -from ops import SendOp, ReceiveOp +from ops import SendOp, ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError _ = gettext.gettext @@ -585,6 +585,21 @@ def _send_files(uri_list): util.add_to_recents_if_single_selection(uri_list) self.rpc_call(_send_files, uri_list) + def send_text_message(self, message): + op = TextMessageOp(TransferDirection.TO_REMOTE_MACHINE, self.local_ident) + op.message = message + op.status = OpStatus.FINISHED + self.add_op(op) + self.rpc_call(self.do_send_text_message, op) + + def do_send_text_message(self, op): + try: + self.stub.SendTextMessage(warp_pb2.TextMessage(ident=self.local_ident, timestamp=op.start_time, message=op.message)) + except Exception as e: + logging.error("Sending message failed: %s" % e) + op.status = OpStatus.FAILED + op.emit_status_changed() + @misc._idle def add_op(self, op): if op not in self.transfer_ops: @@ -657,8 +672,13 @@ def op_command_issued(self, op, command): elif command == OpCommand.STOP_TRANSFER_BY_SENDER: self.rpc_call(self.stop_transfer_op, op, by_sender=True) elif command == OpCommand.RETRY_TRANSFER: - op.set_status(OpStatus.WAITING_PERMISSION) - self.rpc_call(self.send_transfer_op_request, op) + if isinstance(op, TextMessageOp): + op.status = OpStatus.FINISHED + op.emit_status_changed() + self.rpc_call(self.do_send_text_message, op) + else: + op.set_status(OpStatus.WAITING_PERMISSION) + self.rpc_call(self.send_transfer_op_request, op) elif command == OpCommand.REMOVE_TRANSFER: self.remove_op(op) # receive diff --git a/src/server.py b/src/server.py index 829fac6f3..994a01529 100644 --- a/src/server.py +++ b/src/server.py @@ -26,7 +26,7 @@ import util import misc import transfers -from ops import ReceiveOp +from ops import ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, RemoteStatus import zeroconf @@ -669,3 +669,20 @@ def StopTransfer(self, request, context): op.set_status(OpStatus.FAILED) return void + + def SendTextMessage(self, request, context): + logging.debug("Server RPC: SendTextMessage from '%s'" % request.ident) + try: + remote_machine:remote.RemoteMachine = self.remote_machines[request.ident] + except KeyError as e: + logging.warning("Received text message from unknown remote: %s" % e) + return + + op = TextMessageOp(TransferDirection.FROM_REMOTE_MACHINE, request.ident) + op.sender_name = remote_machine.display_name + op.message = request.message + op.status = OpStatus.FINISHED + remote_machine.add_op(op) + op.send_notification() + + return void diff --git a/src/warp.proto b/src/warp.proto index 71c8558b6..365df1596 100644 --- a/src/warp.proto +++ b/src/warp.proto @@ -18,6 +18,7 @@ service Warp { rpc GetRemoteMachineAvatar(LookupName) returns (stream RemoteMachineAvatar) {} rpc ProcessTransferOpRequest(TransferOpRequest) returns (VoidType) {} rpc PauseTransferOp(OpInfo) returns (VoidType) {} + rpc SendTextMessage(TextMessage) returns (VoidType) {} // Receiver methods rpc StartTransfer(OpInfo) returns (stream FileChunk) {} @@ -112,3 +113,8 @@ message ServiceRegistration { uint32 auth_port = 6; } +message TextMessage { + string ident = 1; + uint64 timestamp = 2; + string message = 3; +} diff --git a/src/warp_pb2.py b/src/warp_pb2.py index 0144fa269..89b2057da 100644 --- a/src/warp_pb2.py +++ b/src/warp_pb2.py @@ -13,7 +13,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"*\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"}\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r2\xf2\x03\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"*\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"}\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\"@\n\x0bTextMessage\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t2\xa0\x04\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12,\n\x0fSendTextMessage\x12\x0c.TextMessage\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -46,8 +46,10 @@ _globals['_REGRESPONSE']._serialized_end=846 _globals['_SERVICEREGISTRATION']._serialized_start=848 _globals['_SERVICEREGISTRATION']._serialized_end=973 - _globals['_WARP']._serialized_start=976 - _globals['_WARP']._serialized_end=1474 - _globals['_WARPREGISTRATION']._serialized_start=1477 - _globals['_WARPREGISTRATION']._serialized_end=1611 + _globals['_TEXTMESSAGE']._serialized_start=975 + _globals['_TEXTMESSAGE']._serialized_end=1039 + _globals['_WARP']._serialized_start=1042 + _globals['_WARP']._serialized_end=1586 + _globals['_WARPREGISTRATION']._serialized_start=1589 + _globals['_WARPREGISTRATION']._serialized_end=1723 # @@protoc_insertion_point(module_scope) diff --git a/src/warp_pb2_grpc.py b/src/warp_pb2_grpc.py index ea8ae76b1..9ae83d06a 100644 --- a/src/warp_pb2_grpc.py +++ b/src/warp_pb2_grpc.py @@ -51,6 +51,11 @@ def __init__(self, channel): request_serializer=warp__pb2.OpInfo.SerializeToString, response_deserializer=warp__pb2.VoidType.FromString, ) + self.SendTextMessage = channel.unary_unary( + '/Warp/SendTextMessage', + request_serializer=warp__pb2.TextMessage.SerializeToString, + response_deserializer=warp__pb2.VoidType.FromString, + ) self.StartTransfer = channel.unary_stream( '/Warp/StartTransfer', request_serializer=warp__pb2.OpInfo.SerializeToString, @@ -122,6 +127,12 @@ def PauseTransferOp(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SendTextMessage(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def StartTransfer(self, request, context): """Receiver methods """ @@ -181,6 +192,11 @@ def add_WarpServicer_to_server(servicer, server): request_deserializer=warp__pb2.OpInfo.FromString, response_serializer=warp__pb2.VoidType.SerializeToString, ), + 'SendTextMessage': grpc.unary_unary_rpc_method_handler( + servicer.SendTextMessage, + request_deserializer=warp__pb2.TextMessage.FromString, + response_serializer=warp__pb2.VoidType.SerializeToString, + ), 'StartTransfer': grpc.unary_stream_rpc_method_handler( servicer.StartTransfer, request_deserializer=warp__pb2.OpInfo.FromString, @@ -320,6 +336,23 @@ def PauseTransferOp(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def SendTextMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/Warp/SendTextMessage', + warp__pb2.TextMessage.SerializeToString, + warp__pb2.VoidType.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def StartTransfer(request, target, diff --git a/src/warpinator.py b/src/warpinator.py index 1ac186619..09a29b706 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -25,7 +25,7 @@ import auth import misc import networkmonitor -from ops import SendOp, ReceiveOp +from ops import SendOp, ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, RemoteStatus # XApp 2.0 required for favorites. @@ -65,7 +65,8 @@ "transfer_resume", \ "transfer_stop", \ "transfer_remove", \ - "transfer_open_folder") + "transfer_open_folder", \ + "transfer_copy_message") INIT_BUTTONS = () PERM_TO_SEND_BUTTONS = ("transfer_cancel_request",) @@ -84,6 +85,7 @@ TRANSFER_COMPLETED_SENDER_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_FILE_NOT_FOUND_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_COMPLETED_RECEIVER_BUTTONS = ("transfer_remove", "transfer_open_folder") +TRANSFER_TEXT_MESSAGE_BUTTONS = ("transfer_remove", "transfer_copy_message") class OpItem(object): def __init__(self, op): @@ -109,6 +111,7 @@ def __init__(self, op): self.stop_button = self.builder.get_object("transfer_stop") self.remove_button = self.builder.get_object("transfer_remove") self.folder_button = self.builder.get_object("transfer_open_folder") + self.copy_button = self.builder.get_object("transfer_copy_message") self.accept_button.connect("clicked", self.accept_button_clicked) self.decline_button.connect("clicked", self.decline_button_clicked) @@ -118,6 +121,7 @@ def __init__(self, op): self.stop_button.connect("clicked", self.stop_button_clicked) self.remove_button.connect("clicked", self.remove_button_clicked) self.folder_button.connect("clicked", self.folder_button_clicked) + self.copy_button.connect("clicked", self.copy_button_clicked) self.op.connect("progress-changed", self.update_progress) @@ -179,7 +183,14 @@ def refresh_status_widgets(self): else: self.op_transfer_problem_label.set_text(_("Some files not found")) elif self.op.status == OpStatus.FINISHED: - self.op_transfer_status_message.set_text(_("Completed")) + if isinstance(self.op, TextMessageOp): + msg = "\n".join(self.op.message.split("\n")[:4]) # Max 4 lines + if len(msg) > 120: # Max 120 chars (4*30 per line) -- FIXME: This might still exceed 4 lines + msg = msg[:117] + "..." + self.op_transfer_status_message.set_text(msg) + self.op_transfer_status_message.set_selectable(True) + else: + self.op_transfer_status_message.set_text(_("Completed")) elif self.op.status == OpStatus.FINISHED_WARNING: self.op_transfer_status_message.set_text(_("Completed, but with errors")) @@ -235,6 +246,8 @@ def refresh_buttons_and_icons(self): self.op_status_stack.set_visible_child_name("message") if isinstance(self.op, SendOp): self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) + elif isinstance(self.op, TextMessageOp): + self.set_visible_buttons(TRANSFER_TEXT_MESSAGE_BUTTONS) else: self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) elif self.op.status in (OpStatus.CANCELLED_PERMISSION_BY_SENDER, @@ -273,6 +286,9 @@ def folder_button_clicked(self, button): util.open_save_folder(self.op.top_dir_basenames[0]) else: util.open_save_folder() + + def copy_button_clicked(self, button): + self.op.copy_message() def destroy(self): self.builder = None @@ -504,6 +520,8 @@ def __init__(self): self.user_ip_label = self.builder.get_object("user_ip") self.user_op_list = self.builder.get_object("user_op_list") self.user_send_button = self.builder.get_object("user_send_button") + self.user_send_msg_button = self.builder.get_object("user_send_msg_button") + self.user_send_msg_button.connect("clicked", self.send_msg_button_clicked) self.user_online_box = self.builder.get_object("user_online_box") self.user_online_image = self.builder.get_object("user_online_image") self.user_online_label = self.builder.get_object("user_online_label") @@ -734,6 +752,9 @@ def recent_item_selected(self, recent_chooser, data=None): def favorite_selected(self, favorites, uri): self.current_selected_remote_machine.send_files([uri]) + def send_msg_button_clicked(self, button): + SendMessageDialog(self).show() + def open_file_picker(self, button, data=None): dialog = util.create_file_and_folder_picker(self.window) @@ -847,6 +868,9 @@ def restart_service_clicked(self, menuitem): def manual_connect_to_host(self, host): logging.debug("Connecting to " + host) + def send_text_message(self, message): + self.current_selected_remote_machine.send_text_message(message) + def report_bad_save_folder(self): path = prefs.get_save_path() self.bad_save_folder_label.set_text(path) @@ -1051,6 +1075,7 @@ def current_selected_remote_status_changed(self, remote_machine): (entry,), Gdk.DragAction.COPY) self.user_send_button.set_sensitive(True) + self.user_send_msg_button.set_sensitive(True) self.user_online_label.set_text(_("Online")) self.user_online_image.set_from_icon_name(ICON_ONLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1058,6 +1083,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.OFFLINE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Offline")) self.user_online_image.set_from_icon_name(ICON_OFFLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1065,6 +1091,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.UNREACHABLE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Unable to connect")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1072,6 +1099,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.AWAITING_DUPLEX: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Waiting for two-way connection")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1079,6 +1107,7 @@ def current_selected_remote_status_changed(self, remote_machine): else: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Connecting")) self.user_online_image.hide() self.user_online_spinner.show() @@ -1258,6 +1287,49 @@ def validate_address(self, entry): m = self.ip_validator_re.match(address) self.connect_button.set_sensitive(m is not None) +class SendMessageDialog(Gtk.Window): + def __init__(self, parent:WarpWindow): + super().__init__(title=_("Send message"), transient_for=parent.window, modal=True, resizable=False) + self.parent = parent + + self.set_default_size(300, 100) + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.add(vbox) + + scrollView = Gtk.ScrolledWindow() + scrollView.set_size_request(300, 50) + scrollView.set_shadow_type(Gtk.ShadowType.OUT) + vbox.add(scrollView) + + self.textView = Gtk.TextView() + self.textView.set_editable(True) + self.textView.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + scrollView.add(self.textView) + + btnClose = Gtk.Button(_("Cancel")) + btnClose.connect("clicked", lambda _ : self.close()) + btnSend = Gtk.Button(_("Send")) + btnSend.connect("clicked", self.send_clicked) + btnBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + btnBox.pack_end(btnSend, False, False, 0) + btnBox.pack_end(btnClose, False, False, 0) + vbox.add(btnBox) + + vbox.set_margin_bottom(10) + vbox.set_margin_top(10) + vbox.set_margin_left(10) + vbox.set_margin_right(10) + self.show_all() + + def send_clicked(self, btn): + buf = self.textView.get_buffer() + buf_s, buf_e = buf.get_bounds() + self.parent.send_text_message(buf.get_text(buf_s, buf_e, False)) + self.close() + class WarpApplication(Gtk.Application): def __init__(self, testing=False): super(WarpApplication, self).__init__(application_id="org.x.Warpinator", register_session=True)