GCC Code Coverage Report


Directory: ./
File: modules/ui/qt/notifier.cpp
Date: 2024-05-31 17:23:24
Exec Total Coverage
Lines: 49 188 26.1%
Branches: 54 440 12.3%

Line Branch Exec Source
1 /************************************************************************
2 *
3 * Copyright (C) 2020-2024 IRCAD France
4 * Copyright (C) 2021 IHU Strasbourg
5 *
6 * This file is part of Sight.
7 *
8 * Sight is free software: you can redistribute it and/or modify it under
9 * the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Sight is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with Sight. If not, see <https://www.gnu.org/licenses/>.
20 *
21 ***********************************************************************/
22
23 #include "modules/ui/qt/notifier.hpp"
24
25 #include <core/base.hpp>
26 #include <core/com/slots.hxx>
27 #include <core/runtime/path.hpp>
28
29 #include <service/macros.hpp>
30
31 #include <ui/__/registry.hpp>
32
33 #include <boost/range/iterator_range_core.hpp>
34
35 #include <QApplication>
36
37 namespace sight::module::ui::qt
38 {
39
40 static const core::com::slots::key_t POP_NOTIFICATION_SLOT = "pop";
41 static const core::com::slots::key_t CLOSE_NOTIFICATION_SLOT = "close_notification";
42 static const core::com::slots::key_t SET_ENUM_PARAMETER_SLOT = "set_enum_parameter";
43
44 static const std::string POSITION_KEY("position");
45 static const std::string DURATION_KEY("duration");
46 static const std::string SIZE_KEY("size");
47 static const std::string MAX_KEY("max");
48 static const std::string CLOSABLE_KEY("closable");
49
50 static const std::string INFINITE("infinite");
51
52 static const std::vector<std::filesystem::path> SOUND_BOARD = {
53 std::filesystem::canonical(
54 sight::core::runtime::get_resource_file_path("sight::module::ui::qt/sounds/info_beep.wav")
55 ),
56 std::filesystem::canonical(
57 sight::core::runtime::get_resource_file_path("sight::module::ui::qt/sounds/success_beep.wav")
58 ),
59 std::filesystem::canonical(
60 sight::core::runtime::get_resource_file_path("sight::module::ui::qt/sounds/failure_beep.wav")
61 )
62 };
63
64 static const std::map<const std::string, const sight::ui::dialog::notification::position> POSITION_MAP = {
65 {"TOP_RIGHT", service::notification::position::top_right},
66 {"TOP_LEFT", service::notification::position::top_left},
67 {"CENTERED_TOP", service::notification::position::centered_top},
68 {"CENTERED", service::notification::position::centered},
69 {"BOTTOM_RIGHT", service::notification::position::bottom_right},
70 {"BOTTOM_LEFT", service::notification::position::bottom_left},
71 {"CENTERED_BOTTOM", service::notification::position::centered_bottom}
72 };
73
74 //-----------------------------------------------------------------------------
75
76
4/4
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 11 times.
✓ Branch 15 taken 77 times.
✓ Branch 16 taken 11 times.
99 notifier::notifier() noexcept
77 {
78
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 new_slot(POP_NOTIFICATION_SLOT, &notifier::pop, this);
79
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 new_slot(CLOSE_NOTIFICATION_SLOT, &notifier::close_notification, this);
80
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 new_slot(SET_ENUM_PARAMETER_SLOT, &notifier::set_enum_parameter, this);
81 11 }
82
83 //-----------------------------------------------------------------------------
84
85 11 void notifier::configuring()
86 {
87 11 const auto& config = this->get_config();
88
89
2/4
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 11 times.
✗ Branch 6 not taken.
11 if(const auto& channels = config.get_child_optional("channels"); channels)
90 {
91
2/2
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 11 times.
22 for(const auto& channel : boost::make_iterator_range(channels->equal_range("channel")))
92 {
93 11 configuration channel_config {};
94
95 // UID
96
3/6
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 11 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 11 times.
11 const auto& uid = channel.second.get_optional<std::string>("<xmlattr>.uid").value_or("");
97
98 // Position
99
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 11 times.
✗ Branch 9 not taken.
22 if(const auto& position = channel.second.get_optional<std::string>("<xmlattr>." + POSITION_KEY);
100
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 position)
101 {
102
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if(POSITION_MAP.contains(*position))
103 {
104
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 channel_config.position = POSITION_MAP.at(*position);
105 }
106 else
107 {
108 SIGHT_ERROR(
109 "Position '"
110 + *position
111 + "' isn't a valid position value, accepted values are:"
112 "TOP_RIGHT, TOP_LEFT, CENTERED_TOP, CENTERED, BOTTOM_RIGHT, BOTTOM_LEFT, CENTERED_BOTTOM."
113 )
114 }
115 }
116
117 // Duration
118
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 11 times.
✗ Branch 9 not taken.
22 if(const auto& duration = channel.second.get_optional<std::string>("<xmlattr>." + DURATION_KEY);
119
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 duration)
120 {
121
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if(*duration == INFINITE)
122 {
123 channel_config.duration = std::chrono::milliseconds(0);
124 }
125 else
126 {
127 11 try
128 {
129
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 11 times.
✗ Branch 6 not taken.
22 channel_config.duration = std::chrono::milliseconds(std::stoul(*duration));
130 }
131 catch(...)
132 {
133 SIGHT_ERROR(
134 "Duration '"
135 + *duration
136 + "' is not valid. Accepted values are: '"
137 + INFINITE
138 + "' or a positive number of milliseconds."
139 )
140 }
141 }
142 }
143
144 // Size
145
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 11 times.
22 if(const auto& size = channel.second.get_optional<std::string>("<xmlattr>." + SIZE_KEY); size)
146 {
147 try
148 {
149 if(const auto pos = size->find_first_of('x'); pos != std::string::npos)
150 {
151 const auto width = std::stoul(size->substr(0, pos));
152 const auto height = std::stoul(size->substr(pos + 1));
153
154
1/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
11 channel_config.size = {int(width), int(height)};
155 }
156 else
157 {
158 throw std::runtime_error("No 'x' found.");
159 }
160 }
161 catch(...)
162 {
163 SIGHT_ERROR(
164 "Size '"
165 + *size
166 + "' is not valid. Accepted values are: `n x n` where 'n' is a positive number."
167 )
168 }
169 }
170
171 // Max
172
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 8 taken 11 times.
✗ Branch 9 not taken.
22 if(const auto& max = channel.second.get_optional<std::string>("<xmlattr>." + MAX_KEY); max)
173 {
174 11 try
175 {
176
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 channel_config.max = std::stoul(*max);
177 }
178 catch(...)
179 {
180 SIGHT_ERROR(
181 "Maximum '"
182 + *max
183 + "' is not valid. Accepted values are positive numbers."
184 )
185 }
186 }
187
188 // Closable
189
3/6
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 11 times.
22 if(const auto& closable = channel.second.get_optional<std::string>("<xmlattr>." + CLOSABLE_KEY);
190
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 closable)
191 {
192 channel_config.closable = *closable == "true";
193 11 }
194
195
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 m_channels.insert_or_assign(uid, channel_config);
196 11 }
197 }
198
199 // Lastly, initialize sound strutures.
200 11 m_sound = std::make_unique<QSoundEffect>(qApp);
201 11 m_sound->setVolume(1.0);
202
203
1/2
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 m_default_message = config.get<std::string>("message", m_default_message);
204
1/2
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 m_parent_container_id = config.get<std::string>("parent.<xmlattr>.uid", m_parent_container_id);
205
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
11 }
206
207 //-----------------------------------------------------------------------------
208
209 11 void notifier::starting()
210 {
211
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if(!m_parent_container_id.empty())
212 {
213
1/2
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 auto container = sight::ui::registry::get_sid_container(m_parent_container_id);
214
215
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 if(!container)
216 {
217
3/8
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 11 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
22 container = sight::ui::registry::get_wid_container(m_parent_container_id);
218 }
219
220 // If we have an SID/WID set the container.
221
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if(container)
222 {
223
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
11 m_container_where_to_display_notifs = container;
224 }
225 11 }
226 11 }
227
228 //-----------------------------------------------------------------------------
229
230 11 void notifier::stopping()
231 {
232
2/2
✓ Branch 0 taken 77 times.
✓ Branch 1 taken 11 times.
88 for(const auto& [position, stack] : m_stacks)
233 {
234
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 77 times.
77 for(const auto& popup : stack.popups)
235 {
236 popup->close();
237 }
238 }
239
240 11 m_stacks.clear();
241 11 }
242
243 //-----------------------------------------------------------------------------
244
245 void notifier::updating()
246 {
247 }
248
249 //-----------------------------------------------------------------------------
250
251 void notifier::set_enum_parameter(std::string _val, std::string _key)
252 {
253 try
254 {
255 if(_key == POSITION_KEY)
256 {
257 m_channels[""].position = POSITION_MAP.at(_val);
258 }
259 else if(_key == DURATION_KEY)
260 {
261 if(_val == INFINITE)
262 {
263 m_channels[""].duration = std::chrono::milliseconds(0);
264 }
265 else
266 {
267 m_channels[""].duration = std::chrono::milliseconds(std::stoul(_val));
268 }
269 }
270 else if(_key == SIZE_KEY)
271 {
272 if(const auto pos = _val.find_first_of('x'); pos != std::string::npos)
273 {
274 const auto width = std::stoul(_val.substr(0, pos));
275 const auto height = std::stoul(_val.substr(pos + 1));
276
277 m_channels[""].size = {int(width), int(height)};
278 }
279 }
280 else if(_key == MAX_KEY)
281 {
282 m_channels[""].max = std::stoul(_val);
283 }
284 else if(_key == CLOSABLE_KEY)
285 {
286 m_channels[""].closable = _val == "true";
287 }
288 }
289 catch(...)
290 {
291 SIGHT_ERROR(std::string("Value '") + _val + "' is not handled for key " + _key);
292 }
293 }
294
295 //-----------------------------------------------------------------------------
296
297 void notifier::pop(service::notification _notification)
298 {
299 const bool channel_configured = m_channels.contains(_notification.channel);
300
301 // Get channel configuration (or global configuration if there is no channel)
302 const auto& channel_configuration = channel_configured
303 ? m_channels[_notification.channel]
304 : m_channels[""];
305
306 const auto& default_configuration = m_channels[""];
307
308 // Get the stack configuration. First try the channel, then the default, then the notification itself
309 // If you want that services totally control the notification, associate them to an unconfigured notifier
310 const auto& position = channel_configured && channel_configuration.position
311 ? *channel_configuration.position
312 : (channel_configured && !channel_configuration.position) || !default_configuration.position
313 ? _notification.position
314 : *default_configuration.position;
315
316 const auto& duration = channel_configured && channel_configuration.duration
317 ? *channel_configuration.duration
318 : (channel_configured && !channel_configuration.duration) || !default_configuration.duration
319 ? _notification.duration
320 : *default_configuration.duration;
321
322 const auto& size = channel_configured && channel_configuration.size
323 ? *channel_configuration.size
324 : (channel_configured && !channel_configuration.size) || !default_configuration.size
325 ? _notification.size
326 : *default_configuration.size;
327
328 const auto& max = channel_configuration.max
329 ? *channel_configuration.max
330 : default_configuration.max
331 ? *default_configuration.max
332 : 0;
333
334 const auto& closable = channel_configured && channel_configuration.closable
335 ? *channel_configuration.closable
336 : (channel_configured && !channel_configuration.closable) || !default_configuration.closable
337 ? _notification.closable
338 : *default_configuration.closable;
339
340 // Get the wanted stack
341 auto& target_stack = m_stacks[position];
342
343 // Compute harmonized max and size
344 target_stack.max = target_stack.max
345 ? std::max(*target_stack.max, max)
346 : max;
347
348 target_stack.size = target_stack.size
349 ? std::array<int, 2>
350 {
351 std::max((*target_stack.size)[0], size[0]),
352 std::max((*target_stack.size)[1], size[1])
353 }
354 : size;
355
356 // If the maximum number of notification is reached, remove the oldest one.
357 clean_notifications(position, *target_stack.max, *target_stack.size);
358
359 // Get or create the notification
360 const auto& popup =
361 [&]
362 {
363 // If a channel is present, try to retrieve the associated dialog
364 if(!_notification.channel.empty())
365 {
366 for(auto& [old_position, stack] : m_stacks)
367 {
368 for(const auto& popup : stack.popups)
369 {
370 if(popup->get_channel() == _notification.channel)
371 {
372 // If the position doesn't match, fix it
373 if(old_position != position)
374 {
375 // Explicit copy
376 auto copy = popup;
377 copy->set_index(static_cast<unsigned int>(target_stack.popups.size()));
378 target_stack.popups.emplace_back(copy);
379
380 // Remove the original
381 stack.popups.remove(popup);
382
383 return copy;
384 }
385
386 return popup;
387 }
388 }
389 }
390 }
391
392 // No channel or the dialog was not found, create a new one
393 auto popup = std::make_shared<sight::ui::dialog::notification>();
394 popup->set_index(static_cast<unsigned int>(target_stack.popups.size()));
395 target_stack.popups.emplace_back(popup);
396
397 return popup;
398 }();
399
400 popup->set_container(m_container_where_to_display_notifs);
401
402 const std::string& message_to_show = _notification.message.empty() ? m_default_message : _notification.message;
403 popup->set_message(message_to_show);
404
405 popup->set_type(_notification.type);
406 popup->set_position(position);
407 popup->set_duration(duration);
408 popup->set_size(*target_stack.size);
409 popup->set_closed_callback([this, popup](auto&& ...){on_notification_closed(popup);});
410 popup->set_channel(_notification.channel);
411 popup->set_closable(closable);
412 popup->show();
413
414 if(_notification.sound.has_value() && _notification.sound.value())
415 {
416 m_sound->setSource(QUrl::fromLocalFile(QString::fromStdString(SOUND_BOARD[_notification.type].string())));
417 m_sound->play();
418 }
419 }
420
421 //------------------------------------------------------------------------------
422
423 void notifier::close_notification(std::string _channel)
424 {
425 bool found = false;
426
427 for(const auto& [position, stack] : m_stacks)
428 {
429 for(const auto& popup : stack.popups)
430 {
431 if(popup->get_channel() == _channel)
432 {
433 found = true;
434 popup->close();
435 }
436 }
437 }
438
439 SIGHT_WARN_IF("Notification on channel '" << _channel << "' is already closed.", !found);
440 }
441
442 //------------------------------------------------------------------------------
443
444 void notifier::on_notification_closed(const sight::ui::dialog::notification::sptr& _notif)
445 {
446 // If the notification still exist
447 for(auto& [position, stack] : m_stacks)
448 {
449 if(auto it = std::find(stack.popups.begin(), stack.popups.end(), _notif); it != stack.popups.end())
450 {
451 erase_notification(position, it);
452 }
453 }
454 }
455
456 //------------------------------------------------------------------------------
457
458 std::list<sight::ui::dialog::notification::sptr>::iterator notifier::erase_notification(
459 const enum service::notification::position& _position,
460 const std::list<sight::ui::dialog::notification::sptr>::iterator& _it
461 )
462 {
463 // Remove the notification from the container
464 auto& stack = m_stacks[_position];
465 const auto next_it = stack.popups.erase(_it);
466 auto remaining_it = next_it;
467
468 // Move all the remaining notifications one index lower
469 while(remaining_it != stack.popups.end())
470 {
471 (*remaining_it)->move_down();
472 ++remaining_it;
473 }
474
475 // Return the it pointing after the erased one
476 return next_it;
477 }
478
479 //------------------------------------------------------------------------------
480
481 void notifier::clean_notifications(
482 const enum service::notification::position& _position,
483 std::size_t _max,
484 std::array<int, 2> _size,
485 bool _skip_permanent
486 )
487 {
488 // Get the correct "stack"
489 auto& stack = m_stacks[_position];
490
491 std::size_t removable_popups = 0;
492
493 // Count how many popups that can be removed there are
494 for(const auto& popup : stack.popups)
495 {
496 if(!_skip_permanent || popup->get_duration())
497 {
498 ++removable_popups;
499 }
500 }
501
502 for(auto it = stack.popups.begin() ; removable_popups >= _max && it != stack.popups.end() ; )
503 {
504 // If the popup is removable
505 if(const auto& duration = (*it)->get_duration(); !_skip_permanent || (duration && duration->count() > 0))
506 {
507 // Remove it
508 (*it)->close();
509 it = erase_notification(_position, it);
510 --removable_popups;
511 }
512 else
513 {
514 ++it;
515 }
516 }
517
518 // Adjust sizes
519 for(const auto& popup : stack.popups)
520 {
521 popup->set_size(_size);
522 }
523 }
524
525 //-----------------------------------------------------------------------------
526
527 } // namespace sight::module::ui::qt
528