3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
7 * SPDX-License-Identifier: GPL-2.0-or-later
14 #include <epan/expert.h>
15 #include <epan/prefs.h>
17 #include <wsutil/filesystem.h>
18 #include <wsutil/utf8_entities.h>
20 #include "ui/main_statusbar.h"
21 #include <ui/qt/utils/qt_ui_utils.h>
22 #include <ui/qt/main_window.h>
24 #include "capture_file.h"
25 #include "main_status_bar.h"
26 #include "profile_dialog.h"
27 #include <ui/qt/utils/stock_icon.h>
28 #include <ui/qt/utils/color_utils.h>
29 #include <ui/qt/capture_file.h>
30 #include <ui/qt/widgets/clickable_label.h>
33 #include <QActionGroup>
34 #include <QHBoxLayout>
36 #include <QToolButton>
37 #include <QLatin1Char>
40 // - Use the CaptureFile class.
42 // XXX - The GTK+ code assigns priorities to these and pushes/pops accordingly.
44 Q_DECLARE_METATYPE(ProfileDialog::ProfileAction
)
46 // If we ever add support for multiple windows this will need to be replaced.
47 // See also: main_window.cpp
48 static MainStatusBar
*cur_main_status_bar_
;
51 * Push a formatted temporary message onto the statusbar.
54 statusbar_push_temporary_msg(const char *msg_format
, ...)
59 if (!cur_main_status_bar_
) return;
61 va_start(ap
, msg_format
);
62 push_msg
= QString::vasprintf(msg_format
, ap
);
65 mainApp
->pushStatus(WiresharkApplication::TemporaryStatus
, push_msg
);
69 * Update the packets statusbar to the current values
72 packets_bar_update(void)
74 if (!cur_main_status_bar_
) return;
76 cur_main_status_bar_
->updateCaptureStatistics(NULL
);
79 static const int icon_size
= 14; // px
81 MainStatusBar::MainStatusBar(QWidget
*parent
) :
85 ready_msg_(tr("Ready to load or capture")),
87 ready_msg_(tr("Ready to load file")),
92 QSplitter
*splitter
= new QSplitter(this);
93 QWidget
*info_progress
= new QWidget(this);
94 QHBoxLayout
*info_progress_hb
= new QHBoxLayout(info_progress
);
97 // Handles are the same color as widgets, at least on Windows 7.
98 splitter
->setHandleWidth(3);
99 splitter
->setStyleSheet(QString(
100 "QSplitter::handle {"
101 " border-left: 1px solid palette(mid);"
102 " border-right: 1px solid palette(mid);"
108 profile_status_
.setAttribute(Qt::WA_MacSmallSize
, true);
114 " background: transparent;" // Disables platform style on Windows.
119 expert_button_
= new QToolButton(this);
120 expert_button_
->setIconSize(QSize(icon_size
, icon_size
));
121 expert_button_
->setStyleSheet(button_ss
);
122 expert_button_
->hide();
124 // We just want a clickable image. Using a QPushButton or QToolButton would require
125 // a lot of adjustment.
126 StockIcon
comment_icon("x-capture-comment-update");
127 comment_button_
= new QToolButton(this);
128 comment_button_
->setIcon(comment_icon
);
129 comment_button_
->setIconSize(QSize(icon_size
, icon_size
));
130 comment_button_
->setStyleSheet(button_ss
);
132 comment_button_
->setToolTip(tr("Open the Capture File Properties dialog"));
133 comment_button_
->setEnabled(false);
134 connect(expert_button_
, &QToolButton::clicked
, this, &MainStatusBar::showExpertInfo
);
135 connect(comment_button_
, &QToolButton::clicked
, this, &MainStatusBar::editCaptureComment
);
137 info_progress_hb
->setContentsMargins(icon_size
/ 2, 0, 0, 0);
139 info_status_
.setTemporaryContext(STATUS_CTX_TEMPORARY
);
140 info_status_
.setShrinkable(true);
142 info_progress_hb
->addWidget(expert_button_
);
143 info_progress_hb
->addWidget(comment_button_
);
144 info_progress_hb
->addWidget(&info_status_
);
145 info_progress_hb
->addWidget(&progress_frame_
);
146 info_progress_hb
->addStretch(10);
148 splitter
->addWidget(info_progress
);
149 splitter
->addWidget(&packet_status_
);
150 splitter
->addWidget(&profile_status_
);
152 splitter
->setStretchFactor(0, 3);
153 splitter
->setStretchFactor(1, 3);
154 splitter
->setStretchFactor(2, 1);
156 addWidget(splitter
, 1);
158 cur_main_status_bar_
= this;
161 info_status_
.pushText(ready_msg_
, STATUS_CTX_MAIN
);
162 packets_bar_update();
164 #ifdef QWINTASKBARPROGRESS_H
165 progress_frame_
.enableTaskbarUpdates(true);
168 connect(mainApp
, &MainApplication::appInitialized
, splitter
, &QSplitter::show
);
169 connect(mainApp
, &MainApplication::appInitialized
, this, &MainStatusBar::appInitialized
);
170 connect(&info_status_
, &LabelStack::toggleTemporaryFlash
, this, &MainStatusBar::toggleBackground
);
171 connect(mainApp
, &MainApplication::profileNameChanged
, this, &MainStatusBar::setProfileName
);
172 connect(&profile_status_
, &ClickableLabel::clickedAt
, this, &MainStatusBar::showProfileMenu
);
174 connect(&progress_frame_
, &ProgressFrame::stopLoading
, this, &MainStatusBar::stopLoading
);
177 void MainStatusBar::showExpert() {
181 void MainStatusBar::captureFileClosing() {
182 expert_button_
->hide();
183 progress_frame_
.captureFileClosing();
184 popGenericStatus(STATUS_CTX_FIELD
);
187 void MainStatusBar::expertUpdate() {
188 // <img> won't load @2x versions in Qt versions earlier than 5.4.
189 // https://bugreports.qt.io/browse/QTBUG-36383
190 // We might have to switch to a QPushButton.
191 QString stock_name
= "x-expert-";
192 QString tt_text
= tr(" is the highest expert information level");
194 switch(expert_get_highest_severity()) {
196 stock_name
.append("error");
197 tt_text
.prepend(tr("ERROR"));
200 stock_name
.append("warn");
201 tt_text
.prepend(tr("WARNING"));
204 stock_name
.append("note");
205 tt_text
.prepend(tr("NOTE"));
208 stock_name
.append("chat");
209 tt_text
.prepend(tr("CHAT"));
212 // m_expertStatus.setText("<img src=\":/expert/expert_comment.png\"></img>");
215 stock_name
.append("none");
216 tt_text
= tr("No expert information");
220 StockIcon
expert_icon(stock_name
);
221 expert_button_
->setIcon(expert_icon
);
222 expert_button_
->setToolTip(tt_text
);
223 expert_button_
->show();
226 // ui/gtk/main_statusbar.c
227 void MainStatusBar::setFileName(CaptureFile
&cf
)
230 popGenericStatus(STATUS_CTX_FILE
);
231 QString msgtip
= QStringLiteral("%1 (%2)")
232 .arg(cf
.capFile()->filename
, file_size_to_qstring(cf
.capFile()->f_datalen
));
233 pushGenericStatus(STATUS_CTX_FILE
, cf
.fileName(), msgtip
);
237 void MainStatusBar::changeEvent(QEvent
*event
)
239 if (event
->type() == QEvent::LanguageChange
) {
240 info_status_
.popText(STATUS_CTX_MAIN
);
241 info_status_
.pushText(ready_msg_
, STATUS_CTX_MAIN
);
242 setStatusbarForCaptureFile();
243 showCaptureStatistics();
246 QStatusBar::changeEvent(event
);
249 void MainStatusBar::setCaptureFile(capture_file
*cf
)
252 comment_button_
->setEnabled(cap_file_
!= NULL
);
255 void MainStatusBar::setStatusbarForCaptureFile()
257 if (cap_file_
&& cap_file_
->filename
&& (cap_file_
->state
!= FILE_CLOSED
)) {
258 popGenericStatus(STATUS_CTX_FILE
);
259 QString msgtip
= QStringLiteral("%1 (%2)")
260 .arg(cap_file_
->filename
, file_size_to_qstring(cap_file_
->f_datalen
));
261 pushGenericStatus(STATUS_CTX_FILE
,
262 gchar_free_to_qstring(cf_get_display_name(cap_file_
)), msgtip
);
266 void MainStatusBar::selectedFieldChanged(FieldInformation
* finfo
)
271 pushGenericStatus(STATUS_CTX_FIELD
, item_info
);
275 FieldInformation::HeaderInfo hInfo
= finfo
->headerInfo();
279 if (hInfo
.description
.length() > 0) {
280 item_info
.append(hInfo
.description
);
282 item_info
.append(hInfo
.name
);
286 if (!item_info
.isEmpty()) {
289 item_info
.append(" (" + hInfo
.abbreviation
+ ")");
291 finfo_length
= finfo
->position().length
+ finfo
->appendix().length
;
292 if (finfo_length
> 0) {
293 int finfo_bits
= FI_GET_BITS_SIZE(finfo
->fieldInfo());
294 if (finfo_bits
% 8 == 0) {
295 item_info
.append(", " + tr("%Ln byte(s)", "", finfo_length
));
297 item_info
.append(", " + tr("%Ln bit(s)", "", finfo_bits
));
302 pushGenericStatus(STATUS_CTX_FIELD
, item_info
);
305 void MainStatusBar::highlightedFieldChanged(FieldInformation
* finfo
)
311 FieldInformation::Position pos
= finfo
->position();
313 if (pos
.length
< 2) {
314 hint
= tr("Byte %1").arg(pos
.start
);
316 hint
= tr("Bytes %1-%2").arg(pos
.start
).arg(pos
.start
+ pos
.length
- 1);
318 hint
+= QStringLiteral(": %1 (%2)")
319 .arg(finfo
->headerInfo().name
, finfo
->headerInfo().abbreviation
);
322 pushGenericStatus(STATUS_CTX_BYTE
, hint
);
325 void MainStatusBar::pushGenericStatus(StatusContext status
, const QString
&message
, const QString
&messagetip
)
327 LabelStack
* stack
= &info_status_
;
329 if (status
== STATUS_CTX_MAIN
)
330 stack
= &packet_status_
;
332 if (message
.isEmpty() && status
!= STATUS_CTX_FILE
&& status
!= STATUS_CTX_TEMPORARY
&& status
!= STATUS_CTX_PROGRESS
)
333 popGenericStatus(status
);
335 stack
->pushText(message
, status
, messagetip
);
337 if (status
== STATUS_CTX_FILTER
|| status
== STATUS_CTX_FILE
)
341 void MainStatusBar::popGenericStatus(StatusContext status
)
343 LabelStack
* stack
= &info_status_
;
345 if (status
== STATUS_CTX_MAIN
)
346 stack
= &packet_status_
;
348 stack
->popText(status
);
351 void MainStatusBar::setProfileName()
353 profile_status_
.setText(tr("Profile: %1").arg(get_profile_name()));
356 void MainStatusBar::appInitialized()
359 connect(qobject_cast
<MainWindow
*>(mainApp
->mainWindow()), &MainWindow::framesSelected
, this, &MainStatusBar::selectedFrameChanged
);
362 void MainStatusBar::selectedFrameChanged(QList
<int>)
364 showCaptureStatistics();
367 void MainStatusBar::showCaptureStatistics()
372 MainWindow
* mw
= qobject_cast
<MainWindow
*>(mainApp
->mainWindow());
374 rows
= mw
->selectedRows(true);
378 /* Do we have any packets? */
380 cs_count_
= cap_file_
->count
;
383 if (prefs
.gui_show_selected_packet
&& rows
.count() == 1) {
384 if (is_packet_configuration_namespace()) {
385 packets_str
.append(tr("Selected Packet: %1 %2 ")
387 .arg(UTF8_MIDDLE_DOT
));
389 packets_str
.append(tr("Selected Event: %1 %2 ")
391 .arg(UTF8_MIDDLE_DOT
));
394 if (is_packet_configuration_namespace()) {
395 packets_str
.append(tr("Packets: %1")
398 packets_str
.append(tr("Events: %1")
401 if (cap_file_
->dfilter
) {
402 packets_str
.append(tr(" %1 Displayed: %2 (%3%)")
403 .arg(UTF8_MIDDLE_DOT
)
404 .arg(cap_file_
->displayed_count
)
405 .arg((100.0*cap_file_
->displayed_count
)/cs_count_
, 0, 'f', 1));
407 if (rows
.count() > 1) {
408 packets_str
.append(tr(" %1 Selected: %2 (%3%)")
409 .arg(UTF8_MIDDLE_DOT
)
411 .arg((100.0*rows
.count())/cs_count_
, 0, 'f', 1));
413 if (cap_file_
->marked_count
> 0) {
414 packets_str
.append(tr(" %1 Marked: %2 (%3%)")
415 .arg(UTF8_MIDDLE_DOT
)
416 .arg(cap_file_
->marked_count
)
417 .arg((100.0*cap_file_
->marked_count
)/cs_count_
, 0, 'f', 1));
419 if (cap_file_
->drops_known
) {
420 packets_str
.append(tr(" %1 Dropped: %2 (%3%)")
421 .arg(UTF8_MIDDLE_DOT
)
422 .arg(cap_file_
->drops
)
423 .arg((100.0*cap_file_
->drops
)/cs_count_
, 0, 'f', 1));
425 if (cap_file_
->ignored_count
> 0) {
426 packets_str
.append(tr(" %1 Ignored: %2 (%3%)")
427 .arg(UTF8_MIDDLE_DOT
)
428 .arg(cap_file_
->ignored_count
)
429 .arg((100.0*cap_file_
->ignored_count
)/cs_count_
, 0, 'f', 1));
431 if (cap_file_
->packet_comment_count
> 0) {
432 packets_str
.append(tr(" %1 Comments: %2")
433 .arg(UTF8_MIDDLE_DOT
)
434 .arg(cap_file_
->packet_comment_count
));
436 if (prefs
.gui_show_file_load_time
&& !cap_file_
->is_tempfile
) {
437 /* Loading an existing file */
438 unsigned long computed_elapsed
= cf_get_computed_elapsed(cap_file_
);
439 packets_str
.append(tr(" %1 Load time: %2:%3.%4")
440 .arg(UTF8_MIDDLE_DOT
)
441 .arg(computed_elapsed
/60000, 2, 10, QLatin1Char('0'))
442 .arg(computed_elapsed
%60000/1000, 2, 10, QLatin1Char('0'))
443 .arg(computed_elapsed
%1000, 3, 10, QLatin1Char('0')));
446 } else if (cs_fixed_
&& cs_count_
> 0) {
447 /* There shouldn't be any rows without a cap_file_ but this is benign */
448 if (is_packet_configuration_namespace()) {
449 if (prefs
.gui_show_selected_packet
&& rows
.count() == 1) {
450 packets_str
.append(tr("Selected Packet: %1 %2 ")
452 .arg(UTF8_MIDDLE_DOT
));
454 packets_str
.append(tr("Packets: %1")
457 if (prefs
.gui_show_selected_packet
&& rows
.count() == 1) {
458 packets_str
.append(tr("Selected Event: %1 %2 ")
460 .arg(UTF8_MIDDLE_DOT
));
462 packets_str
.append(tr("Events: %1")
466 #endif // HAVE_LIBPCAP
468 if (packets_str
.isEmpty()) {
469 if (is_packet_configuration_namespace()) {
470 packets_str
= tr("No Packets");
472 packets_str
= tr("No Events");
476 popGenericStatus(STATUS_CTX_MAIN
);
477 pushGenericStatus(STATUS_CTX_MAIN
, packets_str
);
480 void MainStatusBar::updateCaptureStatistics(capture_session
*cap_session
)
485 Q_UNUSED(cap_session
)
487 if ((!cap_session
|| cap_session
->cf
== cap_file_
) && cap_file_
&& cap_file_
->count
) {
488 cs_count_
= cap_file_
->count
;
492 #endif // HAVE_LIBPCAP
494 showCaptureStatistics();
497 void MainStatusBar::updateCaptureFixedStatistics(capture_session
*cap_session
)
502 Q_UNUSED(cap_session
)
504 if (cap_session
&& cap_session
->count
) {
505 cs_count_
= cap_session
->count
;
509 #endif // HAVE_LIBPCAP
511 showCaptureStatistics();
514 void MainStatusBar::showProfileMenu(const QPoint
&global_pos
, Qt::MouseButton button
)
519 QMenu
* profile_menu
;
520 if (button
== Qt::LeftButton
) {
522 profile_menu
= new QMenu(this);
523 profile_menu
->setAttribute(Qt::WA_DeleteOnClose
);
525 ctx_menu_
= new QMenu(this);
526 ctx_menu_
->setAttribute(Qt::WA_DeleteOnClose
);
527 profile_menu
= new QMenu(ctx_menu_
);
529 QActionGroup
* global
= new QActionGroup(profile_menu
);
530 QActionGroup
* user
= new QActionGroup(profile_menu
);
532 for (int cnt
= 0; cnt
< model
.rowCount(); cnt
++)
534 QModelIndex idx
= model
.index(cnt
, ProfileModel::COL_NAME
);
539 QAction
* pa
= Q_NULLPTR
;
540 QString name
= idx
.data().toString();
542 // An ampersand in the menu item's text sets Alt+F as a shortcut for this menu.
543 // Use "&&" to get a real ampersand in the menu bar.
544 name
.replace('&', "&&");
546 if (idx
.data(ProfileModel::DATA_IS_DEFAULT
).toBool())
548 pa
= profile_menu
->addAction(name
);
550 else if (idx
.data(ProfileModel::DATA_IS_GLOBAL
).toBool())
552 /* Check if this profile does not exist as user */
553 if (cnt
== model
.findByName(name
))
554 pa
= global
->addAction(name
);
557 pa
= user
->addAction(name
);
562 pa
->setCheckable(true);
563 if (idx
.data(ProfileModel::DATA_IS_SELECTED
).toBool())
564 pa
->setChecked(true);
566 pa
->setFont(idx
.data(Qt::FontRole
).value
<QFont
>());
567 pa
->setProperty("profile_name", idx
.data());
568 pa
->setProperty("profile_is_global", idx
.data(ProfileModel::DATA_IS_GLOBAL
));
570 connect(pa
, &QAction::triggered
, this, &MainStatusBar::switchToProfile
);
573 profile_menu
->addActions(user
->actions());
574 profile_menu
->addSeparator();
575 profile_menu
->addActions(global
->actions());
577 if (button
== Qt::LeftButton
) {
578 profile_menu
->popup(global_pos
);
581 bool enable_edit
= false;
583 QModelIndex idx
= model
.activeProfile();
584 if (! idx
.data(ProfileModel::DATA_IS_DEFAULT
).toBool() && ! idx
.data(ProfileModel::DATA_IS_GLOBAL
).toBool())
587 profile_menu
->setTitle(tr("Switch to"));
588 QAction
* action
= ctx_menu_
->addAction(tr("Manage Profiles…"), this, SLOT(manageProfile()));
589 action
->setProperty("dialog_action_", (int)ProfileDialog::ShowProfiles
);
591 ctx_menu_
->addSeparator();
592 action
= ctx_menu_
->addAction(tr("New…"), this, SLOT(manageProfile()));
593 action
->setProperty("dialog_action_", (int)ProfileDialog::NewProfile
);
594 action
= ctx_menu_
->addAction(tr("Edit…"), this, SLOT(manageProfile()));
595 action
->setProperty("dialog_action_", (int)ProfileDialog::EditCurrentProfile
);
596 action
->setEnabled(enable_edit
);
597 action
= ctx_menu_
->addAction(tr("Delete"), this, SLOT(manageProfile()));
598 action
->setProperty("dialog_action_", (int)ProfileDialog::DeleteCurrentProfile
);
599 action
->setEnabled(enable_edit
);
600 ctx_menu_
->addSeparator();
602 #if defined(HAVE_MINIZIP) || defined(HAVE_MINIZIPNG)
603 QMenu
* importMenu
= new QMenu(tr("Import"), ctx_menu_
);
604 action
= importMenu
->addAction(tr("From Zip File..."), this, SLOT(manageProfile()));
605 action
->setProperty("dialog_action_", (int)ProfileDialog::ImportZipProfile
);
606 action
= importMenu
->addAction(tr("From Directory..."), this, SLOT(manageProfile()));
607 action
->setProperty("dialog_action_", (int)ProfileDialog::ImportDirProfile
);
608 ctx_menu_
->addMenu(importMenu
);
610 if (model
.userProfilesExist())
612 QMenu
* exportMenu
= new QMenu(tr("Export"), ctx_menu_
);
615 action
= exportMenu
->addAction(tr("Selected Personal Profile..."), this, SLOT(manageProfile()));
616 action
->setProperty("dialog_action_", (int)ProfileDialog::ExportSingleProfile
);
617 action
->setEnabled(enable_edit
);
619 action
= exportMenu
->addAction(tr("All Personal Profiles..."), this, SLOT(manageProfile()));
620 action
->setProperty("dialog_action_", (int)ProfileDialog::ExportAllProfiles
);
621 ctx_menu_
->addMenu(exportMenu
);
625 action
= ctx_menu_
->addAction(tr("Import"), this, SLOT(manageProfile()));
626 action
->setProperty("dialog_action_", (int)ProfileDialog::ImportDirProfile
);
628 ctx_menu_
->addSeparator();
630 ctx_menu_
->addMenu(profile_menu
);
631 ctx_menu_
->popup(global_pos
);
635 void MainStatusBar::toggleBackground(bool enabled
)
638 setStyleSheet(QString(
640 " background-color: %2;"
643 .arg(ColorUtils::warningBackground().name()));
645 setStyleSheet(QString());
649 void MainStatusBar::switchToProfile()
651 QAction
*pa
= qobject_cast
<QAction
*>(sender());
653 if (pa
&& pa
->property("profile_name").isValid()) {
654 QString profile
= pa
->property("profile_name").toString();
655 mainApp
->setConfigurationProfile(profile
.toUtf8().constData());
659 void MainStatusBar::manageProfile()
661 QAction
*pa
= qobject_cast
<QAction
*>(sender());
664 ProfileDialog
* cp_dialog
= new ProfileDialog(this);
665 cp_dialog
->setAttribute(Qt::WA_DeleteOnClose
);
667 int profileAction
= pa
->property("dialog_action_").toInt();
668 cp_dialog
->execAction(static_cast<ProfileDialog::ProfileAction
>(profileAction
));
672 void MainStatusBar::captureEventHandler(CaptureEvent ev
)
674 switch(ev
.captureContext())
677 case CaptureEvent::Update
:
678 switch (ev
.eventType())
680 case CaptureEvent::Continued
:
681 updateCaptureStatistics(ev
.capSession());
683 case CaptureEvent::Finished
:
684 updateCaptureStatistics(ev
.capSession());
690 case CaptureEvent::Fixed
:
691 switch (ev
.eventType())
693 case CaptureEvent::Continued
:
694 updateCaptureFixedStatistics(ev
.capSession());
701 case CaptureEvent::Save
:
702 switch (ev
.eventType())
704 case CaptureEvent::Finished
:
705 case CaptureEvent::Failed
:
706 case CaptureEvent::Stopped
:
707 popGenericStatus(STATUS_CTX_FILE
);