1 /*
2     This file is part of the Okteta Kasten module, made within the KDE community.
3 
4     SPDX-FileCopyrightText: 2008-2009, 2012 Friedrich W. H. Kossebau <kossebau@kde.org>
5 
6     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8 
9 #include "viewstatuscontroller.hpp"
10 
11 // Okteta Kasten gui
12 #include <Kasten/Okteta/ByteArrayView>
13 // Kasten ui
14 #include <Kasten/ToggleButton>
15 #include <Kasten/StatusBar>
16 // Okteta core
17 #include <Okteta/CharCodec>
18 #include <Okteta/OktetaCore>
19 // KF
20 #include <KSqueezedTextLabel>
21 #include <KComboBox>
22 #include <KLocalizedString>
23 // Qt
24 #include <QLayout>
25 #include <QLabel>
26 #include <QFontMetrics>
27 
28 // TODO: make status bar capable to hide entries if size is too small, use priorisation
29 
30 namespace Kasten {
31 
ViewStatusController(StatusBar * statusBar)32 ViewStatusController::ViewStatusController(StatusBar* statusBar)
33     : mStatusBar(statusBar)
34 {
35     mPrintFunction = Okteta::OffsetFormat::printFunction(Okteta::OffsetFormat::Hexadecimal);
36 
37     mOffsetLabel = new QLabel(statusBar);
38     statusBar->addWidget(mOffsetLabel);
39 
40     mSelectionLabel = new QLabel(statusBar);
41     statusBar->addWidget(mSelectionLabel);
42 
43     const QString insertModeText = i18nc("@info:status short for: Insert mode",    "INS");
44     const QString overwriteModeText = i18nc("@info:status short for: Overwrite mode", "OVR");
45     const QString insertModeTooltip = i18nc("@info:tooltip", "Insert mode");
46     const QString overwriteModeTooltip = i18nc("@info:tooltip", "Overwrite mode");
47     mOverwriteModeToggleButton = new ToggleButton(insertModeText, insertModeTooltip, statusBar);
48     mOverwriteModeToggleButton->setCheckedState(overwriteModeText, overwriteModeTooltip);
49     statusBar->addWidget(mOverwriteModeToggleButton);
50     connect(mOverwriteModeToggleButton, &ToggleButton::clicked, this, &ViewStatusController::setOverwriteMode);
51 
52     mValueCodingComboBox = new KComboBox(statusBar);
53     const QStringList list {
54         i18nc("@item:inmenu encoding of the bytes as values in the hexadecimal format", "Hexadecimal"),
55         i18nc("@item:inmenu encoding of the bytes as values in the decimal format",     "Decimal"),
56         i18nc("@item:inmenu encoding of the bytes as values in the octal format",       "Octal"),
57         i18nc("@item:inmenu encoding of the bytes as values in the binary format",      "Binary"),
58     };
59     mValueCodingComboBox->addItems(list);
60     mValueCodingComboBox->setToolTip(
61         i18nc("@info:tooltip", "Coding of the value interpretation in the current view."));
62     connect(mValueCodingComboBox, QOverload<int>::of(&KComboBox::activated),
63             this, &ViewStatusController::setValueCoding);
64     statusBar->addWidget(mValueCodingComboBox);
65 
66     mCharCodingComboBox = new KComboBox(statusBar);
67     mCharCodingComboBox->addItems(Okteta::CharCodec::codecNames());
68     mCharCodingComboBox->setToolTip(
69         i18nc("@info:tooltip", "Encoding in the character column of the current view."));
70     connect(mCharCodingComboBox, QOverload<int>::of(&KComboBox::activated),
71             this, &ViewStatusController::setCharCoding);
72     statusBar->addWidget(mCharCodingComboBox);
73 
74     fixWidths(Okteta::OffsetFormat::Hexadecimal);
75 
76     setTargetModel(nullptr);
77 }
78 
79 // the letter C can be very wide, that is why with proportional fonts there seems too much space used, but isn't
80 // see https://frinring.wordpress.com/2008/10/14/better-width-with-open-sources/
fixWidths(int offsetCoding)81 void ViewStatusController::fixWidths(int offsetCoding)
82 {
83     const QFontMetrics metrics = mStatusBar->fontMetrics();
84 
85     // mOffsetLabel
86     constexpr int hexDigitsCount = 16;
87     constexpr int decimalDigitsCount = 10;
88     constexpr int firstLetterIndex = 10;
89     constexpr char digits[hexDigitsCount] = {
90         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
91         'A', 'B', 'C', 'D', 'E', 'F'
92     };
93 
94     int largestOffsetWidth = 0;
95     int largestSelectionWidth = 0;
96     int widestDigitIndex = 0;
97     const int digitsCount = (offsetCoding == Okteta::OffsetFormat::Hexadecimal) ? hexDigitsCount : decimalDigitsCount;
98     for (int i = 0; i < digitsCount; ++i) {
99         QString offset;
100         if (offsetCoding == Okteta::OffsetFormat::Hexadecimal) {
101             offset = QString(9, QLatin1Char(digits[i]));
102             offset[4] = QLatin1Char(':');
103         } else {
104             offset = QString(10, QLatin1Char(digits[i]));
105         }
106 
107         const QString offsetText = i18n("Offset: %1", offset);
108         const int offsetWidth = metrics.boundingRect(offsetText).width();
109         if (largestOffsetWidth < offsetWidth) {
110             largestOffsetWidth = offsetWidth;
111         }
112 
113         const char countDigit = (i < firstLetterIndex) ? digits[i] : digits[widestDigitIndex];
114         const int maxNumber = QByteArray('1' + QByteArray(9, countDigit)).toInt();
115         const QString bytesCount = i18n("%1 bytes", maxNumber);
116         const QString selectionString = i18nc("@info:status selection: start offset - end offset ()",
117                                               "Selection: %1 - %2 (%3)", offset, offset, bytesCount);
118 
119         const int selectionWidth = metrics.boundingRect(selectionString).width();
120         if (largestSelectionWidth < selectionWidth) {
121             if (i < firstLetterIndex) {
122                 widestDigitIndex = i;
123             }
124             largestSelectionWidth = selectionWidth;
125         }
126     }
127 
128     mOffsetLabel->setFixedWidth(largestOffsetWidth);
129     mSelectionLabel->setFixedWidth(largestSelectionWidth);
130     mStatusBar->updateLayout();
131 }
132 
setTargetModel(AbstractModel * model)133 void ViewStatusController::setTargetModel(AbstractModel* model)
134 {
135     if (mByteArrayView) {
136         mByteArrayView->disconnect(this);
137         mByteArrayView->disconnect(mOverwriteModeToggleButton);
138     }
139 
140     mByteArrayView = model ? model->findBaseModel<ByteArrayView*>() : nullptr;
141 
142     const bool hasView = (mByteArrayView != nullptr);
143     if (hasView) {
144         mStartOffset = mByteArrayView->startOffset();
145 
146         onCursorPositionChanged(mByteArrayView->cursorPosition());
147         onSelectedDataChanged(mByteArrayView->modelSelection());
148         mOverwriteModeToggleButton->setChecked(mByteArrayView->isOverwriteMode());
149         onOffsetCodingChanged(mByteArrayView->offsetCoding());
150         onValueCodingChanged(mByteArrayView->valueCoding());
151         onCharCodecChanged(mByteArrayView->charCodingName());
152 
153         connect(mByteArrayView, &ByteArrayView::cursorPositionChanged, this, &ViewStatusController::onCursorPositionChanged);
154         connect(mByteArrayView, &ByteArrayView::selectedDataChanged,
155                 this, &ViewStatusController::onSelectedDataChanged);
156         connect(mByteArrayView, &ByteArrayView::overwriteModeChanged,
157                 mOverwriteModeToggleButton, &ToggleButton::setChecked);
158         connect(mByteArrayView, &ByteArrayView::offsetCodingChanged, this, &ViewStatusController::onOffsetCodingChanged);
159         connect(mByteArrayView, &ByteArrayView::valueCodingChanged, this, &ViewStatusController::onValueCodingChanged);
160         connect(mByteArrayView, &ByteArrayView::charCodecChanged,
161                 this, &ViewStatusController::onCharCodecChanged);
162     } else {
163         mOffsetLabel->setText(i18nc("@info:status offset value not available", "Offset: -"));
164         mSelectionLabel->setText(i18nc("@info:status offset value not available", "Selection: -"));
165         mOverwriteModeToggleButton->setChecked(false);
166         mValueCodingComboBox->setCurrentIndex(0);
167         mCharCodingComboBox->setCurrentIndex(0);
168     }
169 
170     mOffsetLabel->setEnabled(hasView);
171     mSelectionLabel->setEnabled(hasView);
172     mOverwriteModeToggleButton->setEnabled(hasView);
173     mValueCodingComboBox->setEnabled(hasView);
174     mCharCodingComboBox->setEnabled(hasView);
175 }
176 
setOverwriteMode(bool overwrite)177 void ViewStatusController::setOverwriteMode(bool overwrite)
178 {
179     mByteArrayView->setOverwriteMode(overwrite);
180 }
181 
setValueCoding(int valueCoding)182 void ViewStatusController::setValueCoding(int valueCoding)
183 {
184     mByteArrayView->setValueCoding(valueCoding);
185     mByteArrayView->setFocus();
186 }
187 
setCharCoding(int charCoding)188 void ViewStatusController::setCharCoding(int charCoding)
189 {
190     mByteArrayView->setCharCoding(Okteta::CharCodec::codecNames()[charCoding]);
191     mByteArrayView->setFocus();
192 }
193 
onCursorPositionChanged(Okteta::Address offset)194 void ViewStatusController::onCursorPositionChanged(Okteta::Address offset)
195 {
196     char codedOffset[Okteta::OffsetFormat::MaxFormatWidth + 1];
197 
198     mPrintFunction(codedOffset, mStartOffset + offset);
199 
200     mOffsetLabel->setText(i18n("Offset: %1", QString::fromUtf8(codedOffset)));
201 }
202 
203 // TODO: fix selection by cursor not sending updates
onSelectedDataChanged(const Kasten::AbstractModelSelection * modelSelection)204 void ViewStatusController::onSelectedDataChanged(const Kasten::AbstractModelSelection* modelSelection)
205 {
206     const auto* byteArraySelection = static_cast<const ByteArraySelection*>(modelSelection);
207     const Okteta::AddressRange selection = byteArraySelection->range();
208 
209     QString selectionString;
210     if (!selection.isEmpty()) {
211         char codedSelectionStart[Okteta::OffsetFormat::MaxFormatWidth + 1];
212         char codedSelectionEnd[Okteta::OffsetFormat::MaxFormatWidth + 1];
213 
214         mPrintFunction(codedSelectionStart, mStartOffset + selection.start());
215         mPrintFunction(codedSelectionEnd,   mStartOffset + selection.end());
216 
217         const QString bytesCount = i18np("1 byte", "%1 bytes", selection.width());
218         selectionString = i18nc("@info:status selection: start offset - end offset (number of bytes)",
219                                 "Selection: %1 - %2 (%3)", QString::fromUtf8(codedSelectionStart), QString::fromUtf8(codedSelectionEnd), bytesCount);
220     } else {
221         selectionString = i18nc("@info:status offset value not available", "Selection: -");
222     }
223 
224     mSelectionLabel->setText(selectionString);
225 }
226 
onOffsetCodingChanged(int offsetCoding)227 void ViewStatusController::onOffsetCodingChanged(int offsetCoding)
228 {
229     mPrintFunction = Okteta::OffsetFormat::printFunction((Okteta::OffsetFormat::Format)offsetCoding);
230     fixWidths(offsetCoding);
231 
232     // trigger updates of offset printing labels
233     onCursorPositionChanged(mByteArrayView->cursorPosition());
234     onSelectedDataChanged(mByteArrayView->modelSelection());
235 }
236 
onValueCodingChanged(int valueCoding)237 void ViewStatusController::onValueCodingChanged(int valueCoding)
238 {
239     mValueCodingComboBox->setCurrentIndex(valueCoding);
240 }
241 
onCharCodecChanged(const QString & charCodecName)242 void ViewStatusController::onCharCodecChanged(const QString& charCodecName)
243 {
244     const int charCodingIndex = Okteta::CharCodec::codecNames().indexOf(charCodecName);
245 
246     mCharCodingComboBox->setCurrentIndex(charCodingIndex);
247 }
248 
249 }
250