replace SPDX identifier GPL-2.0+ with GPL-2.0-or-later.
[metze/wireshark/wip.git] / ui / qt / widgets / byte_view_text.cpp
1 /* byte_view_text.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  */
9
10 #include "byte_view_text.h"
11
12 #include <epan/charsets.h>
13
14 #include <wsutil/utf8_entities.h>
15
16 #include <ui/qt/utils/color_utils.h>
17 #include "wireshark_application.h"
18 #include "ui/recent.h"
19
20 #include <QActionGroup>
21 #include <QMouseEvent>
22 #include <QPainter>
23 #include <QScrollBar>
24 #include <QStyle>
25 #include <QStyleOption>
26 #include <QTextLayout>
27
28 // To do:
29 // - Add recent settings and context menu items to show/hide the offset.
30 // - Add a UTF-8 and possibly UTF-xx option to the ASCII display.
31 // - Move more common metrics to DataPrinter.
32
33 Q_DECLARE_METATYPE(bytes_view_type)
34 Q_DECLARE_METATYPE(bytes_encoding_type)
35 Q_DECLARE_METATYPE(DataPrinter::DumpType)
36
37 ByteViewText::ByteViewText(const QByteArray &data, packet_char_enc encoding, QWidget *parent) :
38     QAbstractScrollArea(parent),
39     layout_(new QTextLayout()),
40     data_(data),
41     encoding_(encoding),
42     hovered_byte_offset_(-1),
43     marked_byte_offset_(-1),
44     proto_start_(0),
45     proto_len_(0),
46     field_start_(0),
47     field_len_(0),
48     field_a_start_(0),
49     field_a_len_(0),
50     show_offset_(true),
51     show_hex_(true),
52     show_ascii_(true),
53     row_width_(recent.gui_bytes_view == BYTES_HEX ? 16 : 8),
54     font_width_(0),
55     line_height_(0)
56 {
57     layout_->setCacheEnabled(true);
58
59     offset_normal_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.35);
60     offset_field_fg_ = ColorUtils::alphaBlend(palette().windowText(), palette().window(), 0.65);
61
62     createContextMenu();
63
64     setMouseTracking(true);
65
66 #ifdef Q_OS_MAC
67     setAttribute(Qt::WA_MacShowFocusRect, true);
68 #endif
69 }
70
71 ByteViewText::~ByteViewText()
72 {
73     ctx_menu_.clear();
74     delete(layout_);
75 }
76
77 void ByteViewText::createContextMenu()
78 {
79     QAction *action;
80
81     QActionGroup * copy_actions = DataPrinter::copyActions(this);
82     ctx_menu_.addActions(copy_actions->actions());
83     ctx_menu_.addSeparator();
84
85     QActionGroup * format_actions = new QActionGroup(this);
86     action = format_actions->addAction(tr("Show bytes as hexadecimal"));
87     action->setData(QVariant::fromValue(BYTES_HEX));
88     action->setCheckable(true);
89     if (recent.gui_bytes_view == BYTES_HEX) {
90         action->setChecked(true);
91     }
92     action = format_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as bits"));
93     action->setData(QVariant::fromValue(BYTES_BITS));
94     action->setCheckable(true);
95     if (recent.gui_bytes_view == BYTES_BITS) {
96         action->setChecked(true);
97     }
98
99     ctx_menu_.addActions(format_actions->actions());
100     connect(format_actions, SIGNAL(triggered(QAction*)), this, SLOT(setHexDisplayFormat(QAction*)));
101
102     ctx_menu_.addSeparator();
103
104     QActionGroup * encoding_actions = new QActionGroup(this);
105     action = encoding_actions->addAction(tr("Show text based on packet"));
106     action->setData(QVariant::fromValue(BYTES_ENC_FROM_PACKET));
107     action->setCheckable(true);
108     if (recent.gui_bytes_encoding == BYTES_ENC_FROM_PACKET) {
109         action->setChecked(true);
110     }
111     action = encoding_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as ASCII"));
112     action->setData(QVariant::fromValue(BYTES_ENC_ASCII));
113     action->setCheckable(true);
114     if (recent.gui_bytes_encoding == BYTES_ENC_ASCII) {
115         action->setChecked(true);
116     }
117     action = encoding_actions->addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as EBCDIC"));
118     action->setData(QVariant::fromValue(BYTES_ENC_EBCDIC));
119     action->setCheckable(true);
120     if (recent.gui_bytes_encoding == BYTES_ENC_EBCDIC) {
121         action->setChecked(true);
122     }
123
124     ctx_menu_.addActions(encoding_actions->actions());
125     connect(encoding_actions, SIGNAL(triggered(QAction*)), this, SLOT(setCharacterEncoding(QAction*)));
126 }
127
128 bool ByteViewText::isEmpty() const
129 {
130     return data_.isEmpty();
131 }
132
133 QSize ByteViewText::minimumSizeHint() const
134 {
135     // Allow panel to shrink to any size
136     return QSize();
137 }
138
139 void ByteViewText::markProtocol(int start, int length)
140 {
141     proto_start_ = start;
142     proto_len_ = length;
143     viewport()->update();
144 }
145
146 void ByteViewText::markField(int start, int length, bool scroll_to)
147 {
148     field_start_ = start;
149     field_len_ = length;
150     // This might be called as a result of (de)selecting a proto tree
151     // item, so take us out of marked mode.
152     marked_byte_offset_ = -1;
153     if (scroll_to) {
154         scrollToByte(start);
155     }
156     viewport()->update();
157 }
158
159 void ByteViewText::markAppendix(int start, int length)
160 {
161     field_a_start_ = start;
162     field_a_len_ = length;
163     viewport()->update();
164 }
165
166 void ByteViewText::setMonospaceFont(const QFont &mono_font)
167 {
168     mono_font_ = QFont(mono_font);
169     mono_font_.setStyleStrategy(QFont::ForceIntegerMetrics);
170
171     const QFontMetricsF fm(mono_font_);
172     font_width_  = fm.width('M');
173
174     setFont(mono_font_);
175     viewport()->setFont(mono_font_);
176     layout_->setFont(mono_font_);
177
178     // We should probably use ProtoTree::rowHeight.
179     line_height_ = fontMetrics().height();
180
181     updateScrollbars();
182     viewport()->update();
183 }
184
185 void ByteViewText::paintEvent(QPaintEvent *)
186 {
187     QPainter painter(viewport());
188     painter.translate(-horizontalScrollBar()->value() * font_width_, 0);
189
190     // Pixel offset of this row
191     int row_y = 0;
192
193     // Starting byte offset
194     int offset = verticalScrollBar()->value() * row_width_;
195
196     // Clear the area
197     painter.fillRect(viewport()->rect(), palette().base());
198
199     // Offset background. We want the entire height to be filled.
200     if (show_offset_) {
201         QRect offset_rect = QRect(viewport()->rect());
202         offset_rect.setWidth(offsetPixels());
203         painter.fillRect(offset_rect, palette().window());
204     }
205
206     if ( data_.isEmpty() ) {
207         return;
208     }
209
210     // Data rows
211     int widget_height = height();
212     int leading = fontMetrics().leading();
213     painter.save();
214
215     x_pos_to_column_.clear();
216     while( (int) (row_y + line_height_) < widget_height && offset < (int) data_.count()) {
217         drawLine(&painter, offset, row_y);
218         offset += row_width_;
219         row_y += line_height_ + leading;
220     }
221
222     painter.restore();
223
224     // We can't do this in drawLine since the next line might draw over our rect.
225     if (!hover_outlines_.isEmpty()) {
226         qreal pen_width = 1.0;
227         qreal hover_alpha = 0.6;
228         QPen ho_pen;
229         QColor ho_color = palette().text().color();
230         if (marked_byte_offset_ < 0) {
231             hover_alpha = 0.3;
232 #if QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)
233             if (devicePixelRatio() > 1) {
234                 pen_width = 0.5;
235             }
236 #endif
237         }
238         ho_pen.setWidthF(pen_width);
239         ho_color.setAlphaF(hover_alpha);
240         ho_pen.setColor(ho_color);
241
242         painter.save();
243         painter.setPen(ho_pen);
244         painter.setBrush(Qt::NoBrush);
245         foreach (QRect ho_rect, hover_outlines_) {
246             // These look good on retina and non-retina displays on macOS.
247             // We might want to use fontMetrics numbers instead.
248             ho_rect.adjust(-1, 0, -1, -1);
249             painter.drawRect(ho_rect);
250         }
251         painter.restore();
252     }
253     hover_outlines_.clear();
254
255     QStyleOptionFocusRect option;
256     option.initFrom(this);
257     style()->drawPrimitive(QStyle::PE_FrameFocusRect, &option, &painter, this);
258 }
259
260 void ByteViewText::resizeEvent(QResizeEvent *)
261 {
262     updateScrollbars();
263 }
264
265 void ByteViewText::mousePressEvent (QMouseEvent *event) {
266     if (isEmpty() || !event || event->button() != Qt::LeftButton) {
267         return;
268     }
269
270     // byteSelected does the following:
271     // - Triggers selectedFieldChanged in ProtoTree, which clears the
272     //   selection and selects the corresponding (or no) item.
273     // - The new tree selection triggers markField, which clobbers
274     //   marked_byte_offset_.
275
276     const bool hover_mode = marked_byte_offset_ < 0;
277     const int byte_offset = byteOffsetAtPixel(event->pos());
278     setUpdatesEnabled(false);
279     emit byteSelected(byte_offset);
280     if (hover_mode && byte_offset >= 0) {
281         // Switch to marked mode.
282         hovered_byte_offset_ = -1;
283         marked_byte_offset_ = byte_offset;
284         viewport()->update();
285     } else {
286         // Back to hover mode.
287         mouseMoveEvent(event);
288     }
289     setUpdatesEnabled(true);
290 }
291
292 void ByteViewText::mouseMoveEvent(QMouseEvent *event)
293 {
294     if (marked_byte_offset_ >= 0) {
295         return;
296     }
297
298     hovered_byte_offset_ = byteOffsetAtPixel(event->pos());
299     emit byteHovered(hovered_byte_offset_);
300     viewport()->update();
301 }
302
303 void ByteViewText::leaveEvent(QEvent *event)
304 {
305     hovered_byte_offset_ = -1;
306     emit byteHovered(hovered_byte_offset_);
307
308     viewport()->update();
309     QAbstractScrollArea::leaveEvent(event);
310 }
311
312 void ByteViewText::contextMenuEvent(QContextMenuEvent *event)
313 {
314     ctx_menu_.exec(event->globalPos());
315 }
316
317 // Private
318
319 const int ByteViewText::separator_interval_ = DataPrinter::separatorInterval();
320
321 // Draw a line of byte view text for a given offset.
322 // Text highlighting is handled using QTextLayout::FormatRange.
323 void ByteViewText::drawLine(QPainter *painter, const int offset, const int row_y)
324 {
325     if (isEmpty()) {
326         return;
327     }
328
329     // Build our pixel to byte offset vector the first time through.
330     bool build_x_pos = x_pos_to_column_.empty() ? true : false;
331     int tvb_len = data_.count();
332     int max_tvb_pos = qMin(offset + row_width_, tvb_len) - 1;
333     QList<QTextLayout::FormatRange> fmt_list;
334
335     static const guchar hexchars[16] = {
336         '0', '1', '2', '3', '4', '5', '6', '7',
337         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
338
339     QString line;
340     HighlightMode offset_mode = ModeOffsetNormal;
341
342     // Offset.
343     if (show_offset_) {
344         line = QString(" %1 ").arg(offset, offsetChars(false), 16, QChar('0'));
345         if (build_x_pos) {
346             x_pos_to_column_.fill(-1, fontMetrics().width(line));
347         }
348     }
349
350     // Hex
351     if (show_hex_) {
352         int ascii_start = line.length() + DataPrinter::hexChars() + 3;
353         // Extra hover space before and after each byte.
354         int slop = font_width_ / 2;
355
356         if (build_x_pos) {
357             x_pos_to_column_ += QVector<int>().fill(-1, slop);
358         }
359
360         for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
361             line += ' ';
362             /* insert a space every separator_interval_ bytes */
363             if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
364                 line += ' ';
365                 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset - 1, font_width_);
366             }
367
368             switch (recent.gui_bytes_view) {
369             case BYTES_HEX:
370                 line += hexchars[(data_[tvb_pos] & 0xf0) >> 4];
371                 line += hexchars[data_[tvb_pos] & 0x0f];
372                 break;
373             case BYTES_BITS:
374                 /* XXX, bitmask */
375                 for (int j = 7; j >= 0; j--) {
376                     line += (data_[tvb_pos] & (1 << j)) ? '1' : '0';
377                 }
378                 break;
379             }
380             if (build_x_pos) {
381                 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset, fontMetrics().width(line) - x_pos_to_column_.size() + slop);
382             }
383             if (tvb_pos == hovered_byte_offset_ || tvb_pos == marked_byte_offset_) {
384                 int ho_len = recent.gui_bytes_view == BYTES_HEX ? 2 : 8;
385                 QRect ho_rect = painter->boundingRect(QRect(), Qt::AlignHCenter|Qt::AlignVCenter, line.right(ho_len));
386                 ho_rect.moveRight(fontMetrics().width(line));
387                 ho_rect.moveTop(row_y);
388                 hover_outlines_.append(ho_rect);
389             }
390         }
391         line += QString(ascii_start - line.length(), ' ');
392         if (build_x_pos) {
393             x_pos_to_column_ += QVector<int>().fill(-1, fontMetrics().width(line) - x_pos_to_column_.size());
394         }
395
396         addHexFormatRange(fmt_list, proto_start_, proto_len_, offset, max_tvb_pos, ModeProtocol);
397         if (addHexFormatRange(fmt_list, field_start_, field_len_, offset, max_tvb_pos, ModeField)) {
398             offset_mode = ModeOffsetField;
399         }
400         addHexFormatRange(fmt_list, field_a_start_, field_a_len_, offset, max_tvb_pos, ModeField);
401     }
402
403     // ASCII
404     if (show_ascii_) {
405         bool in_non_printable = false;
406         int np_start = 0;
407         int np_len = 0;
408         guchar c;
409
410         for (int tvb_pos = offset; tvb_pos <= max_tvb_pos; tvb_pos++) {
411             /* insert a space every separator_interval_ bytes */
412             if ((tvb_pos != offset) && ((tvb_pos % separator_interval_) == 0)) {
413                 line += ' ';
414                 if (build_x_pos) {
415                     x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset - 1, font_width_ / 2);
416                 }
417             }
418
419             if (recent.gui_bytes_encoding != BYTES_ENC_EBCDIC && encoding_ == PACKET_CHAR_ENC_CHAR_ASCII) {
420                 c = data_[tvb_pos];
421             } else {
422                 c = EBCDIC_to_ASCII1(data_[tvb_pos]);
423             }
424
425             if (g_ascii_isprint(c)) {
426                 line += c;
427                 if (in_non_printable) {
428                     in_non_printable = false;
429                     addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
430                 }
431             } else {
432                 line += UTF8_MIDDLE_DOT;
433                 if (!in_non_printable) {
434                     in_non_printable = true;
435                     np_start = tvb_pos;
436                     np_len = 1;
437                 } else {
438                     np_len++;
439                 }
440             }
441             if (build_x_pos) {
442                 x_pos_to_column_ += QVector<int>().fill(tvb_pos - offset, fontMetrics().width(line) - x_pos_to_column_.size());
443             }
444             if (tvb_pos == hovered_byte_offset_ || tvb_pos == marked_byte_offset_) {
445                 QRect ho_rect = painter->boundingRect(QRect(), 0, line.right(1));
446                 ho_rect.moveRight(fontMetrics().width(line));
447                 ho_rect.moveTop(row_y);
448                 hover_outlines_.append(ho_rect);
449             }
450         }
451         if (in_non_printable) {
452             addAsciiFormatRange(fmt_list, np_start, np_len, offset, max_tvb_pos, ModeNonPrintable);
453         }
454         addAsciiFormatRange(fmt_list, proto_start_, proto_len_, offset, max_tvb_pos, ModeProtocol);
455         if (addAsciiFormatRange(fmt_list, field_start_, field_len_, offset, max_tvb_pos, ModeField)) {
456             offset_mode = ModeOffsetField;
457         }
458         addAsciiFormatRange(fmt_list, field_a_start_, field_a_len_, offset, max_tvb_pos, ModeField);
459     }
460
461     // XXX Fields won't be highlighted if neither hex nor ascii are enabled.
462     addFormatRange(fmt_list, 0, offsetChars(), offset_mode);
463
464     layout_->clearLayout();
465     layout_->clearAdditionalFormats();
466     layout_->setText(line);
467     layout_->setAdditionalFormats(fmt_list);
468     layout_->beginLayout();
469     QTextLine tl = layout_->createLine();
470     tl.setLineWidth(totalPixels());
471     tl.setLeadingIncluded(true);
472     layout_->endLayout();
473     layout_->draw(painter, QPointF(0.0, row_y));
474 }
475
476 bool ByteViewText::addFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int start, int length, HighlightMode mode)
477 {
478     if (length < 1)
479         return false;
480
481     QTextLayout::FormatRange format_range;
482     format_range.start = start;
483     format_range.length = length;
484     switch (mode) {
485     case ModeNormal:
486         return false;
487     case ModeField:
488         format_range.format.setBackground(palette().highlight());
489         break;
490     case ModeProtocol:
491         format_range.format.setBackground(palette().window());
492         break;
493     case ModeOffsetNormal:
494         format_range.format.setForeground(offset_normal_fg_);
495         break;
496     case ModeOffsetField:
497         format_range.format.setForeground(offset_field_fg_);
498         break;
499     case ModeNonPrintable:
500         format_range.format.setForeground(offset_normal_fg_);
501         break;
502     }
503     fmt_list << format_range;
504     return true;
505 }
506
507 bool ByteViewText::addHexFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int mark_start, int mark_length, int tvb_offset, int max_tvb_pos, ByteViewText::HighlightMode mode)
508 {
509     int mark_end = mark_start + mark_length - 1;
510     if (mark_start < 0 || mark_length < 1) return false;
511     if (mark_start > max_tvb_pos && mark_end < tvb_offset) return false;
512
513     int chars_per_byte = recent.gui_bytes_view == BYTES_HEX ? 2 : 8;
514     int chars_plus_pad = chars_per_byte + 1;
515     int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
516     int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
517     int fmt_start = offsetChars() + 1 // offset + spacing
518             + (byte_start / separator_interval_)
519             + (byte_start * chars_plus_pad);
520     int fmt_length = offsetChars() + 1 // offset + spacing
521             + (byte_end / separator_interval_)
522             + (byte_end * chars_plus_pad)
523             + chars_per_byte
524             - fmt_start;
525     return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
526 }
527
528 bool ByteViewText::addAsciiFormatRange(QList<QTextLayout::FormatRange> &fmt_list, int mark_start, int mark_length, int tvb_offset, int max_tvb_pos, ByteViewText::HighlightMode mode)
529 {
530     int mark_end = mark_start + mark_length - 1;
531     if (mark_start < 0 || mark_length < 1) return false;
532     if (mark_start > max_tvb_pos && mark_end < tvb_offset) return false;
533
534     int byte_start = qMax(tvb_offset, mark_start) - tvb_offset;
535     int byte_end = qMin(max_tvb_pos, mark_end) - tvb_offset;
536     int fmt_start = offsetChars() + DataPrinter::hexChars() + 3 // offset + hex + spacing
537             + (byte_start / separator_interval_)
538             + byte_start;
539     int fmt_length = offsetChars() + DataPrinter::hexChars() + 3 // offset + hex + spacing
540             + (byte_end / separator_interval_)
541             + byte_end
542             + 1 // Just one character.
543             - fmt_start;
544     return addFormatRange(fmt_list, fmt_start, fmt_length, mode);
545 }
546
547 void ByteViewText::scrollToByte(int byte)
548 {
549     verticalScrollBar()->setValue(byte / row_width_);
550 }
551
552 // Offset character width
553 int ByteViewText::offsetChars(bool include_pad)
554 {
555     int padding = include_pad ? 2 : 0;
556     if (! isEmpty() && data_.count() > 0xffff) {
557         return 8 + padding;
558     }
559     return 4 + padding;
560 }
561
562 // Offset pixel width
563 int ByteViewText::offsetPixels()
564 {
565     if (show_offset_) {
566         // One pad space before and after
567         QString zeroes = QString(offsetChars(), '0');
568         return fontMetrics().width(zeroes);
569     }
570     return 0;
571 }
572
573 // Hex pixel width
574 int ByteViewText::hexPixels()
575 {
576     if (show_hex_) {
577         // One pad space before and after
578         QString zeroes = QString(DataPrinter::hexChars() + 2, '0');
579         return fontMetrics().width(zeroes);
580     }
581     return 0;
582 }
583
584 int ByteViewText::asciiPixels()
585 {
586     if (show_ascii_) {
587         // Two pad spaces before, one after
588         int ascii_chars = (row_width_ + ((row_width_ - 1) / separator_interval_));
589         QString zeroes = QString(ascii_chars + 3, '0');
590         return fontMetrics().width(zeroes);
591     }
592     return 0;
593 }
594
595 int ByteViewText::totalPixels()
596 {
597     return offsetPixels() + hexPixels() + asciiPixels();
598 }
599
600 void ByteViewText::copyBytes(bool)
601 {
602     QAction* action = qobject_cast<QAction*>(sender());
603     if (!action) {
604         return;
605     }
606
607     int dump_type = action->data().toInt();
608
609     if (dump_type <= DataPrinter::DP_Binary) {
610         DataPrinter printer;
611         printer.toClipboard((DataPrinter::DumpType) dump_type, this);
612     }
613 }
614
615 // We do chunky (per-character) scrolling because it makes some of the
616 // math easier. Should we do smooth scrolling?
617 void ByteViewText::updateScrollbars()
618 {
619     const int length = data_.count();
620     if (length > 0) {
621         int all_lines_height = length / row_width_ + ((length % row_width_) ? 1 : 0) - viewport()->height() / line_height_;
622
623         verticalScrollBar()->setRange(0, qMax(0, all_lines_height));
624         horizontalScrollBar()->setRange(0, qMax(0, int((totalPixels() - viewport()->width()) / font_width_)));
625     }
626 }
627
628 int ByteViewText::byteOffsetAtPixel(QPoint pos)
629 {
630     int byte = (verticalScrollBar()->value() + (pos.y() / line_height_)) * row_width_;
631     int x = (horizontalScrollBar()->value() * font_width_) + pos.x();
632     int col = x_pos_to_column_.value(x, -1);
633
634     if (col < 0) {
635         return -1;
636     }
637
638     byte += col;
639     if (byte > data_.count()) {
640         return -1;
641     }
642     return byte;
643 }
644
645 void ByteViewText::setHexDisplayFormat(QAction *action)
646 {
647     if (!action) {
648         return;
649     }
650
651     recent.gui_bytes_view = action->data().value<bytes_view_type>();
652     row_width_ = recent.gui_bytes_view == BYTES_HEX ? 16 : 8;
653     updateScrollbars();
654     viewport()->update();
655 }
656
657 void ByteViewText::setCharacterEncoding(QAction *action)
658 {
659     if (!action) {
660         return;
661     }
662
663     recent.gui_bytes_encoding = action->data().value<bytes_encoding_type>();
664     viewport()->update();
665 }
666
667 /*
668  * Editor modelines
669  *
670  * Local Variables:
671  * c-basic-offset: 4
672  * tab-width: 8
673  * indent-tabs-mode: nil
674  * End:
675  *
676  * ex: set shiftwidth=4 tabstop=8 expandtab:
677  * :indentSize=4:tabSize=8:noTabs=true:
678  */