1
0
Fork 0

Merge pull request #3686 from x64dbg/release-notes

Release notes dialog
This commit is contained in:
Duncan Ogilvie 2025-08-19 20:51:04 +02:00 committed by GitHub
commit d6294a8370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 756 additions and 284 deletions

View File

@ -15,7 +15,7 @@
<value>style=allman, convert-tabs, align-pointer=type, align-reference=middle, indent=spaces, indent-namespaces, indent-col1-comments, pad-oper, unpad-paren, keep-one-line-blocks, close-templates</value>
</setting>
<setting name="Ignore" serializeAs="String">
<value>src/cross/vendor</value>
<value>src/cross/vendor;src/gui/Src/ThirdPartyLibs/md4c</value>
</setting>
<setting name="License" serializeAs="String">
<value />

View File

@ -30,7 +30,7 @@ jobs:
- name: Build
run: |
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_UNITY_BUILD=ON -DCMAKE_UNITY_BUILD_BATCH_SIZE=6
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_UNITY_BUILD=ON -DCMAKE_UNITY_BUILD_BATCH_SIZE=6 -DX64DBG_RELEASE=${{ startsWith(github.ref, 'refs/tags/') && 'ON' || 'OFF' }}
cmake --build build
- name: Upload Artifacts
@ -42,8 +42,31 @@ jobs:
include-hidden-files: true
retention-days: 1
docs:
# Skip building pull requests from the same repository
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository }}
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Build Documentation
run: |
docs\makechm.bat
- name: Upload Documentation
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/x64dbg.chm
if-no-files-found: error
include-hidden-files: true
retention-days: 1
package:
needs: cmake
needs: [cmake, docs]
runs-on: windows-latest
steps:
- name: Checkout
@ -63,7 +86,13 @@ jobs:
name: build-x86
path: bin
- name: Prepare release
- name: Download Documentation
uses: actions/download-artifact@v4
with:
name: docs
path: docs
- name: Prepare Release
run: |
curl.exe -L https://github.com/x64dbg/translations/releases/download/translations/qm.zip -o bin\qm.zip
7z x bin\qm.zip -obin
@ -71,7 +100,7 @@ jobs:
$timestamp = Get-Date (Get-Date).ToUniversalTime() -Format "yyyy-MM-dd_HH-mm"
echo "timestamp=$timestamp" >> $env:GITHUB_ENV
- name: Upload Artifacts
- name: Upload Snapshot
uses: actions/upload-artifact@v4
with:
name: snapshot_${{ env.timestamp }}
@ -83,7 +112,7 @@ jobs:
include-hidden-files: true
compression-level: 9
- name: Upload Artifacts
- name: Upload Symbols
uses: actions/upload-artifact@v4
with:
name: symbols-snapshot_${{ env.timestamp }}
@ -93,3 +122,13 @@ jobs:
if-no-files-found: error
include-hidden-files: true
compression-level: 9
- name: Upload Plugin SDK
uses: actions/upload-artifact@v4
with:
name: x64dbg-pluginsdk
path: |
release/pluginsdk
if-no-files-found: error
include-hidden-files: true
compression-level: 9

2
.gitignore vendored
View File

@ -5,6 +5,8 @@
/bin/*.ini
/bin/*.chm
/bin/*.zip
/bin/release-notes.md
!/bin/themes/
/src/**/x64/
/src/**/Win32/
/src/gui_build/

63
CMakeLists.txt generated
View File

@ -33,6 +33,7 @@ endif()
# Options
option(X64DBG_BUILD_IN_TREE "" ON)
option(X64DBG_RELEASE "" OFF)
# Variables
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
@ -53,32 +54,25 @@ find_package(Qt5 REQUIRED
WinExtras
)
# Target: zydis_wrapper
set(zydis_wrapper_SOURCES
cmake.toml
"src/zydis_wrapper/Zydis/Zydis.c"
"src/zydis_wrapper/Zydis/Zydis.h"
"src/zydis_wrapper/zydis_wrapper.cpp"
"src/zydis_wrapper/zydis_wrapper.h"
)
# Subdirectory: src/zydis_wrapper
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
if(CMAKE_FOLDER)
set(CMAKE_FOLDER "${CMAKE_FOLDER}/src/zydis_wrapper")
else()
set(CMAKE_FOLDER "src/zydis_wrapper")
endif()
add_subdirectory("src/zydis_wrapper")
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
add_library(zydis_wrapper STATIC)
target_sources(zydis_wrapper PRIVATE ${zydis_wrapper_SOURCES})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${zydis_wrapper_SOURCES})
target_compile_definitions(zydis_wrapper PUBLIC
ZYCORE_STATIC_BUILD
ZYDIS_STATIC_BUILD
)
target_include_directories(zydis_wrapper PUBLIC
"src/zydis_wrapper"
)
target_include_directories(zydis_wrapper PRIVATE
"src/zydis_wrapper/Zydis"
)
# Subdirectory: src/gui/Src/ThirdPartyLibs/md4c
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
if(CMAKE_FOLDER)
set(CMAKE_FOLDER "${CMAKE_FOLDER}/src/gui/Src/ThirdPartyLibs/md4c")
else()
set(CMAKE_FOLDER "src/gui/Src/ThirdPartyLibs/md4c")
endif()
add_subdirectory("src/gui/Src/ThirdPartyLibs/md4c")
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
# Target: bridge
set(bridge_SOURCES
@ -629,6 +623,8 @@ set(gui_SOURCES
"src/gui/Src/Gui/HexLineEdit.cpp"
"src/gui/Src/Gui/HexLineEdit.h"
"src/gui/Src/Gui/HexLineEdit.ui"
"src/gui/Src/Gui/ImageTextBrowser.cpp"
"src/gui/Src/Gui/ImageTextBrowser.h"
"src/gui/Src/Gui/LineEditDialog.cpp"
"src/gui/Src/Gui/LineEditDialog.h"
"src/gui/Src/Gui/LineEditDialog.ui"
@ -665,6 +661,9 @@ set(gui_SOURCES
"src/gui/Src/Gui/ReferenceManager.h"
"src/gui/Src/Gui/RegistersView.cpp"
"src/gui/Src/Gui/RegistersView.h"
"src/gui/Src/Gui/ReleaseNotesDialog.cpp"
"src/gui/Src/Gui/ReleaseNotesDialog.h"
"src/gui/Src/Gui/ReleaseNotesDialog.ui"
"src/gui/Src/Gui/RichTextItemDelegate.cpp"
"src/gui/Src/Gui/RichTextItemDelegate.h"
"src/gui/Src/Gui/SEHChainView.cpp"
@ -731,6 +730,9 @@ set(gui_SOURCES
"src/gui/Src/QHexEdit/XByteArray.cpp"
"src/gui/Src/QHexEdit/XByteArray.h"
"src/gui/Src/ThirdPartyLibs/ldconvert/ldconvert.h"
"src/gui/Src/ThirdPartyLibs/md4c/md4c-entity.h"
"src/gui/Src/ThirdPartyLibs/md4c/md4c-html.h"
"src/gui/Src/ThirdPartyLibs/md4c/md4c.h"
"src/gui/Src/Tracer/TraceBrowser.cpp"
"src/gui/Src/Tracer/TraceBrowser.h"
"src/gui/Src/Tracer/TraceDump.cpp"
@ -806,6 +808,12 @@ add_library(gui SHARED)
target_sources(gui PRIVATE ${gui_SOURCES})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${gui_SOURCES})
if(X64DBG_RELEASE) # X64DBG_RELEASE
target_compile_definitions(gui PUBLIC
X64DBG_RELEASE
)
endif()
target_compile_definitions(gui PRIVATE
BUILD_LIB
NOMINMAX
@ -833,11 +841,16 @@ if(NOT TARGET bridge)
message(FATAL_ERROR "Target \"bridge\" referenced by \"gui\" does not exist!")
endif()
if(NOT TARGET md4c-html)
message(FATAL_ERROR "Target \"md4c-html\" referenced by \"gui\" does not exist!")
endif()
target_link_libraries(gui PRIVATE
Qt5::Widgets
Qt5::WinExtras
zydis_wrapper
bridge
md4c-html
winmm
wininet
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -4,6 +4,7 @@ cmkr-include = "cmake/cmkr.cmake"
[options]
X64DBG_BUILD_IN_TREE = true
X64DBG_RELEASE = false
[variables]
CMAKE_MODULE_PATH = "${CMAKE_SOURCE_DIR}/cmake"
@ -26,24 +27,8 @@ x64 = "CMAKE_SIZEOF_VOID_P EQUAL 8"
[find-package]
Qt5 = { components = ["Widgets", "WinExtras"] }
[target.zydis_wrapper]
type = "static"
sources = [
"src/zydis_wrapper/*.cpp",
"src/zydis_wrapper/*.h",
"src/zydis_wrapper/Zydis/Zydis.h",
"src/zydis_wrapper/Zydis/Zydis.c",
]
include-directories = [
"src/zydis_wrapper",
]
private-include-directories = [
"src/zydis_wrapper/Zydis",
]
compile-definitions = [
"ZYCORE_STATIC_BUILD",
"ZYDIS_STATIC_BUILD",
]
[subdir."src/zydis_wrapper"]
[subdir."src/gui/Src/ThirdPartyLibs/md4c"]
[target.bridge]
type = "shared"
@ -160,6 +145,7 @@ private-link-libraries = [
"Qt5::WinExtras",
"::zydis_wrapper",
"::bridge",
"::md4c-html",
"winmm",
"wininet",
]
@ -186,6 +172,7 @@ private-compile-definitions = [
"NOMINMAX",
"X64DBG",
]
X64DBG_RELEASE.compile-definitions = ["X64DBG_RELEASE"]
include-after = ["cmake/deps.cmake"]
[target.gui.properties]

4
docs/.gitignore vendored
View File

@ -1,3 +1,7 @@
_build*/
*.chm
python-2.7.18.amd64.portable/
hha.dll
hhc.exe
itcc.dll
*.7z

View File

@ -89,7 +89,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build*', 'Thumbs.db', '.DS_Store', 'README.md', '.git', 'python-2.7.18.amd64.portable']
exclude_patterns = ['_build*', 'Thumbs.db', '.DS_Store', 'README.md', '.git', 'python-2.7.18.amd64.portable*']
# The reST default role (used for this markup: `text`) to use for all
# documents.

View File

@ -2,8 +2,14 @@
REM Command file for Sphinx documentation
set PYTHONNOUSERSITE=1
set PYTHONHOME=%CD%\python-2.7.18.amd64.portable
set PATH=%PYTHONHOME%;%PYTHONHOME%\Scripts;%PATH%
set PORTABLE_PYTHON=%~dp0python-2.7.18.amd64.portable
if not exist "%PORTABLE_PYTHON%" (
echo Portable Python not found!
exit /b 1
)
set PATH=%PORTABLE_PYTHON%;%PORTABLE_PYTHON%\Scripts;%PATH%
set SPHINXBUILD=sphinx-build
if "%SPHINXBUILD%" == "" (

View File

@ -1,13 +1,32 @@
@echo off
if "%VSVARSALLPATH%"=="" set VSVARSALLPATH=c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat
echo Setting VS Path
call "%VSVARSALLPATH%"
cd /d "%~dp0"
set PORTABLE_PYTHON=%~dp0python-2.7.18.amd64.portable
if not exist "%PORTABLE_PYTHON%\python.exe" (
echo Downloading portable Python...
curl.exe -L -O https://github.com/x64dbg/docs/releases/download/python27-portable/python-2.7.18.amd64.portable.7z
7z x python-2.7.18.amd64.portable.7z -opython-2.7.18.amd64.portable
)
if not exist "%~dp0hhc.exe" (
echo Downloading HTML Help Workshop...
pause
curl.exe -L -O https://github.com/x64dbg/deps/releases/download/dependencies/hhc-4.74.8702.7z
7z x hhc-4.74.8702.7z
)
echo Building Help Project
call make htmlhelp
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%
echo Applying CHM hacks
copy theme.js .\_build\htmlhelp\_static\js\theme.js
type hacks.css >> .\_build\htmlhelp\_static\css\theme.css
echo Building CHM File
hhc .\_build\htmlhelp\x64dbgdoc.hhp
copy .\_build\htmlhelp\x64dbgdoc.chm x64dbg.chm
echo Finished
hhc.exe .\_build\htmlhelp\x64dbgdoc.hhp
copy /Y .\_build\htmlhelp\x64dbgdoc.chm x64dbg.chm
if not exist "x64dbg.chm" (
echo Failed to build CHM file!
exit /b 1
)
echo Finished

View File

@ -143,11 +143,6 @@ endif()
# Target: release_notes
set(release_notes_SOURCES
cmake.toml
"release_notes/ImageTextBrowser.cpp"
"release_notes/ImageTextBrowser.h"
"release_notes/ReleaseNotesDialog.cpp"
"release_notes/ReleaseNotesDialog.h"
"release_notes/ReleaseNotesDialog.ui"
"release_notes/main.cpp"
)
@ -162,12 +157,8 @@ target_include_directories(release_notes PRIVATE
release_notes
)
if(NOT TARGET md4c-html)
message(FATAL_ERROR "Target \"md4c-html\" referenced by \"release_notes\" does not exist!")
endif()
target_link_libraries(release_notes PRIVATE
md4c-html
x64dbg::widgets
)
get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT)

View File

@ -59,7 +59,7 @@ sources = [
]
include-directories = ["release_notes"]
link-libraries = [
"::md4c-html",
"x64dbg::widgets",
]
[target.hex_viewer]

View File

@ -1,52 +0,0 @@
#include "ImageTextBrowser.h"
#include <QDebug>
#include <QResizeEvent>
ImageTextBrowser::ImageTextBrowser(QWidget* parent)
: QTextBrowser(parent)
, mResizeTimer(new QTimer(this))
{
mResizeTimer->setSingleShot(true);
mResizeTimer->setInterval(300);
connect(mResizeTimer, &QTimer::timeout, this, [this]()
{
qDebug() << "timeout";
setText(toHtml());
});
}
QVariant ImageTextBrowser::loadResource(int type, const QUrl & name)
{
auto url = name.toString();
if(url.startsWith("http"))
{
// TODO: download
}
else if(url.startsWith("data:"))
{
auto base64Index = url.indexOf(";base64,");
if(base64Index != -1)
{
auto data = QByteArray::fromBase64(url.mid(base64Index + 8).toUtf8());
auto image = QImage::fromData(data);
auto maxWidth = document()->textWidth() - document()->documentMargin() * 2 - 1;
if(image.width() > maxWidth)
{
image = image.scaledToWidth((int)maxWidth, Qt::SmoothTransformation);
}
return image;
}
}
return QTextBrowser::loadResource(type, name);
}
void ImageTextBrowser::resizeEvent(QResizeEvent* event)
{
if(event->size() != event->oldSize())
{
//qDebug() << "resizeEvent";
//mResizeTimer->start();
}
QTextBrowser::resizeEvent(event);
}

View File

@ -1,18 +0,0 @@
#pragma once
#include <QTextBrowser>
#include <QTimer>
class ImageTextBrowser : public QTextBrowser
{
Q_OBJECT
public:
explicit ImageTextBrowser(QWidget* parent = nullptr);
protected:
QVariant loadResource(int type, const QUrl & name) override;
void resizeEvent(QResizeEvent* event) override;
private:
QTimer* mResizeTimer = nullptr;
};

View File

@ -1,69 +0,0 @@
#include "ReleaseNotesDialog.h"
#include "ui_ReleaseNotesDialog.h"
#include <md4c-html.h>
static QString fixupHtmlEmojiBug(const QString & html)
{
// For some reason surrogates do not always display correctly in HTML elements.
// As a workaround we add a zero-width space in front of the high surrogate.
QString result;
int size = html.size();
result.reserve(size);
for(int i = 0; i < size; i++)
{
auto ch = html[i];
if(ch.isHighSurrogate() && i + 1 < size)
{
result += QChar(0x200B);
}
result += ch;
}
return result;
}
static QString markdownToHtml(const QByteArray & markdown)
{
auto appendString = [](const MD_CHAR * text, MD_SIZE size, void* userdata)
{
((std::string*)userdata)->append(text, size);
};
std::string html;
unsigned int parserFlags =
MD_FLAG_COLLAPSEWHITESPACE |
MD_FLAG_TABLES |
MD_FLAG_STRIKETHROUGH |
MD_FLAG_TASKLISTS |
MD_FLAG_PERMISSIVEAUTOLINKS |
MD_FLAG_LATEXMATHSPANS;
unsigned int rendererFlags = 0;
auto result = md_html(markdown.constData(), markdown.size(), appendString, &html, parserFlags, rendererFlags);
if(result != 0)
{
// TODO: throw an exception instead?
return {};
}
return QString::fromStdString(html);
}
ReleaseNotesDialog::ReleaseNotesDialog(const QByteArray & markdown, QWidget* parent)
: QDialog(parent)
, ui(new Ui::ReleaseNotesDialog)
{
ui->setupUi(this);
auto font = QApplication::font();
font.setPointSize(16);
ui->textBrowser->document()->setDefaultFont(font);
ui->textBrowser->setOpenExternalLinks(true);
auto html = markdownToHtml(markdown);
html = fixupHtmlEmojiBug(html);
ui->textBrowser->setText(html);
}
ReleaseNotesDialog::~ReleaseNotesDialog()
{
delete ui;
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <QDialog>
namespace Ui
{
class ReleaseNotesDialog;
}
class ReleaseNotesDialog : public QDialog
{
Q_OBJECT
public:
ReleaseNotesDialog(const QByteArray & markdown, QWidget* parent = nullptr);
~ReleaseNotesDialog();
private:
Ui::ReleaseNotesDialog* ui;
};

View File

@ -1,7 +1,7 @@
#include <QApplication>
#include <QFile>
#include "ReleaseNotesDialog.h"
#include <Gui/ReleaseNotesDialog.h>
int main(int argc, char* argv[])
{
@ -20,6 +20,10 @@ int main(int argc, char* argv[])
auto markdown = f.readAll();
QApplication a(argc, argv);
ReleaseNotesDialog d(markdown);
ReleaseNotesDialog d({});
if(!d.setMarkdown(QString::fromUtf8(markdown), "https://github.com/x64dbg/x64dbg/issues/"))
{
puts("Failed to convert markdown!");
}
return d.exec();
}

20
src/cross/vendor/CMakeLists.txt generated vendored
View File

@ -66,23 +66,3 @@ FetchContent_Declare(udmp-parser
src
)
FetchContent_MakeAvailable(udmp-parser)
# Target: md4c-html
set(md4c-html_SOURCES
cmake.toml
"md4c/md4c-entity.c"
"md4c/md4c-entity.h"
"md4c/md4c-html.c"
"md4c/md4c-html.h"
"md4c/md4c.c"
"md4c/md4c.h"
)
add_library(md4c-html STATIC)
target_sources(md4c-html PRIVATE ${md4c-html_SOURCES})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${md4c-html_SOURCES})
target_include_directories(md4c-html PUBLIC
md4c
)

View File

@ -26,9 +26,3 @@ tag = "3e98b047c52c07bb1816bd0936f561ce7797469d"
git = "https://github.com/mrexodia/udmp-parser"
tag = "bc5952eda39ba0b1a07fc57f57d166292f1e9563"
subdir = "src"
# https://github.com/mity/md4c/commit/481fbfbdf72daab2912380d62bb5f2187d438408
[target.md4c-html]
type = "static"
sources = ["md4c/*.c", "md4c/*.h"]
include-directories = ["md4c"]

View File

@ -5,6 +5,11 @@ add_subdirectory(
${CMAKE_CURRENT_BINARY_DIR}/zydis_wrapper
)
add_subdirectory(
${widgets_SOURCE_DIR}/ThirdPartyLibs/md4c
${CMAKE_CURRENT_BINARY_DIR}/md4c
)
include(Qt.cmake)
set(hook_SOURCES
@ -58,6 +63,11 @@ set(widgets_SOURCES
${widgets_SOURCE_DIR}/Gui/TypeWidget.h
${widgets_SOURCE_DIR}/Gui/RichTextItemDelegate.cpp
${widgets_SOURCE_DIR}/Gui/RichTextItemDelegate.h
${widgets_SOURCE_DIR}/Gui/ReleaseNotesDialog.cpp
${widgets_SOURCE_DIR}/Gui/ReleaseNotesDialog.h
${widgets_SOURCE_DIR}/Gui/ReleaseNotesDialog.ui
${widgets_SOURCE_DIR}/Gui/ImageTextBrowser.cpp
${widgets_SOURCE_DIR}/Gui/ImageTextBrowser.h
${widgets_SOURCE_DIR}/Memory/MemoryPage.cpp
${widgets_SOURCE_DIR}/Memory/MemoryPage.h
${widgets_SOURCE_DIR}/Utils/ActionHelpers.h
@ -98,6 +108,7 @@ target_compile_definitions(x64dbg_widgets PRIVATE
target_link_libraries(x64dbg_widgets PUBLIC
${QT_LIBRARIES}
zydis_wrapper
md4c-html
)
# https://doc.qt.io/qt-6/wasm.html#asyncify

View File

@ -4,6 +4,7 @@
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <string.h>
// TODO: do something cross platform
using duint = uint64_t;
@ -18,7 +19,12 @@ int sprintf_s(char (&Dest)[Count], const char* fmt, Args... args)
inline size_t strcpy_s(char* dst, size_t size, const char* src)
{
return strlcpy(dst, src, size);
if(!dst || !src || !size)
return 0;
size_t i = 0;
while(i < size - 1 && src[i]) dst[i] = src[i], i++;
dst[i] = 0;
return i;
}
#endif // _WIN32

View File

@ -0,0 +1,68 @@
#include "ImageTextBrowser.h"
#include <QResizeEvent>
#include <QApplication>
#include <QScrollBar>
#include <QTextBlock>
ImageTextBrowser::ImageTextBrowser(QWidget* parent)
: QTextBrowser(parent)
, mResizeTimer(new QTimer(this))
{
mResizeTimer->setSingleShot(true);
mResizeTimer->setInterval(300);
connect(mResizeTimer, &QTimer::timeout, this, [this]()
{
setText(toHtml());
auto vbar = verticalScrollBar();
vbar->setValue(vbar->maximum() * mSavedScrollPercentage);
});
}
void ImageTextBrowser::resizeImages()
{
auto vbar = verticalScrollBar();
auto max = vbar->maximum();
mSavedScrollPercentage = (max > 0) ? (qreal)vbar->value() / max : 0.0;
mResizeTimer->start();
}
QVariant ImageTextBrowser::loadResource(int type, const QUrl & name)
{
QImage image;
auto url = name.toString();
if(url.startsWith("http"))
{
auto itr = mImageCache.find(url);
if(itr != mImageCache.end())
{
image = itr.value();
}
else if(mDownloadFn)
{
image = mDownloadFn(url);
mImageCache.insert(url, image);
}
}
else if(url.startsWith("data:"))
{
auto base64Index = url.indexOf(";base64,");
if(base64Index != -1)
{
auto data = QByteArray::fromBase64(url.mid(base64Index + 8).toUtf8());
image = QImage::fromData(data);
}
}
else
{
return QTextBrowser::loadResource(type, name);
}
// Scale the image to the width of the document
auto maxWidth = viewport()->width() - document()->documentMargin() * 2;
if(image.width() > maxWidth)
{
image = image.scaledToWidth((int)maxWidth, Qt::SmoothTransformation);
}
return image;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <QTextBrowser>
#include <QTextCursor>
#include <QTimer>
#include <QMap>
#include <functional>
class ImageTextBrowser : public QTextBrowser
{
Q_OBJECT
public:
explicit ImageTextBrowser(QWidget* parent = nullptr);
void resizeImages();
using DownloadFn = std::function<QImage(const QString &)>;
void setDownloadFn(DownloadFn fn)
{
mDownloadFn = std::move(fn);
}
protected:
QVariant loadResource(int type, const QUrl & name) override;
private:
qreal mSavedScrollPercentage = 0.0;
QTimer* mResizeTimer = nullptr;
DownloadFn mDownloadFn;
QMap<QString, QImage> mImageCache;
};

View File

@ -2,6 +2,7 @@
#include "ui_MainWindow.h"
#include <QMutex>
#include <QMessageBox>
#include <QToolButton>
#include <QIcon>
#include <QUrl>
#include <QFileDialog>
@ -53,6 +54,7 @@
#include "MRUList.h"
#include "AboutDialog.h"
#include "UpdateChecker.h"
#include "Gui/ReleaseNotesDialog.h"
#include "Tracer/TraceManager.h"
//#include "Tracer/TraceWidget.h"
#include "Utils/MethodInvoker.h"
@ -390,6 +392,11 @@ MainWindow::MainWindow(QWidget* parent)
setupThemesMenu();
setupMenuCustomization();
ui->actionAbout_Qt->setIcon(QApplication::style()->standardIcon(QStyle::SP_TitleBarMenuButton));
auto toolButton = qobject_cast<QToolButton*>(ui->mainToolBar->widgetForAction(ui->actionCheckUpdates));
if(toolButton)
{
toolButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
}
// Set default setttings (when not set)
SettingsDialog defaultSettings;
@ -1056,6 +1063,21 @@ void MainWindow::loadWindowSettings()
tr("You are running x64dbg on an unsupported operating system version. <b>Future updates will completely stop running on this system.</b><br><br>For more information, see the official <a href=\"%1\">announcement</a>.").arg("https://transition.x64dbg.com")
);
}
#ifdef X64DBG_RELEASE
auto compileDate = QDateTime(GetCompileDate());
compileDate.setTimeSpec(Qt::UTC);
auto compileEpoch = compileDate.toSecsSinceEpoch();
duint releaseNotesEpoch = 0;
BridgeSettingGetUint("Gui", "ReleaseNotesEpoch", &releaseNotesEpoch);
if(releaseNotesEpoch < compileEpoch)
{
showReleaseNotes(releaseNotesEpoch);
BridgeSettingSetUint("Gui", "ReleaseNotesPrevEpoch", releaseNotesEpoch);
BridgeSettingSetUint("Gui", "ReleaseNotesEpoch", compileEpoch);
BridgeSettingFlush();
}
#endif // X64DBG_RELEASE
}
void MainWindow::setGlobalShortcut(QAction* action, const QKeySequence & key)
@ -1185,6 +1207,50 @@ QAction* MainWindow::makeCommandAction(QAction* action, const QString & command)
return action;
}
void MainWindow::showReleaseNotes(duint cutoffEpoch)
{
QFile file(QString("%1/../release-notes.md").arg(QCoreApplication::applicationDirPath()));
if(!file.open(QFile::ReadOnly))
{
SimpleErrorBox(
this,
tr("Error"),
tr("Release notes are not available, see <a href=\"%1\">%2</a> for the latest updates.")
.arg("https://update.x64dbg.com")
.arg("update.x64dbg.com")
);
return;
}
auto markdown = QString::fromUtf8(file.readAll());
file.close();
if(cutoffEpoch)
{
static QRegularExpression re(R"(<!-- *(\d\d\d\d.\d\d.\d\d) *-->)");
auto i = re.globalMatch(markdown);
QStringList words;
while(i.hasNext())
{
QRegularExpressionMatch match = i.next();
auto matchText = match.captured(1);
auto matchDate = QDateTime::fromString(matchText, "yyyy.MM.dd");
matchDate.setTimeSpec(Qt::UTC);
auto matchEpoch = matchDate.toSecsSinceEpoch();
if(matchEpoch <= cutoffEpoch)
{
markdown = markdown.left(match.capturedStart(0));
break;
}
}
}
ReleaseNotesDialog dialog({}, this);
dialog.move(frameGeometry().center() - dialog.rect().center());
dialog.setMarkdown(markdown, "https://github.com/x64dbg/x64dbg/issues/");
dialog.setWindowIcon(DIcon("bug"));
dialog.exec();
}
void MainWindow::execCommandSlot()
{
QAction* action = qobject_cast<QAction*>(sender());
@ -2765,6 +2831,13 @@ void MainWindow::on_actionAbout_Qt_triggered()
delete w;
}
void MainWindow::on_actionReleaseNotes_triggered()
{
duint cutoffEpoch = 0;
BridgeSettingGetUint("Gui", "ReleaseNotesPrevEpoch", &cutoffEpoch);
showReleaseNotes(cutoffEpoch);
}
void MainWindow::updateStyle()
{
// Set configured link color

View File

@ -196,6 +196,7 @@ private:
void onMenuCustomized();
void setupMenuCustomization();
QAction* makeCommandAction(QAction* action, const QString & command);
void showReleaseNotes(duint cutoffEpoch);
//lists for menu customization
QList<QAction*> mFileMenuStrings;
@ -289,4 +290,5 @@ private slots:
void on_actionCheckUpdates_triggered();
void on_actionDefaultTheme_triggered();
void on_actionAbout_Qt_triggered();
void on_actionReleaseNotes_triggered();
};

View File

@ -16,6 +16,9 @@
<property name="windowTitle">
<string>x64dbg</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<widget class="QWidget" name="centralWidget"/>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
@ -160,6 +163,7 @@
<addaction name="actionManual"/>
<addaction name="actionFaq"/>
<addaction name="actionAbout"/>
<addaction name="actionReleaseNotes"/>
<addaction name="actionAbout_Qt"/>
<addaction name="separator"/>
<addaction name="actionCrashDump"/>
@ -300,7 +304,7 @@
<bool>false</bool>
</property>
<property name="allowedAreas">
<set>Qt::BottomToolBarArea</set>
<set>Qt::NoToolBarArea</set>
</property>
<attribute name="toolBarArea">
<enum>BottomToolBarArea</enum>
@ -1549,6 +1553,15 @@
<string>Output the detailed help information about an assembly mnemonic to the log. Equivalent command &quot;mnemonichelp name&quot;.</string>
</property>
</action>
<action name="actionReleaseNotes">
<property name="icon">
<iconset theme="bug" resource="../../resource.qrc">
<normaloff>:/Default/icons/bug.png</normaloff>:/Default/icons/bug.png</iconset>
</property>
<property name="text">
<string>Release Notes</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>

View File

@ -0,0 +1,267 @@
#include "ReleaseNotesDialog.h"
#include "ui_ReleaseNotesDialog.h"
#include <QRegularExpression>
#include <QDesktopServices>
#include <QResizeEvent>
#include <QScrollBar>
#include <md4c-html.h>
struct BadEmoji
{
QString original;
QString replacement;
BadEmoji(const char* emoji) // NOLINT(google-explicit-constructor)
: original(QString::fromUtf8(emoji))
{
replacement = QString("<span class=\"emoji\">%1</span>").arg(original);
}
};
static QString fixupHtmlEmojiBug(QString html)
{
// Certain emojis do not display correctly on Windows with the Segoe UI font.
// As a workaround we enclose them in a <span class="emoji">
#ifdef Q_OS_WINDOWS
static BadEmoji badEmojis[] =
{
"\xe2\x98\xba", // ☺
"\xe2\x98\xb9", // ☹
"\xe2\x98\xa0", // ☠
"\xe2\x9d\xa3", // ❣
"\xe2\x9d\xa4", // ❤
"\xf0\x9f\x97\xa8", // 🗨
"\xe2\x9c\x8c", // ✌
"\xe2\x98\x9d", // ☝
"\xe2\x9c\x8d", // ✍
"\xe2\x99\xa8", // ♨
"\xe2\x9c\x88", // ✈
"\xe2\x98\x80", // ☀
"\xe2\x98\x81", // ☁
"\xe2\x9d\x84", // ❄
"\xe2\x98\x84", // ☄
"\xe2\x99\xa0", // ♠
"\xe2\x99\xa5", // ♥
"\xe2\x99\xa6", // ♦
"\xe2\x99\xa3", // ♣
"\xe2\x99\x9f", // ♟
"\xe2\x9c\x8f", // ✏
"\xe2\x9c\x92", // ✒
"\xe2\x9c\x82", // ✂
"\xe2\x98\xa2", // ☢
"\xe2\x98\xa3", // ☣
"\xe2\x86\x97", // ↗
"\xe2\x9e\xa1", // ➡
"\xe2\x86\x98", // ↘
"\xe2\x86\x99", // ↙
"\xe2\x86\x96", // ↖
"\xe2\x86\x95", // ↕
"\xe2\x86\x94", // ↔
"\xe2\x86\xa9", // ↩
"\xe2\x86\xaa", // ↪
"\xe2\xa4\xb4", // ⤴
"\xe2\xa4\xb5", // ⤵
"\xe2\x9c\xa1", // ✡
"\xe2\x98\xb8", // ☸
"\xe2\x98\xaf", // ☯
"\xe2\x9c\x9d", // ✝
"\xe2\x98\xa6", // ☦
"\xe2\x98\xaa", // ☪
"\xe2\x98\xae", // ☮
"\xe2\x99\x88", // ♈
"\xe2\x99\x89", // ♉
"\xe2\x99\x8a", // ♊
"\xe2\x99\x8b", // ♋
"\xe2\x99\x8c", // ♌
"\xe2\x99\x8d", // ♍
"\xe2\x99\x8e", // ♎
"\xe2\x99\x8f", // ♏
"\xe2\x99\x90", // ♐
"\xe2\x99\x91", // ♑
"\xe2\x99\x92", // ♒
"\xe2\x99\x93", // ♓
"\xe2\x96\xb6", // ▶
"\xe2\x97\x80", // ◀
"\xe2\x99\x80", // ♀
"\xe2\x99\x82", // ♂
"\xe2\x9a\xa7", // ⚧
"\xe2\x9c\x96", // ✖
"\xe2\x80\xbc", // ‼
"\xe2\x81\x89", // ⁉
"\xe3\x80\xb0", // 〰
"\xe2\x98\x91", // ☑
"\xe2\x9c\x94", // ✔
"\xe3\x80\xbd", // 〽
"\xe2\x9c\xb3", // ✳
"\xe2\x9c\xb4", // ✴
"\xe2\x9d\x87", // ❇
"\xe2\x84\xa2", // ™
"\xe2\x93\x82", // Ⓜ
"\xf0\x9f\x88\x81", // 🈁
"\xf0\x9f\x88\x82", // 🈂
"\xf0\x9f\x88\xb7", // 🈷
"\xf0\x9f\x88\xb6", // 🈶
"\xf0\x9f\x88\xaf", // 🈯
"\xf0\x9f\x89\x90", // 🉐
"\xf0\x9f\x88\xb9", // 🈹
"\xf0\x9f\x88\x9a", // 🈚
"\xf0\x9f\x88\xb2", // 🈲
"\xf0\x9f\x89\x91", // 🉑
"\xf0\x9f\x88\xb8", // 🈸
"\xf0\x9f\x88\xb4", // 🈴
"\xf0\x9f\x88\xb3", // 🈳
"\xe3\x8a\x97", // ㊗
"\xe3\x8a\x99", // ㊙
"\xf0\x9f\x88\xba", // 🈺
"\xf0\x9f\x88\xb5", // 🈵
"\xe2\x97\xbc", // ◼
"\xe2\x98\x82", // ☂
"\xe2\x9c\x89", // ✉
"\xe2\x96\xab", // ▫
"\xe2\x96\xaa", // ▪
"\xe2\x97\xbd", // ◽
"\xe2\x97\xbe", // ◾
};
for(const auto & badEmoji : badEmojis)
{
html = html.replace(badEmoji.original, badEmoji.replacement);
}
#endif // Q_OS_WINDOWS
// For some reason surrogates do not always display correctly in HTML elements.
// As a workaround we add a zero-width space in front of the high surrogate.
QString result;
int size = html.size();
result.reserve(size);
for(int i = 0; i < size; i++)
{
auto ch = html.at(i);
if(ch.isHighSurrogate())
{
result += QChar(0x200B);
for(; i < size; i++)
{
ch = html.at(i);
result += ch;
if(ch.unicode() < 0x0080)
{
break;
}
}
}
else
{
result += ch;
}
}
return result;
}
static QString markdownToHtml(const QString & markdown)
{
auto appendString = [](const MD_CHAR * text, MD_SIZE size, void* userdata)
{
((std::string*)userdata)->append(text, size);
};
std::string html = "<body>";
unsigned int parserFlags =
MD_FLAG_COLLAPSEWHITESPACE |
MD_FLAG_TABLES |
MD_FLAG_STRIKETHROUGH |
MD_FLAG_TASKLISTS |
MD_FLAG_PERMISSIVEAUTOLINKS |
MD_FLAG_LATEXMATHSPANS;
unsigned int rendererFlags = 0;
auto markdownUtf8 = markdown.toUtf8();
if(md_html(markdownUtf8.constData(), markdownUtf8.size(), appendString, &html, parserFlags, rendererFlags) != 0)
{
return {};
}
html += "</body>";
return QString::fromStdString(html);
}
static void markdownGithubLinks(QString & markdown, const QString & issueUrl)
{
static QRegularExpression usernameRegex(R"((?<=^|\s)@([a-zA-Z0-9-]{1,39})(?=\s|$))");
markdown.replace(usernameRegex, R"([@\1](https://github.com/\1))");
if(!issueUrl.isEmpty())
{
static QRegularExpression issueRegex(R"((?<=^|\s)#(\d+)(?=\s|$))");
markdown.replace(issueRegex, QString(R"([#\1](%1\1))").arg(issueUrl));
}
}
ReleaseNotesDialog::ReleaseNotesDialog(ImageTextBrowser::DownloadFn downloadFn, QWidget* parent)
: QDialog(parent)
, ui(new Ui::ReleaseNotesDialog)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
ui->textBrowser->setDownloadFn(std::move(downloadFn));
#ifdef Q_OS_WINDOWS
QFont font("Segoe UI");
#else
QFont font = QApplication::font();
#endif // Q_OS_WINDOWS
font.setHintingPreference(QFont::PreferNoHinting);
font.setPixelSize(16);
font.setStyleHint(QFont::SansSerif);
ui->textBrowser->document()->setDefaultFont(font);
QPalette palette = ui->textBrowser->palette();
if(palette.color(QPalette::Text) == Qt::black)
{
palette.setColor(QPalette::Text, QColor("#1f2328"));
ui->textBrowser->setPalette(palette);
}
ui->textBrowser->document()->setDocumentMargin(14);
QString styleSheet = R"(
h1, h2, h3, h4, h5, h6, strong, b {
font-weight: 500;
})";
#ifdef Q_OS_WINDOWS
styleSheet += R"(
.emoji {
font-family: "Segoe UI Emoji";
})";
#endif // Q_OS_WINDOWS
ui->textBrowser->document()->setDefaultStyleSheet(styleSheet);
ui->textBrowser->setOpenLinks(false);
connect(ui->textBrowser, &QTextBrowser::anchorClicked, this, [this](const QUrl & url)
{
ui->textBrowser->clearFocus();
QDesktopServices::openUrl(url);
});
}
ReleaseNotesDialog::~ReleaseNotesDialog()
{
delete ui;
}
bool ReleaseNotesDialog::setMarkdown(QString markdown, const QString & issueUrl)
{
markdownGithubLinks(markdown, issueUrl);
auto html = markdownToHtml(markdown);
html = fixupHtmlEmojiBug(html);
ui->textBrowser->setText(html);
return !html.isEmpty();
}
void ReleaseNotesDialog::setLabel(const QString & text)
{
ui->label->setText(text);
}
void ReleaseNotesDialog::resizeEvent(QResizeEvent* event)
{
QDialog::resizeEvent(event);
ui->textBrowser->resizeImages();
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <QDialog>
#include <functional>
#include "ImageTextBrowser.h"
namespace Ui
{
class ReleaseNotesDialog;
}
class ReleaseNotesDialog : public QDialog
{
Q_OBJECT
public:
explicit ReleaseNotesDialog(ImageTextBrowser::DownloadFn downloadFn, QWidget* parent = nullptr);
~ReleaseNotesDialog() override;
bool setMarkdown(QString markdown, const QString & issueUrl);
void setLabel(const QString & text);
protected:
void resizeEvent(QResizeEvent* event) override;
private:
Ui::ReleaseNotesDialog* ui;
};

View File

@ -35,15 +35,15 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>4</number>
<number>8</number>
</property>
<property name="rightMargin">
<number>4</number>
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -53,6 +53,19 @@
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
@ -68,7 +81,7 @@
<customwidget>
<class>ImageTextBrowser</class>
<extends>QTextBrowser</extends>
<header>ImageTextBrowser.h</header>
<header location="global">Gui/ImageTextBrowser.h</header>
</customwidget>
</customwidgets>
<resources/>

View File

@ -0,0 +1,12 @@
# https://github.com/mity/md4c/commit/481fbfbdf72daab2912380d62bb5f2187d438408
add_library(md4c-html STATIC
md4c.c
md4c.h
md4c-entity.c
md4c-entity.h
md4c-html.c
md4c-html.h
)
target_include_directories(md4c-html PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@ -2,6 +2,10 @@
#include <QMessageBox>
#include <QIcon>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <Gui/ReleaseNotesDialog.h>
#include "StringUtil.h"
#include "MiscUtil.h"
@ -136,10 +140,25 @@ static std::string httpGet(const char* url,
#endif // _WIN32
UpdateChecker::UpdateChecker(QWidget* parent)
: QThread(parent),
mParent(parent)
: QThread(parent)
, mParent(parent)
, mUserAgent("x64dbg " + ToDateString(GetCompileDate()) + " " __TIME__)
{
connect(this, &UpdateChecker::updateCheckFinished, this, &UpdateChecker::finishedSlot);
auto downloadFn = [this](const QString & url) -> QImage
{
auto data = httpGet(url.toUtf8().constData(), mUserAgent.toUtf8().constData(), 3000);
if(data.empty())
{
return {};
}
QByteArray byteArray(data.c_str(), (int)(data.size()));
return QImage::fromData(byteArray);
};
mReleaseNotes = new ReleaseNotesDialog(downloadFn, mParent);
mReleaseNotes->setWindowIcon(DIcon("bug"));
}
void UpdateChecker::checkForUpdates()
@ -153,9 +172,8 @@ void UpdateChecker::checkForUpdates()
void UpdateChecker::run()
{
QString userAgent = "x64dbg " + ToDateString(GetCompileDate()) + " " __TIME__;
std::string result = httpGet("https://update.x64dbg.com/releases.json",
userAgent.toUtf8().constData(), 3000);
mUserAgent.toUtf8().constData(), 3000);
emit updateCheckFinished(QString::fromStdString(result));
}
@ -167,26 +185,72 @@ void UpdateChecker::finishedSlot(const QString & json)
return;
}
QRegExp regExp("\"published_at\": ?\"([^\"]+)\"");
QDateTime serverTime;
if(regExp.indexIn(json) >= 0)
serverTime = QDateTime::fromString(regExp.cap(1), Qt::ISODate);
if(!serverTime.isValid())
QJsonParseError error;
auto releases = QJsonDocument::fromJson(json.toUtf8(), &error).array();
if(error.error != QJsonParseError::NoError || releases.isEmpty())
{
SimpleErrorBox(mParent, tr("Error!"), tr("File on server could not be parsed..."));
return;
}
QRegExp regUrl("\"browser_download_url\": ?\"([^\"]+)\"");
auto url = regUrl.indexIn(json) >= 0 ? regUrl.cap(1) : "https://releases.x64dbg.com";
auto server = serverTime.date();
auto build = GetCompileDate();
QString info;
if(server > build)
info = QString(tr("New build %1 available!<br>Download <a href=\"%2\">here</a><br><br>You are now on build %3")).arg(ToDateString(server)).arg(url).arg(ToDateString(build));
else if(server < build)
info = QString(tr("You have a development build (%1) of x64dbg!")).arg(ToDateString(build));
else
info = QString(tr("You have the latest build (%1) of x64dbg!")).arg(ToDateString(build));
GuiAddStatusBarMessage((info + "\n").toUtf8().constData());
SimpleInfoBox(mParent, tr("Information"), info);
QString markdown;
QString label;
auto buildDate = GetCompileDate();
for(int i = 0; i < releases.size(); i++)
{
QJsonObject release = releases[i].toObject();
auto publishedAt = release.value("published_at").toString();
auto publishedDate = QDateTime::fromString(publishedAt, Qt::ISODate).date();
auto tagName = release.value("tag_name").toString();
if(i == 0)
{
if(publishedDate <= buildDate)
{
QString info;
if(publishedDate < buildDate)
{
info = QString(tr("You have a development build (%1) of x64dbg!")).arg(ToDateString(buildDate));
}
else
{
info = tr("You have the latest build (%1) of x64dbg!").arg(ToDateString(buildDate));
}
GuiAddStatusBarMessage((info + "\n").toUtf8().constData());
SimpleInfoBox(mParent, tr("Information"), info);
return;
}
QString downloadUrl;
auto assets = release.value("assets").toArray();
for(int j = 0; j < assets.size(); j++)
{
auto asset = assets[i].toObject();
downloadUrl = asset.value("browser_download_url").toString();
break;
}
label = tr("<p><b>New x64dbg version available</b>: <a href=\"%1\">%2</a></p>").arg(downloadUrl, tagName);
GuiAddLogMessageHtml((label + "\n").toUtf8().constData());
}
// Do not show release notes older than the current build
if(publishedDate <= buildDate)
break;
auto body = release.value("body").toString();
if(!body.startsWith("#"))
{
markdown += QString("# %1\n").arg(tagName);
}
markdown += body;
markdown += QString("\n\n<sub>%1</sub>").arg(publishedAt);
markdown += "\n\n";
}
mReleaseNotes->move(mParent->frameGeometry().center() - mReleaseNotes->rect().center());
mReleaseNotes->setMarkdown(markdown, "https://github.com/x64dbg/x64dbg/issues/");
mReleaseNotes->setLabel(label);
mReleaseNotes->exec();
}

View File

@ -2,6 +2,8 @@
#include <QThread>
class ReleaseNotesDialog;
class UpdateChecker : public QThread
{
Q_OBJECT
@ -20,4 +22,6 @@ private slots:
private:
QWidget* mParent = nullptr;
ReleaseNotesDialog* mReleaseNotes = nullptr;
QString mUserAgent;
};