diff --git a/src/urh/models/LabelValueTableModel.py b/src/urh/models/LabelValueTableModel.py index ef6386dca5..9eaaa94ffa 100644 --- a/src/urh/models/LabelValueTableModel.py +++ b/src/urh/models/LabelValueTableModel.py @@ -10,7 +10,7 @@ class LabelValueTableModel(QAbstractTableModel): - header_labels = ["Name", 'Display format', 'Bit order', 'Value'] + header_labels = ["Name", 'Display format', 'Order [Bit/Byte]', 'Value'] def __init__(self, proto_analyzer: ProtocolAnalyzer, controller, parent=None): super().__init__(parent) @@ -28,7 +28,8 @@ def __display_data(self, lbl: ProtocolLabel, expected_checksum: array = None): lsb = lbl.display_bit_order_index == 1 lsd = lbl.display_bit_order_index == 2 - data = util.convert_bits_to_string(data, lbl.display_format_index, pad_zeros=True, lsb=lsb, lsd=lsd) + data = util.convert_bits_to_string(data, lbl.display_format_index, pad_zeros=True, lsb=lsb, lsd=lsd, + endianness=lbl.display_endianness) if data is None: return None @@ -98,7 +99,7 @@ def data(self, index: QModelIndex, role=Qt.DisplayRole): elif j == 1: return lbl.DISPLAY_FORMATS[lbl.display_format_index] elif j == 2: - return lbl.DISPLAY_BIT_ORDERS[lbl.display_bit_order_index] + return lbl.display_order_str elif j == 3: return self.__display_data(lbl, calculated_crc) @@ -139,7 +140,7 @@ def setData(self, index: QModelIndex, value, role=None): if index.column() == 1: lbl.display_format_index = value elif index.column() == 2: - lbl.display_bit_order_index = value + lbl.display_order_str = value self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount())) diff --git a/src/urh/signalprocessing/ProtocoLabel.py b/src/urh/signalprocessing/ProtocoLabel.py index 9d209f5dc6..a4520f30c7 100644 --- a/src/urh/signalprocessing/ProtocoLabel.py +++ b/src/urh/signalprocessing/ProtocoLabel.py @@ -23,7 +23,7 @@ class ProtocolLabel(object): __slots__ = ("__name", "start", "end", "apply_decoding", "color_index", "show", "__fuzz_me", "fuzz_values", "fuzz_created", "__field_type", "display_format_index", "display_bit_order_index", - "auto_created", "copied") + "display_endianness", "auto_created", "copied") def __init__(self, name: str, start: int, end: int, color_index: int, fuzz_created=False, auto_created=False, field_type: FieldType = None): @@ -44,6 +44,7 @@ def __init__(self, name: str, start: int, end: int, color_index: int, fuzz_creat self.display_format_index = 0 if field_type is None else field_type.display_format_index self.display_bit_order_index = 0 + self.display_endianness = "big" self.auto_created = auto_created @@ -104,6 +105,31 @@ def range_complete_fuzzed(self) -> bool: upper_limit = 2 ** (self.end - self.start) return len(self.fuzz_values) == upper_limit + @property + def display_order_str(self) -> str: + try: + bit_order = self.DISPLAY_BIT_ORDERS[self.display_bit_order_index] + return bit_order + "/{}".format("BE" if self.display_endianness == "big" else "LE") + except IndexError: + return "" + + @display_order_str.setter + def display_order_str(self, value: str): + prefix = value.strip().split("/")[0] + suffix = value.strip().split("/")[-1] + if suffix == "BE": + endianness = "big" + elif suffix == "LE": + endianness = "little" + else: + return + + try: + self.display_bit_order_index = self.DISPLAY_BIT_ORDERS.index(prefix) + self.display_endianness = endianness + except ValueError: + return + def get_copy(self): if self.copied: return self @@ -153,6 +179,7 @@ def to_xml(self) -> ET.Element: "apply_decoding": str(self.apply_decoding), "show": str(self.show), "display_format_index": str(self.display_format_index), "display_bit_order_index": str(self.display_bit_order_index), + "display_endianness": str(self.display_endianness), "fuzz_me": str(self.fuzz_me), "fuzz_values": ",".join(self.fuzz_values), "auto_created": str(self.auto_created)}) @@ -185,5 +212,6 @@ def from_xml(cls, tag: ET.Element, field_types_by_caption=None): # set this after result.field_type because this would change display_format_index to field_types default result.display_format_index = int(tag.get("display_format_index", 0)) result.display_bit_order_index = int(tag.get("display_bit_order_index", 0)) + result.display_endianness = tag.get("display_endianness", "big") return result diff --git a/src/urh/ui/delegates/ComboBoxDelegate.py b/src/urh/ui/delegates/ComboBoxDelegate.py index 20c0b8040d..07b16643b6 100644 --- a/src/urh/ui/delegates/ComboBoxDelegate.py +++ b/src/urh/ui/delegates/ComboBoxDelegate.py @@ -1,6 +1,7 @@ import sys + from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel, pyqtSlot -from PyQt5.QtGui import QImage, QPainter, QColor, QPixmap, QFontMetrics +from PyQt5.QtGui import QImage, QPainter, QColor, QPixmap from PyQt5.QtWidgets import QStyledItemDelegate, QWidget, QStyleOptionViewItem, QComboBox diff --git a/src/urh/ui/delegates/SectionComboBoxDelegate.py b/src/urh/ui/delegates/SectionComboBoxDelegate.py new file mode 100644 index 0000000000..7e1fe8c8bc --- /dev/null +++ b/src/urh/ui/delegates/SectionComboBoxDelegate.py @@ -0,0 +1,88 @@ +import sys +from collections import OrderedDict + +from PyQt5.QtCore import QModelIndex, pyqtSlot, QAbstractItemModel, Qt +from PyQt5.QtGui import QPainter, QStandardItem +from PyQt5.QtWidgets import QItemDelegate, QStyleOptionViewItem, QStyle, QComboBox, QStyledItemDelegate, QWidget + + +class SectionItemDelegate(QItemDelegate): + def __init__(self, parent=None): + super().__init__(parent) + + def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex): + item_type = index.data(Qt.AccessibleDescriptionRole) + if item_type == "parent": + parent_option = option + parent_option.state |= QStyle.State_Enabled + super().paint(painter, parent_option, index) + elif item_type == "child": + child_option = option + indent = option.fontMetrics.width(4 * " ") + child_option.rect.adjust(indent, 0, 0, 0) + child_option.textElideMode = Qt.ElideNone + super().paint(painter, child_option, index) + else: + super().paint(painter, option, index) + + +class SectionComboBox(QComboBox): + def __init__(self, parent=None): + super().__init__(parent) + + def add_parent_item(self, text): + item = QStandardItem(text) + item.setFlags(item.flags() & ~(Qt.ItemIsEnabled | Qt.ItemIsSelectable)) + item.setData("parent", Qt.AccessibleDescriptionRole) + + font = item.font() + font.setBold(True) + item.setFont(font) + + self.model().appendRow(item) + + def add_child_item(self, text): + item = QStandardItem(text) + item.setData("child", Qt.AccessibleDescriptionRole) + self.model().appendRow(item) + + +class SectionComboBoxDelegate(QStyledItemDelegate): + def __init__(self, items: OrderedDict, parent=None): + """ + + :param items: + :param parent: + """ + super().__init__(parent) + self.items = items + + def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex): + editor = SectionComboBox(parent) + editor.setItemDelegate(SectionItemDelegate(editor.itemDelegate().parent())) + if sys.platform == "win32": + # Ensure text entries are visible with windows combo boxes + editor.setMinimumHeight(self.sizeHint(option, index).height() + 10) + + for title, items in self.items.items(): + editor.add_parent_item(title) + for item in items: + editor.add_child_item(item) + editor.currentIndexChanged.connect(self.current_index_changed) + return editor + + def setEditorData(self, editor: SectionComboBox, index: QModelIndex): + editor.blockSignals(True) + item = index.model().data(index) + editor.setCurrentText(item) + editor.blockSignals(False) + + def setModelData(self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex): + model.setData(index, editor.currentText(), Qt.EditRole) + + def updateEditorGeometry(self, editor: QWidget, option: QStyleOptionViewItem, index: QModelIndex): + editor.setGeometry(option.rect) + + @pyqtSlot() + def current_index_changed(self): + self.commitData.emit(self.sender()) diff --git a/src/urh/ui/views/LabelValueTableView.py b/src/urh/ui/views/LabelValueTableView.py index 5208636758..912fa1f60b 100644 --- a/src/urh/ui/views/LabelValueTableView.py +++ b/src/urh/ui/views/LabelValueTableView.py @@ -1,15 +1,22 @@ +from collections import OrderedDict + from PyQt5.QtWidgets import QTableView from urh.models.LabelValueTableModel import LabelValueTableModel from urh.signalprocessing.ProtocoLabel import ProtocolLabel from urh.ui.delegates.ComboBoxDelegate import ComboBoxDelegate +from urh.ui.delegates.SectionComboBoxDelegate import SectionComboBoxDelegate class LabelValueTableView(QTableView): def __init__(self, parent=None): super().__init__(parent) self.setItemDelegateForColumn(1, ComboBoxDelegate(ProtocolLabel.DISPLAY_FORMATS, parent=self)) - self.setItemDelegateForColumn(2, ComboBoxDelegate(ProtocolLabel.DISPLAY_BIT_ORDERS, parent=self)) + + orders = OrderedDict([("Big Endian (BE)", [bo + "/BE" for bo in ProtocolLabel.DISPLAY_BIT_ORDERS]), + ("Little Endian (LE)", [bo + "/LE" for bo in ProtocolLabel.DISPLAY_BIT_ORDERS])]) + + self.setItemDelegateForColumn(2, SectionComboBoxDelegate(orders, parent=self)) self.setEditTriggers(QTableView.AllEditTriggers) def model(self) -> LabelValueTableModel: diff --git a/src/urh/util/util.py b/src/urh/util/util.py index bb3ac8595b..d0f691184d 100644 --- a/src/urh/util/util.py +++ b/src/urh/util/util.py @@ -61,9 +61,10 @@ def get_windows_lib_path(): return dll_dir -def convert_bits_to_string(bits, output_view_type: int, pad_zeros=False, lsb=False, lsd=False): +def convert_bits_to_string(bits, output_view_type: int, pad_zeros=False, lsb=False, lsd=False, endianness="big"): """ Convert bit array to string + :param endianness: Endianness little or big :param bits: Bit array :param output_view_type: Output view type index 0 = bit, 1=hex, 2=ascii, 3=decimal 4=binary coded decimal (bcd) @@ -86,7 +87,11 @@ def convert_bits_to_string(bits, output_view_type: int, pad_zeros=False, lsb=Fal # Reverse bit string bits_str = bits_str[::-1] - if output_view_type == 0: # bt + if endianness == "little": + # reverse byte wise + bits_str = "".join(bits_str[max(i-8, 0):i] for i in range(len(bits_str), 0, -8)) + + if output_view_type == 0: # bit result = bits_str elif output_view_type == 1: # hex diff --git a/tests/.coveragerc b/tests/.coveragerc index ece282d972..84886ae341 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -26,3 +26,7 @@ exclude_lines = def on_new_project_action_triggered def on_label_non_project_mode_link_activated raise NotImplementedError + class SectionItemDelegate + class SectionComboBox + def createEditor + def updateEditorGeometry diff --git a/tests/test_analysis_tab_GUI.py b/tests/test_analysis_tab_GUI.py index d3859ee552..b8562db372 100644 --- a/tests/test_analysis_tab_GUI.py +++ b/tests/test_analysis_tab_GUI.py @@ -328,6 +328,13 @@ def test_label_value_table(self): self.assertIn("display type", model.data(model.index(0, 1), Qt.ToolTipRole)) self.assertIn("bit order", model.data(model.index(0, 2), Qt.ToolTipRole)) + lbl = self.cfc.proto_analyzer.default_message_type[0] + self.assertEqual(lbl.display_endianness, "big") + model.setData(model.index(0, 2), "MSB/LE", role=Qt.EditRole) + self.assertEqual(lbl.display_endianness, "little") + model.setData(model.index(0, 2), "LSB/BE", role=Qt.EditRole) + self.assertEqual(lbl.display_endianness, "big") + def test_label_list_view(self): menus_before = [w for w in QApplication.topLevelWidgets() if isinstance(w, QMenu)]