VoIP Calls: Streams related to calls can be selected in RTP Streams

When user press S(elect)/D(eselect) key, all RTP streams related to
selected call/calls are selected/deselected in RTP Streams window. If
window is not shown, it is opened.
Documentation updated.
This commit is contained in:
Jirka Novak 2021-04-19 15:54:58 +02:00 committed by Wireshark GitLab Utility
parent 42c54434a8
commit a8ccb67921
15 changed files with 197 additions and 64 deletions

View File

@ -674,17 +674,18 @@ Available controls:
.Flow Graph window showing VoIP call sequences
image::wsug_graphics/ws-tel-seq-dialog.png[{screenshot-attrs}]
Available controls:
Additional shortcuts available for VoIP calls:
* On selected RTP stream
** kbd:[S] - Selects the stream in <<ChTelRTPStreams,RTP Streams>> window (if not opened, it opens it and put it on background).
** kbd:[D] - Deselects the stream in <<ChTelRTPStreams,RTP Streams>> window (if not opened, it opens it and put it on background).
Additional controls available for VoIP calls:
* btn:[Reset Diagram] resets view position and zoom to default state.
* btn:[Play Streams] sends selected RTP stream to playlist of <<ChTelRtpPlayer,RTP Player>> window.
* btn:[Export] allows to export diagram as image in multiple different formats (PDF, PNG, BMP, JPEG and ASCII (diagram is stored with ASCII characters only)).
Additional available shortcuts:
* On selected RTP stream
** kbd:[S] - Selects the stream in <<ChTelRTPStreams,RTP Streams>> window (if not opened, it opens it and put on background).
** kbd:[D] - Deselects the stream in <<ChTelRTPStreams,RTP Streams>> window (if not opened, it opens it and put on background).
[[ChStatHARTIP]]

View File

@ -129,6 +129,17 @@ VoIP Calls window can be opened as window showing all protocol types (menu:Telep
.VoIP Calls window
image::wsug_graphics/ws-tel-voip-calls.png[{screenshot-attrs}]
User can use shortcuts:
* Selection
** kbd:[Ctrl + A] - Select all streams
** kbd:[Ctrl + I] - Invert selection
** kbd:[Ctrl + Shift + A] - Select none
** Note: Common kbd:[Mouse click], kbd:[Shift + Mouse click] and kbd:[Ctrl + Mouse click] works too
* On selected call/calls
** kbd:[S] - Selects stream/streams related to call in RTP Streams window (if not opened, it opens it and put it on background).
** kbd:[D] - Deselects stream/streams related to call in RTP Streams window (if not opened, it opens it and put it on background).
Available controls are:
* btn:[Limit to display filter] filters calls just to ones matching display filter. When display filter is active before window is opened, checkbox is checked.

View File

@ -366,11 +366,11 @@ public slots:
void rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogAddRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *> stream_infos);
void rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *> stream_infos);
void rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *> stream_infos);
void rtpStreamsDialogSelectRtpStream(rtpstream_id_t *id);
void rtpStreamsDialogDeselectRtpStream(rtpstream_id_t *id);
void rtpAnalysisDialogReplaceRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void rtpAnalysisDialogAddRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *> stream_ids);
private slots:

View File

@ -4181,19 +4181,19 @@ void MainWindow::rtpAnalysisDialogRemoveRtpStreams(QVector<rtpstream_id_t *> str
telephony_dialog_mutex.unlock();
}
void MainWindow::rtpStreamsDialogSelectRtpStream(rtpstream_id_t *id)
void MainWindow::rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *> stream_ids)
{
telephony_dialog_mutex.lock();
openTelephonyRtpStreamsDialog();
rtp_stream_dialog_->selectRtpStream(id);
rtp_stream_dialog_->selectRtpStream(stream_ids);
telephony_dialog_mutex.unlock();
}
void MainWindow::rtpStreamsDialogDeselectRtpStream(rtpstream_id_t *id)
void MainWindow::rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *> stream_ids)
{
telephony_dialog_mutex.lock();
openTelephonyRtpStreamsDialog();
rtp_stream_dialog_->deselectRtpStream(id);
rtp_stream_dialog_->deselectRtpStream(stream_ids);
telephony_dialog_mutex.unlock();
}

View File

@ -71,11 +71,9 @@ public:
static QPushButton *addAnalyzeButton(QDialogButtonBox *button_box, QDialog *dialog);
/** Replace/Add/Remove an RTP streams to analyse.
* Requires array of rtpstream_info_t.
* Each item must have filled items: src_addr, src_port, dest_addr,
* dest_port, ssrc, packet_count, setup_frame_number, and start_rel_time.
* Requires array of rtpstream_id_t.
*
* @param stream_ids structs with rtpstream info
* @param stream_ids structs with rtpstream_id
*/
void replaceRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void addRtpStreams(QVector<rtpstream_id_t *> stream_ids);

View File

@ -339,14 +339,18 @@ void RtpStreamDialog::setRtpStreamSelection(rtpstream_id_t *id, bool state)
}
}
void RtpStreamDialog::selectRtpStream(rtpstream_id_t *id)
void RtpStreamDialog::selectRtpStream(QVector<rtpstream_id_t *> stream_ids)
{
setRtpStreamSelection(id, true);
foreach(rtpstream_id_t *id, stream_ids) {
setRtpStreamSelection(id, true);
}
}
void RtpStreamDialog::deselectRtpStream(rtpstream_id_t *id)
void RtpStreamDialog::deselectRtpStream(QVector<rtpstream_id_t *> stream_ids)
{
setRtpStreamSelection(id, false);
foreach(rtpstream_id_t *id, stream_ids) {
setRtpStreamSelection(id, false);
}
}
bool RtpStreamDialog::eventFilter(QObject *, QEvent *event)

View File

@ -29,8 +29,8 @@ class RtpStreamDialog : public WiresharkDialog
public:
explicit RtpStreamDialog(QWidget &parent, CaptureFile &cf);
~RtpStreamDialog();
void selectRtpStream(rtpstream_id_t *id);
void deselectRtpStream(rtpstream_id_t *id);
void selectRtpStream(QVector<rtpstream_id_t *> stream_ids);
void deselectRtpStream(QVector<rtpstream_id_t *> stream_ids);
signals:
// Tells the packet list to redraw. An alternative might be to add a

View File

@ -151,11 +151,11 @@ SequenceDialog::SequenceDialog(QWidget &parent, CaptureFile &cf, SequenceInfo *i
ctx_menu_.addAction(ui->actionGoToNextPacket);
ctx_menu_.addAction(ui->actionGoToPreviousPacket);
ctx_menu_.addSeparator();
action = ui->actionSelectRtpStream;
action = ui->actionSelectRtpStreams;
ctx_menu_.addAction(action);
action->setVisible(false);
action->setEnabled(false);
action = ui->actionDeselectRtpStream;
action = ui->actionDeselectRtpStreams;
ctx_menu_.addAction(action);
action->setVisible(false);
action->setEnabled(false);
@ -217,8 +217,8 @@ void SequenceDialog::enableVoIPFeatures()
{
voipFeaturesEnabled = true;
player_button_->setVisible(true);
ui->actionSelectRtpStream->setVisible(true);
ui->actionDeselectRtpStream->setVisible(true);
ui->actionSelectRtpStreams->setVisible(true);
ui->actionDeselectRtpStreams->setVisible(true);
}
void SequenceDialog::updateWidgets()
@ -296,12 +296,12 @@ void SequenceDialog::keyPressEvent(QKeyEvent *event)
break;
case Qt::Key_S:
if (voipFeaturesEnabled) {
on_actionSelectRtpStream_triggered();
on_actionSelectRtpStreams_triggered();
}
break;
case Qt::Key_D:
if (voipFeaturesEnabled) {
on_actionDeselectRtpStream_triggered();
on_actionDeselectRtpStreams_triggered();
}
break;
}
@ -343,13 +343,13 @@ void SequenceDialog::diagramClicked(QMouseEvent *event)
if (event) {
seq_analysis_item_t *sai = seq_diagram_->itemForPosY(event->pos().y());
if (voipFeaturesEnabled) {
ui->actionSelectRtpStream->setEnabled(false);
ui->actionDeselectRtpStream->setEnabled(false);
ui->actionSelectRtpStreams->setEnabled(false);
ui->actionDeselectRtpStreams->setEnabled(false);
player_button_->setEnabled(false);
if (sai) {
if (GA_INFO_TYPE_RTP == sai->info_type) {
ui->actionSelectRtpStream->setEnabled(true && !file_closed_);
ui->actionDeselectRtpStream->setEnabled(true && !file_closed_);
ui->actionSelectRtpStreams->setEnabled(true && !file_closed_);
ui->actionDeselectRtpStreams->setEnabled(true && !file_closed_);
player_button_->setEnabled(true && !file_closed_);
current_rtp_sai_ = sai;
}
@ -378,8 +378,8 @@ void SequenceDialog::mouseMoved(QMouseEvent *event)
seq_analysis_item_t *sai = seq_diagram_->itemForPosY(event->pos().y());
if (sai) {
if (GA_INFO_TYPE_RTP == sai->info_type) {
ui->actionSelectRtpStream->setEnabled(true);
ui->actionDeselectRtpStream->setEnabled(true);
ui->actionSelectRtpStreams->setEnabled(true);
ui->actionDeselectRtpStreams->setEnabled(true);
current_rtp_sai_ = sai;
}
packet_num_ = sai->frame_number;
@ -752,18 +752,24 @@ void SequenceDialog::on_actionZoomOut_triggered()
zoomXAxis(false);
}
void SequenceDialog::on_actionSelectRtpStream_triggered()
void SequenceDialog::on_actionSelectRtpStreams_triggered()
{
if (current_rtp_sai_ && GA_INFO_TYPE_RTP == current_rtp_sai_->info_type) {
emit rtpStreamsDialogSelectRtpStream(&((rtpstream_info_t *)current_rtp_sai_->info_ptr)->id);
QVector<rtpstream_id_t *> stream_ids;
stream_ids << &((rtpstream_info_t *)current_rtp_sai_->info_ptr)->id;
emit rtpStreamsDialogSelectRtpStreams(stream_ids);
raise();
}
}
void SequenceDialog::on_actionDeselectRtpStream_triggered()
void SequenceDialog::on_actionDeselectRtpStreams_triggered()
{
if (current_rtp_sai_ && GA_INFO_TYPE_RTP == current_rtp_sai_->info_type) {
emit rtpStreamsDialogDeselectRtpStream(&((rtpstream_info_t *)current_rtp_sai_->info_ptr)->id);
QVector<rtpstream_id_t *> stream_ids;
stream_ids << &((rtpstream_info_t *)current_rtp_sai_->info_ptr)->id;
emit rtpStreamsDialogDeselectRtpStreams(stream_ids);
raise();
}
}

View File

@ -59,8 +59,8 @@ protected:
void keyPressEvent(QKeyEvent *event);
signals:
void rtpStreamsDialogSelectRtpStream(rtpstream_id_t *id);
void rtpStreamsDialogDeselectRtpStream(rtpstream_id_t *id);
void rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *> stream_infos);
void rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *> stream_infos);
void rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogAddRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_info_t *> stream_infos);
@ -96,8 +96,8 @@ private slots:
void on_actionMoveDown1_triggered();
void on_actionZoomIn_triggered();
void on_actionZoomOut_triggered();
void on_actionSelectRtpStream_triggered();
void on_actionDeselectRtpStream_triggered();
void on_actionSelectRtpStreams_triggered();
void on_actionDeselectRtpStreams_triggered();
void on_buttonBox_helpRequested();
void rtpPlayerReplace();

View File

@ -376,7 +376,7 @@
<string>P</string>
</property>
</action>
<action name="actionSelectRtpStream">
<action name="actionSelectRtpStreams">
<property name="text">
<string>Select RTP Stream</string>
</property>
@ -387,7 +387,7 @@
<string>S</string>
</property>
</action>
<action name="actionDeselectRtpStream">
<action name="actionDeselectRtpStreams">
<property name="text">
<string>Deselect RTP Stream</string>
</property>

View File

@ -257,12 +257,43 @@ void set_action_shortcuts_visible_in_context_menu(QList<QAction *> actions)
#endif
}
QString make_filter_based_on_rtpstream_id(QVector<rtpstream_id_t *> ids)
QVector<rtpstream_id_t *>make_rtpstream_ids_from_rtpstream_infos(QVector<rtpstream_info_t *> stream_infos)
{
QVector<rtpstream_id_t *>stream_ids;
foreach(rtpstream_info_t *stream, stream_infos) {
stream_ids << &stream->id;
}
return stream_ids;
}
QVector<rtpstream_id_t *>qvector_rtpstream_ids_copy(QVector<rtpstream_id_t *> stream_ids)
{
QVector<rtpstream_id_t *>new_ids;
foreach(rtpstream_id_t *id, stream_ids) {
rtpstream_id_t *new_id = g_new0(rtpstream_id_t, 1);
rtpstream_id_copy(id, new_id);
new_ids << new_id;
}
return new_ids;
}
void qvector_rtpstream_ids_free(QVector<rtpstream_id_t *> stream_ids)
{
foreach(rtpstream_id_t *id, stream_ids) {
rtpstream_id_free(id);
}
}
QString make_filter_based_on_rtpstream_id(QVector<rtpstream_id_t *> stream_ids)
{
QStringList stream_filters;
QString filter;
foreach(rtpstream_id_t *id, ids) {
foreach(rtpstream_id_t *id, stream_ids) {
QString ip_proto = id->src_addr.type == AT_IPv6 ? "ipv6" : "ip";
stream_filters << QString("(%1.src==%2 && udp.srcport==%3 && %1.dst==%4 && udp.dstport==%5 && rtp.ssrc==0x%6)")
.arg(ip_proto) // %1
@ -279,14 +310,8 @@ QString make_filter_based_on_rtpstream_id(QVector<rtpstream_id_t *> ids)
return filter;
}
QString make_filter_based_on_rtpstream_info(QVector<rtpstream_info_t *> streams)
QString make_filter_based_on_rtpstream_info(QVector<rtpstream_info_t *> stream_infos)
{
QVector<rtpstream_id_t *> ids;
foreach(rtpstream_info_t *stream_info, streams) {
ids << &stream_info->id;
}
return make_filter_based_on_rtpstream_id(ids);
return make_filter_based_on_rtpstream_id(make_rtpstream_ids_from_rtpstream_infos(stream_infos));
}

View File

@ -233,21 +233,45 @@ bool rect_on_screen(const QRect &rect);
*/
void set_action_shortcuts_visible_in_context_menu(QList<QAction *> actions);
/**
* Extract rtpstream ids from rtpstream infos
*
* @param rtpstream_infos List of infos
* @return Vector of rtpstream_ids
*/
QVector<rtpstream_id_t *>make_rtpstream_ids_from_rtpstream_infos(QVector<rtpstream_info_t *> stream_infos);
/**
* Create copy of all rtpstream_ids to new QVector
* => caller must release it with qvector_rtpstream_ids_free()
*
* @param rtpstream_ids List of infos
* @return Vector of rtpstream_ids
*/
QVector<rtpstream_id_t *>qvector_rtpstream_ids_copy(QVector<rtpstream_id_t *> stream_ids);
/**
* Free all rtpstream_ids in QVector
*
* @param rtpstream_infos List of infos
*/
void qvector_rtpstream_ids_free(QVector<rtpstream_id_t *> stream_ids);
/**
* Make display filter from list of rtpstream_id
*
* @param ids List of ids
* @param stream_ids List of ids
* @return Filter or empty string
*/
QString make_filter_based_on_rtpstream_id(QVector<rtpstream_id_t *> ids);
QString make_filter_based_on_rtpstream_id(QVector<rtpstream_id_t *> stream_ids);
/**
* Make display filter from list of rtpstream_infos
*
* @param streams List of streams
* @param stream_infos List of stream infos
* @return Filter or empty string
*/
QString make_filter_based_on_rtpstream_info(QVector<rtpstream_info_t *> streams);
QString make_filter_based_on_rtpstream_info(QVector<rtpstream_info_t *> stream_infos);
#endif /* __QT_UI_UTILS__H__ */

View File

@ -86,6 +86,9 @@ VoipCallsDialog::VoipCallsDialog(QWidget &parent, CaptureFile &cf, bool all_flow
connect(&cap_file_, SIGNAL(captureEvent(CaptureEvent)),
this, SLOT(captureEvent(CaptureEvent)));
connect(this, SIGNAL(rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *>)), &parent_, SLOT(rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *>)));
connect(this, SIGNAL(rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *>)), &parent_, SLOT(rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *>)));
memset (&tapinfo_, 0, sizeof(tapinfo_));
tapinfo_.tap_packet = tapPacket;
tapinfo_.tap_reset = tapReset;
@ -137,6 +140,12 @@ bool VoipCallsDialog::eventFilter(QObject *, QEvent *event)
return true;
}
break;
case Qt::Key_S:
on_actionSelectRtpStreams_triggered();
break;
case Qt::Key_D:
on_actionDeselectRtpStreams_triggered();
break;
default:
break;
}
@ -214,6 +223,9 @@ void VoipCallsDialog::contextMenuEvent(QContextMenuEvent *event)
action->setToolTip(tr("Copy stream list as CSV."));
action = popupMenu.addAction(tr("Copy as YAML"), this, SLOT(copyAsYAML()));
action->setToolTip(tr("Copy stream list as YAML."));
popupMenu.addSeparator();
popupMenu.addAction(ui->actionSelectRtpStreams);
popupMenu.addAction(ui->actionDeselectRtpStreams);
popupMenu.exec(event->globalPos());
}
@ -372,6 +384,8 @@ void VoipCallsDialog::updateWidgets()
prepare_button_->setEnabled(enable);
sequence_button_->setEnabled(enable);
ui->actionSelectRtpStreams->setEnabled(enable);
ui->actionDeselectRtpStreams->setEnabled(enable);
#if defined(QT_MULTIMEDIA_LIB)
player_button_->setEnabled(enable);
#endif
@ -532,11 +546,12 @@ void VoipCallsDialog::showSequence()
SequenceDialog *sequence_dialog = new SequenceDialog(parent_, cap_file_, sequence_info_);
// Bypass this dialog and forward signals to parent
connect(sequence_dialog, SIGNAL(rtpStreamsDialogSelectRtpStream(rtpstream_id_t *)), &parent_, SLOT(rtpStreamsDialogSelectRtpStream(rtpstream_id_t *)));
connect(sequence_dialog, SIGNAL(rtpStreamsDialogDeselectRtpStream(rtpstream_id_t *)), &parent_, SLOT(rtpStreamsDialogDeselectRtpStream(rtpstream_id_t *)));
connect(sequence_dialog, SIGNAL(rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *>)), &parent_, SLOT(rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *>)));
connect(sequence_dialog, SIGNAL(rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *>)), &parent_, SLOT(rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *>)));
connect(sequence_dialog, SIGNAL(rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_info_t *>)), &parent_, SLOT(rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_info_t *>)));
connect(sequence_dialog, SIGNAL(rtpPlayerDialogAddRtpStreams(QVector<rtpstream_info_t *>)), &parent_, SLOT(rtpPlayerDialogAddRtpStreams(QVector<rtpstream_info_t *>)));
connect(sequence_dialog, SIGNAL(rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_info_t *>)), &parent_, SLOT(rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_info_t *>)));
sequence_dialog->setAttribute(Qt::WA_DeleteOnClose);
sequence_dialog->enableVoIPFeatures();
sequence_dialog->show();
@ -558,7 +573,10 @@ QVector<rtpstream_info_t *>VoipCallsDialog::getSelectedRtpStreams()
// rsi->call_num, rsi->start_fd->num, rsi->setup_frame_number);
if (vci->call_num == static_cast<guint>(rsi->call_num)) {
//VOIP_CALLS_DEBUG("adding call number %u", vci->call_num);
stream_infos << rsi;
if (-1 == stream_infos.indexOf(rsi)) {
// Add only new stream
stream_infos << rsi;
}
}
}
}
@ -743,3 +761,23 @@ void VoipCallsDialog::on_actionSelectNone_triggered()
ui->callTreeView->clearSelection();
}
void VoipCallsDialog::on_actionSelectRtpStreams_triggered()
{
QVector<rtpstream_id_t *>stream_ids = qvector_rtpstream_ids_copy(make_rtpstream_ids_from_rtpstream_infos(getSelectedRtpStreams()));
emit rtpStreamsDialogSelectRtpStreams(stream_ids);
qvector_rtpstream_ids_free(stream_ids);
raise();
}
void VoipCallsDialog::on_actionDeselectRtpStreams_triggered()
{
QVector<rtpstream_id_t *>stream_ids = qvector_rtpstream_ids_copy(make_rtpstream_ids_from_rtpstream_infos(getSelectedRtpStreams()));
emit rtpStreamsDialogDeselectRtpStreams(stream_ids);
qvector_rtpstream_ids_free(stream_ids);
raise();
}

View File

@ -50,6 +50,8 @@ signals:
void rtpPlayerDialogReplaceRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogAddRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpPlayerDialogRemoveRtpStreams(QVector<rtpstream_info_t *> stream_infos);
void rtpStreamsDialogSelectRtpStreams(QVector<rtpstream_id_t *> stream_ids);
void rtpStreamsDialogDeselectRtpStreams(QVector<rtpstream_id_t *> stream_ids);
public slots:
void displayFilterSuccess(bool success);
@ -114,6 +116,8 @@ private slots:
void on_actionSelectAll_triggered();
void on_actionSelectInvert_triggered();
void on_actionSelectNone_triggered();
void on_actionSelectRtpStreams_triggered();
void on_actionDeselectRtpStreams_triggered();
};
#endif // VOIP_CALLS_DIALOG_H

View File

@ -153,6 +153,28 @@
<string notr="true">Ctrl+I</string>
</property>
</action>
<action name="actionSelectRtpStreams">
<property name="text">
<string>Select related RTP streams</string>
</property>
<property name="toolTip">
<string>Select RTP streams related to selected calls in RTP Streams dialog</string>
</property>
<property name="shortcut">
<string>S</string>
</property>
</action>
<action name="actionDeselectRtpStreams">
<property name="text">
<string>Deselect related RTP Streams</string>
</property>
<property name="toolTip">
<string>Select RTP streams related to selected calls in RTP Streams dialog</string>
</property>
<property name="shortcut">
<string>D</string>
</property>
</action>
</widget>
<resources/>
<connections>