Sat, 09 Jan 2016 19:04:34 +0100
First commit for the new hex editor tool.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HexEdit/HexEditChunks.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the storage backend for the hex editor. +""" + +from __future__ import unicode_literals + +import sys + +from PyQt5.QtCore import QBuffer, QIODevice, QByteArray + + +class HexEditChunk(object): + """ + Class implementing a container for the data chunks. + """ + def __init__(self): + """ + Constructor + """ + self.data = bytearray() + self.dataChanged = bytearray() + self.absPos = 0 + + +class HexEditChunks(object): + """ + Class implementing the storage backend for the hex editor. + + When HexEditWidget loads data, HexEditChunks access them using a QIODevice + interface. When the app uses a QByteArray or Python bytearray interface, + QBuffer is used to provide again a QIODevice like interface. No data will + be changed, therefore HexEditChunks opens the QIODevice in + QIODevice.ReadOnly mode. After every access HexEditChunks closes the + QIODevice. That's why external applications can overwrite files while + HexEditWidget shows them. + + When the the user starts to edit the data, HexEditChunks creates a local + copy of a chunk of data (4 kilobytes) and notes all changes there. Parallel + to that chunk, there is a second chunk, which keeps track of which bytes + are changed and which are not. + """ + BUFFER_SIZE = 0x10000 + CHUNK_SIZE = 0x1000 + READ_CHUNK_MASK = 0xfffffffffffff000 + + def __init__(self, ioDevice=None): + """ + Constructor + + @param ioDevice io device to get the data from + @type QIODevice + """ + self.__ioDevice = None + self.__pos = 0 + self.__size = 0 + self.__chunks = [] + + if ioDevice is None: + buf = QBuffer() + self.setIODevice(buf) + else: + self.setIODevice(ioDevice) + + def setIODevice(self, ioDevice): + """ + Public method to set an io device to read the binary data from. + + @param ioDevice io device to get the data from + @type QIODevice + @return flag indicating successful operation + @rtype bool + """ + self.__ioDevice = ioDevice + ok = self.__ioDevice.open(QIODevice.ReadOnly) + if ok: + # open successfully + self.__size = self.__ioDevice.size() + self.__ioDevice.close() + else: + # fallback is an empty buffer + self.__ioDevice = QBuffer() + self.__size = 0 + + self.__chunks = [] + self.__pos = 0 + + return ok + + def data(self, pos=0, maxSize=-1, highlighted=None): + """ + Public method to get data out of the chunks. + + @param pos position to get bytes from + @type int + @param maxSize maximum amount of bytes to get + @type int + @param highlighted reference to a byte array storing highlighting info + @byte bytearray + @return retrieved data + @rtype bytearray + """ + ioDelta = 0 + chunkIdx = 0 + + chunk = HexEditChunk() + buffer = bytearray() + + if highlighted is not None: + del highlighted[:] + + if pos >= self.__size: + return buffer + + if maxSize < 0: + maxSize = self.__size + elif (pos + maxSize) > self.__size: + maxSize = self.__size - pos + + self.__ioDevice.open(QIODevice.ReadOnly) + + while maxSize > 0: + chunk.absPos = sys.maxsize + chunksLoopOngoing = True + while chunkIdx < len(self.__chunks) and chunksLoopOngoing: + # In this section, we track changes before our required data + # and we take the edited data, if availible. ioDelta is a + # difference counter to justify the read pointer to the + # original data, if data in between was deleted or inserted. + + chunk = self.__chunks[chunkIdx] + if chunk.absPos > pos: + chunksLoopOngoing = False + else: + chunkIdx += 1 + chunkOfs = pos - chunk.absPos + if maxSize > (len(chunk.data) - chunkOfs): + count = len(chunk.data) - chunkOfs + ioDelta += self.CHUNK_SIZE - len(chunk.data) + else: + count = maxSize + if count > 0: + buffer += chunk.data[chunkOfs:chunkOfs + count] + maxSize -= count + pos += count + if highlighted is not None: + highlighted += \ + chunk.dataChanged[chunkOfs:chunkOfs + count] + + if maxSize > 0 and pos < chunk.absPos: + # In this section, we read data from the original source. This + # will only happen, when no copied data is available. + if chunk.absPos - pos > maxSize: + byteCount = maxSize + else: + byteCount = chunk.absPos - pos + + maxSize -= byteCount + self.__ioDevice.seek(pos + ioDelta) + readBuffer = bytearray(self.__ioDevice.read(byteCount)) + buffer += readBuffer + if highlighted is not None: + highlighted += bytearray(len(readBuffer)) + pos += len(readBuffer) + + self.__ioDevice.close() + return buffer + + def write(self, ioDevice, pos=0, count=-1): + """ + Public method to write data to an io device. + + @param ioDevice io device to write the data to + @type QIODevice + @param pos position to write bytes from + @type int + @param count amount of bytes to write + @type int + @return flag indicating success + @rtype bool + """ + if count == -1: + # write all data + count = self.__size + + ok = ioDevice.open(QIODevice.WriteOnly) + if ok: + idx = pos + while idx < count: + data = self.data(idx, self.BUFFER_SIZE) + ioDevice.write(QByteArray(data)) + + # increment loop variable + idx += self.BUFFER_SIZE + + ioDevice.close() + + return ok + + def setDataChanged(self, pos, dataChanged): + """ + Public method to set highlighting info. + + @param pos position to set highlighting info for + @type int + @param dataChanged flag indicating changed data + @type bool + """ + if pos < 0 or pos >= self.__size: + # position is out of range, do nothing + return + chunkIdx = self.__getChunkIndex(pos) + posInChunk = pos - self.__chunks[chunkIdx].absPos + self.__chunks[chunkIdx].dataChanged[posInChunk] = int(dataChanged) + + def dataChanged(self, pos): + """ + Public method to test, if some data was changed. + + @param pos byte position to check + @type int + @return flag indicating the changed state + @rtype bool + """ + highlighted = bytearray() + self.data(pos, 1, highlighted) + return highlighted and bool(highlighted[0]) + + def indexOf(self, byteArray, start): + """ + Public method to search the first occurrence of some data. + + @param byteArray data to search for + @type bytearray + @param start position to start the search at + @type int + @return position the data was found at or -1 if nothing could be found + @rtype int + """ + ba = bytearray(byteArray) + + result = -1 + pos = start + while pos < self.__size: + buffer = self.data(pos, self.BUFFER_SIZE + len(ba) - 1) + findPos = buffer.find(ba) + if findPos >= 0: + result = pos + findPos + break + + # increment loop variable + pos += self.BUFFER_SIZE + + return result + + def lastIndexOf(self, byteArray, start): + """ + Public method to search the last occurrence of some data. + + @param byteArray data to search for + @type bytearray + @param start position to start the search at + @type int + @return position the data was found at or -1 if nothing could be found + @rtype int + """ + ba = bytearray(byteArray) + + result = -1 + pos = start + while pos > 0 and result < 0: + sPos = pos - self.BUFFER_SIZE - len(ba) + 1 + if sPos < 0: + sPos = 0 + + buffer = self.data(sPos, pos - sPos) + findPos = buffer.rfind(ba) + if findPos >= 0: + result = sPos + findPos + break + + # increment loop variable + pos -= self.BUFFER_SIZE + + return result + + def insert(self, pos, data): + """ + Public method to insert a byte. + + @param pos position to insert at + @type int + @param data byte to insert + @type int (range 0 to 255) + @return flag indicating success + @rtype bool + """ + if pos < 0 or pos > self.__size: + # position is out of range, do nothing + return False + + if pos == self.__size: + chunkIdx = self.__getChunkIndex(pos - 1) + else: + chunkIdx = self.__getChunkIndex(pos) + chunk = self.__chunks[chunkIdx] + posInChunk = pos - chunk.absPos + chunk.data.insert(posInChunk, data) + chunk.dataChanged.insert(posInChunk, 1) + for idx in range(chunkIdx + 1, len(self.__chunks)): + self.__chunks[idx].absPos += 1 + self.__size += 1 + self.__pos = pos + return True + + def overwrite(self, pos, data): + """ + Public method to overwrite a byte. + + @param pos position to overwrite + @type int + @param data byte to overwrite with + @type int (range 0 to 255) + @return flag indicating success + @rtype bool + """ + if pos < 0 or pos >= self.__size: + # position is out of range, do nothing + return False + + chunkIdx = self.__getChunkIndex(pos) + chunk = self.__chunks[chunkIdx] + posInChunk = pos - chunk.absPos + chunk.data[posInChunk] = data + chunk.dataChanged[posInChunk] = 1 + self.__pos = pos + return True + + def removeAt(self, pos): + """ + Public method to remove a byte. + + @param pos position to remove + @type int + @return flag indicating success + @rtype bool + """ + if pos < 0 or pos >= self.__size: + # position is out of range, do nothing + return + + chunkIdx = self.__getChunkIndex(pos) + chunk = self.__chunks[chunkIdx] + posInChunk = pos - chunk.absPos + chunk.data.pop(posInChunk) + chunk.dataChanged.pop(posInChunk) + for idx in range(chunkIdx + 1, len(self.__chunks)): + self.__chunks[idx].absPos -= 1 + self.__size -= 1 + self.__pos = pos + return True + + def __getitem__(self, pos): + """ + Special method to get a byte at a position. + + Note: This realizes the [] get operator. + + @param pos position of byte to get + @type int + @return requested byte + @rtype int (range 0 to 255) + """ + if pos >= self.__size: + return 0 +## raise IndexError + + return self.data(pos, 1)[0] + + def pos(self): + """ + Public method to get the current position. + + @return current position + @rtype int + """ + return self.__pos + + def size(self): + """ + Public method to get the current data size. + + @return current data size + @rtype int + """ + return self.__size + + def __getChunkIndex(self, absPos): + """ + Private method to get the chunk index for a position. + + This method checks, if there is already a copied chunk available. If + there is one, it returns its index. If there is no copied chunk + available, original data will be copied into a new chunk. + + @param absPos absolute position of the data. + @type int + @return index of the chunk containing the position + @rtype int + """ + foundIdx = -1 + insertIdx = 0 + ioDelta = 0 + + for idx in range(len(self.__chunks)): + chunk = self.__chunks[idx] + if absPos >= chunk.absPos and \ + absPos < (chunk.absPos + len(chunk.data)): + foundIdx = idx + break + + if absPos < chunk.absPos: + insertIdx = idx + break + + ioDelta += len(chunk.data) - self.CHUNK_SIZE + insertIdx = idx + 1 + + if foundIdx == -1: + newChunk = HexEditChunk() + readAbsPos = absPos - ioDelta + readPos = readAbsPos & self.READ_CHUNK_MASK + self.__ioDevice.open(QIODevice.ReadOnly) + self.__ioDevice.seek(readPos) + newChunk.data = bytearray(self.__ioDevice.read(self.CHUNK_SIZE)) + self.__ioDevice.close() + newChunk.absPos = absPos - (readAbsPos - readPos) + newChunk.dataChanged = bytearray(len(newChunk.data)) + self.__chunks.insert(insertIdx, newChunk) + foundIdx = insertIdx + + return foundIdx
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HexEdit/HexEditMainWindow.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,1017 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the hex editor main window. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFile, QFileInfo, QSize +from PyQt5.QtGui import QKeySequence +from PyQt5.QtWidgets import QWhatsThis, QLabel + +from E5Gui.E5Action import E5Action +from E5Gui.E5MainWindow import E5MainWindow +from E5Gui import E5FileDialog, E5MessageBox + +from .HexEditWidget import HexEditWidget + +import UI.PixmapCache +import UI.Config + +import Preferences + + +# TODO: implement a configuration page for the hex editor +# TODO: implement an action in the eric IDE to open a hex editor +class HexEditMainWindow(E5MainWindow): + """ + Class implementing the web browser main window. + + @signal editorClosed() emitted after the window was requested to close down + """ + editorClosed = pyqtSignal() + + windows = [] + + def __init__(self, fileName="", parent=None, fromEric=False, project=None): + """ + Constructor + + @param fileName name of a file to load on startup (string) + @param parent parent widget of this window (QWidget) + @keyparam fromEric flag indicating whether it was called from within + eric6 (boolean) + @keyparam project reference to the project object (Project) + """ + super(HexEditMainWindow, self).__init__(parent) + self.setObjectName("eric6_hex_editor") + + self.fromEric = fromEric + self.setWindowIcon(UI.PixmapCache.getIcon("hexEditor.png")) + + if not self.fromEric: + self.setStyle(Preferences.getUI("Style"), + Preferences.getUI("StyleSheet")) + + self.__editor = HexEditWidget() + self.setCentralWidget(self.__editor) + + g = Preferences.getGeometry("HexEditorGeometry") + if g.isEmpty(): + s = QSize(600, 500) + self.resize(s) + else: + self.restoreGeometry(g) + + self.__initActions() + self.__initMenus() + self.__initToolbars() + self.__createStatusBar() + + self.__class__.windows.append(self) + + self.__editor.currentAddressChanged.connect(self.__showAddress) + self.__editor.currentSizeChanged.connect(self.__showSize) + self.__editor.dataChanged.connect(self.__modificationChanged) + self.__editor.overwriteModeChanged.connect(self.__showEditMode) + self.__editor.readOnlyChanged.connect(self.__showReadOnlyMode) + + self.__project = project + self.__lastOpenPath = "" + self.__lastSavePath = "" + + self.__setCurrentFile("") + if fileName: + self.__loadHexFile(fileName) + + self.__checkActions() + + def __initActions(self): + """ + Private method to define the user interface actions. + """ + # list of all actions + self.__actions = [] + + self.__initFileActions() + self.__initEditActions() +## self.__initViewActions() +## self.__initToolsActions() + self.__initHelpActions() + + def __initFileActions(self): + """ + Private method to define the file related user interface actions. + """ + self.newWindowAct = E5Action( + self.tr('New Window'), + UI.PixmapCache.getIcon("newWindow.png"), + self.tr('New &Window'), + 0, 0, self, 'hexEditor_file_new_window') + self.newWindowAct.setStatusTip(self.tr( + 'Open a binary file for editing in a new hex editor window')) + self.newWindowAct.setWhatsThis(self.tr( + """<b>New Window</b>""" + """<p>This opens a binary file for editing in a new hex editor""" + """ window.</p>""" + )) + self.newWindowAct.triggered.connect(self.__openHexFileNewWindow) + self.__actions.append(self.newWindowAct) + + self.openAct = E5Action( + self.tr('Open'), + UI.PixmapCache.getIcon("open.png"), + self.tr('&Open...'), + QKeySequence(self.tr("Ctrl+O", "File|Open")), + 0, self, 'hexEditor_file_open') + self.openAct.setStatusTip(self.tr('Open a binary file for editing')) + self.openAct.setWhatsThis(self.tr( + """<b>Open File</b>""" + """<p>This opens a binary file for editing.""" + """ It pops up a file selection dialog.</p>""" + )) + self.openAct.triggered.connect(self.__openHexFile) + self.__actions.append(self.openAct) + + self.openReadOnlyAct = E5Action( + self.tr('Open Read Only'), + self.tr('Open Read Only...'), + 0, 0, self, 'hexEditor_file_open_read_only') + self.openReadOnlyAct.setStatusTip(self.tr( + 'Open a binary file for viewing')) + self.openReadOnlyAct.setWhatsThis(self.tr( + """<b>Open Read Only</b>""" + """<p>This opens a binary file for viewing (i.e. in read only""" + """ mode). It pops up a file selection dialog.</p>""" + )) + self.openReadOnlyAct.triggered.connect(self.__openHexFileReadOnly) + self.__actions.append(self.openReadOnlyAct) + + self.saveAct = E5Action( + self.tr('Save'), + UI.PixmapCache.getIcon("fileSave.png"), + self.tr('&Save'), + QKeySequence(self.tr("Ctrl+S", "File|Save")), + 0, self, 'hexEditor_file_save') + self.saveAct.setStatusTip(self.tr('Save the current binary file')) + self.saveAct.setWhatsThis(self.tr( + """<b>Save File</b>""" + """<p>Save the contents of the hex editor window.</p>""" + )) + self.saveAct.triggered.connect(self.__saveHexFile) + self.__actions.append(self.saveAct) + + self.saveAsAct = E5Action( + self.tr('Save As'), + UI.PixmapCache.getIcon("fileSaveAs.png"), + self.tr('Save &As...'), + QKeySequence(self.tr("Shift+Ctrl+S", "File|Save As")), + 0, self, 'hexEditor_file_save_as') + self.saveAsAct.setStatusTip( + self.tr('Save the current binary data to a new file')) + self.saveAsAct.setWhatsThis(self.tr( + """<b>Save As...</b>""" + """<p>Saves the current binary data to a new file.</p>""" + )) + self.saveAsAct.triggered.connect(self.__saveHexFileAs) + self.__actions.append(self.saveAsAct) + + self.saveReadableAct = E5Action( + self.tr('Save As Readable'), + self.tr('Save As &Readable...'), + 0, 0, self, 'hexEditor_file_save_readable') + self.saveReadableAct.setStatusTip( + self.tr('Save the current binary data to a new file in a readable' + ' format')) + self.saveReadableAct.setWhatsThis(self.tr( + """<b>Save As Readable...</b>""" + """<p>Saves the current binary data to a new file in a readable""" + """ format.</p>""" + )) + self.saveReadableAct.triggered.connect(self.__saveHexFileReadable) + self.__actions.append(self.saveReadableAct) + + self.closeAct = E5Action( + self.tr('Close'), + UI.PixmapCache.getIcon("close.png"), + self.tr('&Close'), + QKeySequence(self.tr("Ctrl+W", "File|Close")), + 0, self, 'hexEditor_file_close') + self.closeAct.setStatusTip(self.tr( + 'Close the current hex editor window')) + self.closeAct.setWhatsThis(self.tr( + """<b>Close</b>""" + """<p>Closes the current hex editor window.</p>""" + )) + self.closeAct.triggered.connect(self.close) + self.__actions.append(self.closeAct) + + self.closeAllAct = E5Action( + self.tr('Close All'), + self.tr('Close &All'), + 0, 0, self, 'hexEditor_file_close_all') + self.closeAllAct.setStatusTip(self.tr( + 'Close all hex editor windows')) + self.closeAllAct.setWhatsThis(self.tr( + """<b>Close All</b>""" + """<p>Closes all hex editor windows.</p>""" + )) + self.closeAllAct.triggered.connect(self.__closeAll) + self.__actions.append(self.closeAllAct) + + self.closeOthersAct = E5Action( + self.tr('Close Others'), + self.tr('Close Others'), + 0, 0, self, 'hexEditor_file_close_others') + self.closeOthersAct.setStatusTip(self.tr( + 'Close all hex other editor windows')) + self.closeOthersAct.setWhatsThis(self.tr( + """<b>Close Others</b>""" + """<p>Closes all other hex editor windows.</p>""" + )) + self.closeOthersAct.triggered.connect(self.__closeOthers) + self.__actions.append(self.closeOthersAct) + + self.exitAct = E5Action( + self.tr('Quit'), + UI.PixmapCache.getIcon("exit.png"), + self.tr('&Quit'), + QKeySequence(self.tr("Ctrl+Q", "File|Quit")), + 0, self, 'hexEditor_file_quit') + self.exitAct.setStatusTip(self.tr('Quit the hex editor')) + self.exitAct.setWhatsThis(self.tr( + """<b>Quit</b>""" + """<p>Quit the hex editor.</p>""" + )) + if not self.fromEric: + self.exitAct.triggered.connect(self.__closeAll) + self.__actions.append(self.exitAct) + + def __initEditActions(self): + """ + Private method to create the Edit actions. + """ + self.undoAct = E5Action( + self.tr('Undo'), + UI.PixmapCache.getIcon("editUndo.png"), + self.tr('&Undo'), + QKeySequence(self.tr("Ctrl+Z", "Edit|Undo")), + QKeySequence(self.tr("Alt+Backspace", "Edit|Undo")), + self, 'hexEditor_edit_undo') + self.undoAct.setStatusTip(self.tr('Undo the last change')) + self.undoAct.setWhatsThis(self.tr( + """<b>Undo</b>""" + """<p>Undo the last change done.</p>""" + )) + self.undoAct.triggered.connect(self.__editor.undo) + self.__actions.append(self.undoAct) + + self.redoAct = E5Action( + self.tr('Redo'), + UI.PixmapCache.getIcon("editRedo.png"), + self.tr('&Redo'), + QKeySequence(self.tr("Ctrl+Shift+Z", "Edit|Redo")), + 0, self, 'hexEditor_edit_redo') + self.redoAct.setStatusTip(self.tr('Redo the last change')) + self.redoAct.setWhatsThis(self.tr( + """<b>Redo</b>""" + """<p>Redo the last change done.</p>""" + )) + self.redoAct.triggered.connect(self.__editor.redo) + self.__actions.append(self.redoAct) + + self.revertAct = E5Action( + self.tr('Revert to last saved state'), + self.tr('Re&vert to last saved state'), + QKeySequence(self.tr("Ctrl+Y", "Edit|Revert")), + 0, + self, 'hexEditor_edit_revert') + self.revertAct.setStatusTip(self.tr('Revert to last saved state')) + self.revertAct.setWhatsThis(self.tr( + """<b>Revert to last saved state</b>""" + """<p>Undo all changes up to the last saved state of the""" + """ editor.</p>""" + )) + self.revertAct.triggered.connect(self.__editor.revertToUnmodified) + self.__actions.append(self.revertAct) + + self.cutAct = E5Action( + self.tr('Cut'), + UI.PixmapCache.getIcon("editCut.png"), + self.tr('Cu&t'), + QKeySequence(self.tr("Ctrl+X", "Edit|Cut")), + QKeySequence(self.tr("Shift+Del", "Edit|Cut")), + self, 'hexEditor_edit_cut') + self.cutAct.setStatusTip(self.tr('Cut the selection')) + self.cutAct.setWhatsThis(self.tr( + """<b>Cut</b>""" + """<p>Cut the selected binary area to the clipboard.</p>""" + )) + self.cutAct.triggered.connect(self.__editor.cut) + self.__actions.append(self.cutAct) + + self.copyAct = E5Action( + self.tr('Copy'), + UI.PixmapCache.getIcon("editCopy.png"), + self.tr('&Copy'), + QKeySequence(self.tr("Ctrl+C", "Edit|Copy")), + QKeySequence(self.tr("Ctrl+Ins", "Edit|Copy")), + self, 'hexEditor_edit_copy') + self.copyAct.setStatusTip(self.tr('Copy the selection')) + self.copyAct.setWhatsThis(self.tr( + """<b>Copy</b>""" + """<p>Copy the selected binary area to the clipboard.</p>""" + )) + self.copyAct.triggered.connect(self.__editor.copy) + self.__actions.append(self.copyAct) + + self.pasteAct = E5Action( + self.tr('Paste'), + UI.PixmapCache.getIcon("editPaste.png"), + self.tr('&Paste'), + QKeySequence(self.tr("Ctrl+V", "Edit|Paste")), + QKeySequence(self.tr("Shift+Ins", "Edit|Paste")), + self, 'hexEditor_edit_paste') + self.pasteAct.setStatusTip(self.tr('Paste the clipboard contents')) + self.pasteAct.setWhatsThis(self.tr( + """<b>Paste</b>""" + """<p>Paste the clipboard contents.</p>""" + )) + self.pasteAct.triggered.connect(self.__editor.paste) + self.__actions.append(self.pasteAct) + + self.selectAllAct = E5Action( + self.tr('Select All'), + UI.PixmapCache.getIcon("editSelectAll.png"), + self.tr('&Select All'), + QKeySequence(self.tr("Ctrl+A", "Edit|Select All")), + 0, + self, 'hexEditor_edit_select_all') + self.selectAllAct.setStatusTip(self.tr( + 'Select the complete binary data')) + self.selectAllAct.setWhatsThis(self.tr( + """<b>Select All</b>""" + """<p>Selects the complete binary data.</p>""" + )) + self.selectAllAct.triggered.connect(self.__editor.selectAll) + self.__actions.append(self.selectAllAct) + + self.deselectAllAct = E5Action( + self.tr('Deselect all'), + self.tr('&Deselect all'), + QKeySequence(self.tr("Alt+Ctrl+A", "Edit|Deselect all")), + 0, + self, 'hexEditor_edit_deselect_all') + self.deselectAllAct.setStatusTip(self.tr('Deselect all binary data')) + self.deselectAllAct.setWhatsThis(self.tr( + """<b>Deselect All</b>""" + """<p>Deselect all all binary data.</p>""" + )) + self.deselectAllAct.triggered.connect(self.__editor.deselectAll) + self.__actions.append(self.deselectAllAct) + + self.saveSelectionReadableAct = E5Action( + self.tr('Save Selection Readable'), + self.tr('Save Selection Readable...'), + 0, 0, self, 'hexEditor_edit_selection_save_readable') + self.saveSelectionReadableAct.setStatusTip( + self.tr('Save the binary data of the current selection to a file' + ' in a readable format')) + self.saveSelectionReadableAct.setWhatsThis(self.tr( + """<b>Save Selection Readable...</b>""" + """<p>Saves the binary data of the current selection to a file""" + """ in a readable format.</p>""" + )) + self.saveSelectionReadableAct.triggered.connect( + self.__saveSelectionReadable) + self.__actions.append(self.saveSelectionReadableAct) + + self.readonlyAct = E5Action( + self.tr('Set Read Only'), + self.tr('Set Read Only'), + 0, 0, self, 'hexEditor_edit_readonly', True) + self.readonlyAct.setStatusTip(self.tr( + 'Change the edit mode to read only')) + self.readonlyAct.setWhatsThis(self.tr( + """<b>Set Read Only</b>""" + """<p>This changes the edit mode to read only (i.e. to view""" + """ mode).</p>""" + )) + self.readonlyAct.setChecked(False) + self.readonlyAct.toggled[bool].connect(self.__editor.setReadOnly) + self.__actions.append(self.readonlyAct) + + self.redoAct.setEnabled(False) + self.__editor.canRedoChanged.connect(self.redoAct.setEnabled) + + self.undoAct.setEnabled(False) + self.__editor.canUndoChanged.connect(self.undoAct.setEnabled) + + self.revertAct.setEnabled(False) + self.__editor.dataChanged.connect(self.revertAct.setEnabled) + + self.cutAct.setEnabled(False) + self.copyAct.setEnabled(False) + self.saveSelectionReadableAct.setEnabled(False) + self.__editor.selectionAvailable.connect(self.__checkActions) + self.__editor.selectionAvailable.connect(self.copyAct.setEnabled) + self.__editor.selectionAvailable.connect( + self.saveSelectionReadableAct.setEnabled) + + def __initHelpActions(self): + """ + Private method to create the Help actions. + """ + self.aboutAct = E5Action( + self.tr('About'), + self.tr('&About'), + 0, 0, self, 'hexEditor_help_about') + self.aboutAct.setStatusTip(self.tr( + 'Display information about this software')) + self.aboutAct.setWhatsThis(self.tr( + """<b>About</b>""" + """<p>Display some information about this software.</p>""")) + self.aboutAct.triggered.connect(self.__about) + self.__actions.append(self.aboutAct) + + self.aboutQtAct = E5Action( + self.tr('About Qt'), + self.tr('About &Qt'), + 0, 0, self, 'hexEditor_help_about_qt') + self.aboutQtAct.setStatusTip( + self.tr('Display information about the Qt toolkit')) + self.aboutQtAct.setWhatsThis(self.tr( + """<b>About Qt</b>""" + """<p>Display some information about the Qt toolkit.</p>""" + )) + self.aboutQtAct.triggered.connect(self.__aboutQt) + self.__actions.append(self.aboutQtAct) + + self.whatsThisAct = E5Action( + self.tr('What\'s This?'), + UI.PixmapCache.getIcon("whatsThis.png"), + self.tr('&What\'s This?'), + QKeySequence(self.tr("Shift+F1", "Help|What's This?'")), + 0, self, 'hexEditor_help_whats_this') + self.whatsThisAct.setStatusTip(self.tr('Context sensitive help')) + self.whatsThisAct.setWhatsThis(self.tr( + """<b>Display context sensitive help</b>""" + """<p>In What's This? mode, the mouse cursor shows an arrow""" + """ with a question mark, and you can click on the interface""" + """ elements to get a short description of what they do and""" + """ how to use them. In dialogs, this feature can be accessed""" + """ using the context help button in the titlebar.</p>""" + )) + self.whatsThisAct.triggered.connect(self.__whatsThis) + self.__actions.append(self.whatsThisAct) + + def __initMenus(self): + """ + Private method to create the menus. + """ + mb = self.menuBar() + + menu = mb.addMenu(self.tr('&File')) + menu.setTearOffEnabled(True) + menu.addAction(self.newWindowAct) + menu.addAction(self.openAct) + menu.addAction(self.openReadOnlyAct) + menu.addSeparator() + menu.addAction(self.saveAct) + menu.addAction(self.saveAsAct) + menu.addAction(self.saveReadableAct) + menu.addSeparator() + menu.addAction(self.closeAct) + menu.addAction(self.closeOthersAct) + if self.fromEric: + menu.addAction(self.closeAllAct) + else: + menu.addSeparator() + menu.addAction(self.exitAct) + + menu = mb.addMenu(self.tr("&Edit")) + menu.setTearOffEnabled(True) + menu.addAction(self.undoAct) + menu.addAction(self.redoAct) + menu.addAction(self.revertAct) + menu.addSeparator() + menu.addAction(self.cutAct) + menu.addAction(self.copyAct) + menu.addAction(self.pasteAct) + menu.addSeparator() + menu.addAction(self.selectAllAct) + menu.addAction(self.deselectAllAct) + menu.addAction(self.saveSelectionReadableAct) + menu.addSeparator() + menu.addAction(self.readonlyAct) + + mb.addSeparator() + + menu = mb.addMenu(self.tr("&Help")) + menu.addAction(self.aboutAct) + menu.addAction(self.aboutQtAct) + menu.addSeparator() + menu.addAction(self.whatsThisAct) + + def __initToolbars(self): + """ + Private method to create the toolbars. + """ + filetb = self.addToolBar(self.tr("File")) + filetb.setObjectName("FileToolBar") + filetb.setIconSize(UI.Config.ToolBarIconSize) + filetb.addAction(self.newWindowAct) + filetb.addAction(self.openAct) + filetb.addSeparator() + filetb.addAction(self.saveAct) + filetb.addAction(self.saveAsAct) + filetb.addSeparator() + filetb.addAction(self.closeAct) + if not self.fromEric: + filetb.addAction(self.exitAct) + + edittb = self.addToolBar(self.tr("Edit")) + edittb.setObjectName("EditToolBar") + edittb.setIconSize(UI.Config.ToolBarIconSize) + edittb.addAction(self.undoAct) + edittb.addAction(self.redoAct) + edittb.addSeparator() + edittb.addAction(self.cutAct) + edittb.addAction(self.copyAct) + edittb.addAction(self.pasteAct) + + helptb = self.addToolBar(self.tr("Help")) + helptb.setObjectName("HelpToolBar") + helptb.setIconSize(UI.Config.ToolBarIconSize) + helptb.addAction(self.whatsThisAct) + + def __createStatusBar(self): + """ + Private method to initialize the status bar. + """ + self.__statusBar = self.statusBar() + self.__statusBar.setSizeGripEnabled(True) + + self.__sbEditMode = QLabel(self.__statusBar) + self.__statusBar.addPermanentWidget(self.__sbEditMode) + self.__sbEditMode.setWhatsThis(self.tr( + """<p>This part of the status bar displays the edit mode.</p>""" + )) + + self.__sbReadOnly = QLabel(self.__statusBar) + self.__statusBar.addPermanentWidget(self.__sbReadOnly) + self.__sbReadOnly.setWhatsThis(self.tr( + """<p>This part of the status bar displays the read""" + """ only mode.</p>""" + )) + + self.__sbAddress = QLabel(self.__statusBar) + self.__statusBar.addPermanentWidget(self.__sbAddress) + self.__sbAddress.setWhatsThis(self.tr( + """<p>This part of the status bar displays the cursor""" + """ address.</p>""" + )) + + self.__showEditMode(self.__editor.overwriteMode()) + self.__showReadOnlyMode(self.__editor.isReadOnly()) + + self.__sbSize = QLabel(self.__statusBar) + self.__statusBar.addPermanentWidget(self.__sbSize) + self.__sbSize.setWhatsThis(self.tr( + """<p>This part of the status bar displays the size of the""" + """ binary data.</p>""" + )) + + @pyqtSlot(int) + def __showAddress(self, address): + """ + Private slot to show the address of the cursor position. + + @param address address of the cursor + @type int + """ + self.__sbAddress.setText(self.tr("Address: {0:#x}").format(address)) + + @pyqtSlot(bool) + def __showReadOnlyMode(self, on): + """ + Private slot to show the read only mode. + + @param on flag indicating the read only state + @type bool + """ + self.__sbReadOnly.setText(self.tr("ro") if on else self.tr("rw")) + + @pyqtSlot(bool) + def __showEditMode(self, overwrite): + """ + Private slot to show the edit mode. + + @param overwrite flag indicating overwrite mode + @type bool + """ + self.__sbEditMode.setText( + self.tr("Overwrite") if overwrite else self.tr("Insert")) + + @pyqtSlot(int) + def __showSize(self, size): + """ + Private slot to show the binary data size. + + @param size size of the binary data + @type int + """ + self.__sbSize.setText(self.tr("Size: {0:n}").format(size)) + + def closeEvent(self, evt): + """ + Protected event handler for the close event. + + @param evt reference to the close event + <br />This event is simply accepted after the history has been + saved and all window references have been deleted. + @type QCloseEvent + """ + if self.__maybeSave(): + Preferences.setGeometry("HexEditorGeometry", self.saveGeometry()) + + try: + if self.fromEric or len(self.__class__.windows) > 1: + del self.__class__.windows[ + self.__class__.windows.index(self)] + except ValueError: + pass + + if not self.fromEric: + Preferences.syncPreferences() + + evt.accept() + self.editorClosed.emit() + else: + evt.ignore() + + def __openHexFileNewWindow(self): + """ + Private slot called to open a binary file in new hex editor window. + """ + if not self.__lastOpenPath: + if self.__project and self.__project.isOpen(): + self.__lastOpenPath = self.__project.getProjectPath() + + fileName = E5FileDialog.getOpenFileName( + self, + self.tr("Open binary file in new window"), + self.__lastOpenPath, + self.tr("All Files (*)")) + if fileName: + he = HexEditMainWindow(fileName=fileName, + parent=self.parent(), + fromEric=self.fromEric, + project=self.__project) + he.setRecentPaths("", self.__lastSavePath) + he.show() + + def __maybeSave(self): + """ + Private method to ask the user to save the file, if it was modified. + + @return flag indicating, if it is ok to continue + @rtype bool + """ + if self.__editor.isModified(): + ret = E5MessageBox.okToClearData( + self, + self.tr("eric6 Hex Editor"), + self.tr("""The loaded file has unsaved changes."""), + self.__saveHexFile) + if not ret: + return False + return True + + def __loadHexFile(self, fileName): + """ + Private method to load a binary file. + + @param fileName name of the binary file to load + @type str + """ + file = QFile(fileName) + if not file.exists(): + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("The file '{0}' does not exist.") + .format(fileName)) + return + + if not file.open(QFile.ReadOnly): + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("Cannot read file '{0}:\n{1}.") + .format(fileName, file.errorString())) + return + + data = file.readAll() + file.close() + + self.__lastOpenPath = os.path.dirname(fileName) + self.__editor.setData(data) + self.__setCurrentFile(fileName) + + def __openHexFile(self): + """ + Private slot to open a binary file. + """ + if self.__maybeSave(): + if not self.__lastOpenPath: + if self.__project and self.__project.isOpen(): + self.__lastOpenPath = self.__project.getProjectPath() + + fileName = E5FileDialog.getOpenFileName( + self, + self.tr("Open binary file"), + self.__lastOpenPath, + self.tr("All Files (*)")) + if fileName: + self.__loadHexFile(fileName) + self.__checkActions() + + def __openHexFileReadOnly(self): + """ + Private slot to open a binary file in read only mode. + """ + self.__openHexFile() + self.__editor.setReadOnly(True) + self.__checkActions() + + def __saveHexFile(self): + """ + Private method to save a binary file. + + @return flag indicating success + @rtype bool + """ + if not self.__fileName: + ok = self.__saveHexFileAs() + else: + ok = self.__saveHexDataFile(self.__fileName) + + if ok: + self.__editor.undoStack().setClean() + + return ok + + def __saveHexFileAs(self): + """ + Private method to save the data to a new file. + + @return flag indicating success + @rtype bool + """ + if not self.__lastSavePath: + if self.__project and self.__project.isOpen(): + self.__lastSavePath = self.__project.getProjectPath() + if not self.__lastSavePath and self.__lastOpenPath: + self.__lastSavePath = self.__lastOpenPath + + fileName = E5FileDialog.getSaveFileName( + self, + self.tr("Save binary file"), + self.__lastSavePath, + self.tr("All Files (*)"), + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if not fileName: + return False + + if QFileInfo(fileName).exists(): + res = E5MessageBox.yesNo( + self, + self.tr("Save binary file"), + self.tr("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fileName), + icon=E5MessageBox.Warning) + if not res: + return False + + self.__lastSavePath = os.path.dirname(fileName) + + return self.__saveHexDataFile(fileName) + + def __saveHexDataFile(self, fileName): + """ + Private method to save the binary data to a file. + + @param fileName name of the file to write to + @type str + @return flag indicating success + @rtype bool + """ + file = QFile(fileName) + if not file.open(QFile.WriteOnly): + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + + self.__checkActions() + + return False + + res = file.write(self.__editor.data()) != -1 + file.close() + + if not res: + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + + self.__checkActions() + + return False + + self.__editor.setModified(False, setCleanState=True) + + self.__setCurrentFile(fileName) + self.__statusBar.showMessage(self.tr("File saved"), 2000) + + self.__checkActions() + + return True + + def __saveHexFileReadable(self, selectionOnly=False): + """ + Private method to save the binary data in readable format. + + @param selectionOnly flag indicating to save the selection only + @type bool + """ + savePath = self.__lastSavePath + if not savePath: + if self.__project and self.__project.isOpen(): + savePath = self.__project.getProjectPath() + if not savePath and self.__lastOpenPath: + savePath = self.__lastOpenPath + + fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save to readable file"), + savePath, + self.tr("Text Files (*.txt);;All Files (*)"), + self.tr("Text Files (*.txt)"), + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if not fileName: + return + + ext = QFileInfo(fileName).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fileName += ex + if QFileInfo(fileName).exists(): + res = E5MessageBox.yesNo( + self, + self.tr("Save to readable file"), + self.tr("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fileName), + icon=E5MessageBox.Warning) + if not res: + return + + file = QFile(fileName) + if not file.open(QFile.WriteOnly): + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + return + + if selectionOnly: + readableData = self.__editor.selectionToReadableString() + else: + readableData = self.__editor.toReadableString() + res = file.write(readableData.encode("latin1")) != -1 + file.close() + + if not res: + E5MessageBox.warning( + self, self.tr("eric6 Hex Editor"), + self.tr("Cannot write file '{0}:\n{1}.") + .format(fileName, file.errorString())) + return + + self.__statusBar.showMessage(self.tr("File saved"), 2000) + + def __saveSelectionReadable(self): + """ + Private method to save the data of the current selection in readable + format. + """ + self.__saveHexFileReadable(selectionOnly=True) + + def __closeAll(self): + """ + Private slot to close all windows. + """ + self.__closeOthers() + self.close() + + def __closeOthers(self): + """ + Private slot to close all other windows. + """ + for win in self.__class__.windows[:]: + if win != self: + win.close() + + def __setCurrentFile(self, fileName): + """ + Private method to register the file name of the current file. + + @param fileName name of the file to register + @type str + """ + self.__fileName = fileName + + if not self.__fileName: + shownName = self.tr("Untitled") + else: + shownName = self.__strippedName(self.__fileName) + + self.setWindowTitle(self.tr("{0}[*] - {1}") + .format(shownName, self.tr("Hex Editor"))) + + self.setWindowModified(self.__editor.isModified()) + + def __strippedName(self, fullFileName): + """ + Private method to return the filename part of the given path. + + @param fullFileName full pathname of the given file + @type str + @return filename part + @rtype str + """ + return QFileInfo(fullFileName).fileName() + + def setRecentPaths(self, openPath, savePath): + """ + Public method to set the last open and save paths. + + @param openPath least recently used open path + @type str + @param savePath least recently used save path + @type str + """ + if openPath: + self.__lastOpenPath = openPath + if savePath: + self.__lastSavePath = savePath + + @pyqtSlot() + def __checkActions(self): + """ + Private slot to check some actions for their enable/disable status. + """ + self.saveAct.setEnabled(self.__editor.isModified()) + + self.cutAct.setEnabled(not self.__editor.isReadOnly() and + self.__editor.hasSelection()) + self.pasteAct.setEnabled(not self.__editor.isReadOnly()) + + @pyqtSlot(bool) + def __modificationChanged(self, m): + """ + Private slot to handle the dataChanged signal. + + @param m modification status + @type bool + """ + self.setWindowModified(m) + self.__checkActions() + + def __about(self): + """ + Private slot to show a little About message. + """ + E5MessageBox.about( + self, self.tr("About eric6 Hex Editor"), + self.tr("The eric6 Hex Editor is a simple editor component" + " to edit binary files.")) + + def __aboutQt(self): + """ + Private slot to handle the About Qt dialog. + """ + E5MessageBox.aboutQt(self, "eric6 Hex Editor") + + def __whatsThis(self): + """ + Private slot called in to enter Whats This mode. + """ + QWhatsThis.enterWhatsThisMode()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HexEdit/HexEditUndoStack.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Undo stack for the hex edit widget. +""" + +from __future__ import unicode_literals + +try: + from enum import Enum +except ImportError: + from ThirdParty.enum import Enum + +from PyQt5.QtWidgets import QUndoStack, QUndoCommand + + +class HexEditCommand(Enum): + """ + Class implementing the edit comands. + """ + Insert = 0 + RemoveAt = 1 + Overwrite = 2 + + +class HexEditUndoCommand(QUndoCommand): + """ + Class implementing the Undo command. + """ + def __init__(self, chunks, cmd, pos, newByte, parent=None): + """ + Constructor + + @param chunks reference to the data container + @type HexEditChunks + @param cmd edit command + @type HexEditCommand + @param pos edit position + @type int + @param newByte new byte value + @type int (range 0 to 255) + @param parent reference to the parent command + @type QUndoCommand + """ + super(HexEditUndoCommand, self).__init__(parent) + + self.__chunks = chunks + self._pos = pos + self._newByte = newByte + self._cmd = cmd + + self.__wasChanged = False + self.__oldByte = 0 + + def undo(self): + """ + Public method to undo the command. + """ + if self._cmd == HexEditCommand.Insert: + self.__chunks.removeAt(self._pos) + elif self._cmd == HexEditCommand.Overwrite: + self.__chunks.overwrite(self._pos, self.__oldByte) + self.__chunks.setDataChanged(self._pos, self.__wasChanged) + elif self._cmd == HexEditCommand.RemoveAt: + self.__chunks.insert(self._pos, self.__oldByte) + self.__chunks.setDataChanged(self._pos, self.__wasChanged) + + def redo(self): + """ + Public method to redo the command. + """ + if self._cmd == HexEditCommand.Insert: + self.__chunks.insert(self._pos, self._newByte) + elif self._cmd == HexEditCommand.Overwrite: + self.__oldByte = self.__chunks[self._pos] + self.__wasChanged = self.__chunks.dataChanged(self._pos) + self.__chunks.overwrite(self._pos, self._newByte) + elif self._cmd == HexEditCommand.RemoveAt: + self.__oldByte = self.__chunks[self._pos] + self.__wasChanged = self.__chunks.dataChanged(self._pos) + self.__chunks.removeAt(self._pos) + + def mergeWith(self, command): + """ + Public method to merge this command with another one. + + @param command reference to the command to merge with + @type QUndoCommand + @return flag indicating a successful merge + @rtype bool + """ + result = False + + if self._cmd != HexEditCommand.RemoveAt: + if command._cmd == HexEditCommand.Overwrite: + if command._pos == self._pos: + self._newByte = command._newByte + result = True + + return result + + def id(self): + """ + Public method to get the ID of this undo command class. + + @return ID of the undo command class + @rtype int + """ + return 4242 + + +class HexEditUndoStack(QUndoStack): + """ + Class implementing an Undo stack for the hex edit widget. + """ + def __init__(self, chunks, parent=None): + """ + Constructor + + @param chunks reference to the data container + @type HexEditChunks + @param parent reference to the parent object + @type QObject + """ + super(HexEditUndoStack, self).__init__(parent) + + self.__chunks = chunks + self.__parent = parent + + def insert(self, pos, data): + """ + Public method to insert a byte. + + @param pos position to insert at + @type int + @param data byte to be inserted + @type int (range 0 to 255) + """ + if pos >= 0 and pos <= self.__chunks.size(): + uc = HexEditUndoCommand( + self.__chunks, HexEditCommand.Insert, pos, data) + self.push(uc) + + def insertByteArray(self, pos, byteArray): + """ + Public method to insert bytes. + + @param pos position to insert at + @type int + @param byteArray data to be inserted + @type byteArray or QByteArray + """ + ba = bytearray(byteArray) + + if pos >= 0 and pos <= self.__chunks.size(): + txt = self.tr("Inserting %n byte(s)", "", len(ba)) + self.beginMacro(txt) + for idx in range(len(ba)): + uc = HexEditUndoCommand( + self.__chunks, HexEditCommand.Insert, pos + idx, ba[idx]) + self.push(uc) + self.endMacro() + + def removeAt(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 + """ + if pos >= 0 and pos <= self.__chunks.size(): + if length == 1: + uc = HexEditUndoCommand( + self.__chunks, HexEditCommand.RemoveAt, pos, 0) + self.push(uc) + else: + txt = self.tr("Deleting %n byte(s)", "", length) + self.beginMacro(txt) + for cnt in range(length): + uc = HexEditUndoCommand( + self.__chunks, HexEditCommand.RemoveAt, pos, 0) + self.push(uc) + self.endMacro() + + def overwrite(self, pos, data): + """ + Public method to replace a byte. + + @param pos position to replace the byte at + @type int + @param data byte to replace with + @type int (range 0 to 255) + """ + if pos >= 0 and pos <= self.__chunks.size(): + uc = HexEditUndoCommand( + self.__chunks, HexEditCommand.Overwrite, pos, data) + self.push(uc) + + def overwriteByteArray(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 + """ + ba = bytearray(byteArray) + + if pos >= 0 and pos <= self.__chunks.size(): + txt = self.tr("Inserting %n byte(s)", "", len(ba)) + self.beginMacro(txt) + self.removeAt(pos, length) + self.insertByteArray(pos, ba) + self.endMacro()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HexEdit/HexEditWidget.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,1658 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an editor for binary data. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray, QTimer, QRect, \ + QBuffer, QIODevice +from PyQt5.QtGui import QBrush, QPen, QColor, QFont, QPalette, QKeySequence, \ + QPainter +from PyQt5.QtWidgets import QAbstractScrollArea, QApplication + +from .HexEditChunks import HexEditChunks +from .HexEditUndoStack import HexEditUndoStack + +import Globals + + +# TODO: implement cursor in ASCII area +# TODO: implement editing in ASCII area + + +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, embedded=False): + """ + Constructor + + @param parent refernce to the parent widget + @type QWidget + @param embedded flag indicating an eric embedded hex editor + @type bool + """ + super(HexEditWidget, self).__init__(parent) + + self.__embedded = embedded + + # Properties + self.__addressArea = True + # switch the address area on/off + self.__addressAreaColor = QColor() + # color 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 positioin of cursor, 1 Byte == 2 tics + + self.__addrDigits = 0 + self.__blink = True + self.__bData = QBuffer() + self.__cursorRect = 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)) + + self.setAddressAreaColor(self.palette().alternateBase().color()) + self.setHighlightColor(QColor(0xff, 0xff, 0x99, 0xff)) + self.setSelectionColor(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 addressAreaColor(self): + """ + Public method to get the address area color. + + @return address area color + @rtype QColor + """ + return QColor(self.__addressAreaColor) + + def setAddressAreaColor(self, color): + """ + Public method to set the address area color. + + @param color address area color + @type QColor + """ + self.__addressAreaColor = QColor(color) + 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 minimum width of the address area in + characters. + + @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 + + if n > self.__addressWidth: + return n + else: + return self.__addressWidth + + def setAddressWidth(self, width): + """ + Public method to set the width of the address area. + + @param width width of the address area in characters + @type int + """ + self.__addressWidth = width + 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) + + # 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) + + 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) + + # step 4: draw new cursor + self.__blink = True + self.viewport().update(self.__cursorRect) + self.currentAddressChanged.emit(self.__bPosCurrent) + + 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 isinstance(dataOrDevice, (bytearray, QByteArray)): + self.__data = bytearray(dataOrDevice) + self.__bData.setData(self.__data) + return self.__setData(self.__bData) + elif isinstance(dataOrDevice, QIODevice): + return self.__setData(dataOrDevice) + else: + raise TypeError( + "setData: parameter must be bytearray, " + "QByteArray or QIODevice") + + 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 highlightingColor(self): + """ + Public method to get the highlighting color. + + @return highlighting color + @rtype QColor + """ + return self.__highlightingBrush.color() + + def setHighlightColor(self, color): + """ + Public method to set the highlight color. + + @param color new highlight color + @type QColor + """ + self.__highlightingBrush = QBrush(color) + self.__highlightingPen = QPen( + self.viewport().palette().color(QPalette.WindowText)) + 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) + + def selectionColor(self): + """ + Public method to get the selection color. + + @return selection color + @rtype QColor + """ + return self.__selectionBrush.color() + + def setSelectionColor(self, color): + """ + Public method to set the selection color. + + @param color new selection color + @type QColor + """ + self.__selectionBrush = QBrush(color) + self.__selectionPen = QPen(Qt.white) + 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(HexEditWidget, self).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(HexEditWidget, self).setFont(font) + 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.__pxCharHeight // 5 + 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 + """ + assert 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 + """ + assert 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, len, byteArray): + """ + Public method to replace bytes. + + @param pos position to replace the bytes at + @type int + @param len amount of bytes to replace + @type int + @param byteArray bytes to replace with + @type bytearray or QByteArray + """ + self.__undoStack.overwriteByteArray(pos, len, 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 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() + + def editorCommand(self, cmd): + """ + Public method to execute an editor command sent by the eric + view manager. + + @param cmd QScintilla command + @type int + """ + if self.__embedded: + from PyQt5.Qsci import QsciScintilla + + # Cursor movements + if cmd == QsciScintilla.SCI_CHARLEFT: + self.moveCursorToPreviousChar() + elif cmd == QsciScintilla.SCI_CHARRIGHT: + self.moveCursorToNextChar() + elif cmd == QsciScintilla.SCI_LINEEND: + self.moveCursorToEndOfLine() + elif cmd == QsciScintilla.SCI_VCHOME: + self.moveCursorToStartOfLine() + elif cmd == QsciScintilla.SCI_LINEUP: + self.moveCursorToPreviousLine() + elif cmd == QsciScintilla.SCI_LINEDOWN: + self.moveCursorToNextLine() + elif cmd == QsciScintilla.SCI_PAGEDOWN: + self.moveCursorToNextPage() + elif cmd == QsciScintilla.SCI_PAGEUP: + self.moveCursorToPreviousPage() + elif cmd == QsciScintilla.SCI_DOCUMENTEND: + self.moveCursorToEndOfDocument() + elif cmd == QsciScintilla.SCI_DOCUMENTSTART: + self.moveCursorToStartOfDocument() + + # Selection commands + elif cmd == QsciScintilla.SCI_CHARRIGHTEXTEND: + self.selectNextChar() + elif cmd == QsciScintilla.SCI_CHARLEFTEXTEND: + self.selectPreviousChar() + elif cmd == QsciScintilla.SCI_LINEENDEXTEND: + self.selectToEndOfLine() + elif cmd == QsciScintilla.SCI_VCHOMEEXTEND: + self.selectToStartOfLine() + elif cmd == QsciScintilla.SCI_LINEUPEXTEND: + self.selectPreviousLine() + elif cmd == QsciScintilla.SCI_LINEDOWNEXTEND: + self.selectNextLine() + elif cmd == QsciScintilla.SCI_PAGEDOWNEXTEND: + self.selectNextPage() + elif cmd == QsciScintilla.SCI_PAGEUPEXTEND: + self.selectPreviousPage() + elif cmd == QsciScintilla.SCI_DOCUMENTENDEXTEND: + self.selectEndOfDocument() + elif cmd == QsciScintilla.SCI_DOCUMENTSTARTEXTEND: + self.selectStartOfDocument() + elif cmd == QsciScintilla.SCI_EDITTOGGLEOVERTYPE: + self.setOverwriteMode(not self.overwriteMode()) + self.setCursorPosition(self.__cursorPosition) + + # Edit commands + if not self.__readOnly: + if cmd == QsciScintilla.SCI_CLEAR: + self.deleteByte() + elif cmd == QsciScintilla.SCI_DELETEBACK: + self.deleteByteBack() + + 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 + """ + if not self.__embedded: + # Cursor movements + if evt.matches(QKeySequence.MoveToNextChar): + self.moveCursorToNextChar() + elif evt.matches(QKeySequence.MoveToPreviousChar): + self.moveCursorToPreviousChar() + elif evt.matches(QKeySequence.MoveToEndOfLine): + self.moveCursorToEndOfLine() + elif evt.matches(QKeySequence.MoveToStartOfLine): + self.moveCursorToStartOfLine() + elif evt.matches(QKeySequence.MoveToPreviousLine): + self.moveCursorToPreviousLine() + elif evt.matches(QKeySequence.MoveToNextLine): + self.moveCursorToNextLine() + elif evt.matches(QKeySequence.MoveToNextPage): + self.moveCursorToNextPage() + elif evt.matches(QKeySequence.MoveToPreviousPage): + self.moveCursorToPreviousPage() + elif evt.matches(QKeySequence.MoveToEndOfDocument): + self.moveCursorToEndOfDocument() + elif evt.matches(QKeySequence.MoveToStartOfDocument): + self.moveCursorToStartOfDocument() + + # Selection commands + elif evt.matches(QKeySequence.SelectAll): + self.selectAll() + elif evt.matches(QKeySequence.SelectNextChar): + self.selectNextChar() + elif evt.matches(QKeySequence.SelectPreviousChar): + self.selectPreviousChar() + elif evt.matches(QKeySequence.SelectEndOfLine): + self.selectToEndOfLine() + elif evt.matches(QKeySequence.SelectStartOfLine): + self.selectToStartOfLine() + elif evt.matches(QKeySequence.SelectPreviousLine): + self.selectPreviousLine() + elif evt.matches(QKeySequence.SelectNextLine): + self.selectNextLine() + elif evt.matches(QKeySequence.SelectNextPage): + self.selectNextPage() + elif evt.matches(QKeySequence.SelectPreviousPage): + self.selectPreviousPage() + elif evt.matches(QKeySequence.SelectEndOfDocument): + self.selectEndOfDocument() + elif evt.matches(QKeySequence.SelectStartOfDocument): + self.selectStartOfDocument() + + # Edit commands + elif evt.matches(QKeySequence.Copy): + self.copy() + elif evt.key() == Qt.Key_Insert and \ + evt.modifiers() == Qt.NoModifier: + self.setOverwriteMode(not self.overwriteMode()) + self.setCursorPosition(self.__cursorPosition) + + if not self.__readOnly: + if evt.matches(QKeySequence.Cut): + self.cut() + elif evt.matches(QKeySequence.Paste): + self.paste() + elif evt.matches(QKeySequence.Delete): + self.deleteByte() + elif evt.key() == Qt.Key_Backspace and \ + evt.modifiers() == Qt.NoModifier: + self.deleteByteBack() + elif evt.matches(QKeySequence.Undo): + self.undo() + elif evt.matches(QKeySequence.Redo): + self.redo() + + if not self.__readOnly and \ + QApplication.keyboardModifiers() in [ + Qt.NoModifier, Qt.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: + if (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) + + 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.pos()) + 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.pos()) + if cPos >= 0: + if evt.modifiers() == Qt.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: + pxOfsX = self.horizontalScrollBar().value() + pxPosStartY = self.__pxCharHeight + + # draw some patterns if needed + painter.fillRect( + evt.rect(), self.viewport().palette().color(QPalette.Base)) + if self.__addressArea: + painter.fillRect( + QRect(-pxOfsX, evt.rect().top(), + self.__pxPosHexX - self.__pxGapAdrHex // 2 - pxOfsX, + self.height()), + self.__addressAreaColor) + if self.__asciiArea: + linePos = self.__pxPosAsciiX - (self.__pxGapHexAscii // 2) + painter.setPen(Qt.gray) + painter.drawLine(linePos - pxOfsX, evt.rect().top(), + linePos - pxOfsX, self.height()) + + painter.setPen( + self.viewport().palette().color(QPalette.WindowText)) + + # paint the address area + if self.__addressArea: + 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) + 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.WindowText)) + + painter.setBackgroundMode(Qt.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.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: + if 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) + hex = chr(self.__hexDataShown[(bPosLine + colIdx) * 2]) + \ + chr(self.__hexDataShown[(bPosLine + colIdx) * 2 + 1]) + painter.drawText(pxPosX, pxPosY, hex) + pxPosX += 3 * self.__pxCharWidth + + # render ascii value + if self.__asciiArea: + by = self.__dataShown[bPosLine + colIdx] + if by < 0x20 or by > 0x7e: + 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.TransparentMode) + painter.setPen( + self.viewport().palette().color(QPalette.WindowText)) + + # paint cursor + if self.__blink and not self.__readOnly and self.hasFocus(): + painter.fillRect( + self.__cursorRect, self.palette().color(QPalette.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) + + # 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 = 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 = 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): + """ + Private method to get the start of the selection. + + @return selection start + @rtype int + """ + return self.__bSelectionBegin + + def __getSelectionEnd(self): + """ + Private method to get the end of the selection. + + @return selection end + @rtype int + """ + return self.__bSelectionEnd + + def __getSelectionLength(self): + """ + Private 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: + 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.__pxPosHexX = self.__pxGapAdr + \ + self.__addrDigits * 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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/HexEdit/__init__.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing a hex editor. +""" + +# +# This is a Python only variant of QHexEdit2 created by Winfried Simon. +# Winfried Simon <winfried.simon@gmail.com> +#
--- a/Preferences/__init__.py Sat Jan 09 19:01:17 2016 +0100 +++ b/Preferences/__init__.py Sat Jan 09 19:04:34 2016 +0100 @@ -1128,6 +1128,7 @@ "HelpViewerGeometry": QByteArray(), "HelpInspectorGeometry": QByteArray(), "IconEditorGeometry": QByteArray(), + "HexEditorGeometry": QByteArray(), "MainGeometry": QByteArray(), "MainMaximized": False, }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/QScintilla/KeySequenceTranslator.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing functions to map QScintilla keyboard commands to +QKeySequence standard keys. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import qVersion +from PyQt5.QtGui import QKeySequence +from PyQt5.Qsci import QsciScintilla + +__all__ = ["s2qTranslate"] + +Scintilla2QKeySequence = { + QsciScintilla.SCI_CHARLEFT: QKeySequence.MoveToPreviousChar, + QsciScintilla.SCI_CHARRIGHT: QKeySequence.MoveToNextChar, + QsciScintilla.SCI_LINEUP: QKeySequence.MoveToPreviousLine, + QsciScintilla.SCI_LINEDOWN: QKeySequence.MoveToNextLine, + QsciScintilla.SCI_WORDPARTLEFT: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDPARTRIGHT: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDLEFT: QKeySequence.MoveToNextWord, + QsciScintilla.SCI_WORDRIGHT: QKeySequence.MoveToPreviousWord, + QsciScintilla.SCI_VCHOME: QKeySequence.MoveToStartOfLine, + QsciScintilla.SCI_HOMEDISPLAY: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEEND: QKeySequence.MoveToEndOfLine, + QsciScintilla.SCI_LINESCROLLDOWN: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINESCROLLUP: QKeySequence.UnknownKey, + QsciScintilla.SCI_PARAUP: QKeySequence.MoveToStartOfBlock, + QsciScintilla.SCI_PARADOWN: QKeySequence.MoveToEndOfBlock, + QsciScintilla.SCI_PAGEUP: QKeySequence.MoveToPreviousPage, + QsciScintilla.SCI_PAGEDOWN: QKeySequence.MoveToNextPage, + QsciScintilla.SCI_DOCUMENTSTART: QKeySequence.MoveToStartOfDocument, + QsciScintilla.SCI_DOCUMENTEND: QKeySequence.MoveToEndOfDocument, + QsciScintilla.SCI_TAB: QKeySequence.UnknownKey, + QsciScintilla.SCI_BACKTAB: QKeySequence.UnknownKey, + QsciScintilla.SCI_CHARLEFTEXTEND: QKeySequence.SelectPreviousChar, + QsciScintilla.SCI_CHARRIGHTEXTEND: QKeySequence.SelectNextChar, + QsciScintilla.SCI_LINEUPEXTEND: QKeySequence.SelectPreviousLine, + QsciScintilla.SCI_LINEDOWNEXTEND: QKeySequence.SelectNextLine, + QsciScintilla.SCI_WORDPARTLEFTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDPARTRIGHTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDLEFTEXTEND: QKeySequence.SelectPreviousWord, + QsciScintilla.SCI_WORDRIGHTEXTEND: QKeySequence.SelectNextWord, + QsciScintilla.SCI_VCHOMEEXTEND: QKeySequence.SelectStartOfLine, + QsciScintilla.SCI_LINEENDEXTEND: QKeySequence.SelectEndOfLine, + QsciScintilla.SCI_PARAUPEXTEND: QKeySequence.SelectStartOfBlock, + QsciScintilla.SCI_PARADOWNEXTEND: QKeySequence.SelectEndOfBlock, + QsciScintilla.SCI_PAGEUPEXTEND: QKeySequence.SelectPreviousPage, + QsciScintilla.SCI_PAGEDOWNEXTEND: QKeySequence.SelectNextPage, + QsciScintilla.SCI_DOCUMENTSTARTEXTEND: QKeySequence.SelectStartOfDocument, + QsciScintilla.SCI_DOCUMENTENDEXTEND: QKeySequence.SelectEndOfDocument, + QsciScintilla.SCI_DELETEBACK: QKeySequence.UnknownKey, + QsciScintilla.SCI_DELETEBACKNOTLINE: QKeySequence.UnknownKey, + QsciScintilla.SCI_CLEAR: QKeySequence.Delete, + QsciScintilla.SCI_DELWORDLEFT: QKeySequence.DeleteStartOfWord, + QsciScintilla.SCI_DELWORDRIGHT: QKeySequence.DeleteEndOfWord, + QsciScintilla.SCI_DELLINELEFT: QKeySequence.UnknownKey, + QsciScintilla.SCI_DELLINERIGHT: QKeySequence.DeleteEndOfLine, + QsciScintilla.SCI_NEWLINE: QKeySequence.InsertLineSeparator, + QsciScintilla.SCI_LINEDELETE: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEDUPLICATE: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINETRANSPOSE: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINECUT: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINECOPY: QKeySequence.UnknownKey, + QsciScintilla.SCI_EDITTOGGLEOVERTYPE: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEENDDISPLAY: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEENDDISPLAYEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_FORMFEED: QKeySequence.UnknownKey, + QsciScintilla.SCI_CANCEL: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEDOWNRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEUPRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_CHARLEFTRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_CHARRIGHTRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_VCHOMERECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEENDRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_PAGEUPRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_PAGEDOWNRECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_SELECTIONDUPLICATE: QKeySequence.UnknownKey, + QsciScintilla.SCI_SCROLLTOSTART: QKeySequence.UnknownKey, + QsciScintilla.SCI_SCROLLTOEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_VERTICALCENTRECARET: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDRIGHTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDRIGHTENDEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDLEFTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_WORDLEFTENDEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOME: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOMEEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOMERECTEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOMEDISPLAYEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOMEWRAP: QKeySequence.UnknownKey, + QsciScintilla.SCI_HOMEWRAPEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_VCHOMEWRAP: QKeySequence.UnknownKey, + QsciScintilla.SCI_VCHOMEWRAPEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEENDWRAP: QKeySequence.UnknownKey, + QsciScintilla.SCI_LINEENDWRAPEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_STUTTEREDPAGEUP: QKeySequence.UnknownKey, + QsciScintilla.SCI_STUTTEREDPAGEUPEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_STUTTEREDPAGEDOWN: QKeySequence.UnknownKey, + QsciScintilla.SCI_STUTTEREDPAGEDOWNEXTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_DELWORDRIGHTEND: QKeySequence.UnknownKey, + QsciScintilla.SCI_MOVESELECTEDLINESUP: QKeySequence.UnknownKey, + QsciScintilla.SCI_MOVESELECTEDLINESDOWN: QKeySequence.UnknownKey, + QsciScintilla.SCI_LOWERCASE: QKeySequence.UnknownKey, + QsciScintilla.SCI_UPPERCASE: QKeySequence.UnknownKey, +} +if qVersion() >= "5.2.0": + Scintilla2QKeySequence[QsciScintilla.SCI_LINEDELETE] = \ + QKeySequence.DeleteCompleteLine, +if qVersion() >= "5.5.0": + Scintilla2QKeySequence[QsciScintilla.SCI_DELETEBACK] = \ + QKeySequence.Backspace + + +def s2qTranslate(scintillaCommand): + """ + Function to translate a QScintilla command to a QKeySequence. + + @param scintillaCommand QScintilla command + @type int + @return Qt key sequence + @rtype QKeySequence.StandardKey + """ + assert scintillaCommand in Scintilla2QKeySequence + return Scintilla2QKeySequence[scintillaCommand]
--- a/eric6.e4p Sat Jan 09 19:01:17 2016 +0100 +++ b/eric6.e4p Sat Jan 09 19:04:34 2016 +0100 @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE Project SYSTEM "Project-5.1.dtd"> <!-- eric project file for project eric6 --> -<!-- Copyright (C) 2015 Detlev Offenbach, detlev@die-offenbachs.de --> +<!-- Copyright (C) 2016 Detlev Offenbach, detlev@die-offenbachs.de --> <Project version="5.1"> <Language>en_US</Language> <ProjectWordList>Dictionaries/words.dic</ProjectWordList> @@ -466,6 +466,11 @@ <Source>Helpviewer/data/html_rc.py</Source> <Source>Helpviewer/data/icons_rc.py</Source> <Source>Helpviewer/data/javascript_rc.py</Source> + <Source>HexEdit/HexEditChunks.py</Source> + <Source>HexEdit/HexEditMainWindow.py</Source> + <Source>HexEdit/HexEditUndoStack.py</Source> + <Source>HexEdit/HexEditWidget.py</Source> + <Source>HexEdit/__init__.py</Source> <Source>IconEditor/IconEditorGrid.py</Source> <Source>IconEditor/IconEditorPalette.py</Source> <Source>IconEditor/IconEditorWindow.py</Source> @@ -866,6 +871,7 @@ <Source>QScintilla/Exporters/ExporterTEX.py</Source> <Source>QScintilla/Exporters/__init__.py</Source> <Source>QScintilla/GotoDialog.py</Source> + <Source>QScintilla/KeySequenceTranslator.py</Source> <Source>QScintilla/Lexers/Lexer.py</Source> <Source>QScintilla/Lexers/LexerBash.py</Source> <Source>QScintilla/Lexers/LexerBatch.py</Source> @@ -1248,6 +1254,8 @@ <Source>eric6_doc.py</Source> <Source>eric6_editor.py</Source> <Source>eric6_editor.pyw</Source> + <Source>eric6_hexeditor.py</Source> + <Source>eric6_hexeditor.pyw</Source> <Source>eric6_iconeditor.py</Source> <Source>eric6_iconeditor.pyw</Source> <Source>eric6_plugininstall.py</Source>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6_hexeditor.py Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Eric6 Hex Editor. + +This is the main Python script that performs the necessary initialization +of the hex editor and starts the Qt event loop. This is a standalone version +of the integrated hex editor. +""" + +from __future__ import unicode_literals + +import Toolbox.PyQt4ImportHook # __IGNORE_WARNING__ + +try: # Only for Py2 + import Globals.compatibility_fixes # __IGNORE_WARNING__ +except (ImportError): + pass + +import sys +import os + +for arg in sys.argv[:]: + if arg.startswith("--config="): + import Globals + configDir = arg.replace("--config=", "") + Globals.setConfigDir(configDir) + sys.argv.remove(arg) + elif arg.startswith("--settings="): + from PyQt5.QtCore import QSettings + settingsDir = os.path.expanduser(arg.replace("--settings=", "")) + if not os.path.isdir(settingsDir): + os.makedirs(settingsDir) + QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, + settingsDir) + sys.argv.remove(arg) + +from Globals import AppInfo + +from Toolbox import Startup + + +def createMainWidget(argv): + """ + Function to create the main widget. + + @param argv list of commandline parameters (list of strings) + @return reference to the main widget (QWidget) + """ + from HexEdit.HexEditMainWindow import HexEditMainWindow + + try: + fileName = argv[1] + except IndexError: + fileName = "" + + editor = HexEditMainWindow(fileName, None) + return editor + + +def main(): + """ + Main entry point into the application. + """ + options = [ + ("--config=configDir", + "use the given directory as the one containing the config files"), + ("--settings=settingsDir", + "use the given directory to store the settings files"), + ("", "name of file to edit") + ] + appinfo = AppInfo.makeAppInfo(sys.argv, + "Eric6 Hex Editor", + "", + "Little tool to edit binary files.", + options) + res = Startup.simpleAppStartup(sys.argv, + appinfo, + createMainWidget) + sys.exit(res) + +if __name__ == '__main__': + main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6_hexeditor.pyw Sat Jan 09 19:04:34 2016 +0100 @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Windows entry point. +""" + +from __future__ import unicode_literals + +from eric6_hexeditor import main + +main()