Sun, 26 Dec 2021 17:40:29 +0100
Added some style sheet and color related TODO markers.
# -*- coding: utf-8 -*- # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing an editor for binary data. """ import math from PyQt6.QtCore import ( pyqtSignal, pyqtSlot, Qt, QByteArray, QTimer, QRect, QBuffer, QIODevice ) from PyQt6.QtGui import ( QBrush, QPen, QColor, QFont, QPalette, QKeySequence, QPainter ) from PyQt6.QtWidgets import QAbstractScrollArea, QApplication from .HexEditChunks import HexEditChunks from .HexEditUndoStack import HexEditUndoStack import Globals class HexEditWidget(QAbstractScrollArea): """ Class implementing an editor for binary data. @signal currentAddressChanged(address) emitted to indicate the new cursor position @signal currentSizeChanged(size) emitted to indicate the new size of the data @signal dataChanged(modified) emitted to indicate a change of the data @signal overwriteModeChanged(state) emitted to indicate a change of the overwrite mode @signal readOnlyChanged(state) emitted to indicate a change of the read only state @signal canRedoChanged(bool) emitted after the redo status has changed @signal canUndoChanged(bool) emitted after the undo status has changed @signal selectionAvailable(bool) emitted to signal a change of the selection """ currentAddressChanged = pyqtSignal(int) currentSizeChanged = pyqtSignal(int) dataChanged = pyqtSignal(bool) overwriteModeChanged = pyqtSignal(bool) readOnlyChanged = pyqtSignal(bool) canRedoChanged = pyqtSignal(bool) canUndoChanged = pyqtSignal(bool) selectionAvailable = pyqtSignal(bool) HEXCHARS_PER_LINE = 47 BYTES_PER_LINE = 16 def __init__(self, parent=None): """ Constructor @param parent refernce to the parent widget @type QWidget """ super().__init__(parent) # Properties self.__addressArea = True # switch the address area on/off self.__addressAreaBrush = QBrush() self.__addressAreaPen = QPen() # background and pen of the address area self.__addressOffset = 0 # offset into the shown address range self.__addressWidth = 4 # address area width in characters self.__asciiArea = True # switch the ASCII area on/off self.__data = bytearray() # contents of the hex editor self.__highlighting = True # switch the highlighting feature on/off self.__highlightingBrush = QBrush() self.__highlightingPen = QPen() # background and pen of highlighted text self.__overwriteMode = True # set overwrite mode on/off self.__selectionBrush = QBrush() self.__selectionPen = QPen() # background and pen of selected text self.__readOnly = False # set read only mode on/off self.__cursorPosition = 0 # absolute position of cursor, 1 Byte == 2 tics self.__addrDigits = 0 self.__addrSeparators = 0 self.__blink = True self.__bData = QBuffer() self.__cursorRect = QRect() self.__cursorRectAscii = QRect() self.__dataShown = bytearray() self.__hexDataShown = bytearray() self.__lastEventSize = 0 self.__markedShown = bytearray() self.__modified = False self.__rowsShown = 0 # pixel related attributes (starting with __px) self.__pxCharWidth = 0 self.__pxCharHeight = 0 self.__pxPosHexX = 0 self.__pxPosAdrX = 0 self.__pxPosAsciiX = 0 self.__pxGapAdr = 0 self.__pxGapAdrHex = 0 self.__pxGapHexAscii = 0 self.__pxSelectionSub = 0 self.__pxCursorWidth = 0 self.__pxCursorX = 0 self.__pxCursorY = 0 # absolute byte position related attributes (starting with __b) self.__bSelectionBegin = 0 self.__bSelectionEnd = 0 self.__bSelectionInit = 0 self.__bPosFirst = 0 self.__bPosLast = 0 self.__bPosCurrent = 0 self.__chunks = HexEditChunks() self.__undoStack = HexEditUndoStack(self.__chunks, self) if Globals.isWindowsPlatform(): self.setFont(QFont(["Courier"], 10)) else: self.setFont(QFont(["Monospace"], 10)) # TODO: make some colors custimizable self.setAddressAreaColors( self.palette().color(QPalette.ColorRole.WindowText), self.palette().alternateBase().color()) self.setHighlightColors( self.palette().color(QPalette.ColorRole.WindowText), QColor(0xff, 0xff, 0x99, 0xff)) self.setSelectionColors( self.palette().highlightedText().color(), self.palette().highlight().color()) self.__cursorTimer = QTimer() self.__cursorTimer.timeout.connect(self.__updateCursor) self.verticalScrollBar().valueChanged.connect(self.__adjust) self.__undoStack.indexChanged.connect(self.__dataChangedPrivate) self.__undoStack.canRedoChanged.connect(self.__canRedoChanged) self.__undoStack.canUndoChanged.connect(self.__canUndoChanged) self.readOnlyChanged.connect(self.__canRedoChanged) self.readOnlyChanged.connect(self.__canUndoChanged) self.__cursorTimer.setInterval(500) self.__cursorTimer.start() self.setAddressWidth(4) self.setAddressArea(True) self.setAsciiArea(True) self.setOverwriteMode(True) self.setHighlighting(True) self.setReadOnly(False) self.__initialize() def undoStack(self): """ Public method to get a reference to the undo stack. @return reference to the undo stack @rtype HexEditUndoStack """ return self.__undoStack @pyqtSlot() def __canRedoChanged(self): """ Private slot handling changes of the Redo state. """ self.canRedoChanged.emit( self.__undoStack.canRedo() and not self.__readOnly) @pyqtSlot() def __canUndoChanged(self): """ Private slot handling changes of the Undo state. """ self.canUndoChanged.emit( self.__undoStack.canUndo() and not self.__readOnly) def addressArea(self): """ Public method to get the address area visibility. @return flag indicating the address area visibility @rtype bool """ return self.__addressArea def setAddressArea(self, on): """ Public method to set the address area visibility. @param on flag indicating the address area visibility @type bool """ self.__addressArea = on self.__adjust() self.setCursorPosition(self.__cursorPosition) self.viewport().update() def addressAreaColors(self): """ Public method to get the address area colors. @return address area foreground and background colors @rtype tuple of 2 QColor """ return self.__addressAreaPen.color(), self.__addressAreaBrush.color() def setAddressAreaColors(self, foreground, background): """ Public method to set the address area colors. @param foreground address area foreground color @type QColor @param background address area background color @type QColor """ self.__addressAreaPen = QPen(foreground) self.__addressAreaBrush = QBrush(background) self.viewport().update() def addressOffset(self): """ Public method to get the address offset. @return address offset @rtype int """ return self.__addressOffset def setAddressOffset(self, offset): """ Public method to set the address offset. @param offset address offset @type int """ self.__addressOffset = offset self.__adjust() self.setCursorPosition(self.__cursorPosition) self.viewport().update() def addressWidth(self): """ Public method to get the width of the address area in characters. Note: The address area width is always a multiple of four. @return minimum width of the address area @rtype int """ size = self.__chunks.size() n = 1 if size > 0x100000000: n += 8 size //= 0x100000000 if size > 0x10000: n += 4 size //= 0x10000 if size > 0x100: n += 2 size //= 0x100 if size > 0x10: n += 1 size //= 0x10 n = int(math.ceil(n / 4)) * 4 if n > self.__addressWidth: return n else: return self.__addressWidth def setAddressWidth(self, width): """ Public method to set the width of the address area. Note: The address area width is always a multiple of four. The given value will be adjusted as required. @param width width of the address area in characters @type int """ self.__addressWidth = int(math.ceil(width / 4)) * 4 self.__adjust() self.setCursorPosition(self.__cursorPosition) self.viewport().update() def asciiArea(self): """ Public method to get the visibility of the ASCII area. @return visibility of the ASCII area @rtype bool """ return self.__asciiArea def setAsciiArea(self, on): """ Public method to set the visibility of the ASCII area. @param on flag indicating the visibility of the ASCII area @type bool """ self.__asciiArea = on self.viewport().update() def cursorPosition(self): """ Public method to get the cursor position. @return cursor position @rtype int """ return self.__cursorPosition def setCursorPosition(self, pos): """ Public method to set the cursor position. @param pos cursor position @type int """ # step 1: delete old cursor self.__blink = False self.viewport().update(self.__cursorRect) if self.__asciiArea: self.viewport().update(self.__cursorRectAscii) # step 2: check, if cursor is in range if self.__overwriteMode and pos > (self.__chunks.size() * 2 - 1): pos = self.__chunks.size() * 2 - 1 if (not self.__overwriteMode) and pos > (self.__chunks.size() * 2): pos = self.__chunks.size() * 2 if pos < 0: pos = 0 # step 3: calculate new position of cursor self.__cursorPosition = pos self.__bPosCurrent = pos // 2 self.__pxCursorY = ( ((pos // 2 - self.__bPosFirst) // self.BYTES_PER_LINE + 1) * self.__pxCharHeight) x = (pos % (2 * self.BYTES_PER_LINE)) self.__pxCursorX = ( (((x // 2) * 3) + (x % 2)) * self.__pxCharWidth + self.__pxPosHexX) self.__setHexCursorRect() # step 4: calculate position of ASCII cursor x = self.__bPosCurrent % self.BYTES_PER_LINE self.__cursorRectAscii = QRect( self.__pxPosAsciiX + x * self.__pxCharWidth - 1, self.__pxCursorY - self.__pxCharHeight + 4, self.__pxCharWidth + 1, self.__pxCharHeight + 1) # step 5: draw new cursors self.__blink = True self.viewport().update(self.__cursorRect) if self.__asciiArea: self.viewport().update(self.__cursorRectAscii) self.currentAddressChanged.emit(self.__bPosCurrent) def __setHexCursorRect(self): """ Private method to set the cursor. """ if self.__overwriteMode: self.__cursorRect = QRect( self.__pxCursorX, self.__pxCursorY + self.__pxCursorWidth, self.__pxCharWidth, self.__pxCursorWidth) else: self.__cursorRect = QRect( self.__pxCursorX, self.__pxCursorY - self.__pxCharHeight + 4, self.__pxCursorWidth, self.__pxCharHeight) def cursorBytePosition(self): """ Public method to get the cursor position in bytes. @return cursor position in bytes @rtype int """ return self.__bPosCurrent def setCursorBytePosition(self, pos): """ Public method to set the cursor position in bytes. @param pos cursor position in bytes @type int """ self.setCursorPosition(pos * 2) def goto(self, offset, fromCursor=False, backwards=False, extendSelection=False): """ Public method to move the cursor. @param offset offset to move to @type int @param fromCursor flag indicating a move relative to the current cursor @type bool @param backwards flag indicating a backwards move @type bool @param extendSelection flag indicating to extend the selection @type bool """ if fromCursor: if backwards: newPos = self.cursorBytePosition() - offset else: newPos = self.cursorBytePosition() + offset else: if backwards: newPos = self.__chunks.size() - offset else: newPos = offset self.setCursorBytePosition(newPos) if extendSelection: self.__setSelection(self.__cursorPosition) else: self.__resetSelection(self.__cursorPosition) self.__refresh() def data(self): """ Public method to get the binary data. @return binary data @rtype bytearray """ return self.__chunks.data(0, -1) def setData(self, dataOrDevice): """ Public method to set the data to show. @param dataOrDevice byte array or device containing the data @type bytearray, QByteArray or QIODevice @return flag indicating success @rtype bool @exception TypeError raised to indicate a wrong parameter type """ if not isinstance(dataOrDevice, (bytearray, QByteArray, QIODevice)): raise TypeError( "setData: parameter must be bytearray, " "QByteArray or QIODevice") if isinstance(dataOrDevice, (bytearray, QByteArray)): self.__data = bytearray(dataOrDevice) self.__bData.setData(self.__data) return self.__setData(self.__bData) else: return self.__setData(dataOrDevice) def __setData(self, ioDevice): """ Private method to set the data to show. @param ioDevice device containing the data @type QIODevice @return flag indicating success @rtype bool """ ok = self.__chunks.setIODevice(ioDevice) self.__initialize() self.__dataChangedPrivate() return ok def highlighting(self): """ Public method to get the highlighting state. @return highlighting state @rtype bool """ return self.__highlighting def setHighlighting(self, on): """ Public method to set the highlighting state. @param on new highlighting state @type bool """ self.__highlighting = on self.viewport().update() def highlightColors(self): """ Public method to get the highlight colors. @return highlight foreground and background colors @rtype tuple of 2 QColor """ return self.__highlightingPen.color(), self.__highlightingBrush.color() def setHighlightColors(self, foreground, background): """ Public method to set the highlight colors. @param foreground highlight foreground color @type QColor @param background highlight background color @type QColor """ self.__highlightingPen = QPen(foreground) self.__highlightingBrush = QBrush(background) self.viewport().update() def overwriteMode(self): """ Public method to get the overwrite mode. @return overwrite mode @rtype bool """ return self.__overwriteMode def setOverwriteMode(self, on): """ Public method to set the overwrite mode. @param on flag indicating the new overwrite mode @type bool """ self.__overwriteMode = on self.overwriteModeChanged.emit(self.__overwriteMode) # step 1: delete old cursor self.__blink = False self.viewport().update(self.__cursorRect) # step 2: change the cursor rectangle self.__setHexCursorRect() # step 3: draw new cursors self.__blink = True self.viewport().update(self.__cursorRect) def selectionColors(self): """ Public method to get the selection colors. @return selection foreground and background colors @rtype tuple of 2 QColor """ return self.__selectionPen.color(), self.__selectionBrush.color() def setSelectionColors(self, foreground, background): """ Public method to set the selection colors. @param foreground selection foreground color @type QColor @param background selection background color @type QColor """ self.__selectionPen = QPen(foreground) self.__selectionBrush = QBrush(background) self.viewport().update() def isReadOnly(self): """ Public method to test the read only state. @return flag indicating the read only state @rtype bool """ return self.__readOnly def setReadOnly(self, on): """ Public method to set the read only state. @param on new read only state @type bool """ self.__readOnly = on self.readOnlyChanged.emit(self.__readOnly) def font(self): """ Public method to get the font used to show the data. @return font used to show the data @rtype QFont """ return super().font() def setFont(self, font): """ Public method to set the font used to show the data. @param font font used to show the data @type QFont """ super().setFont(font) try: self.__pxCharWidth = self.fontMetrics().horizontalAdvance("2") except AttributeError: self.__pxCharWidth = self.fontMetrics().width("2") self.__pxCharHeight = self.fontMetrics().height() self.__pxGapAdr = self.__pxCharWidth // 2 self.__pxGapAdrHex = self.__pxCharWidth self.__pxGapHexAscii = 2 * self.__pxCharWidth self.__pxCursorWidth = self.__pxCharHeight // 7 self.__pxSelectionSub = self.fontMetrics().descent() self.__adjust() self.viewport().update() def dataAt(self, pos, count=-1): """ Public method to get data from a given position. @param pos position to get data from @type int @param count amount of bytes to get @type int @return requested data @rtype bytearray """ return bytearray(self.__chunks.data(pos, count)) def write(self, device, pos=0, count=-1): """ Public method to write data from a given position to a device. @param device device to write to @type QIODevice @param pos position to start the write at @type int @param count amount of bytes to write @type int @return flag indicating success @rtype bool """ return self.__chunks.write(device, pos, count) def insert(self, pos, ch): """ Public method to insert a byte. @param pos position to insert the byte at @type int @param ch byte to insert @type int in the range 0x00 to 0xff """ if ch in range(0, 256): self.__undoStack.insert(pos, ch) self.__refresh() def remove(self, pos, length=1): """ Public method to remove bytes. @param pos position to remove bytes from @type int @param length amount of bytes to remove @type int """ self.__undoStack.removeAt(pos, length) self.__refresh() def replace(self, pos, ch): """ Public method to replace a byte. @param pos position to replace the byte at @type int @param ch byte to replace with @type int in the range 0x00 to 0xff """ if ch in range(0, 256): self.__undoStack.overwrite(pos, ch) self.__refresh() def insertByteArray(self, pos, byteArray): """ Public method to insert bytes. @param pos position to insert the bytes at @type int @param byteArray bytes to be insert @type bytearray or QByteArray """ self.__undoStack.insertByteArray(pos, bytearray(byteArray)) self.__refresh() def replaceByteArray(self, pos, length, byteArray): """ Public method to replace bytes. @param pos position to replace the bytes at @type int @param length amount of bytes to replace @type int @param byteArray bytes to replace with @type bytearray or QByteArray """ self.__undoStack.overwriteByteArray(pos, length, bytearray(byteArray)) self.__refresh() def cursorPositionFromPoint(self, point): """ Public method to calculate a cursor position from a graphics position. @param point graphics position @type QPoint @return cursor position @rtype int """ result = -1 if (point.x() >= self.__pxPosHexX) and ( point.x() < (self.__pxPosHexX + (1 + self.HEXCHARS_PER_LINE) * self.__pxCharWidth)): x = ( (point.x() - self.__pxPosHexX - self.__pxCharWidth // 2) // self.__pxCharWidth ) x = (x // 3) * 2 + x % 3 y = ( ((point.y() - 3) // self.__pxCharHeight) * 2 * self.BYTES_PER_LINE ) result = self.__bPosFirst * 2 + x + y return result def ensureVisible(self): """ Public method to ensure, that the cursor is visible. """ if self.__cursorPosition < 2 * self.__bPosFirst: self.verticalScrollBar().setValue( self.__cursorPosition // 2 // self.BYTES_PER_LINE) if self.__cursorPosition > ( (self.__bPosFirst + (self.__rowsShown - 1) * self.BYTES_PER_LINE) * 2): self.verticalScrollBar().setValue( self.__cursorPosition // 2 // self.BYTES_PER_LINE - self.__rowsShown + 1) self.viewport().update() def indexOf(self, byteArray, start): """ Public method to find the first occurrence of a byte array in our data. @param byteArray data to search for @type bytearray or QByteArray @param start start position of the search @type int @return position of match (or -1 if not found) @rtype int """ byteArray = bytearray(byteArray) pos = self.__chunks.indexOf(byteArray, start) if pos > -1: curPos = pos * 2 self.setCursorPosition(curPos + len(byteArray) * 2) self.__resetSelection(curPos) self.__setSelection(curPos + len(byteArray) * 2) self.ensureVisible() return pos def lastIndexOf(self, byteArray, start): """ Public method to find the last occurrence of a byte array in our data. @param byteArray data to search for @type bytearray or QByteArray @param start start position of the search @type int @return position of match (or -1 if not found) @rtype int """ byteArray = bytearray(byteArray) pos = self.__chunks.lastIndexOf(byteArray, start) if pos > -1: curPos = pos * 2 self.setCursorPosition(curPos - 1) self.__resetSelection(curPos) self.__setSelection(curPos + len(byteArray) * 2) self.ensureVisible() return pos def isModified(self): """ Public method to check for any modification. @return flag indicating a modified state @rtype bool """ return self.__modified def setModified(self, modified, setCleanState=False): """ Public slot to set the modified flag. @param modified flag indicating the new modification status @type bool @param setCleanState flag indicating to set the undo stack to clean @type bool """ self.__modified = modified self.dataChanged.emit(modified) if not modified and setCleanState: self.__undoStack.setClean() def selectionToHexString(self): """ Public method to get a hexadecimal representation of the selection. @return hexadecimal representation of the selection @rtype str """ byteArray = self.__chunks.data(self.getSelectionBegin(), self.getSelectionLength()) return self.__toHex(byteArray).decode(encoding="ascii") def selectionToReadableString(self): """ Public method to get a formatted representation of the selection. @return formatted representation of the selection @rtype str """ byteArray = self.__chunks.data(self.getSelectionBegin(), self.getSelectionLength()) return self.__toReadable(byteArray) def toReadableString(self): """ Public method to get a formatted representation of our data. @return formatted representation of our data @rtype str """ byteArray = self.__chunks.data() return self.__toReadable(byteArray) @pyqtSlot() def redo(self): """ Public slot to redo the last operation. """ self.__undoStack.redo() self.setCursorPosition(self.__chunks.pos() * 2) self.__refresh() @pyqtSlot() def undo(self): """ Public slot to undo the last operation. """ self.__undoStack.undo() self.setCursorPosition(self.__chunks.pos() * 2) self.__refresh() @pyqtSlot() def revertToUnmodified(self): """ Public slot to revert all changes. """ cleanIndex = self.__undoStack.cleanIndex() if cleanIndex >= 0: self.__undoStack.setIndex(cleanIndex) self.setCursorPosition(self.__chunks.pos() * 2) self.__refresh() #################################################### ## Cursor movement commands #################################################### def moveCursorToNextChar(self): """ Public method to move the cursor to the next byte. """ self.setCursorPosition(self.__cursorPosition + 1) self.__resetSelection(self.__cursorPosition) def moveCursorToPreviousChar(self): """ Public method to move the cursor to the previous byte. """ self.setCursorPosition(self.__cursorPosition - 1) self.__resetSelection(self.__cursorPosition) def moveCursorToEndOfLine(self): """ Public method to move the cursor to the end of the current line. """ self.setCursorPosition(self.__cursorPosition | (2 * self.BYTES_PER_LINE - 1)) self.__resetSelection(self.__cursorPosition) def moveCursorToStartOfLine(self): """ Public method to move the cursor to the beginning of the current line. """ self.setCursorPosition( self.__cursorPosition - (self.__cursorPosition % (2 * self.BYTES_PER_LINE))) self.__resetSelection(self.__cursorPosition) def moveCursorToPreviousLine(self): """ Public method to move the cursor to the previous line. """ self.setCursorPosition(self.__cursorPosition - 2 * self.BYTES_PER_LINE) self.__resetSelection(self.__cursorPosition) def moveCursorToNextLine(self): """ Public method to move the cursor to the next line. """ self.setCursorPosition(self.__cursorPosition + 2 * self.BYTES_PER_LINE) self.__resetSelection(self.__cursorPosition) def moveCursorToNextPage(self): """ Public method to move the cursor to the next page. """ self.setCursorPosition( self.__cursorPosition + (self.__rowsShown - 1) * 2 * self.BYTES_PER_LINE) self.__resetSelection(self.__cursorPosition) def moveCursorToPreviousPage(self): """ Public method to move the cursor to the previous page. """ self.setCursorPosition( self.__cursorPosition - (self.__rowsShown - 1) * 2 * self.BYTES_PER_LINE) self.__resetSelection(self.__cursorPosition) def moveCursorToEndOfDocument(self): """ Public method to move the cursor to the end of the data. """ self.setCursorPosition(self.__chunks.size() * 2) self.__resetSelection(self.__cursorPosition) def moveCursorToStartOfDocument(self): """ Public method to move the cursor to the start of the data. """ self.setCursorPosition(0) self.__resetSelection(self.__cursorPosition) #################################################### ## Selection commands #################################################### def deselectAll(self): """ Public method to deselect all data. """ self.__resetSelection(0) self.__refresh() def selectAll(self): """ Public method to select all data. """ self.__resetSelection(0) self.__setSelection(2 * self.__chunks.size() + 1) self.__refresh() def selectNextChar(self): """ Public method to extend the selection by one byte right. """ pos = self.__cursorPosition + 1 self.setCursorPosition(pos) self.__setSelection(pos) def selectPreviousChar(self): """ Public method to extend the selection by one byte left. """ pos = self.__cursorPosition - 1 self.setCursorPosition(pos) self.__setSelection(pos) def selectToEndOfLine(self): """ Public method to extend the selection to the end of line. """ pos = ( self.__cursorPosition - (self.__cursorPosition % (2 * self.BYTES_PER_LINE)) + 2 * self.BYTES_PER_LINE ) self.setCursorPosition(pos) self.__setSelection(pos) def selectToStartOfLine(self): """ Public method to extend the selection to the start of line. """ pos = ( self.__cursorPosition - (self.__cursorPosition % (2 * self.BYTES_PER_LINE)) ) self.setCursorPosition(pos) self.__setSelection(pos) def selectPreviousLine(self): """ Public method to extend the selection one line up. """ pos = self.__cursorPosition - 2 * self.BYTES_PER_LINE self.setCursorPosition(pos) self.__setSelection(pos) def selectNextLine(self): """ Public method to extend the selection one line down. """ pos = self.__cursorPosition + 2 * self.BYTES_PER_LINE self.setCursorPosition(pos) self.__setSelection(pos) def selectNextPage(self): """ Public method to extend the selection one page down. """ pos = ( self.__cursorPosition + ((self.viewport().height() // self.__pxCharHeight) - 1) * 2 * self.BYTES_PER_LINE ) self.setCursorPosition(pos) self.__setSelection(pos) def selectPreviousPage(self): """ Public method to extend the selection one page up. """ pos = ( self.__cursorPosition - ((self.viewport().height() // self.__pxCharHeight) - 1) * 2 * self.BYTES_PER_LINE ) self.setCursorPosition(pos) self.__setSelection(pos) def selectEndOfDocument(self): """ Public method to extend the selection to the end of the data. """ pos = self.__chunks.size() * 2 self.setCursorPosition(pos) self.__setSelection(pos) def selectStartOfDocument(self): """ Public method to extend the selection to the start of the data. """ pos = 0 self.setCursorPosition(pos) self.__setSelection(pos) #################################################### ## Edit commands #################################################### def cut(self): """ Public method to cut the selected bytes and move them to the clipboard. """ if not self.__readOnly: byteArray = self.__toHex(self.__chunks.data( self.getSelectionBegin(), self.getSelectionLength())) idx = 32 while idx < len(byteArray): byteArray.insert(idx, "\n") idx += 33 cb = QApplication.clipboard() cb.setText(byteArray.decode(encoding="latin1")) if self.__overwriteMode: length = self.getSelectionLength() self.replaceByteArray(self.getSelectionBegin(), length, bytearray(length)) else: self.remove(self.getSelectionBegin(), self.getSelectionLength()) self.setCursorPosition(2 * self.getSelectionBegin()) self.__resetSelection(2 * self.getSelectionBegin()) def copy(self): """ Public method to copy the selected bytes to the clipboard. """ byteArray = self.__toHex(self.__chunks.data( self.getSelectionBegin(), self.getSelectionLength())) idx = 32 while idx < len(byteArray): byteArray.insert(idx, "\n") idx += 33 cb = QApplication.clipboard() cb.setText(byteArray.decode(encoding="latin1")) def paste(self): """ Public method to paste bytes from the clipboard. """ if not self.__readOnly: cb = QApplication.clipboard() byteArray = self.__fromHex(cb.text().encode(encoding="latin1")) if self.__overwriteMode: self.replaceByteArray(self.__bPosCurrent, len(byteArray), byteArray) else: self.insertByteArray(self.__bPosCurrent, byteArray) self.setCursorPosition( self.__cursorPosition + 2 * len(byteArray)) self.__resetSelection(2 * self.getSelectionBegin()) def deleteByte(self): """ Public method to delete the current byte. """ if not self.__readOnly: if self.hasSelection(): self.__bPosCurrent = self.getSelectionBegin() if self.__overwriteMode: byteArray = bytearray(self.getSelectionLength()) self.replaceByteArray(self.__bPosCurrent, len(byteArray), byteArray) else: self.remove(self.__bPosCurrent, self.getSelectionLength()) else: if self.__overwriteMode: self.replace(self.__bPosCurrent, 0) else: self.remove(self.__bPosCurrent, 1) self.setCursorPosition(2 * self.__bPosCurrent) self.__resetSelection(2 * self.__bPosCurrent) def deleteByteBack(self): """ Public method to delete the previous byte. """ if not self.__readOnly: if self.hasSelection(): self.__bPosCurrent = self.getSelectionBegin() self.setCursorPosition(2 * self.__bPosCurrent) if self.__overwriteMode: byteArray = bytearray(self.getSelectionLength()) self.replaceByteArray(self.__bPosCurrent, len(byteArray), byteArray) else: self.remove(self.__bPosCurrent, self.getSelectionLength()) else: self.__bPosCurrent -= 1 if self.__overwriteMode: self.replace(self.__bPosCurrent, 0) else: self.remove(self.__bPosCurrent, 1) self.setCursorPosition(2 * self.__bPosCurrent) self.__resetSelection(2 * self.__bPosCurrent) #################################################### ## Event handling methods #################################################### def keyPressEvent(self, evt): """ Protected method to handle key press events. @param evt reference to the key event @type QKeyEvent """ # Cursor movements if evt.matches(QKeySequence.StandardKey.MoveToNextChar): self.moveCursorToNextChar() elif evt.matches(QKeySequence.StandardKey.MoveToPreviousChar): self.moveCursorToPreviousChar() elif evt.matches(QKeySequence.StandardKey.MoveToEndOfLine): self.moveCursorToEndOfLine() elif evt.matches(QKeySequence.StandardKey.MoveToStartOfLine): self.moveCursorToStartOfLine() elif evt.matches(QKeySequence.StandardKey.MoveToPreviousLine): self.moveCursorToPreviousLine() elif evt.matches(QKeySequence.StandardKey.MoveToNextLine): self.moveCursorToNextLine() elif evt.matches(QKeySequence.StandardKey.MoveToNextPage): self.moveCursorToNextPage() elif evt.matches(QKeySequence.StandardKey.MoveToPreviousPage): self.moveCursorToPreviousPage() elif evt.matches(QKeySequence.StandardKey.MoveToEndOfDocument): self.moveCursorToEndOfDocument() elif evt.matches(QKeySequence.StandardKey.MoveToStartOfDocument): self.moveCursorToStartOfDocument() # Selection commands elif evt.matches(QKeySequence.StandardKey.SelectAll): self.selectAll() elif evt.matches(QKeySequence.StandardKey.SelectNextChar): self.selectNextChar() elif evt.matches(QKeySequence.StandardKey.SelectPreviousChar): self.selectPreviousChar() elif evt.matches(QKeySequence.StandardKey.SelectEndOfLine): self.selectToEndOfLine() elif evt.matches(QKeySequence.StandardKey.SelectStartOfLine): self.selectToStartOfLine() elif evt.matches(QKeySequence.StandardKey.SelectPreviousLine): self.selectPreviousLine() elif evt.matches(QKeySequence.StandardKey.SelectNextLine): self.selectNextLine() elif evt.matches(QKeySequence.StandardKey.SelectNextPage): self.selectNextPage() elif evt.matches(QKeySequence.StandardKey.SelectPreviousPage): self.selectPreviousPage() elif evt.matches(QKeySequence.StandardKey.SelectEndOfDocument): self.selectEndOfDocument() elif evt.matches(QKeySequence.StandardKey.SelectStartOfDocument): self.selectStartOfDocument() # Edit commands elif evt.matches(QKeySequence.StandardKey.Copy): self.copy() elif ( evt.key() == Qt.Key.Key_Insert and evt.modifiers() == Qt.KeyboardModifier.NoModifier ): self.setOverwriteMode(not self.overwriteMode()) self.setCursorPosition(self.__cursorPosition) elif not self.__readOnly: if evt.matches(QKeySequence.StandardKey.Cut): self.cut() elif evt.matches(QKeySequence.StandardKey.Paste): self.paste() elif evt.matches(QKeySequence.StandardKey.Delete): self.deleteByte() elif ( evt.key() == Qt.Key.Key_Backspace and evt.modifiers() == Qt.KeyboardModifier.NoModifier ): self.deleteByteBack() elif evt.matches(QKeySequence.StandardKey.Undo): self.undo() elif evt.matches(QKeySequence.StandardKey.Redo): self.redo() elif QApplication.keyboardModifiers() in [ Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.KeypadModifier ]: # some hex input key = evt.text() if key and key in "0123456789abcdef": if self.hasSelection(): if self.__overwriteMode: length = self.getSelectionLength() self.replaceByteArray( self.getSelectionBegin(), length, bytearray(length)) else: self.remove(self.getSelectionBegin(), self.getSelectionLength()) self.__bPosCurrent = self.getSelectionBegin() self.setCursorPosition(2 * self.__bPosCurrent) self.__resetSelection(2 * self.__bPosCurrent) # if in insert mode, insert a byte if ( not self.__overwriteMode and (self.__cursorPosition % 2) == 0 ): self.insert(self.__bPosCurrent, 0) # change content if self.__chunks.size() > 0: hexValue = self.__toHex( self.__chunks.data(self.__bPosCurrent, 1)) if (self.__cursorPosition % 2) == 0: hexValue[0] = ord(key) else: hexValue[1] = ord(key) self.replace(self.__bPosCurrent, self.__fromHex(hexValue)[0]) self.setCursorPosition(self.__cursorPosition + 1) self.__resetSelection(self.__cursorPosition) else: return else: return else: return else: return self.__refresh() def mouseMoveEvent(self, evt): """ Protected method to handle mouse moves. @param evt reference to the mouse event @type QMouseEvent """ self.__blink = False self.viewport().update() actPos = self.cursorPositionFromPoint(evt.position().toPoint()) if actPos >= 0: self.setCursorPosition(actPos) self.__setSelection(actPos) def mousePressEvent(self, evt): """ Protected method to handle mouse button presses. @param evt reference to the mouse event @type QMouseEvent """ self.__blink = False self.viewport().update() cPos = self.cursorPositionFromPoint(evt.position().toPoint()) if cPos >= 0: if evt.modifiers() == Qt.KeyboardModifier.ShiftModifier: self.__setSelection(cPos) else: self.__resetSelection(cPos) self.setCursorPosition(cPos) def paintEvent(self, evt): """ Protected method to handle paint events. @param evt reference to the paint event @type QPaintEvent """ painter = QPainter(self.viewport()) if ( evt.rect() != self.__cursorRect and evt.rect() != self.__cursorRectAscii ): pxOfsX = self.horizontalScrollBar().value() pxPosStartY = self.__pxCharHeight # draw some patterns if needed painter.fillRect( evt.rect(), self.viewport().palette().color(QPalette.ColorRole.Base)) if self.__addressArea: painter.fillRect( QRect(-pxOfsX, evt.rect().top(), self.__pxPosHexX - self.__pxGapAdrHex // 2 - pxOfsX, self.height()), self.__addressAreaBrush) if self.__asciiArea: linePos = self.__pxPosAsciiX - (self.__pxGapHexAscii // 2) painter.setPen(Qt.GlobalColor.gray) painter.drawLine(linePos - pxOfsX, evt.rect().top(), linePos - pxOfsX, self.height()) painter.setPen( self.viewport().palette().color(QPalette.ColorRole.WindowText)) # paint the address area if self.__addressArea: painter.setPen(self.__addressAreaPen) address = "" row = 0 pxPosY = self.__pxCharHeight while row <= len(self.__dataShown) // self.BYTES_PER_LINE: address = "{0:0{1}x}".format( self.__bPosFirst + row * self.BYTES_PER_LINE, self.__addrDigits) address = Globals.strGroup(address, ":", 4) painter.drawText(self.__pxPosAdrX - pxOfsX, pxPosY, address) # increment loop variables row += 1 pxPosY += self.__pxCharHeight # paint hex and ascii area colStandard = QPen( self.viewport().palette().color(QPalette.ColorRole.WindowText)) painter.setBackgroundMode(Qt.BGMode.TransparentMode) row = 0 pxPosY = pxPosStartY while row <= self.__rowsShown: pxPosX = self.__pxPosHexX - pxOfsX pxPosAsciiX2 = self.__pxPosAsciiX - pxOfsX bPosLine = row * self.BYTES_PER_LINE colIdx = 0 while ( bPosLine + colIdx < len(self.__dataShown) and colIdx < self.BYTES_PER_LINE ): c = self.viewport().palette().color( QPalette.ColorRole.Base) painter.setPen(colStandard) posBa = self.__bPosFirst + bPosLine + colIdx if ( self.getSelectionBegin() <= posBa and self.getSelectionEnd() > posBa ): c = self.__selectionBrush.color() painter.setPen(self.__selectionPen) elif ( self.__highlighting and self.__markedShown and self.__markedShown[posBa - self.__bPosFirst] ): c = self.__highlightingBrush.color() painter.setPen(self.__highlightingPen) # render hex value r = QRect() if colIdx == 0: r.setRect( pxPosX, pxPosY - self.__pxCharHeight + self.__pxSelectionSub, 2 * self.__pxCharWidth, self.__pxCharHeight) else: r.setRect( pxPosX - self.__pxCharWidth, pxPosY - self.__pxCharHeight + self.__pxSelectionSub, 3 * self.__pxCharWidth, self.__pxCharHeight) painter.fillRect(r, c) hexStr = ( chr(self.__hexDataShown[(bPosLine + colIdx) * 2]) + chr(self.__hexDataShown[(bPosLine + colIdx) * 2 + 1]) ) painter.drawText(pxPosX, pxPosY, hexStr) pxPosX += 3 * self.__pxCharWidth # render ascii value if self.__asciiArea: by = self.__dataShown[bPosLine + colIdx] if by < 0x20 or (by > 0x7e and by < 0xa0): ch = "." else: ch = chr(by) r.setRect( pxPosAsciiX2, pxPosY - self.__pxCharHeight + self.__pxSelectionSub, self.__pxCharWidth, self.__pxCharHeight) painter.fillRect(r, c) painter.drawText(pxPosAsciiX2, pxPosY, ch) pxPosAsciiX2 += self.__pxCharWidth # increment loop variable colIdx += 1 # increment loop variables row += 1 pxPosY += self.__pxCharHeight painter.setBackgroundMode(Qt.BGMode.TransparentMode) painter.setPen( self.viewport().palette().color(QPalette.ColorRole.WindowText)) # paint cursor if self.__blink and not self.__readOnly and self.isActiveWindow(): painter.fillRect( self.__cursorRect, self.palette().color(QPalette.ColorRole.WindowText)) else: if self.__hexDataShown: try: c = chr(self.__hexDataShown[ self.__cursorPosition - self.__bPosFirst * 2]) except IndexError: c = "" else: c = "" painter.drawText(self.__pxCursorX, self.__pxCursorY, c) if self.__asciiArea: painter.drawRect(self.__cursorRectAscii) # emit event, if size has changed if self.__lastEventSize != self.__chunks.size(): self.__lastEventSize = self.__chunks.size() self.currentSizeChanged.emit(self.__lastEventSize) def resizeEvent(self, evt): """ Protected method to handle resize events. @param evt reference to the resize event @type QResizeEvent """ self.__adjust() def __resetSelection(self, pos=None): """ Private method to reset the selection. @param pos position to set selection start and end to (if this is None, selection end is set to selection start) @type int or None """ if pos is None: self.__bSelectionBegin = self.__bSelectionInit self.__bSelectionEnd = self.__bSelectionInit else: if pos < 0: pos = 0 pos //= 2 self.__bSelectionInit = pos self.__bSelectionBegin = pos self.__bSelectionEnd = pos self.selectionAvailable.emit(False) def __setSelection(self, pos): """ Private method to set the selection. @param pos position @type int """ if pos < 0: pos = 0 pos //= 2 if pos >= self.__bSelectionInit: self.__bSelectionEnd = pos self.__bSelectionBegin = self.__bSelectionInit else: self.__bSelectionBegin = pos self.__bSelectionEnd = self.__bSelectionInit self.selectionAvailable.emit(True) def getSelectionBegin(self): """ Public method to get the start of the selection. @return selection start @rtype int """ return self.__bSelectionBegin def getSelectionEnd(self): """ Public method to get the end of the selection. @return selection end @rtype int """ return self.__bSelectionEnd def getSelectionLength(self): """ Public method to get the length of the selection. @return selection length @rtype int """ return self.__bSelectionEnd - self.__bSelectionBegin def hasSelection(self): """ Public method to test for a selection. @return flag indicating the presence of a selection @rtype bool """ return self.__bSelectionBegin != self.__bSelectionEnd def __initialize(self): """ Private method to do some initialization. """ self.__undoStack.clear() self.setAddressOffset(0) self.__resetSelection(0) self.setCursorPosition(0) self.verticalScrollBar().setValue(0) self.__modified = False def __readBuffers(self): """ Private method to read the buffers. """ self.__dataShown = self.__chunks.data( self.__bPosFirst, self.__bPosLast - self.__bPosFirst + self.BYTES_PER_LINE + 1, self.__markedShown ) self.__hexDataShown = self.__toHex(self.__dataShown) def __toHex(self, byteArray): """ Private method to convert the data of a Python bytearray to hex. @param byteArray byte array to be converted @type bytearray @return converted data @rtype bytearray """ return bytearray(QByteArray(byteArray).toHex()) def __fromHex(self, byteArray): """ Private method to convert data of a Python bytearray from hex. @param byteArray byte array to be converted @type bytearray @return converted data @rtype bytearray """ return bytearray(QByteArray.fromHex(byteArray)) def __toReadable(self, byteArray): """ Private method to convert some data into a readable format. @param byteArray data to be converted @type bytearray or QByteArray @return readable data @rtype str """ byteArray = bytearray(byteArray) result = "" for i in range(0, len(byteArray), 16): addrStr = "{0:0{1}x}".format(self.__addressOffset + i, self.addressWidth()) hexStr = "" ascStr = "" for j in range(16): if (i + j) < len(byteArray): hexStr += " {0:02x}".format(byteArray[i + j]) by = byteArray[i + j] if by < 0x20 or (by > 0x7e and by < 0xa0): ch = "." else: ch = chr(by) ascStr += ch result += "{0} {1:<48} {2:<17}\n".format(addrStr, hexStr, ascStr) return result @pyqtSlot() def __adjust(self): """ Private slot to recalculate pixel positions. """ # recalculate graphics if self.__addressArea: self.__addrDigits = self.addressWidth() self.__addrSeparators = self.__addrDigits // 4 - 1 self.__pxPosHexX = ( self.__pxGapAdr + (self.__addrDigits + self.__addrSeparators) * self.__pxCharWidth + self.__pxGapAdrHex) else: self.__pxPosHexX = self.__pxGapAdrHex self.__pxPosAdrX = self.__pxGapAdr self.__pxPosAsciiX = ( self.__pxPosHexX + self.HEXCHARS_PER_LINE * self.__pxCharWidth + self.__pxGapHexAscii ) # set horizontal scrollbar pxWidth = self.__pxPosAsciiX if self.__asciiArea: pxWidth += self.BYTES_PER_LINE * self.__pxCharWidth self.horizontalScrollBar().setRange( 0, pxWidth - self.viewport().width()) self.horizontalScrollBar().setPageStep(self.viewport().width()) # set vertical scrollbar self.__rowsShown = ( (self.viewport().height() - 4) // self.__pxCharHeight ) lineCount = (self.__chunks.size() // self.BYTES_PER_LINE) + 1 self.verticalScrollBar().setRange(0, lineCount - self.__rowsShown) self.verticalScrollBar().setPageStep(self.__rowsShown) # do the rest value = self.verticalScrollBar().value() self.__bPosFirst = value * self.BYTES_PER_LINE self.__bPosLast = ( self.__bPosFirst + self.__rowsShown * self.BYTES_PER_LINE - 1 ) if self.__bPosLast >= self.__chunks.size(): self.__bPosLast = self.__chunks.size() - 1 self.__readBuffers() self.setCursorPosition(self.__cursorPosition) @pyqtSlot(int) def __dataChangedPrivate(self, idx=0): """ Private slot to handle data changes. @param idx index @type int """ self.__modified = ( self.__undoStack.cleanIndex() == -1 or self.__undoStack.index() != self.__undoStack.cleanIndex()) self.__adjust() self.dataChanged.emit(self.__modified) @pyqtSlot() def __refresh(self): """ Private slot to refresh the display. """ self.ensureVisible() self.__readBuffers() @pyqtSlot() def __updateCursor(self): """ Private slot to update the blinking cursor. """ self.__blink = not self.__blink self.viewport().update(self.__cursorRect)