Skip to content

Commit da77a34

Browse files
authored
Add session switching
Node Mailer now lets you transfer user discovery to a different Nuke session instead of just displaying an empty user list!
2 parents 6576dde + d93b6e4 commit da77a34

File tree

8 files changed

+248
-52
lines changed

8 files changed

+248
-52
lines changed

node_mailer/controller.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
from .user_interface.about import AboutWindow
2020
from .user_interface.history import HistoryWindow
2121
from .user_interface.mailing import MailingWindow
22-
from .user_interface.popups import ReceivedMailPopup, display_error_popup
22+
from .user_interface.popups import (
23+
ReceivedMailPopup,
24+
display_error_popup,
25+
display_yes_no_popup,
26+
)
2327
from .user_interface.settings import SettingsWindow
2428

2529

@@ -59,12 +63,24 @@ def connect_signals(self) -> None:
5963
self.history_window.import_mail.connect(nuke_interfacing.import_mail)
6064
self.mailing_window.send_mail.connect(self.send_mail)
6165
self.direct_messaging_model.message_received.connect(self.mail_received)
66+
self.direct_messaging_model.shutdown_received.connect(self.shutdown_received)
6267

6368
def open_mailing_window(self) -> None:
6469
"""Opens the mailing window."""
65-
self.mailing_window.show()
6670
play_click_sound()
6771

72+
if self.discovery_model.running:
73+
self.mailing_window.show()
74+
return
75+
76+
should_start = display_yes_no_popup(
77+
"Node Mailer is not currently running in this Nuke session. Do you want to start it? This will disable Node Mailer in other running Nuke sessions."
78+
)
79+
if should_start:
80+
self.direct_messaging_model.send_shutdown_message()
81+
self.reinitialize_systems()
82+
self.mailing_window.show()
83+
6884
def open_history_window(self) -> None:
6985
"""Opens the history window."""
7086
self.history_storage_model.retrieve_all_mail_from_database()
@@ -115,6 +131,15 @@ def send_mail(self, client: NodeMailerClient, message: str) -> None:
115131
self.mailing_window.message_text_edit.clear()
116132
self.mailing_window.close()
117133

134+
def reinitialize_systems(self) -> None:
135+
"""Kills other Node Mailer process if running and, after a slight delay,
136+
reinitializes all systems/models required for Node Mailer to function.
137+
Delay is there to give the other Node Mailer instance some time to shut down."""
138+
self.direct_messaging_model.send_shutdown_message()
139+
QtCore.QTimer.singleShot(1000, self.discovery_model.initialize_socket)
140+
QtCore.QTimer.singleShot(1000, self.discovery_model.start_background_processes)
141+
QtCore.QTimer.singleShot(1000, self.direct_messaging_model.start_listening)
142+
118143
def mail_received(self, mail: NodeMailerMail) -> None:
119144
"""Handles a received mail using the received mail popup.
120145
@@ -132,3 +157,10 @@ def mail_received(self, mail: NodeMailerMail) -> None:
132157

133158
if received_mail_popup.picked_option == ReceivedMailPopupOption.IMPORT:
134159
nuke_interfacing.import_mail(mail)
160+
161+
def shutdown_received(self) -> None:
162+
"""Shuts down Node Mailer systems when we receive a shutdown message so another
163+
Node Mailer instance can bind to the ports again."""
164+
self.discovery_model.uninitialize_socket()
165+
self.direct_messaging_model.stop_listening()
166+
self.mailing_window.close()

node_mailer/data_models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class NodeMailerMail:
2727
node_string: str
2828
timestamp: int
2929

30-
def as_json(self) -> str:
31-
"""Returns the message as a JSON string."""
32-
return json.dumps(asdict(self))
30+
def as_dict(self) -> dict:
31+
"""Returns the message as a dictionary."""
32+
return asdict(self)
3333

3434

3535
@dataclass

node_mailer/models/discovery.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ClientDiscovery(QtCore.QAbstractListModel):
2121
def __init__(self) -> None:
2222
"""Initializes the model class."""
2323
super().__init__()
24+
self.running = False
2425
self.mailing_clients: List[NodeMailerClient] = []
2526

2627
self.local_addresses = self.get_local_ip_addresses()
@@ -52,6 +53,14 @@ def initialize_socket(self) -> None:
5253
constants.Ports.BROADCAST.value,
5354
)
5455
self.udp_socket.readyRead.connect(self.on_datagram_received)
56+
self.running = self.udp_socket.state() == QtNetwork.QAbstractSocket.BoundState
57+
58+
def uninitialize_socket(self) -> None:
59+
"""Uninitializes the socket and disconnects the signals."""
60+
self.udp_socket.readyRead.disconnect(self.on_datagram_received)
61+
self.udp_socket.close()
62+
self.running = False
63+
self.mailing_clients = []
5564

5665
def store_icons(self) -> None:
5766
"""Stores the icons this model should return."""

node_mailer/models/messaging.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class DirectMessaging(QtCore.QObject):
2020
Uses TCP to receive/send data."""
2121

2222
message_received = QtCore.Signal(NodeMailerMail)
23+
shutdown_received = QtCore.Signal()
2324

2425
def __init__(self) -> None:
2526
"""Initializes the messaging handler."""
@@ -36,6 +37,10 @@ def start_listening(self) -> None:
3637
port=constants.Ports.MESSAGING.value,
3738
)
3839

40+
def stop_listening(self) -> None:
41+
"""Stops listening for incoming messages."""
42+
self.tcp_server.close()
43+
3944
def on_new_connection(self) -> None:
4045
"""Connects the readyRead signal to the on_message_received slot for new connections."""
4146
new_client = self.tcp_server.nextPendingConnection()
@@ -44,7 +49,7 @@ def on_new_connection(self) -> None:
4449
lambda: self.on_message_received(receiving_connection)
4550
)
4651
new_client.disconnected.connect(
47-
lambda: self.on_connection_closed(receiving_connection)
52+
lambda: self.process_received_message(receiving_connection)
4853
)
4954
self.open_connections.append(receiving_connection)
5055

@@ -56,27 +61,24 @@ def on_message_received(self, connection: ReceivingConnection) -> None:
5661
"""
5762
connection.message += connection.socket.readAll().data().decode("utf-8")
5863

59-
def on_connection_closed(self, connection: ReceivingConnection) -> None:
60-
"""Closed connection means we have received all the data. We can now process the message.
64+
def process_received_message(self, connection: ReceivingConnection) -> None:
65+
"""Processes the complete received message we have stored when the sending connection closes.
6166
6267
Args:
6368
connection: The connection that was closed.
6469
"""
65-
mail = self.get_mail_from_message_string(connection.message)
66-
self.message_received.emit(mail)
67-
self.open_connections.remove(connection)
70+
try:
71+
parsed_message = json.loads(connection.message)
72+
except json.JSONDecodeError:
73+
return
6874

69-
def get_mail_from_message_string(self, message_string: str) -> NodeMailerMail:
70-
"""Returns a NodeMailerMessage object from the network-sent string.
71-
72-
Args:
73-
message_string: The JSON message in string form.
75+
if parsed_message["type"] == "shutdown":
76+
self.shutdown_received.emit()
77+
return
7478

75-
Returns:
76-
The NodeMailerMessage object.
77-
"""
78-
parsed_message = json.loads(message_string)
79-
return NodeMailerMail(**parsed_message)
79+
mail = NodeMailerMail(**parsed_message["mail"])
80+
self.message_received.emit(mail)
81+
self.open_connections.remove(connection)
8082

8183
def send_mail_to_client(
8284
self, mail: NodeMailerMail, client: NodeMailerClient
@@ -90,10 +92,29 @@ def send_mail_to_client(
9092
tcp_socket = QtNetwork.QTcpSocket()
9193
tcp_socket.connectToHost(client.ip_address, constants.Ports.MESSAGING.value)
9294

93-
if not tcp_socket.waitForConnected(2000):
95+
if not tcp_socket.waitForConnected(500):
9496
msg = f"Could not connect to client {client.name}. Is it still running?"
9597
raise ConnectionError(msg)
9698

97-
tcp_socket.write(mail.as_json().encode("utf-8"))
99+
dict_mail = {"type": "mail"}
100+
dict_mail["mail"] = mail.as_dict()
101+
102+
tcp_socket.write(json.dumps(dict_mail).encode("utf-8"))
98103
tcp_socket.waitForBytesWritten()
99104
tcp_socket.disconnectFromHost()
105+
tcp_socket.close()
106+
107+
def send_shutdown_message(self) -> None:
108+
"""Sends a shutdown message to the local running Node Mailer instance."""
109+
tcp_socket = QtNetwork.QTcpSocket()
110+
tcp_socket.connectToHost("localhost", constants.Ports.MESSAGING.value)
111+
112+
if not tcp_socket.waitForConnected(2000):
113+
return
114+
115+
shutdown_message = {"type": "shutdown"}
116+
tcp_socket.write(json.dumps(shutdown_message).encode("utf-8"))
117+
tcp_socket.waitForBytesWritten()
118+
tcp_socket.disconnectFromHost()
119+
tcp_socket.close()
120+

node_mailer/resources/question.png

372 Bytes
Loading

node_mailer/user_interface/popups.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,136 @@ def display_error_popup(error_text: str) -> None:
125125
error_popup.exec_()
126126

127127

128+
class YesNoPopup(NodeMailerWindow):
129+
"""A popup window that displays a yes/no question."""
130+
131+
closed = QtCore.Signal()
132+
133+
def __init__(self, question_text: str) -> None:
134+
"""Initializes the yes/no popup.
135+
136+
Args:
137+
question_text: The question to display.
138+
"""
139+
super().__init__(
140+
self.get_user_interface(question_text),
141+
"Node Mailer: Question",
142+
resizable=False,
143+
)
144+
self.picked_option = False
145+
146+
def get_user_interface(self, question_text: str) -> QtWidgets.QWidget:
147+
"""Returns the user interface for the yes/no popup.
148+
149+
Args:
150+
question_text: The question to display.
151+
152+
Returns:
153+
The widget with the yes/no popup user interface.
154+
"""
155+
widget = QtWidgets.QWidget()
156+
layout = QtWidgets.QVBoxLayout()
157+
layout.setContentsMargins(0, 0, 0, 0)
158+
widget.setLayout(layout)
159+
160+
layout.addWidget(self.get_question_widget(question_text))
161+
layout.addWidget(self.get_bottom_buttons())
162+
163+
return widget
164+
165+
def get_question_widget(self, question_text: str) -> QtWidgets.QWidget:
166+
"""Returns the question widget for the yes/no popup.
167+
168+
Args:
169+
question_text: The question to display.
170+
171+
Returns:
172+
The widget with the question.
173+
"""
174+
widget = QtWidgets.QWidget()
175+
layout = QtWidgets.QHBoxLayout()
176+
layout.setContentsMargins(20, 0, 20, 0)
177+
layout.setAlignment(QtCore.Qt.AlignCenter)
178+
layout.setSpacing(20)
179+
widget.setLayout(layout)
180+
181+
question_icon = QtWidgets.QLabel()
182+
pixmap = QtGui.QPixmap(
183+
str(Path(__file__).parent.parent / "resources" / "question.png")
184+
)
185+
scaled_pixmap = pixmap.scaled(48, 48, QtCore.Qt.KeepAspectRatio)
186+
question_icon.setPixmap(scaled_pixmap)
187+
layout.addWidget(question_icon)
188+
189+
question_text = QtWidgets.QLabel(question_text)
190+
question_text.setWordWrap(True)
191+
question_text.setMinimumWidth(250)
192+
layout.addWidget(question_text)
193+
194+
return widget
195+
196+
def get_bottom_buttons(self) -> QtWidgets.QWidget:
197+
"""Returns the bottom buttons for the yes/no popup.
198+
199+
Returns:
200+
The widget with the bottom buttons.
201+
"""
202+
widget = QtWidgets.QWidget()
203+
layout = QtWidgets.QHBoxLayout()
204+
layout.setContentsMargins(0, 0, 0, 0)
205+
layout.setAlignment(QtCore.Qt.AlignRight)
206+
widget.setLayout(layout)
207+
208+
no_button = NodeMailerButton("No")
209+
no_button.clicked.connect(self.on_no_clicked)
210+
layout.addWidget(no_button)
211+
212+
yes_button = NodeMailerButton("Yes")
213+
yes_button.clicked.connect(self.on_yes_clicked)
214+
layout.addWidget(yes_button)
215+
216+
return widget
217+
218+
def on_no_clicked(self) -> None:
219+
"""Handles the no button being clicked."""
220+
self.picked_option = False
221+
self.close()
222+
223+
def on_yes_clicked(self) -> None:
224+
"""Handles the yes button being clicked."""
225+
self.picked_option = True
226+
self.close()
227+
228+
def exec_(self):
229+
"""Executes the yes/no popup. Mimics the behavior of a blocking dialog."""
230+
self.show()
231+
self.raise_()
232+
self.activateWindow()
233+
234+
loop = QtCore.QEventLoop()
235+
self.closed.connect(loop.quit)
236+
loop.exec_()
237+
238+
def closeEvent(self, event): # noqa: N802
239+
"""Emits the closed signal when the window is closed."""
240+
self.closed.emit()
241+
super().closeEvent(event)
242+
243+
244+
def display_yes_no_popup(question_text: str) -> bool:
245+
"""Displays a yes/no popup with the given question text.
246+
247+
Args:
248+
question_text: The question to display.
249+
250+
Returns:
251+
True if the user clicked yes, False if the user clicked no.
252+
"""
253+
yes_no_popup = YesNoPopup(question_text)
254+
yes_no_popup.exec_()
255+
return yes_no_popup.picked_option
256+
257+
128258
class ReceivedMailPopup(NodeMailerWindow):
129259
"""A popup window that display the received mail prompt."""
130260

0 commit comments

Comments
 (0)