summaryrefslogtreecommitdiff
path: root/ui/qt/wireless_timeline.cpp
diff options
context:
space:
mode:
authorSimon Barber <simon.barber@meraki.net>2017-04-06 17:45:33 -0400
committerMichael Mann <mmann78@netscape.net>2017-06-05 11:25:51 +0000
commit21305e9835d40dbd812514471397fb1de3a0740c (patch)
treefdbd995627e1e7cf059a9bdc3b1d2b8dc7278beb /ui/qt/wireless_timeline.cpp
parentd0865fd619454a9ac06b1c7d287dc438aff50bb0 (diff)
downloadwireshark-21305e9835d40dbd812514471397fb1de3a0740c.tar.gz
Add a timeline view for packets, with the timing data used to generate the display
taken from the timing analysis done in the wlan_radio dissector. QT only. The timeline background is light gray, white for packets displayed in the packetlist, and blue for the currently selected packet. Packets are coloured according to the colouring rules foreground colour. The timeline can be zoomed with controls on the toolbar. At higher zoom levels the duration (NAV) field is plotted as a horizontal line to the right of a packet. The height of a packet in the timeline is proportional to the RSSI. The bottom half of the packet is only shown if it matches the display filter. Todo: Auto detect TSF timing reference point (start/end of packet) Add a scrollbar Add a ruler showing time Improve handling of focus. Do not display NAV for packets with bad FCS. Show related packets graphically Different Y axis modes - bandwidth/channel use display - different transmitters per line - background color from coloring rules Live capture support Change-Id: Ic31fffb0d6854966361ade7abb5c0be50db9a247 Reviewed-on: https://code.wireshark.org/review/20043 Petri-Dish: Michael Mann <mmann78@netscape.net> Tested-by: Petri Dish Buildbot <buildbot-no-reply@wireshark.org> Reviewed-by: Michael Mann <mmann78@netscape.net>
Diffstat (limited to 'ui/qt/wireless_timeline.cpp')
-rw-r--r--ui/qt/wireless_timeline.cpp643
1 files changed, 643 insertions, 0 deletions
diff --git a/ui/qt/wireless_timeline.cpp b/ui/qt/wireless_timeline.cpp
new file mode 100644
index 0000000000..7fb583528c
--- /dev/null
+++ b/ui/qt/wireless_timeline.cpp
@@ -0,0 +1,643 @@
+/* wireless_timeline.cpp
+ * GUI to show an 802.11 wireless timeline of packets
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * Copyright 1998 Gerald Combs
+ *
+ * Copyright 2012 Parc Inc and Samsung Electronics
+ * Copyright 2015, 2016 & 2017 Cisco Inc
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "wireless_timeline.h"
+
+#include <epan/packet.h>
+#include <epan/prefs.h>
+#include <epan/proto_data.h>
+#include <epan/packet_info.h>
+#include <epan/column-utils.h>
+#include <epan/tap.h>
+
+#include <cmath>
+
+#include "globals.h"
+#include "color_utils.h"
+#include "../../log.h"
+#include <epan/dissectors/packet-ieee80211-radio.h>
+
+#include <epan/color_filters.h>
+#include "frame_tvbuff.h"
+
+#include "color_utils.h"
+#include "qt_ui_utils.h"
+#include "wireshark_application.h"
+
+#ifdef Q_OS_WIN
+#include "wsutil/file_util.h"
+#include <QSysInfo>
+#endif
+
+#include <QPaintEvent>
+#include <QPainter>
+#include <QGraphicsScene>
+#include <QToolTip>
+
+#include "packet_list.h"
+#include "packet_list_model.h"
+
+#include "ui/main_statusbar.h"
+
+const float fraction = 0.8F;
+const float base = 0.1F;
+
+class pcolor : public QColor
+{
+public:
+ inline pcolor(float red, float green, float blue) : QColor(
+ (int) (255*(red * fraction + base)),
+ (int) (255*(green * fraction + base)),
+ (int) (255*(blue * fraction + base)) ) { }
+};
+
+static void reset_rgb(float rgb[TIMELINE_HEIGHT][3])
+{
+ int i;
+ for(i = 0; i < TIMELINE_HEIGHT; i++)
+ rgb[i][0] = rgb[i][1] = rgb[i][2] = 1.0;
+}
+
+static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HEIGHT][3], float ratio)
+{
+ int previous = 0, i;
+ for(i = 1; i <= TIMELINE_HEIGHT; i++) {
+ if (i != TIMELINE_HEIGHT &&
+ rgb[previous][0] == rgb[i][0] &&
+ rgb[previous][1] == rgb[i][1] &&
+ rgb[previous][2] == rgb[i][2])
+ continue;
+ if (rgb[previous][0] != 1.0 || rgb[previous][1] != 1.0 || rgb[previous][2] != 1.0) {
+ p.fillRect(QRectF(x/ratio, previous, width/ratio, i-previous), pcolor(rgb[previous][0],rgb[previous][1],rgb[previous][2]));
+ }
+ previous = i;
+ }
+ reset_rgb(rgb);
+}
+
+static void render_rectangle(QPainter &p, gint x, gint width, guint height, int dfilter, float r, float g, float b, float ratio)
+{
+ p.fillRect(QRectF(x/ratio, TIMELINE_HEIGHT/2-height, width/ratio, dfilter ? height * 2 : height), pcolor(r,g,b));
+}
+
+static void accumulate_rgb(float rgb[TIMELINE_HEIGHT][3], int height, int dfilter, float width, float red, float green, float blue)
+{
+ int i;
+ for(i = TIMELINE_HEIGHT/2-height; i < (TIMELINE_HEIGHT/2 + (dfilter ? height : 0)); i++) {
+ rgb[i][0] = rgb[i][0] - width + width * red;
+ rgb[i][1] = rgb[i][1] - width + width * green;
+ rgb[i][2] = rgb[i][2] - width + width * blue;
+ }
+}
+
+
+void WirelessTimeline::mousePressEvent(QMouseEvent *event)
+{
+ start_x = last_x = event->localPos().x();
+}
+
+
+void WirelessTimeline::mouseMoveEvent(QMouseEvent *event)
+{
+ if (event->buttons() == Qt::NoButton)
+ return;
+
+ qreal offset = event->localPos().x() - last_x;
+ last_x = event->localPos().x();
+
+ qreal shift = ((qreal) (end_tsf - start_tsf))/width() * offset;
+ start_tsf -= shift;
+ end_tsf -= shift;
+ clip_tsf();
+
+ // TODO: scroll by moving pixels and redraw only exposed area
+ // render(p, ...)
+ // then update full widget only on release.
+ update();
+}
+
+
+void WirelessTimeline::mouseReleaseEvent(QMouseEvent *event)
+{
+ qreal offset = event->localPos().x() - start_x;
+
+ /* if this was a drag, ignore it */
+ if (std::abs(offset) > 3)
+ return;
+
+ /* this was a click */
+ guint num = find_packet(event->localPos().x());
+ if (num == 0)
+ return;
+
+ frame_data *fdata = frame_data_sequence_find(cfile.frames, num);
+ if (!fdata->flags.passed_dfilter && fdata->prev_dis_num > 0)
+ num = fdata->prev_dis_num;
+
+ cf_goto_frame(&cfile, num);
+}
+
+
+void WirelessTimeline::clip_tsf()
+{
+ // did we go past the start of the file?
+ if (start_tsf < first->start_tsf) {
+ // align the start of the file at the left edge
+ guint64 shift = first->start_tsf - start_tsf;
+ start_tsf += shift;
+ end_tsf += shift;
+ }
+ if (end_tsf > last->end_tsf) {
+ guint64 shift = end_tsf - last->end_tsf;
+ start_tsf -= shift;
+ end_tsf -= shift;
+ }
+}
+
+
+void WirelessTimeline::packetSelectionChanged()
+{
+ if (isHidden())
+ return;
+
+ if (cfile.current_frame) {
+ struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
+
+ guint left_margin = 0.9 * start_tsf + 0.1 * end_tsf;
+ guint right_margin = 0.1 * start_tsf + 0.9 * end_tsf;
+ guint half_window = (end_tsf - start_tsf)/2;
+
+ if (wr) {
+ // are we to the left of the left margin?
+ if (wr->start_tsf < left_margin) {
+ // scroll the left edge back to the left margin
+ guint64 offset = left_margin - wr->start_tsf;
+ if (offset < half_window) {
+ // small movement; keep packet to margin
+ start_tsf -= offset;
+ end_tsf -= offset;
+ } else {
+ // large movement; move packet to center of window
+ guint64 center = (wr->start_tsf + wr->end_tsf)/2;
+ start_tsf = center - half_window;
+ end_tsf = center + half_window;
+ }
+ } else if (wr->end_tsf > right_margin) {
+ guint64 offset = wr->end_tsf - right_margin;
+ if (offset < half_window) {
+ start_tsf += offset;
+ end_tsf += offset;
+ } else {
+ guint64 center = (wr->start_tsf + wr->end_tsf)/2;
+ start_tsf = center - half_window;
+ end_tsf = center + half_window;
+ }
+ }
+ clip_tsf();
+
+ update();
+ }
+ }
+}
+
+
+/* given an x position find which packet that corresponds to.
+ * if it's inter frame space the subsequent packet is returned */
+guint
+WirelessTimeline::find_packet(qreal x_position)
+{
+ guint64 x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf));
+
+ return find_packet_tsf(x_time);
+}
+
+void WirelessTimeline::captureFileReadStarted(capture_file *cf)
+{
+ capfile = cf;
+ hide();
+ // TODO: hide or grey the toolbar controls
+}
+
+void WirelessTimeline::captureFileReadFinished()
+{
+ /* All frames must be included in packet list */
+ if (cfile.count == 0 || g_hash_table_size(radio_packet_list) != cfile.count)
+ return;
+
+ /* check that all frames have start and end tsf time and are reasonable time order.
+ * packet timing reference seems to be off a little on some generators, which
+ * causes frequent IFS values in the range 0 to -30. Some generators emit excessive
+ * data when an FCS error happens, and this results in the duration calculation for
+ * the error frame being excessively long. This can cause larger negative IFS values
+ * (-30 to -1000) for the subsequent frame. Ignore these cases, as they don't seem
+ * to impact the GUI too badly. If the TSF reference point is set wrong (TSF at
+ * start of frame when it is at the end) then larger negative offsets are often
+ * seen. Don't display the timeline in these cases.
+ */
+ /* TODO: update GUI to handle captures with occasional frames missing TSF data */
+ /* TODO: indicate error message to the user */
+ for (guint32 n = 1; n < cfile.count; n++) {
+ struct wlan_radio *w = get_wlan_radio(n);
+ if (w->start_tsf == 0 || w->end_tsf == 0) {
+ statusbar_push_temporary_msg("Packet number %u does not include TSF timestamp, not showing timeline.", n);
+ return;
+ }
+ if (w->ifs < -15000) {
+ statusbar_push_temporary_msg("Packet number %u has large negative jump in TSF, not showing timeline. Perhaps TSF reference point is set wrong?", n);
+ return;
+ }
+ }
+
+ first = get_wlan_radio(1);
+ last = get_wlan_radio(cfile.count);
+
+ start_tsf = first->start_tsf;
+ end_tsf = last->end_tsf;
+
+ /* TODO: only reset the zoom level if the file is changed, not on redissection */
+ zoom_level = 0;
+
+ show();
+ packetSelectionChanged();
+ // TODO: show or ungrey the toolbar controls
+ update();
+}
+
+void WirelessTimeline::appInitialized()
+{
+ register_tap_listener("wlan_radio_timeline", this, NULL, TL_REQUIRES_NOTHING, tap_timeline_reset, tap_timeline_packet, NULL/*tap_draw_cb tap_draw*/);
+}
+
+void WirelessTimeline::resizeEvent(QResizeEvent*)
+{
+ // TODO adjust scrollbar
+}
+
+
+// Calculate the x position on the GUI from the timestamp
+int WirelessTimeline::position(guint64 tsf, float ratio)
+{
+ int position = -100;
+
+ if (tsf != 0xffffffffffffffff) {
+ position = ((double) tsf - start_tsf)*width()*ratio/(end_tsf-start_tsf);
+ }
+ return position;
+}
+
+
+WirelessTimeline::WirelessTimeline(QWidget *parent, PacketList *packet_list) : QWidget(parent)
+{
+ setHidden(true);
+ this->packet_list = packet_list;
+ connect(packet_list->packetListModel(), SIGNAL(bgColorizationProgress(int,int)),
+ this, SLOT(bgColorizationProgress(int,int)));
+ connect(packet_list, SIGNAL(packetSelectionChanged()),
+ this, SLOT(packetSelectionChanged()));
+ connect(wsApp, SIGNAL(appInitialized()),
+ this, SLOT(appInitialized()));
+ zoom_level = 1.0;
+ setFixedHeight(TIMELINE_HEIGHT);
+ first_packet = 1;
+ setMouseTracking(true);
+
+ radio_packet_list = NULL;
+}
+
+void WirelessTimeline::tap_timeline_reset(void* tapdata)
+{
+ WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
+
+ if (timeline->radio_packet_list != NULL)
+ {
+ g_hash_table_destroy(timeline->radio_packet_list);
+ }
+ timeline->hide();
+
+ timeline->radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal);
+}
+
+gboolean WirelessTimeline::tap_timeline_packet(void *tapdata, packet_info* pinfo, epan_dissect_t* edt _U_, const void *data)
+{
+ WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
+ struct wlan_radio *wlan_radio_info = (struct wlan_radio *)data;
+
+ /* Save the radio information in our own (GUI) hashtable */
+ g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), wlan_radio_info);
+ return FALSE;
+}
+
+struct wlan_radio* WirelessTimeline::get_wlan_radio(guint32 packet_num)
+{
+ return (struct wlan_radio*)g_hash_table_lookup(radio_packet_list, GUINT_TO_POINTER(packet_num));
+}
+
+void WirelessTimeline::doToolTip(struct wlan_radio *wr, QPoint pos, int x)
+{
+ if (x < position(wr->start_tsf, 1.0)) {
+ QToolTip::showText(pos, QString("inter frame space %1 us").arg(wr->ifs));
+ } else {
+ QToolTip::showText(pos, QString("total duration %1 us\nNAV %2 us").arg(wr->end_tsf-wr->start_tsf)
+ .arg(wr->nav));
+ }
+}
+
+
+bool WirelessTimeline::event(QEvent *event)
+{
+ if (event->type() == QEvent::ToolTip) {
+ QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
+ guint packet = find_packet(helpEvent->pos().x());
+ if (packet) {
+ doToolTip(get_wlan_radio(packet), helpEvent->globalPos(), helpEvent->x());
+ } else {
+ QToolTip::hideText();
+ event->ignore();
+ }
+ return true;
+ }
+ return QWidget::event(event);
+}
+
+
+void WirelessTimeline::bgColorizationProgress(int first, int last)
+{
+ if (isHidden()) return;
+
+ struct wlan_radio *first_wr = get_wlan_radio(first);
+
+ struct wlan_radio *last_wr = get_wlan_radio(last-1);
+
+ int x = position(first_wr->start_tsf, 1);
+ int x_end = position(last_wr->end_tsf, 1);
+
+ update(x, 0, x_end-x+1, height());
+}
+
+
+guint64 WirelessTimeline::current_frame_center()
+{
+ if (cfile.current_frame == NULL)
+ return 0;
+
+ struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
+ return (wr->start_tsf + wr->end_tsf) /2;
+}
+
+
+void WirelessTimeline::zoom()
+{
+ guint64 center = current_frame_center();
+ int x_position = position(center, 1.0);
+
+ /* adjust the zoom around the selected packet */
+ float fraction = ((float) x_position)/width();
+ guint64 file_range = last->end_tsf - first->start_tsf;
+ guint64 span = pow(file_range, 1.0-zoom_level/25.0);
+ start_tsf = center - span*fraction;
+ end_tsf = center + span*(1.0-fraction);
+
+ /* if we go out of range for the whole file, clamp it */
+ if (start_tsf < first->start_tsf) {
+ end_tsf += first->start_tsf - start_tsf;
+ start_tsf = first->start_tsf;
+ } else if (end_tsf > last->end_tsf) {
+ start_tsf -= end_tsf - last->end_tsf;
+ end_tsf = last->end_tsf;
+ }
+
+ update();
+}
+
+void WirelessTimeline::zoomIn()
+{
+ zoom_level++;
+ zoom();
+}
+
+void WirelessTimeline::zoomOut()
+{
+ zoom_level--;
+ if (zoom_level < 0) zoom_level = 0;
+ zoom();
+}
+
+void WirelessTimeline::zoomFullOut()
+{
+ zoom_level = 0;
+ zoom();
+}
+
+int WirelessTimeline::find_packet_tsf(guint64 tsf)
+{
+ if (cfile.count < 1)
+ return 0;
+
+ if (cfile.count < 2)
+ return 1;
+
+ guint32 min_count = 1;
+ guint32 max_count = cfile.count-1;
+
+ guint64 min_tsf = get_wlan_radio(min_count)->end_tsf;
+ guint64 max_tsf = get_wlan_radio(max_count)->end_tsf;
+
+ for(;;) {
+ if (tsf >= max_tsf)
+ return max_count+1;
+
+ if (tsf < min_tsf)
+ return min_count;
+
+ guint32 middle = (min_count + max_count)/2;
+ if (middle == min_count)
+ return middle+1;
+
+ guint64 middle_tsf = get_wlan_radio(middle)->end_tsf;
+
+ if (tsf >= middle_tsf) {
+ min_count = middle;
+ min_tsf = middle_tsf;
+ } else {
+ max_count = middle;
+ max_tsf = middle_tsf;
+ }
+ };
+}
+
+void
+WirelessTimeline::paintEvent(QPaintEvent *qpe)
+{
+ QPainter p(this);
+
+ // painting is done in device pixels in the x axis, get the ratio here
+ float ratio = p.device()->devicePixelRatio();
+
+ unsigned int packet;
+ double zoom;
+ int last_x=-1;
+ int left = qpe->rect().left()*ratio;
+ int right = qpe->rect().right()*ratio;
+ float rgb[TIMELINE_HEIGHT][3];
+ reset_rgb(rgb);
+
+ zoom = ((double) width())/(end_tsf - start_tsf) * ratio;
+
+ /* background is light grey */
+ p.fillRect(0, 0, width(), TIMELINE_HEIGHT, QColor(240,240,240));
+
+ /* background of packets visible in packet_list is white */
+ int top = packet_list->indexAt(QPoint(0,0)).row();
+ int bottom = packet_list->indexAt(QPoint(0,packet_list->viewport()->height())).row();
+ PacketListModel *model = packet_list->packetListModel();
+ int x1 = top == -1 ? 0 : position(get_wlan_radio(model->getRowFdata(top)->num)->start_tsf, ratio);
+ int x2 = bottom == -1 ? width() : position(get_wlan_radio(model->getRowFdata(bottom)->num)->end_tsf, ratio);
+ p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::white);
+
+ /* background of current packet is blue */
+ if (cfile.current_frame) {
+ struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
+ if (wr) {
+ x1 = position(wr->start_tsf, ratio);
+ x2 = position(wr->end_tsf, ratio);
+ p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::blue);
+ }
+ }
+
+ QGraphicsScene qs;
+ for(packet = find_packet_tsf(start_tsf + left/zoom - 40000); packet <= cfile.count; packet++) {
+ frame_data *fdata = frame_data_sequence_find(cfile.frames, packet);
+ struct wlan_radio *ri = get_wlan_radio(fdata->num);
+ float x, width, red,green,blue;
+ gint8 rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi;
+ guint height = (rssi+100)/2;
+ gint end_nav;
+
+ if (ri == NULL) continue;
+
+ /* leave a margin above the packets so the selected packet can be seen */
+ if (height > TIMELINE_HEIGHT/2-6)
+ height = TIMELINE_HEIGHT/2-6;
+
+ /* ensure shortest packets are clearly visible */
+ if (height < 2)
+ height = 2;
+
+ /* skip frames we don't have start and end data for */
+ /* TODO: show something, so it's clear a frame is missing */
+ if (ri->start_tsf == 0 || ri->end_tsf == 0)
+ continue;
+
+ x = ((gint64) (ri->start_tsf - start_tsf))*zoom;
+ /* is there a previous anti-aliased pixel to output */
+ if (last_x >= 0 && ((int) x) != last_x) {
+ /* write it out now */
+ render_pixels(p, last_x, 1, rgb, ratio);
+ last_x = -1;
+ }
+
+ /* does this packet start past the right edge of the window? */
+ if (x >= right) {
+ break;
+ }
+
+ width = (ri->end_tsf - ri->start_tsf)*zoom;
+ if (width < 0) {
+ continue;
+ }
+
+ /* is this packet completely to the left of the displayed area? */
+ // TODO clip NAV line properly if we are displaying it
+ if ((x + width) < left)
+ continue;
+
+ /* remember the first displayed packet */
+ if (first_packet < 0)
+ first_packet = packet;
+
+ if (fdata->color_filter) {
+ const color_t *c = &((color_filter_t *) fdata->color_filter)->fg_color;
+ red = c->red / 65535.0;
+ green = c->green / 65535.0;
+ blue = c->blue / 65535.0;
+ } else {
+ red = green = blue = 0.0;
+ }
+
+ /* record NAV field at higher magnifications */
+ end_nav = x + width + ri->nav*zoom;
+ if (zoom >= 0.01 && ri->nav && end_nav > 0) {
+ gint y = 2*(packet % (TIMELINE_HEIGHT/2));
+ qs.addLine(QLineF((x+width)/ratio, y, end_nav/ratio, y), QPen(pcolor(red,green,blue)));
+ }
+
+ /* does this rectangle fit within one pixel? */
+ if (((int) x) == ((int) (x+width))) {
+ /* accumulate it for later rendering together
+ * with all other sub pixels that fall within this
+ * pixel */
+ last_x = x;
+ accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, width, red, green, blue);
+ } else {
+ /* it spans more than 1 pixel.
+ * first accumulate the part that does fit */
+ float partial = ((int) x) + 1 - x;
+ accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, partial, red, green, blue);
+ /* and render it */
+ render_pixels(p, (int) x, 1, rgb, ratio);
+ last_x = -1;
+ x += partial;
+ width -= partial;
+ /* are there any whole pixels of width left to draw? */
+ if (width > 1.0) {
+ render_rectangle(p, x, width, height, fdata->flags.passed_dfilter, red, green, blue, ratio);
+ x += (int) width;
+ width -= (int) width;
+ }
+ /* is there a partial pixel left */
+ if (width > 0.0) {
+ last_x = x;
+ accumulate_rgb(rgb, height, fdata->flags.passed_dfilter, width, red, green, blue);
+ }
+ }
+ }
+
+ // draw the NAV lines last, so they appear on top of the packets
+ qs.render(&p, rect(), rect());
+}
+
+
+/*
+ * Editor modelines
+ *
+ * Local Variables:
+ * c-basic-offset: 4
+ * tab-width: 8
+ * indent-tabs-mode: nil
+ * End:
+ *
+ * ex: set shiftwidth=4 tabstop=8 expandtab:
+ * :indentSize=4:tabSize=8:noTabs=true:
+ */