Qt: Conversation time column updates.
Add a checkbox which lets you toggle between absolute and relative start times. Use the local time for now. Fixes bug 11618. Adjust our time precision based on the capture file's time precision. Fixes bug 12803. Update the User's Guide accordingly. Bug: 11618 Bug: 12803 Change-Id: I0049d6db6e4d0b6967bf35e6d056a61bfb4de10f Reviewed-on: https://code.wireshark.org/review/17448 Petri-Dish: Gerald Combs <gerald@wireshark.org> Tested-by: Petri Dish Buildbot <buildbot-no-reply@wireshark.org> Reviewed-by: Gerald Combs <gerald@wireshark.org>
This commit is contained in:
parent
df3bf9ca79
commit
5846524f0b
Binary file not shown.
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 235 KiB |
|
@ -141,10 +141,10 @@ description of the known endpoint types can be found in
|
||||||
The conversations window is similar to the endpoint Window. See
|
The conversations window is similar to the endpoint Window. See
|
||||||
<<ChStatEndpointsWindow>> for a description of their common features. Along with
|
<<ChStatEndpointsWindow>> for a description of their common features. Along with
|
||||||
addresses, packet counters, and byte counters the conversation window adds four
|
addresses, packet counters, and byte counters the conversation window adds four
|
||||||
columns: the time in seconds between the start of the capture and the start of
|
columns: the start time of the conversation (``Rel Start'') or (``Abs Start''),
|
||||||
the conversation (``Rel Start''), the duration of the conversation in seconds, and
|
the duration of the conversation in seconds, and the average bits (not bytes)
|
||||||
the average bits (not bytes) per second in each direction. A timeline graph is
|
per second in each direction. A timeline graph is also drawn across the
|
||||||
also drawn across the ``Rel Start'' and ``Duration'' columns.
|
``Rel Start'' / ``Abs Start'' and ``Duration'' columns.
|
||||||
|
|
||||||
.The ``Conversations'' window
|
.The ``Conversations'' window
|
||||||
image::wsug_graphics/ws-stats-conversations.png[scaledwidth="100%"]
|
image::wsug_graphics/ws-stats-conversations.png[scaledwidth="100%"]
|
||||||
|
@ -154,7 +154,10 @@ Each row in the list shows the statistical values for exactly one conversation.
|
||||||
_Name resolution_ will be done if selected in the window and if it is active for
|
_Name resolution_ will be done if selected in the window and if it is active for
|
||||||
the specific protocol layer (MAC layer for the selected Ethernet endpoints
|
the specific protocol layer (MAC layer for the selected Ethernet endpoints
|
||||||
page). _Limit to display filter_ will only show conversations matching the
|
page). _Limit to display filter_ will only show conversations matching the
|
||||||
current display filter.
|
current display filter. _Absolute start time_ switches the start time column
|
||||||
|
between relative (``Rel Start'') and absolute (``Abs Start'') times. Relative start
|
||||||
|
times match the ``Seconds Since Beginning of Capture'' time display format in the
|
||||||
|
packet list and absolute start times match the ``Time of Day'' display format.
|
||||||
|
|
||||||
The button:[Copy] button will copy the list values to the clipboard in CSV
|
The button:[Copy] button will copy the list values to the clipboard in CSV
|
||||||
(Comma Separated Values) or YAML format. The button:[Follow Stream...] button
|
(Comma Separated Values) or YAML format. The button:[Follow Stream...] button
|
||||||
|
|
|
@ -48,7 +48,7 @@ col_format_to_string(const gint fmt) {
|
||||||
"%At", /* 3) COL_ABS_TIME */
|
"%At", /* 3) COL_ABS_TIME */
|
||||||
"%V", /* 4) COL_VSAN - !! DEPRECATED !!*/
|
"%V", /* 4) COL_VSAN - !! DEPRECATED !!*/
|
||||||
"%B", /* 5) COL_CUMULATIVE_BYTES */
|
"%B", /* 5) COL_CUMULATIVE_BYTES */
|
||||||
"%Cus", /* 6 COL_CUSTOM */
|
"%Cus", /* 6) COL_CUSTOM */
|
||||||
"%y", /* 7) COL_DCE_CALL */
|
"%y", /* 7) COL_DCE_CALL */
|
||||||
"%Tt", /* 8) COL_DELTA_TIME */
|
"%Tt", /* 8) COL_DELTA_TIME */
|
||||||
"%Gt", /* 9) COL_DELTA_TIME_DIS */
|
"%Gt", /* 9) COL_DELTA_TIME_DIS */
|
||||||
|
|
|
@ -99,6 +99,14 @@ struct _packet_info *CaptureFile::packetInfo()
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int CaptureFile::timestampPrecision()
|
||||||
|
{
|
||||||
|
if (capFile() && capFile()->wth) {
|
||||||
|
return wtap_file_tsprec(capFile()->wth);
|
||||||
|
}
|
||||||
|
return WTAP_TSPREC_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
void CaptureFile::retapPackets()
|
void CaptureFile::retapPackets()
|
||||||
{
|
{
|
||||||
if (cap_file_) {
|
if (cap_file_) {
|
||||||
|
|
|
@ -76,6 +76,12 @@ public:
|
||||||
*/
|
*/
|
||||||
struct _packet_info *packetInfo();
|
struct _packet_info *packetInfo();
|
||||||
|
|
||||||
|
/** Timestamp precision for the current file.
|
||||||
|
* @return One of the WTAP_TSPREC_x values defined in wiretap/wtap.h,
|
||||||
|
* or WTAP_TSPREC_UNKNOWN if no file is open.
|
||||||
|
*/
|
||||||
|
int timestampPrecision();
|
||||||
|
|
||||||
/** Reload the capture file
|
/** Reload the capture file
|
||||||
*/
|
*/
|
||||||
void reload();
|
void reload();
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
#include "wireshark_application.h"
|
#include "wireshark_application.h"
|
||||||
|
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
|
||||||
|
@ -58,6 +59,9 @@
|
||||||
// Fixed bugs:
|
// Fixed bugs:
|
||||||
// - Friendly unit displays https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=9231
|
// - Friendly unit displays https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=9231
|
||||||
// - Misleading bps calculation https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=8703
|
// - Misleading bps calculation https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=8703
|
||||||
|
// - Show Absolute time in conversation tables https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=11618
|
||||||
|
// - The value of 'Rel start' and 'Duration' in "Conversations" no need too precise https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=12803
|
||||||
|
|
||||||
|
|
||||||
static const QString table_name_ = QObject::tr("Conversation");
|
static const QString table_name_ = QObject::tr("Conversation");
|
||||||
ConversationDialog::ConversationDialog(QWidget &parent, CaptureFile &cf, int cli_proto_id, const char *filter) :
|
ConversationDialog::ConversationDialog(QWidget &parent, CaptureFile &cf, int cli_proto_id, const char *filter) :
|
||||||
|
@ -71,6 +75,8 @@ ConversationDialog::ConversationDialog(QWidget &parent, CaptureFile &cf, int cli
|
||||||
graph_bt_->setToolTip(tr("Graph a TCP conversation."));
|
graph_bt_->setToolTip(tr("Graph a TCP conversation."));
|
||||||
connect(graph_bt_, SIGNAL(clicked()), this, SLOT(graphTcp()));
|
connect(graph_bt_, SIGNAL(clicked()), this, SLOT(graphTcp()));
|
||||||
|
|
||||||
|
absoluteTimeCheckBox()->show();
|
||||||
|
|
||||||
addProgressFrame(&parent);
|
addProgressFrame(&parent);
|
||||||
|
|
||||||
QList<int> conv_protos;
|
QList<int> conv_protos;
|
||||||
|
@ -163,6 +169,9 @@ bool ConversationDialog::addTrafficTable(register_ct_t* table)
|
||||||
this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
|
this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
|
||||||
connect(nameResolutionCheckBox(), SIGNAL(toggled(bool)),
|
connect(nameResolutionCheckBox(), SIGNAL(toggled(bool)),
|
||||||
conv_tree, SLOT(setNameResolutionEnabled(bool)));
|
conv_tree, SLOT(setNameResolutionEnabled(bool)));
|
||||||
|
connect(absoluteTimeCheckBox(), SIGNAL(toggled(bool)),
|
||||||
|
conv_tree, SLOT(updateStartTime(bool)));
|
||||||
|
|
||||||
|
|
||||||
// XXX Move to ConversationTreeWidget ctor?
|
// XXX Move to ConversationTreeWidget ctor?
|
||||||
QByteArray filter_utf8;
|
QByteArray filter_utf8;
|
||||||
|
@ -365,9 +374,31 @@ public:
|
||||||
case CONV_COLUMN_BYTES_BA:
|
case CONV_COLUMN_BYTES_BA:
|
||||||
return gchar_free_to_qstring(format_size(conv_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
|
return gchar_free_to_qstring(format_size(conv_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
|
||||||
case CONV_COLUMN_START:
|
case CONV_COLUMN_START:
|
||||||
return QString::number(nstime_to_sec(&conv_item->start_time), 'f', 9);
|
{
|
||||||
|
bool use_ns = treeWidget()->window()->property("nanosecond_precision").toBool();
|
||||||
|
int width = use_ns ? 9 : 6;
|
||||||
|
|
||||||
|
if (treeWidget()->window()->property("absolute_start_time").toBool()) {
|
||||||
|
nstime_t *abs_time = &conv_item->start_abs_time;
|
||||||
|
QDateTime abs_dt = QDateTime::fromMSecsSinceEpoch(nstime_to_msec(abs_time));
|
||||||
|
return QString("%1.%2")
|
||||||
|
// Mimic column-utils:set_abs_time as best we can
|
||||||
|
.arg(abs_dt.toString("hh:mm:ss"))
|
||||||
|
.arg(use_ns ? abs_time->nsecs : abs_time->nsecs / 1000, width, 10, QChar('0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString::number(nstime_to_sec(&conv_item->start_time), 'f', width);
|
||||||
|
}
|
||||||
case CONV_COLUMN_DURATION:
|
case CONV_COLUMN_DURATION:
|
||||||
return QString::number(duration, 'f', 6);
|
{
|
||||||
|
// The GTK+ UI uses 9 digit precision for the start time and 4 for the duration.
|
||||||
|
// Do the same here and above for non-nanosecond precision and add a couple
|
||||||
|
// of digits for nanosecond precision.
|
||||||
|
bool use_ns = treeWidget()->window()->property("nanosecond_precision").toBool();
|
||||||
|
int width = use_ns ? 6 : 4;
|
||||||
|
|
||||||
|
return QString::number(duration, 'f', width);
|
||||||
|
}
|
||||||
case CONV_COLUMN_BPS_AB:
|
case CONV_COLUMN_BPS_AB:
|
||||||
if (duration > min_bw_calc_duration_) {
|
if (duration > min_bw_calc_duration_) {
|
||||||
bps_ab = gchar_free_to_qstring(format_size((gint64) conv_item->tx_bytes * 8 / duration, format_size_unit_none|format_size_prefix_si));
|
bps_ab = gchar_free_to_qstring(format_size((gint64) conv_item->tx_bytes * 8 / duration, format_size_unit_none|format_size_prefix_si));
|
||||||
|
@ -665,6 +696,19 @@ void ConversationTreeWidget::tapDraw(void *conv_hash_ptr)
|
||||||
conv_tree->updateItems();
|
conv_tree->updateItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConversationTreeWidget::updateStartTime(bool absolute)
|
||||||
|
{
|
||||||
|
headerItem()->setText(CONV_COLUMN_START, absolute
|
||||||
|
? conv_abs_start_title
|
||||||
|
: conv_column_titles[CONV_COLUMN_START]);
|
||||||
|
|
||||||
|
dataChanged(QModelIndex(), QModelIndex());
|
||||||
|
|
||||||
|
if (topLevelItemCount() > 0) {
|
||||||
|
resizeColumnToContents(CONV_COLUMN_START);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QMap<FilterAction::ActionDirection, conv_direction_e> fad_to_cd_;
|
QMap<FilterAction::ActionDirection, conv_direction_e> fad_to_cd_;
|
||||||
|
|
||||||
void ConversationTreeWidget::initDirectionMap()
|
void ConversationTreeWidget::initDirectionMap()
|
||||||
|
|
|
@ -38,6 +38,9 @@ public:
|
||||||
double minRelStartTime() { return min_rel_start_time_; }
|
double minRelStartTime() { return min_rel_start_time_; }
|
||||||
double maxRelStopTime() { return max_rel_stop_time_; }
|
double maxRelStopTime() { return max_rel_stop_time_; }
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void updateStartTime(bool absolute);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initDirectionMap();
|
void initDirectionMap();
|
||||||
void updateItems();
|
void updateItems();
|
||||||
|
|
|
@ -56,12 +56,14 @@ TrafficTableDialog::TrafficTableDialog(QWidget &parent, CaptureFile &cf, const c
|
||||||
ui(new Ui::TrafficTableDialog),
|
ui(new Ui::TrafficTableDialog),
|
||||||
cap_file_(cf),
|
cap_file_(cf),
|
||||||
file_closed_(false),
|
file_closed_(false),
|
||||||
filter_(filter)
|
filter_(filter),
|
||||||
|
nanosecond_timestamps_(false)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
loadGeometry(parent.width(), parent.height() * 3 / 4);
|
loadGeometry(parent.width(), parent.height() * 3 / 4);
|
||||||
|
|
||||||
ui->enabledTypesPushButton->setText(tr("%1 Types").arg(table_name));
|
ui->enabledTypesPushButton->setText(tr("%1 Types").arg(table_name));
|
||||||
|
ui->absoluteTimeCheckBox->hide();
|
||||||
setWindowSubtitle(QString("%1s").arg(table_name));
|
setWindowSubtitle(QString("%1s").arg(table_name));
|
||||||
|
|
||||||
QMenu *copy_menu = new QMenu();
|
QMenu *copy_menu = new QMenu();
|
||||||
|
@ -78,6 +80,10 @@ TrafficTableDialog::TrafficTableDialog(QWidget &parent, CaptureFile &cf, const c
|
||||||
ui->enabledTypesPushButton->setMenu(&traffic_type_menu_);
|
ui->enabledTypesPushButton->setMenu(&traffic_type_menu_);
|
||||||
ui->trafficTableTabWidget->setFocus();
|
ui->trafficTableTabWidget->setFocus();
|
||||||
|
|
||||||
|
if (cf.timestampPrecision() == WTAP_TSPREC_NSEC) {
|
||||||
|
nanosecond_timestamps_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
connect(wsApp, SIGNAL(addressResolutionChanged()), this, SLOT(currentTabChanged()));
|
connect(wsApp, SIGNAL(addressResolutionChanged()), this, SLOT(currentTabChanged()));
|
||||||
connect(wsApp, SIGNAL(addressResolutionChanged()), this, SLOT(updateWidgets()));
|
connect(wsApp, SIGNAL(addressResolutionChanged()), this, SLOT(updateWidgets()));
|
||||||
connect(ui->trafficTableTabWidget, SIGNAL(currentChanged(int)),
|
connect(ui->trafficTableTabWidget, SIGNAL(currentChanged(int)),
|
||||||
|
@ -93,6 +99,11 @@ TrafficTableDialog::~TrafficTableDialog()
|
||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TrafficTableDialog::absoluteStartTime()
|
||||||
|
{
|
||||||
|
return absoluteTimeCheckBox()->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
const QList<int> TrafficTableDialog::defaultProtos() const
|
const QList<int> TrafficTableDialog::defaultProtos() const
|
||||||
{
|
{
|
||||||
// Reasonable defaults?
|
// Reasonable defaults?
|
||||||
|
@ -144,6 +155,11 @@ QCheckBox *TrafficTableDialog::nameResolutionCheckBox() const
|
||||||
return ui->nameResolutionCheckBox;
|
return ui->nameResolutionCheckBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QCheckBox *TrafficTableDialog::absoluteTimeCheckBox() const
|
||||||
|
{
|
||||||
|
return ui->absoluteTimeCheckBox;
|
||||||
|
}
|
||||||
|
|
||||||
QPushButton *TrafficTableDialog::enabledTypesPushButton() const
|
QPushButton *TrafficTableDialog::enabledTypesPushButton() const
|
||||||
{
|
{
|
||||||
return ui->enabledTypesPushButton;
|
return ui->enabledTypesPushButton;
|
||||||
|
|
|
@ -103,6 +103,8 @@ signals:
|
||||||
class TrafficTableDialog : public WiresharkDialog
|
class TrafficTableDialog : public WiresharkDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool absolute_start_time READ absoluteStartTime)
|
||||||
|
Q_PROPERTY(bool nanosecond_timestamps READ nanosecondTimestamps)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Create a new conversation window.
|
/** Create a new conversation window.
|
||||||
|
@ -115,6 +117,16 @@ public:
|
||||||
explicit TrafficTableDialog(QWidget &parent, CaptureFile &cf, const char *filter = NULL, const QString &table_name = tr("Unknown"));
|
explicit TrafficTableDialog(QWidget &parent, CaptureFile &cf, const char *filter = NULL, const QString &table_name = tr("Unknown"));
|
||||||
~TrafficTableDialog();
|
~TrafficTableDialog();
|
||||||
|
|
||||||
|
/** Use absolute start times.
|
||||||
|
* @return true if the "Absolute start time" checkbox is checked, false otherwise.
|
||||||
|
*/
|
||||||
|
bool absoluteStartTime();
|
||||||
|
|
||||||
|
/** Use nanosecond timestamps.
|
||||||
|
* @return true if the current capture file uses nanosecond timestamps, false otherwise.
|
||||||
|
*/
|
||||||
|
bool nanosecondTimestamps() { return nanosecond_timestamps_; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
@ -143,6 +155,7 @@ protected:
|
||||||
QTabWidget *trafficTableTabWidget() const;
|
QTabWidget *trafficTableTabWidget() const;
|
||||||
QCheckBox *displayFilterCheckBox() const;
|
QCheckBox *displayFilterCheckBox() const;
|
||||||
QCheckBox *nameResolutionCheckBox() const;
|
QCheckBox *nameResolutionCheckBox() const;
|
||||||
|
QCheckBox *absoluteTimeCheckBox() const;
|
||||||
QPushButton *enabledTypesPushButton() const;
|
QPushButton *enabledTypesPushButton() const;
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
|
@ -151,6 +164,7 @@ protected slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString window_name_;
|
QString window_name_;
|
||||||
|
bool nanosecond_timestamps_;
|
||||||
|
|
||||||
QList<QVariant> curTreeRowData(int row) const;
|
QList<QVariant> curTreeRowData(int row) const;
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<widget class="QTabWidget" name="trafficTableTabWidget"/>
|
<widget class="QTabWidget" name="trafficTableTabWidget"/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,1,0">
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,1,0">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="nameResolutionCheckBox">
|
<widget class="QCheckBox" name="nameResolutionCheckBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
|
@ -49,6 +49,29 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="absoluteTimeCheckBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>Show absolute times in the start time column.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Absolute start time</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
@ -83,6 +106,7 @@
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
<sender>buttonBox</sender>
|
<sender>buttonBox</sender>
|
||||||
|
|
|
@ -65,6 +65,7 @@ const char *conv_column_titles[CONV_NUM_COLUMNS] = {
|
||||||
|
|
||||||
const char *conv_conn_a_title = "Connection A";
|
const char *conv_conn_a_title = "Connection A";
|
||||||
const char *conv_conn_b_title = "Connection B";
|
const char *conv_conn_b_title = "Connection B";
|
||||||
|
const char *conv_abs_start_title = "Abs Start";
|
||||||
|
|
||||||
const char *endp_column_titles[ENDP_NUM_COLUMNS] = {
|
const char *endp_column_titles[ENDP_NUM_COLUMNS] = {
|
||||||
"Address",
|
"Address",
|
||||||
|
|
|
@ -54,6 +54,7 @@ typedef enum {
|
||||||
extern const char *conv_column_titles[CONV_NUM_COLUMNS];
|
extern const char *conv_column_titles[CONV_NUM_COLUMNS];
|
||||||
extern const char *conv_conn_a_title;
|
extern const char *conv_conn_a_title;
|
||||||
extern const char *conv_conn_b_title;
|
extern const char *conv_conn_b_title;
|
||||||
|
extern const char *conv_abs_start_title;
|
||||||
|
|
||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue