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
+ 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)