WingHexExplorer2/src/control/editorview.cpp

747 lines
23 KiB
C++

/*==============================================================================
** Copyright (C) 2024-2027 WingSummer
**
** This program is free software: you can redistribute it and/or modify it under
** the terms of the GNU Affero General Public License as published by the Free
** Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
** details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program. If not, see <https://www.gnu.org/licenses/>.
** =============================================================================
*/
#include "editorview.h"
#include "QHexView/document/buffer/qfilebuffer.h"
#include "QHexView/document/buffer/qfileregionbuffer.h"
#include "QHexView/document/buffer/qmemorybuffer.h"
#include "Qt-Advanced-Docking-System/src/DockWidgetTab.h"
#include "class/eventfilter.h"
#include "class/qkeysequences.h"
#include "class/settingmanager.h"
#include "class/workspacemanager.h"
#include "dialog/fileinfodialog.h"
#include "utilities.h"
#include <QFile>
#include <QFileInfo>
#include <QVBoxLayout>
constexpr qsizetype FILEMAXBUFFER = 0x6400000; // 100MB
constexpr auto CLONE_LIMIT = 5;
EditorView::EditorView(QWidget *parent)
: ads::CDockWidget(nullptr, QString(), parent) {
this->setFeatures(
CDockWidget::DockWidgetFocusable | CDockWidget::DockWidgetMovable |
CDockWidget::DockWidgetClosable | CDockWidget::DockWidgetPinnable |
CDockWidget::CustomCloseHandling);
this->setFocusPolicy(Qt::StrongFocus);
this->setObjectName(QStringLiteral("EditorView"));
m_stack = new QStackedWidget(this);
this->setWidget(m_stack);
m_hexContainer = new QWidget(m_stack);
auto hexLayout = new QVBoxLayout(m_hexContainer);
hexLayout->setSpacing(0);
hexLayout->setContentsMargins(0, 0, 0, 0);
m_hex = new QHexView;
hexLayout->addWidget(m_hex, 1);
m_goto = new GotoWidget(this);
connect(m_goto, &GotoWidget::jumpToLine, this,
[=](qsizetype pos, bool isline) {
auto cur = m_hex->cursor();
isline ? cur->moveTo(pos, 0) : cur->moveTo(pos);
});
hexLayout->addWidget(m_goto);
m_goto->hide();
m_stack->addWidget(m_hexContainer);
auto efilter = new EventFilter(QEvent::MouseButtonPress, this->tabWidget());
connect(efilter, &EventFilter::eventTriggered, this,
[this](QObject *obj, QEvent *event) {
Q_UNUSED(obj);
auto e = reinterpret_cast<QMouseEvent *>(event);
if (e->modifiers() == Qt::NoModifier &&
e->button() == Qt::MiddleButton) {
FileInfoDialog fd(m_fileName, this->documentType() ==
DocumentType::RegionFile);
fd.exec();
}
});
auto tabWidget = this->tabWidget();
tabWidget->installEventFilter(efilter);
m_menu = new QMenu(m_hex);
auto &shortcut = QKeySequences::instance();
newAction(m_menu, "cut", tr("Cut"), &EditorView::sigOnCutFile,
QKeySequence::Cut);
newAction(m_menu, "cuthex", tr("CutHex"), &EditorView::sigOnCutHex,
shortcut.keySequence(QKeySequences::Key::CUT_HEX));
newAction(m_menu, "copy", tr("Copy"), &EditorView::sigOnCopyFile,
QKeySequence::Copy);
newAction(m_menu, "copyhex", tr("CopyHex"), &EditorView::sigOnCopyHex,
shortcut.keySequence(QKeySequences::Key::COPY_HEX));
newAction(m_menu, "paste", tr("Paste"), &EditorView::sigOnPasteFile,
QKeySequence::Paste);
newAction(m_menu, "pastehex", tr("PasteHex"), &EditorView::sigOnPasteHex,
shortcut.keySequence(QKeySequences::Key::PASTE_HEX));
newAction(m_menu, "del", tr("Delete"), &EditorView::sigOnDelete,
QKeySequence::Delete);
m_menu->addSeparator();
newAction(m_menu, "find", tr("Find"), &EditorView::sigOnFindFile,
QKeySequence::Find);
newAction(m_menu, "jmp", tr("Goto"), &EditorView::sigOnGoToLine,
shortcut.keySequence(QKeySequences::Key::GOTO));
newAction(m_menu, "fill", tr("Fill"), &EditorView::sigOnFill,
shortcut.keySequence(QKeySequences::Key::HEX_FILL));
newAction(m_menu, "metadata", tr("MetaData"), &EditorView::sigOnMetadata,
shortcut.keySequence(QKeySequences::Key::METADATA));
newAction(m_menu, "bookmark", tr("BookMark"), &EditorView::sigOnBookMark,
shortcut.keySequence(QKeySequences::Key::BOOKMARK));
newAction(m_menu, "encoding", tr("Encoding"), &EditorView::sigOnEncoding,
shortcut.keySequence(QKeySequences::Key::ENCODING));
m_hex->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_hex, &QHexView::customContextMenuRequested, this,
[=](const QPoint &pos) { m_menu->popup(mapToGlobal(pos)); });
m_stack->setCurrentWidget(m_hexContainer);
m_cloneChildren.fill(nullptr, CLONE_LIMIT);
m_findResults = new FindResultModel(this);
auto doc = m_hex->document().get();
m_bookmarks = new BookMarksModel(doc, this);
m_metadata = new MetaDataModel(doc, this);
connect(m_hex, &QHexView::documentChanged, this, [=](QHexDocument *doc) {
m_bookmarks->setDocument(doc);
m_metadata->setDocument(doc);
});
applySettings();
}
EditorView::~EditorView() {}
void EditorView::registerView(WingEditorViewWidget *view) {
Q_ASSERT(view);
m_others << view;
m_stack->addWidget(view);
}
void EditorView::switchView(qsizetype index) {
if (index < 0) {
m_stack->setCurrentWidget(m_hexContainer);
} else {
m_stack->setCurrentWidget(m_others.at(index));
}
emit viewChanged(index);
}
void EditorView::switchView(WingEditorViewWidget *w) {
if (w) {
if (m_others.contains(w)) {
m_stack->setCurrentWidget(w);
emit viewChanged(m_others.indexOf(w));
}
} else {
m_stack->setCurrentWidget(m_hexContainer);
emit viewChanged(-1);
}
}
void EditorView::registerQMenu(QMenu *menu) {
if (menu == nullptr) {
return;
}
if (!_hasRegistered) {
m_menu->addSeparator();
_hasRegistered = true;
}
m_menu->addMenu(menu);
}
EditorView::FindError EditorView::find(const FindDialog::Result &result) {
if (m_findMutex.tryLock(3000)) {
std::unique_lock<QMutex> locker(m_findMutex, std::adopt_lock_t());
auto d = m_hex->document();
QList<qsizetype> results;
qsizetype begin, end;
switch (result.dir) {
case SearchDirection::Foreword: {
begin = 0;
end = m_hex->currentOffset();
} break;
case SearchDirection::Backword: {
begin = m_hex->currentOffset();
end = -1;
} break;
case SearchDirection::Selection: {
auto cur = m_hex->cursor();
begin = cur->selectionStart(0).offset();
end = cur->selectionEnd(0).offset();
} break;
default: {
begin = -1;
end = -1;
} break;
}
QByteArray data;
if (result.isStringFind) {
data = Utilities::encodingString(result.str, result.encoding);
} else {
data = result.buffer;
}
d->findAllBytes(begin, end, data, results);
m_findResults->beginUpdate();
m_findResults->clear();
auto lineWidth = m_hex->renderer()->hexLineWidth();
auto docLen = d->length();
for (auto &ritem : results) {
FindResult r;
r.offset = ritem;
r.line = r.offset / lineWidth;
r.col = r.offset % lineWidth;
m_findResults->results().append(r);
m_findResults->findData().append(
readContextFinding(ritem, data.size(), FIND_CONTEXT_SIZE,
FIND_MAX_DISPLAY_FIND_CHARS));
}
m_findResults->lastFindData() = data;
m_findResults->endUpdate();
if (m_findResults->size() ==
std::numeric_limits<QList<qsizetype>::size_type>::max()) {
return FindError::MayOutOfRange;
} else {
return FindError::Success;
}
} else {
return FindError::Busy;
}
}
void EditorView::clearFindResult() { m_findResults->clear(); }
void EditorView::triggerGoto() {
m_goto->activeInput(int(m_hex->currentRow()), int(m_hex->currentColumn()),
m_hex->currentOffset(), m_hex->documentBytes(),
int(m_hex->documentLines()));
}
ErrFile EditorView::newFile(size_t index) {
if (isCloneFile()) {
return ErrFile::ClonedFile;
}
auto istr = QString::number(index);
m_fileName = tr("Untitled") + istr;
this->setWindowTitle(m_fileName);
m_docType = DocumentType::File;
m_isWorkSpace = false;
m_isNewFile = true;
auto p = QHexDocument::fromMemory<QMemoryBuffer>(QByteArray(), false);
p->setDocSaved();
m_hex->setDocument(QSharedPointer<QHexDocument>(p));
connectDocSavedFlag(this);
return ErrFile::Success;
}
ErrFile EditorView::openFile(const QString &filename, const QString &encoding) {
if (isCloneFile()) {
return ErrFile::ClonedFile;
}
QFileInfo info(filename);
if (info.exists()) {
if (Q_UNLIKELY(!info.permission(QFile::ReadUser))) {
return ErrFile::Permission;
}
auto readonly = !Utilities::fileCanWrite(filename);
auto *p =
info.size() > FILEMAXBUFFER
? QHexDocument::fromLargeFile(filename, readonly)
: QHexDocument::fromFile<QMemoryBuffer>(filename, readonly);
if (Q_UNLIKELY(p == nullptr)) {
return ErrFile::Permission;
}
m_hex->setDocument(QSharedPointer<QHexDocument>(p));
m_hex->setLockedFile(readonly);
m_hex->setKeepSize(true);
if (!encoding.isEmpty()) {
m_hex->renderer()->setEncoding(encoding);
}
m_docType = DocumentType::File;
m_fileName = info.absoluteFilePath();
m_isNewFile = false;
p->setDocSaved();
this->setWindowTitle(info.fileName());
connectDocSavedFlag(this);
auto tab = this->tabWidget();
tab->setIcon(Utilities::getIconFromFile(style(), m_fileName));
tab->setToolTip(m_fileName);
}
return ErrFile::Success;
}
ErrFile EditorView::openWorkSpace(const QString &filename,
const QString &encoding) {
if (isCloneFile()) {
return ErrFile::ClonedFile;
}
Q_ASSERT(!filename.isEmpty());
QFileInfo finfo(filename);
if (!finfo.exists()) {
return ErrFile::NotExist;
}
QString file;
QMap<qsizetype, QString> bookmarks;
QVector<QHexMetadataItem> metas;
WorkSpaceInfo infos;
if (WorkSpaceManager::loadWorkSpace(filename, file, bookmarks, metas,
infos)) {
// it's a workspace project
auto ret = openFile(file, encoding);
// apply the content
auto doc = m_hex->document();
auto render = m_hex->renderer();
doc->applyBookMarks(bookmarks);
doc->setBaseAddress(infos.base);
render->setEncoding(infos.encoding);
doc->metadata()->applyMetas(metas);
applyPluginData(infos.pluginData);
doc->setDocSaved();
m_docType = DocumentType::File;
m_isWorkSpace = true;
this->tabWidget()->setIcon(ICONRES(QStringLiteral("pro")));
return ret;
}
return ErrFile::Error;
}
ErrFile EditorView::openRegionFile(QString filename, qsizetype start,
qsizetype length, const QString &encoding) {
if (isCloneFile()) {
return ErrFile::ClonedFile;
}
QFileInfo info(filename);
if (info.exists()) {
if (Q_UNLIKELY(!info.permission(QFile::ReadUser))) {
return ErrFile::Permission;
}
auto readonly = !Utilities::fileCanWrite(filename);
auto *p =
QHexDocument::fromRegionFile(filename, start, length, readonly);
if (Q_UNLIKELY(p == nullptr)) {
return ErrFile::Permission;
}
m_docType = DocumentType::RegionFile;
m_hex->setDocument(QSharedPointer<QHexDocument>(p));
m_hex->setLockedFile(readonly);
m_hex->setKeepSize(true);
if (!encoding.isEmpty()) {
m_hex->renderer()->setEncoding(encoding);
}
p->setDocSaved();
m_fileName = info.absoluteFilePath();
m_isNewFile = false;
this->setWindowTitle(info.fileName());
connectDocSavedFlag(this);
auto tab = this->tabWidget();
tab->setIcon(Utilities::getIconFromFile(style(), m_fileName));
tab->setToolTip(m_fileName);
}
return ErrFile::Success;
}
ErrFile EditorView::openDriver(const QString &driver, const QString &encoding) {
if (isCloneFile()) {
return ErrFile::ClonedFile;
}
auto sdriver = Utilities::getStorageDevice(driver);
if (!sdriver.isValid()) {
return ErrFile::NotExist;
}
auto *p = QHexDocument::fromStorageDriver(sdriver);
if (Q_UNLIKELY(p == nullptr)) {
return ErrFile::Permission;
}
m_docType = DocumentType::Driver;
m_hex->setDocument(QSharedPointer<QHexDocument>(p));
m_hex->setLockedFile(true);
m_hex->setKeepSize(true);
if (!encoding.isEmpty()) {
m_hex->renderer()->setEncoding(encoding);
}
p->setDocSaved();
m_fileName = driver;
m_isNewFile = false;
this->setWindowTitle(m_fileName);
connectDocSavedFlag(this);
auto tab = this->tabWidget();
tab->setIcon(ICONRES(QStringLiteral("opendriver")));
tab->setToolTip(driver);
return ErrFile::Success;
}
ErrFile EditorView::save(const QString &workSpaceName, const QString &path,
bool ignoreMd5, bool isExport,
SaveWorkSpaceAttr workSpaceAttr) {
if (isCloneFile()) {
return this->cloneParent()->save(workSpaceName, path, ignoreMd5,
isExport, workSpaceAttr);
}
auto fileName = path.isEmpty() ? m_fileName : path;
auto doc = m_hex->document();
if (isNewFile()) {
if (fileName.isEmpty()) {
return ErrFile::IsNewFile;
}
}
if (isDriver()) {
if (!fileName.isEmpty()) {
return ErrFile::IsDirver;
}
}
QFile file(fileName);
switch (m_docType) {
case DocumentType::RegionFile: {
if (!ignoreMd5 && Utilities::getMd5(m_fileName) != m_md5) {
return ErrFile::SourceFileChanged;
}
if (!file.open(QFile::ReadWrite)) {
return ErrFile::Permission;
}
} break;
default: {
if (!file.open(QFile::WriteOnly)) {
return ErrFile::Permission;
}
} break;
}
if (workSpaceAttr == SaveWorkSpaceAttr::ForceWorkSpace ||
(workSpaceAttr == SaveWorkSpaceAttr::AutoWorkSpace &&
(m_isWorkSpace || hasMeta()))) {
WorkSpaceInfo infos;
infos.base = doc->baseAddress();
auto b = WorkSpaceManager::saveWorkSpace(
workSpaceName, m_fileName, doc->bookMarks(),
doc->metadata()->getAllMetadata(), infos);
if (!b)
return ErrFile::WorkSpaceUnSaved;
if (!isExport) {
m_isWorkSpace = true;
}
}
if (doc->saveTo(&file, true)) {
file.close();
if (!isExport) {
m_fileName = QFileInfo(fileName).absoluteFilePath();
doc->setDocSaved();
}
return ErrFile::Success;
}
return ErrFile::Permission;
}
ErrFile EditorView::reload() {
Q_ASSERT(m_docType != DocumentType::InValid);
if (isCloneFile()) {
return this->cloneParent()->reload();
}
if (isNewFile()) {
return ErrFile::IsNewFile;
}
switch (documentType()) {
case DocumentType::File:
return openFile(m_fileName, m_hex->renderer()->encoding());
case DocumentType::RegionFile: {
auto doc = qobject_cast<QFileRegionBuffer *>(m_hex->document());
Q_ASSERT(doc);
return openRegionFile(m_fileName, doc->readOffset(),
doc->readMaxBytes(),
m_hex->renderer()->encoding());
}
case DocumentType::Driver:
return openDriver(m_fileName, m_hex->renderer()->encoding());
default:
break;
}
return ErrFile::Error;
}
bool EditorView::change2WorkSpace() const {
return !m_isWorkSpace && hasMeta();
}
QHexView *EditorView::hexEditor() const { return m_hex; }
EditorView::DocumentType EditorView::documentType() const { return m_docType; }
WingEditorViewWidget *EditorView::otherEditor(qsizetype index) const {
if (index < 0 ||
index >= std::numeric_limits<decltype(m_others)::size_type>::max() ||
index >= m_others.size()) {
return nullptr;
}
return m_others.at(index);
}
void EditorView::setCopyLimit(qsizetype sizeMB) { m_hex->setCopyLimit(sizeMB); }
qsizetype EditorView::copyLimit() const { return m_hex->copyLimit(); }
void EditorView::connectDocSavedFlag(EditorView *editor) {
connect(editor->m_hex->document().get(), &QHexDocument::documentSaved, this,
[=](bool b) {
if (b) {
editor->setWindowTitle(m_fileName);
} else {
editor->setWindowTitle(QStringLiteral("* ") + m_fileName);
}
});
}
BookMarksModel *EditorView::bookmarksModel() const { return m_bookmarks; }
MetaDataModel *EditorView::metadataModel() const { return m_metadata; }
void EditorView::applySettings() {
auto &set = SettingManager::instance();
m_hex->setHeaderVisible(set.editorShowHeader());
m_hex->setAddressVisible(set.editorShowcol());
m_hex->setAsciiVisible(set.editorShowtext());
m_hex->setFontSize(set.editorfontSize());
m_hex->renderer()->SetEncoding(set.editorEncoding());
}
qsizetype EditorView::findAvailCloneIndex() {
return m_cloneChildren.indexOf(nullptr);
}
bool EditorView::hasMeta() const {
auto doc = m_hex->document();
return doc->metadata()->hasMetadata() || doc->bookMarksCount() > 0;
}
void EditorView::applyPluginData(const QHash<QString, QByteArray> &data) {
for (auto p = data.begin(); p != data.end(); p++) {
auto plgw = std::find_if(m_others.begin(), m_others.end(),
[=](const WingEditorViewWidget *editor) {
return editor->id() == p.key();
});
if (plgw != m_others.end()) {
(*plgw)->loadState(p.value());
}
}
}
QHash<QString, QByteArray> EditorView::savePluginData() {
QHash<QString, QByteArray> ret;
for (auto &item : m_others) {
ret.insert(item->id(), item->saveState());
}
return ret;
}
FindResultModel::FindInfo EditorView::readContextFinding(qsizetype offset,
qsizetype findSize,
int contextSize,
int maxDisplayBytes) {
auto doc = m_hex->document();
qsizetype halfSize = maxDisplayBytes / 2;
auto header = doc->read(offset, qMin(findSize, halfSize));
QByteArray tailer;
if (header.size() < findSize) {
tailer = doc->read(
offset, qMin(findSize, qsizetype(maxDisplayBytes) - halfSize));
}
auto left = qsizetype(maxDisplayBytes) - header.size() - tailer.size();
// append to contextSize
contextSize += (left / 2);
auto cheader = doc->read(offset - contextSize, contextSize);
auto ctailer = doc->read(offset + findSize, contextSize);
FindResultModel::FindInfo info;
info.cheader = cheader;
info.hbuffer = header;
info.tbuffer = tailer;
info.ctailer = ctailer;
return info;
}
EditorView *EditorView::cloneParent() const { return m_cloneParent; }
bool EditorView::isCloned() const { return m_cloneParent != nullptr; }
EditorView *EditorView::clone() {
if (isCloneFile()) {
Q_ASSERT(this->cloneParent());
return this->cloneParent()->clone();
}
auto cloneIndex = findAvailCloneIndex();
if (cloneIndex < 0) {
return nullptr;
}
auto ev = new EditorView(this->parentWidget());
connect(ev, &EditorView::destroyed, this, [=] {
this->m_cloneChildren[this->m_cloneChildren.indexOf(ev)] = nullptr;
});
auto doc = this->m_hex->document();
ev->m_cloneParent = this;
ev->m_hex->setDocument(doc, ev->m_hex->cursor());
ev->m_fileName = this->m_fileName;
if (doc->isDocSaved()) {
ev->setWindowTitle(QFileInfo(ev->m_fileName).fileName() +
QStringLiteral(" : ") +
QString::number(cloneIndex + 1));
} else {
ev->setWindowTitle(QStringLiteral("* ") +
QFileInfo(m_fileName).fileName());
}
ev->setIcon(this->icon());
ev->m_docType = DocumentType::Cloned;
for (auto &evw : m_others) {
ev->m_others << evw->clone();
}
this->m_cloneChildren[cloneIndex] = ev;
connectDocSavedFlag(ev);
return ev;
}
bool EditorView::isNewFile() const {
Q_ASSERT(m_docType != DocumentType::InValid);
if (isCloneFile()) {
return this->cloneParent()->isNewFile();
}
return m_isNewFile;
}
bool EditorView::isBigFile() const {
if (isCloneFile()) {
return this->cloneParent()->isBigFile();
}
return qobject_cast<QFileBuffer *>(m_hex->document()) != nullptr;
}
bool EditorView::isCloneFile() const {
return m_docType == EditorView::DocumentType::Cloned;
}
bool EditorView::isDriver() const {
if (isCloneFile()) {
return this->cloneParent()->isDriver();
}
return m_docType == EditorView::DocumentType::Driver;
}
FindResultModel *EditorView::findResultModel() const {
if (isCloneFile()) {
return this->cloneParent()->findResultModel();
}
return m_findResults;
}
void EditorView::setFontSize(qreal size) { m_hex->setFontSize(size); }
int EditorView::findResultCount() const {
if (isCloneFile()) {
return this->cloneParent()->findResultCount();
}
return m_findResults->size();
}
bool EditorView::isOriginWorkSpace() const {
Q_ASSERT(m_docType != DocumentType::InValid);
if (isCloneFile()) {
return this->cloneParent()->isOriginWorkSpace();
}
return m_isWorkSpace;
}
QString EditorView::fileName() const {
Q_ASSERT(m_docType != DocumentType::InValid);
if (isCloneFile()) {
return this->cloneParent()->fileName();
}
return m_fileName;
}