From 23a9148f19071a895d709db3c2c1a60e6a8b5e6c Mon Sep 17 00:00:00 2001 From: Xusheng Date: Tue, 17 Mar 2026 16:42:09 +0800 Subject: [PATCH 1/2] Support bookmarking timestamps in TTD. Fix https://github.com/Vector35/debugger/issues/704 --- api/debuggerapi.h | 19 + api/debuggercontroller.cpp | 45 +++ api/ffi.h | 17 + api/python/debuggercontroller.py | 129 +++++++ core/debuggercommon.h | 12 + core/debuggercontroller.cpp | 150 ++++++++ core/debuggercontroller.h | 7 + core/ffi.cpp | 54 +++ ui/ttdbookmarkwidget.cpp | 610 +++++++++++++++++++++++++++++++ ui/ttdbookmarkwidget.h | 162 ++++++++ ui/ttdcallswidget.cpp | 59 +++ ui/ttdeventswidget.cpp | 54 +++ ui/ttdmemorywidget.cpp | 53 +++ ui/ui.cpp | 6 + 14 files changed, 1377 insertions(+) create mode 100644 ui/ttdbookmarkwidget.cpp create mode 100644 ui/ttdbookmarkwidget.h diff --git a/api/debuggerapi.h b/api/debuggerapi.h index 8f047a32..69d7b3db 100644 --- a/api/debuggerapi.h +++ b/api/debuggerapi.h @@ -627,6 +627,18 @@ namespace BinaryNinjaDebuggerAPI { }; + struct TTDBookmark + { + TTDPosition position; + uint64_t viewAddress; + std::string note; + + TTDBookmark() : viewAddress(0) {} + TTDBookmark(const TTDPosition& pos, const std::string& n = "", uint64_t addr = 0) + : position(pos), viewAddress(addr), note(n) {} + }; + + typedef BNDebugAdapterConnectionStatus DebugAdapterConnectionStatus; typedef BNDebugAdapterTargetStatus DebugAdapterTargetStatus; @@ -831,6 +843,13 @@ namespace BinaryNinjaDebuggerAPI { std::pair GetTTDNextMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); std::pair GetTTDPrevMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); + // TTD Bookmark Methods + std::vector GetTTDBookmarks(); + bool AddTTDBookmark(const TTDPosition& position, const std::string& note = "", uint64_t viewAddress = 0); + bool RemoveTTDBookmark(const TTDPosition& position); + bool UpdateTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress); + void ClearTTDBookmarks(); + // TTD Code Coverage Analysis Methods bool IsInstructionExecuted(uint64_t address); bool RunCodeCoverageAnalysis(uint64_t startAddress, uint64_t endAddress, TTDPosition startTime, TTDPosition endTime); diff --git a/api/debuggercontroller.cpp b/api/debuggercontroller.cpp index a8eb0c59..8db8cb89 100644 --- a/api/debuggercontroller.cpp +++ b/api/debuggercontroller.cpp @@ -1430,6 +1430,51 @@ std::vector DebuggerController::GetAllTTDEvents() } +std::vector DebuggerController::GetTTDBookmarks() +{ + std::vector result; + size_t count = 0; + BNDebuggerTTDBookmark* bookmarks = BNDebuggerGetTTDBookmarks(m_object, &count); + if (!bookmarks) + return result; + + for (size_t i = 0; i < count; ++i) + { + TTDBookmark bm; + bm.position = TTDPosition(bookmarks[i].position.sequence, bookmarks[i].position.step); + bm.viewAddress = bookmarks[i].viewAddress; + bm.note = bookmarks[i].note ? std::string(bookmarks[i].note) : ""; + result.push_back(bm); + } + + BNDebuggerFreeTTDBookmarks(bookmarks, count); + return result; +} + +bool DebuggerController::AddTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress) +{ + BNDebuggerTTDPosition pos = {position.sequence, position.step}; + return BNDebuggerAddTTDBookmark(m_object, pos, note.c_str(), viewAddress); +} + +bool DebuggerController::RemoveTTDBookmark(const TTDPosition& position) +{ + BNDebuggerTTDPosition pos = {position.sequence, position.step}; + return BNDebuggerRemoveTTDBookmark(m_object, pos); +} + +bool DebuggerController::UpdateTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress) +{ + BNDebuggerTTDPosition pos = {position.sequence, position.step}; + return BNDebuggerUpdateTTDBookmark(m_object, pos, note.c_str(), viewAddress); +} + +void DebuggerController::ClearTTDBookmarks() +{ + BNDebuggerClearTTDBookmarks(m_object); +} + + bool DebuggerController::IsInstructionExecuted(uint64_t address) { return BNDebuggerIsInstructionExecuted(m_object, address); diff --git a/api/ffi.h b/api/ffi.h index c0bc91a0..1c035fbd 100644 --- a/api/ffi.h +++ b/api/ffi.h @@ -274,6 +274,8 @@ extern "C" ThreadStateChangedEvent, ForceMemoryCacheUpdateEvent, + + TTDBookmarkChangedEvent, } BNDebuggerEventType; @@ -701,6 +703,21 @@ extern "C" DEBUGGER_FFI_API void BNDebuggerFreeTTDCallEvents(BNDebuggerTTDCallEvent* events, size_t count); DEBUGGER_FFI_API void BNDebuggerFreeTTDEvents(BNDebuggerTTDEvent* events, size_t count); + // TTD Bookmark structures and functions + typedef struct BNDebuggerTTDBookmark + { + BNDebuggerTTDPosition position; + uint64_t viewAddress; + char* note; + } BNDebuggerTTDBookmark; + + DEBUGGER_FFI_API BNDebuggerTTDBookmark* BNDebuggerGetTTDBookmarks(BNDebuggerController* controller, size_t* count); + DEBUGGER_FFI_API bool BNDebuggerAddTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position, const char* note, uint64_t viewAddress); + DEBUGGER_FFI_API bool BNDebuggerRemoveTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position); + DEBUGGER_FFI_API bool BNDebuggerUpdateTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position, const char* note, uint64_t viewAddress); + DEBUGGER_FFI_API void BNDebuggerClearTTDBookmarks(BNDebuggerController* controller); + DEBUGGER_FFI_API void BNDebuggerFreeTTDBookmarks(BNDebuggerTTDBookmark* bookmarks, size_t count); + // TTD Code Coverage Analysis Functions DEBUGGER_FFI_API bool BNDebuggerIsInstructionExecuted(BNDebuggerController* controller, uint64_t address); DEBUGGER_FFI_API bool BNDebuggerRunCodeCoverageAnalysisRange(BNDebuggerController* controller, uint64_t startAddress, uint64_t endAddress, BNDebuggerTTDPosition startTime, BNDebuggerTTDPosition endTime); diff --git a/api/python/debuggercontroller.py b/api/python/debuggercontroller.py index f565655a..e50f0b45 100644 --- a/api/python/debuggercontroller.py +++ b/api/python/debuggercontroller.py @@ -735,6 +735,38 @@ def from_string(cls, timestamp_str): return cls(parts[0], parts[1]) +class TTDBookmark: + """ + TTDBookmark represents a saved position in a TTD trace with an optional note and view address. + + * ``position``: the TTD position (TTDPosition object) + * ``view_address``: the address the user was viewing when the bookmark was created + * ``note``: an optional note describing the bookmark + """ + + def __init__(self, position: TTDPosition, note: str = "", view_address: int = 0): + self.position = position + self.note = note + self.view_address = view_address + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self.position == other.position + + def __ne__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return not (self == other) + + def __hash__(self): + return hash(self.position) + + def __repr__(self): + note_str = f" ({self.note})" if self.note else "" + return f"" + + class TTDMemoryEvent: """ TTDMemoryEvent represents a memory access event in a TTD trace. It has the following fields: @@ -2553,6 +2585,103 @@ def navigate_to_timestamp(self, timestamp_str): binaryninja.log_error(f"Invalid timestamp format: {e}") return False + @property + def ttd_bookmarks(self): + """ + Get all TTD bookmarks. + + :return: list of TTDBookmark objects + :rtype: list[TTDBookmark] + """ + count = ctypes.c_size_t() + bookmarks = dbgcore.BNDebuggerGetTTDBookmarks(self.handle, ctypes.byref(count)) + + result = [] + if not bookmarks or count.value == 0: + return result + + for i in range(count.value): + bm = bookmarks[i] + position = TTDPosition(bm.position.sequence, bm.position.step) + note = bm.note.decode('utf-8') if bm.note else "" + result.append(TTDBookmark(position, note, bm.viewAddress)) + + dbgcore.BNDebuggerFreeTTDBookmarks(bookmarks, count.value) + return result + + def add_ttd_bookmark(self, position, note="", view_address=0): + """ + Add a TTD bookmark. If a bookmark with the same position already exists, it is updated. + + :param position: TTDPosition object or string in format "sequence:step" + :param note: optional note for the bookmark + :param view_address: optional view address to navigate to when the bookmark is activated + :return: True if the bookmark was added/updated successfully + :rtype: bool + """ + if isinstance(position, str): + position = TTDPosition.from_string(position) + elif not isinstance(position, TTDPosition): + raise TypeError("Position must be TTDPosition object or string") + + pos = dbgcore.BNDebuggerTTDPosition() + pos.sequence = position.sequence + pos.step = position.step + + if isinstance(note, str): + note = note.encode('utf-8') + + return dbgcore.BNDebuggerAddTTDBookmark(self.handle, pos, note, view_address) + + def remove_ttd_bookmark(self, position): + """ + Remove a TTD bookmark by position. + + :param position: TTDPosition object or string in format "sequence:step" + :return: True if the bookmark was found and removed + :rtype: bool + """ + if isinstance(position, str): + position = TTDPosition.from_string(position) + elif not isinstance(position, TTDPosition): + raise TypeError("Position must be TTDPosition object or string") + + pos = dbgcore.BNDebuggerTTDPosition() + pos.sequence = position.sequence + pos.step = position.step + + return dbgcore.BNDebuggerRemoveTTDBookmark(self.handle, pos) + + def update_ttd_bookmark(self, position, note="", view_address=0): + """ + Update an existing TTD bookmark's note and view address. + + :param position: TTDPosition object or string in format "sequence:step" + :param note: new note for the bookmark + :param view_address: new view address for the bookmark + :return: True if the bookmark was found and updated + :rtype: bool + """ + if isinstance(position, str): + position = TTDPosition.from_string(position) + elif not isinstance(position, TTDPosition): + raise TypeError("Position must be TTDPosition object or string") + + pos = dbgcore.BNDebuggerTTDPosition() + pos.sequence = position.sequence + pos.step = position.step + + if isinstance(note, str): + note = note.encode('utf-8') + + return dbgcore.BNDebuggerUpdateTTDBookmark(self.handle, pos, note, view_address) + + def clear_ttd_bookmarks(self): + """ + Remove all TTD bookmarks. + """ + dbgcore.BNDebuggerClearTTDBookmarks(self.handle) + def get_ttd_next_memory_access(self, address: int, size: int, access_type = DebuggerTTDMemoryAccessType.DebuggerTTDMemoryRead): """ Get the next memory access to a specific address from the current TTD position. diff --git a/core/debuggercommon.h b/core/debuggercommon.h index 33d1239c..85a99362 100644 --- a/core/debuggercommon.h +++ b/core/debuggercommon.h @@ -119,6 +119,18 @@ namespace BinaryNinjaDebugger { } }; + // TTD Bookmark - a saved position in the trace with optional metadata + struct TTDBookmark + { + TTDPosition position; + uint64_t viewAddress; + std::string note; + + TTDBookmark() : viewAddress(0) {} + TTDBookmark(const TTDPosition& pos, const std::string& n = "", uint64_t addr = 0) + : position(pos), viewAddress(addr), note(n) {} + }; + // TTD Memory Access Event - complete set of fields from Microsoft documentation struct TTDMemoryEvent { diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index cef2b22f..b2ad5e91 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -3220,6 +3220,156 @@ std::pair DebuggerController::GetTTDPrevMemoryAccess(uint6 } +static const char* TTD_BOOKMARKS_METADATA_KEY = "debugger.ttd_bookmarks"; + +std::vector DebuggerController::GetTTDBookmarks() +{ + std::vector result; + auto data = GetData(); + if (!data) + return result; + + Ref metadata = data->QueryMetadata(TTD_BOOKMARKS_METADATA_KEY); + if (!metadata || !metadata->IsArray()) + return result; + + for (auto& element : metadata->GetArray()) + { + if (!element || !element->IsKeyValueStore()) + continue; + + auto info = element->GetKeyValueStore(); + TTDBookmark bookmark; + + if (info.count("sequence") && info["sequence"]->IsUnsignedInteger()) + bookmark.position.sequence = info["sequence"]->GetUnsignedInteger(); + else + continue; + + if (info.count("step") && info["step"]->IsUnsignedInteger()) + bookmark.position.step = info["step"]->GetUnsignedInteger(); + else + continue; + + if (info.count("view_address") && info["view_address"]->IsUnsignedInteger()) + bookmark.viewAddress = info["view_address"]->GetUnsignedInteger(); + + if (info.count("note") && info["note"]->IsString()) + bookmark.note = info["note"]->GetString(); + + result.push_back(bookmark); + } + return result; +} + +static void SaveBookmarks(BinaryViewRef data, const std::vector& bookmarks) +{ + std::vector> arr; + for (const auto& bm : bookmarks) + { + std::map> info; + info["sequence"] = new Metadata(bm.position.sequence); + info["step"] = new Metadata(bm.position.step); + info["view_address"] = new Metadata(bm.viewAddress); + info["note"] = new Metadata(bm.note); + arr.push_back(new Metadata(info)); + } + data->StoreMetadata(TTD_BOOKMARKS_METADATA_KEY, new Metadata(arr)); +} + +bool DebuggerController::AddTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress) +{ + auto data = GetData(); + if (!data) + return false; + + auto bookmarks = GetTTDBookmarks(); + + // Deduplicate by position + for (auto& bm : bookmarks) + { + if (bm.position == position) + { + bm.note = note; + bm.viewAddress = viewAddress; + SaveBookmarks(data, bookmarks); + + DebuggerEvent event; + event.type = TTDBookmarkChangedEvent; + PostDebuggerEvent(event); + return true; + } + } + + bookmarks.emplace_back(position, note, viewAddress); + SaveBookmarks(data, bookmarks); + + DebuggerEvent event; + event.type = TTDBookmarkChangedEvent; + PostDebuggerEvent(event); + return true; +} + +bool DebuggerController::RemoveTTDBookmark(const TTDPosition& position) +{ + auto data = GetData(); + if (!data) + return false; + + auto bookmarks = GetTTDBookmarks(); + auto it = std::remove_if(bookmarks.begin(), bookmarks.end(), + [&](const TTDBookmark& bm) { return bm.position == position; }); + + if (it == bookmarks.end()) + return false; + + bookmarks.erase(it, bookmarks.end()); + SaveBookmarks(data, bookmarks); + + DebuggerEvent event; + event.type = TTDBookmarkChangedEvent; + PostDebuggerEvent(event); + return true; +} + +bool DebuggerController::UpdateTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress) +{ + auto data = GetData(); + if (!data) + return false; + + auto bookmarks = GetTTDBookmarks(); + for (auto& bm : bookmarks) + { + if (bm.position == position) + { + bm.note = note; + bm.viewAddress = viewAddress; + SaveBookmarks(data, bookmarks); + + DebuggerEvent event; + event.type = TTDBookmarkChangedEvent; + PostDebuggerEvent(event); + return true; + } + } + return false; +} + +void DebuggerController::ClearTTDBookmarks() +{ + auto data = GetData(); + if (!data) + return; + + data->StoreMetadata(TTD_BOOKMARKS_METADATA_KEY, new Metadata(std::vector>())); + + DebuggerEvent event; + event.type = TTDBookmarkChangedEvent; + PostDebuggerEvent(event); +} + + bool DebuggerController::IsInstructionExecuted(uint64_t address) { if (!m_state->IsConnected() || !IsTTD()) diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index e4cbd711..5947fb50 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -405,6 +405,13 @@ namespace BinaryNinjaDebugger { std::pair GetTTDNextMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); std::pair GetTTDPrevMemoryAccess(uint64_t address, uint64_t size, TTDMemoryAccessType accessType); + // TTD Bookmark Methods + std::vector GetTTDBookmarks(); + bool AddTTDBookmark(const TTDPosition& position, const std::string& note = "", uint64_t viewAddress = 0); + bool RemoveTTDBookmark(const TTDPosition& position); + bool UpdateTTDBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress); + void ClearTTDBookmarks(); + // TTD Code Coverage Analysis Methods bool IsInstructionExecuted(uint64_t address); bool RunCodeCoverageAnalysis(uint64_t startAddress, uint64_t endAddress, TTDPosition startTime, TTDPosition endTime); diff --git a/core/ffi.cpp b/core/ffi.cpp index 75df04ad..b595de6c 100644 --- a/core/ffi.cpp +++ b/core/ffi.cpp @@ -1339,6 +1339,60 @@ bool BNDebuggerGetTTDPrevMemoryAccess(BNDebuggerController* controller, return true; } +BNDebuggerTTDBookmark* BNDebuggerGetTTDBookmarks(BNDebuggerController* controller, size_t* count) +{ + auto bookmarks = controller->object->GetTTDBookmarks(); + *count = bookmarks.size(); + if (bookmarks.empty()) + return nullptr; + + auto* result = new BNDebuggerTTDBookmark[bookmarks.size()]; + for (size_t i = 0; i < bookmarks.size(); ++i) + { + result[i].position.sequence = bookmarks[i].position.sequence; + result[i].position.step = bookmarks[i].position.step; + result[i].viewAddress = bookmarks[i].viewAddress; + result[i].note = BNDebuggerAllocString(bookmarks[i].note.c_str()); + } + return result; +} + +bool BNDebuggerAddTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position, const char* note, uint64_t viewAddress) +{ + TTDPosition pos(position.sequence, position.step); + return controller->object->AddTTDBookmark(pos, note ? note : "", viewAddress); +} + +bool BNDebuggerRemoveTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position) +{ + TTDPosition pos(position.sequence, position.step); + return controller->object->RemoveTTDBookmark(pos); +} + +bool BNDebuggerUpdateTTDBookmark(BNDebuggerController* controller, BNDebuggerTTDPosition position, const char* note, uint64_t viewAddress) +{ + TTDPosition pos(position.sequence, position.step); + return controller->object->UpdateTTDBookmark(pos, note ? note : "", viewAddress); +} + +void BNDebuggerClearTTDBookmarks(BNDebuggerController* controller) +{ + controller->object->ClearTTDBookmarks(); +} + +void BNDebuggerFreeTTDBookmarks(BNDebuggerTTDBookmark* bookmarks, size_t count) +{ + if (!bookmarks) + return; + + for (size_t i = 0; i < count; ++i) + { + BNDebuggerFreeString(bookmarks[i].note); + } + delete[] bookmarks; +} + + bool BNDebuggerIsInstructionExecuted(BNDebuggerController* controller, uint64_t address) { return controller->object->IsInstructionExecuted(address); diff --git a/ui/ttdbookmarkwidget.cpp b/ui/ttdbookmarkwidget.cpp new file mode 100644 index 00000000..9d706c9f --- /dev/null +++ b/ui/ttdbookmarkwidget.cpp @@ -0,0 +1,610 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "ttdbookmarkwidget.h" +#include "ui.h" +#include +#include + +#include "moc_ttdbookmarkwidget.cpp" + +static uint64_t PositionSortValue(const TTDPosition& position) +{ + return (position.sequence << 32) | (position.step & 0xFFFFFFFF); +} + + +// TTDBookmarkEditDialog implementation + +TTDBookmarkEditDialog::TTDBookmarkEditDialog(QWidget* parent, const QString& position, const QString& note, + const QString& viewAddress) + : QDialog(parent) +{ + setWindowTitle(position.isEmpty() ? "Add TTD Bookmark" : "Edit TTD Bookmark"); + setModal(true); + setMinimumWidth(500); + + auto layout = new QFormLayout(this); + layout->setContentsMargins(10, 10, 10, 10); + layout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow); + + m_positionEdit = new QLineEdit(position); + m_positionEdit->setPlaceholderText("sequence:step (hex), e.g. 1a0:12f"); + layout->addRow("Position:", m_positionEdit); + + m_viewAddressEdit = new QLineEdit(viewAddress); + m_viewAddressEdit->setPlaceholderText("View address (hex, optional)"); + layout->addRow("View Address:", m_viewAddressEdit); + + m_noteEdit = new QLineEdit(note); + m_noteEdit->setPlaceholderText("Optional note for this bookmark"); + m_noteEdit->setMinimumWidth(400); + layout->addRow("Note:", m_noteEdit); + + auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addRow(buttons); + + // Focus the note field by default + m_noteEdit->setFocus(); + m_noteEdit->selectAll(); +} + +QString TTDBookmarkEditDialog::getPosition() const { return m_positionEdit->text().trimmed(); } +QString TTDBookmarkEditDialog::getNote() const { return m_noteEdit->text().trimmed(); } +QString TTDBookmarkEditDialog::getViewAddress() const { return m_viewAddressEdit->text().trimmed(); } + + +// TTDBookmarkWidget implementation + +TTDBookmarkWidget::TTDBookmarkWidget(QWidget* parent, BinaryViewRef data) + : QWidget(parent), m_data(data), m_resultsTable(nullptr), m_statusLabel(nullptr), + m_addButton(nullptr), m_contextMenuManager(nullptr) +{ + m_controller = DebuggerController::GetController(data); + + setupUI(); + setupTable(); + setupUIActions(); + setupContextMenu(); + loadBookmarksFromMetadata(); + refreshTable(); +} + +TTDBookmarkWidget::~TTDBookmarkWidget() +{ + if (m_contextMenuManager) + delete m_contextMenuManager; +} + +void TTDBookmarkWidget::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + + // Button bar + auto buttonLayout = new QHBoxLayout(); + m_addButton = new QPushButton("Add TTD Bookmark"); + connect(m_addButton, &QPushButton::clicked, this, &TTDBookmarkWidget::addBookmarkFromDialog); + buttonLayout->addWidget(m_addButton); + + auto addCurrentButton = new QPushButton("Bookmark Current Position"); + connect(addCurrentButton, &QPushButton::clicked, this, &TTDBookmarkWidget::addBookmarkFromCurrentPosition); + buttonLayout->addWidget(addCurrentButton); + + buttonLayout->addStretch(); + mainLayout->addLayout(buttonLayout); + + // Results table + m_resultsTable = new QTableWidget(); + mainLayout->addWidget(m_resultsTable, 1); + + // Status label + m_statusLabel = new QLabel("No bookmarks."); + m_statusLabel->setContentsMargins(5, 5, 5, 5); + mainLayout->addWidget(m_statusLabel); + + // Connect double-click + connect(m_resultsTable, &QTableWidget::cellDoubleClicked, this, &TTDBookmarkWidget::onCellDoubleClicked); +} + +void TTDBookmarkWidget::setupTable() +{ + QStringList columns; + columns << "Index" << "Position" << "View Address" << "Note"; + + m_resultsTable->setColumnCount(columns.size()); + m_resultsTable->setHorizontalHeaderLabels(columns); + m_resultsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_resultsTable->verticalHeader()->setVisible(false); + m_resultsTable->setSortingEnabled(true); + m_resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_resultsTable->setAlternatingRowColors(true); + + QHeaderView* header = m_resultsTable->horizontalHeader(); + header->setStretchLastSection(true); + header->setSectionResizeMode(QHeaderView::Interactive); + + m_resultsTable->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_resultsTable, &QTableWidget::customContextMenuRequested, this, &TTDBookmarkWidget::showContextMenu); +} + +void TTDBookmarkWidget::setupUIActions() +{ + m_actionHandler.setupActionHandler(this); + m_contextMenuManager = new ContextMenuManager(this); + m_menu = new Menu(); + + m_menu->addAction("Add TTD Bookmark...", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Add TTD Bookmark...", UIAction([&]() { addBookmarkFromDialog(); })); + + m_menu->addAction("Bookmark Current Position", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Bookmark Current Position", UIAction([&]() { addBookmarkFromCurrentPosition(); })); + + m_menu->addAction("Edit Bookmark...", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Edit Bookmark...", UIAction([&]() { editSelectedBookmark(); }, + [&]() { return m_resultsTable->selectionModel()->hasSelection(); })); + + m_menu->addAction("Remove Bookmark", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Remove Bookmark", UIAction([&]() { removeSelectedBookmark(); }, + [&]() { return m_resultsTable->selectionModel()->hasSelection(); })); + + m_menu->addAction("Copy", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy", UIAction([&]() { copy(); }, [&]() { return canCopy(); })); + + m_menu->addAction("Copy Row", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy Row", UIAction([&]() { copySelectedRow(); }, [&]() { return canCopy(); })); + + m_menu->addAction("Copy Table", "Options", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Copy Table", UIAction([&]() { copyEntireTable(); }, + [&]() { return m_resultsTable->rowCount() > 0; })); +} + +void TTDBookmarkWidget::setupContextMenu() +{ + // Already connected in setupTable +} + +void TTDBookmarkWidget::updateStatus(const QString& message) +{ + if (m_statusLabel) + m_statusLabel->setText(message); +} + +bool TTDBookmarkWidget::canCopy() +{ + return m_resultsTable->selectionModel()->hasSelection(); +} + +void TTDBookmarkWidget::loadBookmarksFromMetadata() +{ + m_bookmarks.clear(); + + if (!m_controller) + return; + + m_bookmarks = m_controller->GetTTDBookmarks(); +} + +void TTDBookmarkWidget::refreshTable() +{ + bool sortingEnabled = m_resultsTable->isSortingEnabled(); + m_resultsTable->setSortingEnabled(false); + + m_resultsTable->setRowCount(static_cast(m_bookmarks.size())); + + for (int i = 0; i < static_cast(m_bookmarks.size()); ++i) + { + const auto& bookmark = m_bookmarks[i]; + + m_resultsTable->setItem(i, IndexColumn, new NumericalTableWidgetItem(QString::number(i + 1), i + 1)); + + QString posStr = QString("%1:%2").arg(bookmark.position.sequence, 0, 16).arg(bookmark.position.step, 0, 16); + m_resultsTable->setItem(i, PositionColumn, new NumericalTableWidgetItem(posStr, PositionSortValue(bookmark.position))); + + if (bookmark.viewAddress != 0) + { + m_resultsTable->setItem(i, ViewAddressColumn, + new NumericalTableWidgetItem(QString("0x%1").arg(bookmark.viewAddress, 0, 16), bookmark.viewAddress)); + } + else + { + m_resultsTable->setItem(i, ViewAddressColumn, new NumericalTableWidgetItem("", 0)); + } + + m_resultsTable->setItem(i, NoteColumn, new QTableWidgetItem(QString::fromStdString(bookmark.note))); + } + + m_resultsTable->setSortingEnabled(sortingEnabled); + m_resultsTable->resizeColumnsToContents(); + + if (m_bookmarks.empty()) + updateStatus("No bookmarks."); + else + updateStatus(QString("%1 bookmark(s).").arg(m_bookmarks.size())); +} + +void TTDBookmarkWidget::addBookmark(const TTDPosition& position, const std::string& note, uint64_t viewAddress) +{ + if (!m_controller) + return; + + m_controller->AddTTDBookmark(position, note, viewAddress); + loadBookmarksFromMetadata(); + refreshTable(); +} + +void TTDBookmarkWidget::removeBookmark(int index) +{ + if (!m_controller || index < 0 || index >= static_cast(m_bookmarks.size())) + return; + + m_controller->RemoveTTDBookmark(m_bookmarks[index].position); + loadBookmarksFromMetadata(); + refreshTable(); +} + +void TTDBookmarkWidget::updateBookmark(int index, const TTDPosition& position, const std::string& note, + uint64_t viewAddress) +{ + if (!m_controller || index < 0 || index >= static_cast(m_bookmarks.size())) + return; + + // If position changed, remove old and add new + if (!(m_bookmarks[index].position == position)) + { + m_controller->RemoveTTDBookmark(m_bookmarks[index].position); + m_controller->AddTTDBookmark(position, note, viewAddress); + } + else + { + m_controller->UpdateTTDBookmark(position, note, viewAddress); + } + loadBookmarksFromMetadata(); + refreshTable(); +} + +void TTDBookmarkWidget::onCellDoubleClicked(int row, int column) +{ + if (!m_controller || row < 0 || row >= static_cast(m_bookmarks.size())) + return; + + // Get the original bookmark index from the Index column (1-based display) + QTableWidgetItem* indexItem = m_resultsTable->item(row, IndexColumn); + if (!indexItem) + return; + int bookmarkIndex = static_cast(indexItem->data(Qt::UserRole).toULongLong()) - 1; + if (bookmarkIndex < 0 || bookmarkIndex >= static_cast(m_bookmarks.size())) + return; + + const auto& bookmark = m_bookmarks[bookmarkIndex]; + + // Store pending view address so we can navigate after the TargetStoppedEvent + // handler has finished (it calls navigateToCurrentIP, which would override us). + m_pendingViewAddress = bookmark.viewAddress; + + if (m_controller->SetTTDPosition(bookmark.position)) + { + updateStatus(QString("Navigated to bookmark %1").arg(bookmarkIndex + 1)); + } + else + { + m_pendingViewAddress = 0; + updateStatus("Failed to navigate to bookmark position"); + } +} + +void TTDBookmarkWidget::navigateToPendingViewAddress() +{ + if (m_pendingViewAddress == 0) + return; + + uint64_t addr = m_pendingViewAddress; + m_pendingViewAddress = 0; + + ViewFrame* frame = ViewFrame::viewFrameForWidget(this); + if (frame) + frame->navigate(m_data, addr); +} + +void TTDBookmarkWidget::contextMenuEvent(QContextMenuEvent* event) +{ + if (m_contextMenuManager) + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDBookmarkWidget::showContextMenu(const QPoint& position) +{ + if (m_contextMenuManager) + m_contextMenuManager->show(m_menu, &m_actionHandler); +} + +void TTDBookmarkWidget::addBookmarkFromDialog() +{ + TTDBookmarkEditDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) + return; + + QString posStr = dialog.getPosition(); + QStringList parts = posStr.split(':'); + if (parts.size() != 2) + { + QMessageBox::warning(this, "Invalid Position", "Position must be in format sequence:step (hex)."); + return; + } + + bool ok1, ok2; + uint64_t sequence = parts[0].toULongLong(&ok1, 16); + uint64_t step = parts[1].toULongLong(&ok2, 16); + + if (!ok1 || !ok2) + { + QMessageBox::warning(this, "Invalid Position", "Could not parse position as hex values."); + return; + } + + uint64_t viewAddress = 0; + QString viewAddrStr = dialog.getViewAddress(); + if (!viewAddrStr.isEmpty()) + { + QString cleanText = viewAddrStr.trimmed(); + if (cleanText.startsWith("0x") || cleanText.startsWith("0X")) + cleanText = cleanText.mid(2); + bool ok; + viewAddress = cleanText.toULongLong(&ok, 16); + if (!ok) + viewAddress = 0; + } + + addBookmark(TTDPosition(sequence, step), dialog.getNote().toStdString(), viewAddress); +} + +void TTDBookmarkWidget::addBookmarkFromCurrentPosition() +{ + if (!m_controller) + { + QMessageBox::warning(this, "No Controller", "No debugger controller available."); + return; + } + + if (!m_controller->IsConnected() || !m_controller->IsTTD()) + { + QMessageBox::warning(this, "Not Available", "TTD session is not active."); + return; + } + + TTDPosition currentPos = m_controller->GetCurrentTTDPosition(); + + // Get current view address + uint64_t viewAddress = 0; + ViewFrame* frame = ViewFrame::viewFrameForWidget(this); + if (frame) + viewAddress = frame->getCurrentOffset(); + + // Show dialog pre-filled with current position + QString posStr = QString("%1:%2").arg(currentPos.sequence, 0, 16).arg(currentPos.step, 0, 16); + QString viewAddrStr = viewAddress != 0 ? QString("0x%1").arg(viewAddress, 0, 16) : ""; + + TTDBookmarkEditDialog dialog(this, posStr, "", viewAddrStr); + if (dialog.exec() == QDialog::Accepted) + { + // Re-parse in case user edited the position + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() == 2) + { + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (ok1 && ok2) + { + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool ok; + addr = clean.toULongLong(&ok, 16); + if (!ok) + addr = 0; + } + addBookmark(TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); + } + } + } +} + +void TTDBookmarkWidget::editSelectedBookmark() +{ + QItemSelectionModel* sel = m_resultsTable->selectionModel(); + if (!sel->hasSelection()) + return; + + int row = sel->selectedRows().first().row(); + QTableWidgetItem* indexItem = m_resultsTable->item(row, IndexColumn); + if (!indexItem) + return; + + int bookmarkIndex = static_cast(indexItem->data(Qt::UserRole).toULongLong()) - 1; + if (bookmarkIndex < 0 || bookmarkIndex >= static_cast(m_bookmarks.size())) + return; + + const auto& bookmark = m_bookmarks[bookmarkIndex]; + QString posStr = QString("%1:%2").arg(bookmark.position.sequence, 0, 16).arg(bookmark.position.step, 0, 16); + QString viewAddrStr = bookmark.viewAddress != 0 ? QString("0x%1").arg(bookmark.viewAddress, 0, 16) : ""; + + TTDBookmarkEditDialog dialog(this, posStr, QString::fromStdString(bookmark.note), viewAddrStr); + if (dialog.exec() != QDialog::Accepted) + return; + + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() != 2) + return; + + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (!ok1 || !ok2) + return; + + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool ok; + addr = clean.toULongLong(&ok, 16); + if (!ok) + addr = 0; + } + + updateBookmark(bookmarkIndex, TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); +} + +void TTDBookmarkWidget::removeSelectedBookmark() +{ + QItemSelectionModel* sel = m_resultsTable->selectionModel(); + if (!sel->hasSelection()) + return; + + int row = sel->selectedRows().first().row(); + QTableWidgetItem* indexItem = m_resultsTable->item(row, IndexColumn); + if (!indexItem) + return; + + int bookmarkIndex = static_cast(indexItem->data(Qt::UserRole).toULongLong()) - 1; + removeBookmark(bookmarkIndex); +} + +void TTDBookmarkWidget::copy() +{ + copySelectedRow(); +} + +void TTDBookmarkWidget::copySelectedRow() +{ + QItemSelectionModel* sel = m_resultsTable->selectionModel(); + if (!sel->hasSelection()) + return; + + int row = sel->selectedRows().first().row(); + QStringList rowData; + for (int col = 0; col < m_resultsTable->columnCount(); ++col) + { + QTableWidgetItem* item = m_resultsTable->item(row, col); + rowData << (item ? item->text() : ""); + } + QApplication::clipboard()->setText(rowData.join('\t')); +} + +void TTDBookmarkWidget::copyEntireTable() +{ + QStringList tableData; + + QStringList headers; + headers << "Index" << "Position" << "View Address" << "Note"; + tableData << headers.join('\t'); + + for (int row = 0; row < m_resultsTable->rowCount(); ++row) + { + QStringList rowData; + for (int col = 0; col < m_resultsTable->columnCount(); ++col) + { + QTableWidgetItem* item = m_resultsTable->item(row, col); + rowData << (item ? item->text() : ""); + } + tableData << rowData.join('\t'); + } + + QApplication::clipboard()->setText(tableData.join('\n')); +} + + +// TTDBookmarkSidebarWidget implementation + +TTDBookmarkSidebarWidget::TTDBookmarkSidebarWidget(BinaryViewRef data) + : SidebarWidget("TTD Bookmarks"), m_data(data) +{ + m_controller = DebuggerController::GetController(data); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_bookmarkWidget = new TTDBookmarkWidget(this, data); + layout->addWidget(m_bookmarkWidget); + + // Register for debugger events + if (m_controller) + { + connect(this, &TTDBookmarkSidebarWidget::debuggerEvent, this, &TTDBookmarkSidebarWidget::onDebuggerEvent); + + m_debuggerEventCallback = m_controller->RegisterEventCallback( + [&](const DebuggerEvent& event) { + emit debuggerEvent(event); + }, + "TTD Bookmarks Widget"); + } +} + +TTDBookmarkSidebarWidget::~TTDBookmarkSidebarWidget() +{ + if (m_controller) + m_controller->RemoveEventCallback(m_debuggerEventCallback); +} + +void TTDBookmarkSidebarWidget::onDebuggerEvent(const DebuggerEvent& event) +{ + switch (event.type) + { + case TargetStoppedEventType: + if (m_bookmarkWidget) + m_bookmarkWidget->navigateToPendingViewAddress(); + break; + case TTDBookmarkChangedEvent: + if (m_bookmarkWidget) + { + m_bookmarkWidget->loadBookmarksFromMetadata(); + m_bookmarkWidget->refreshTable(); + } + break; + default: + break; + } +} + + +// TTDBookmarkWidgetType implementation + +TTDBookmarkWidgetType::TTDBookmarkWidgetType() + : SidebarWidgetType(QImage(":/debugger/ttd-timestamp"), "TTD Bookmarks") +{ +} + +SidebarWidget* TTDBookmarkWidgetType::createWidget(ViewFrame* frame, BinaryViewRef data) +{ + return new TTDBookmarkSidebarWidget(data); +} + +SidebarContentClassifier* TTDBookmarkWidgetType::contentClassifier(ViewFrame*, BinaryViewRef data) +{ + return new ActiveDebugSessionSidebarContentClassifier(data); +} diff --git a/ui/ttdbookmarkwidget.h b/ui/ttdbookmarkwidget.h new file mode 100644 index 00000000..2bd67b18 --- /dev/null +++ b/ui/ttdbookmarkwidget.h @@ -0,0 +1,162 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "inttypes.h" +#include "binaryninjaapi.h" +#include "debuggerapi.h" +#include "viewframe.h" +#include "debuggeruicommon.h" +#include "menus.h" +#include "uitypes.h" + +using namespace BinaryNinja; +using namespace BinaryNinjaDebuggerAPI; + + +class TTDBookmarkEditDialog : public QDialog +{ + Q_OBJECT + +public: + TTDBookmarkEditDialog(QWidget* parent, const QString& position = "", const QString& note = "", + const QString& viewAddress = ""); + QString getPosition() const; + QString getNote() const; + QString getViewAddress() const; + +private: + QLineEdit* m_positionEdit; + QLineEdit* m_noteEdit; + QLineEdit* m_viewAddressEdit; +}; + + +class TTDBookmarkWidget : public QWidget +{ + Q_OBJECT + +public: + enum LogicalColumn + { + IndexColumn = 0, + PositionColumn, + ViewAddressColumn, + NoteColumn + }; + +private: + BinaryViewRef m_data; + DbgRef m_controller; + + QTableWidget* m_resultsTable; + QLabel* m_statusLabel; + QPushButton* m_addButton; + + UIActionHandler m_actionHandler; + ContextMenuManager* m_contextMenuManager; + Menu* m_menu; + + std::vector m_bookmarks; + uint64_t m_pendingViewAddress = 0; + + void setupUI(); + void setupTable(); + void setupUIActions(); + void setupContextMenu(); + void updateStatus(const QString& message); + bool canCopy(); + + virtual void contextMenuEvent(QContextMenuEvent* event) override; + +public: + TTDBookmarkWidget(QWidget* parent, BinaryViewRef data); + virtual ~TTDBookmarkWidget(); + + void loadBookmarksFromMetadata(); + void refreshTable(); + + void addBookmark(const TTDPosition& position, const std::string& note = "", uint64_t viewAddress = 0); + void removeBookmark(int index); + void updateBookmark(int index, const TTDPosition& position, const std::string& note, uint64_t viewAddress); + + // Called on TargetStoppedEvent to navigate to the view address saved in the bookmark + void navigateToPendingViewAddress(); + +private Q_SLOTS: + void onCellDoubleClicked(int row, int column); + void showContextMenu(const QPoint& position); + void addBookmarkFromDialog(); + void addBookmarkFromCurrentPosition(); + void editSelectedBookmark(); + void removeSelectedBookmark(); + void copy(); + void copySelectedRow(); + void copyEntireTable(); +}; + + +class TTDBookmarkSidebarWidget : public SidebarWidget +{ + Q_OBJECT + +private: + TTDBookmarkWidget* m_bookmarkWidget; + BinaryViewRef m_data; + DbgRef m_controller; + size_t m_debuggerEventCallback; + +public: + TTDBookmarkSidebarWidget(BinaryViewRef data); + ~TTDBookmarkSidebarWidget(); + + TTDBookmarkWidget* getBookmarkWidget() { return m_bookmarkWidget; } + +signals: + void debuggerEvent(const DebuggerEvent& event); + +private slots: + void onDebuggerEvent(const DebuggerEvent& event); +}; + + +class TTDBookmarkWidgetType : public SidebarWidgetType +{ +public: + TTDBookmarkWidgetType(); + SidebarWidget* createWidget(ViewFrame* frame, BinaryViewRef data) override; + SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; } + SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; } + SidebarIconVisibility defaultIconVisibility() const override { return HideSidebarIconIfNoContent; } + SidebarContentClassifier* contentClassifier(ViewFrame*, BinaryViewRef) override; +}; diff --git a/ui/ttdcallswidget.cpp b/ui/ttdcallswidget.cpp index ed80c57f..36824b70 100644 --- a/ui/ttdcallswidget.cpp +++ b/ui/ttdcallswidget.cpp @@ -15,6 +15,7 @@ limitations under the License. */ #include "ttdcallswidget.h" +#include "ttdbookmarkwidget.h" #include "ui.h" #include #include @@ -187,6 +188,64 @@ void TTDCallsQueryWidget::setupUIActions() m_menu->addAction("Reset Columns to Default", "Options", MENU_ORDER_NORMAL); m_actionHandler.bindAction("Reset Columns to Default", UIAction([&]() { resetColumnsToDefault(); })); + + m_menu->addAction("Add TTD Bookmark...", "Bookmark", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Add TTD Bookmark...", UIAction([&]() { + int row = m_resultsTable->currentRow(); + if (row < 0) + return; + QTableWidgetItem* posItem = m_resultsTable->item(row, TimeStartColumn); + if (!posItem) + return; + QString posStr = posItem->text(); + + // Use function address as view address + uint64_t viewAddress = 0; + QTableWidgetItem* addrItem = m_resultsTable->item(row, FunctionAddressColumn); + if (addrItem && addrItem->text().startsWith("0x")) + { + bool ok; + viewAddress = addrItem->text().mid(2).toULongLong(&ok, 16); + if (!ok) + viewAddress = 0; + } + + // Use function name as default note + QString defaultNote; + QTableWidgetItem* funcItem = m_resultsTable->item(row, FunctionColumn); + if (funcItem) + defaultNote = funcItem->text(); + + TTDBookmarkEditDialog dialog(this, + posStr, defaultNote, viewAddress != 0 ? QString("0x%1").arg(viewAddress, 0, 16) : ""); + if (dialog.exec() == QDialog::Accepted) + { + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() == 2) + { + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (ok1 && ok2) + { + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool aOk; + addr = clean.toULongLong(&aOk, 16); + if (!aOk) + addr = 0; + } + m_controller->AddTTDBookmark(TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); + } + } + } + }, [&]() { return m_resultsTable->currentRow() >= 0; })); } diff --git a/ui/ttdeventswidget.cpp b/ui/ttdeventswidget.cpp index 93162795..cf89c558 100644 --- a/ui/ttdeventswidget.cpp +++ b/ui/ttdeventswidget.cpp @@ -15,6 +15,7 @@ limitations under the License. */ #include "ttdeventswidget.h" +#include "ttdbookmarkwidget.h" #include "ui.h" #include #include @@ -348,6 +349,59 @@ void TTDEventsQueryWidget::setupUIActions() // Refresh action to clear and re-query from backend m_menu->addAction("Refresh", "Options", MENU_ORDER_NORMAL); m_actionHandler.bindAction("Refresh", UIAction([&]() { refreshEvents(); })); + + m_menu->addAction("Add TTD Bookmark...", "Bookmark", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Add TTD Bookmark...", UIAction([&]() { + int row = m_resultsTable->currentRow(); + if (row < 0) + return; + // Find the Position column + int posCol = -1; + for (int c = 0; c < m_resultsTable->columnCount(); ++c) + { + auto* header = m_resultsTable->horizontalHeaderItem(c); + if (header && header->text().contains("Position", Qt::CaseInsensitive)) + { + posCol = c; + break; + } + } + if (posCol < 0) + return; + QTableWidgetItem* posItem = m_resultsTable->item(row, posCol); + if (!posItem) + return; + QString posStr = posItem->text(); + + TTDBookmarkEditDialog dialog(this, posStr, "", ""); + if (dialog.exec() == QDialog::Accepted) + { + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() == 2) + { + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (ok1 && ok2) + { + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool aOk; + addr = clean.toULongLong(&aOk, 16); + if (!aOk) + addr = 0; + } + m_controller->AddTTDBookmark(TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); + } + } + } + }, [&]() { return m_resultsTable && m_resultsTable->currentRow() >= 0; })); } void TTDEventsQueryWidget::setupContextMenu() diff --git a/ui/ttdmemorywidget.cpp b/ui/ttdmemorywidget.cpp index 0edd87d8..3bc9ced5 100644 --- a/ui/ttdmemorywidget.cpp +++ b/ui/ttdmemorywidget.cpp @@ -15,6 +15,7 @@ limitations under the License. */ #include "ttdmemorywidget.h" +#include "ttdbookmarkwidget.h" #include "debuggeruicommon.h" #include "ui.h" #include @@ -324,6 +325,58 @@ void TTDMemoryQueryWidget::setupUIActions() m_menu->addAction("Reset Columns to Default", "Options", MENU_ORDER_NORMAL); m_actionHandler.bindAction("Reset Columns to Default", UIAction([&]() { resetColumnsToDefault(); })); + + m_menu->addAction("Add TTD Bookmark...", "Bookmark", MENU_ORDER_NORMAL); + m_actionHandler.bindAction("Add TTD Bookmark...", UIAction([&]() { + int row = m_resultsTable->currentRow(); + if (row < 0) + return; + QTableWidgetItem* posItem = m_resultsTable->item(row, PositionColumn); + if (!posItem) + return; + QString posStr = posItem->text(); + + // Get the IP address as view address + uint64_t viewAddress = 0; + QTableWidgetItem* ipItem = m_resultsTable->item(row, IPColumn); + if (ipItem && ipItem->text().startsWith("0x")) + { + bool ok; + viewAddress = ipItem->text().mid(2).toULongLong(&ok, 16); + if (!ok) + viewAddress = 0; + } + + TTDBookmarkEditDialog dialog(this, + posStr, "", viewAddress != 0 ? QString("0x%1").arg(viewAddress, 0, 16) : ""); + if (dialog.exec() == QDialog::Accepted) + { + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() == 2) + { + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (ok1 && ok2) + { + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool aOk; + addr = clean.toULongLong(&aOk, 16); + if (!aOk) + addr = 0; + } + m_controller->AddTTDBookmark(TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); + } + } + } + }, [&]() { return m_resultsTable->currentRow() >= 0; })); } void TTDMemoryQueryWidget::updateColumnVisibility() diff --git a/ui/ui.cpp b/ui/ui.cpp index ffe0adb5..401e9f40 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -48,6 +48,7 @@ limitations under the License. #include "ttdmemorywidget.h" #include "ttdcallswidget.h" #include "ttdeventswidget.h" +#include "ttdbookmarkwidget.h" #include "ttdanalysisdialog.h" #include "timestampnavigationdialog.h" #include "freeversion.h" @@ -1273,6 +1274,10 @@ void GlobalDebuggerUI::SetupMenu(UIContext* context) UIAction::registerAction("Column Visibility..."); UIAction::registerAction("Reset Columns to Default"); UIAction::registerAction("Refresh"); + UIAction::registerAction("Add TTD Bookmark..."); + UIAction::registerAction("Bookmark Current Position"); + UIAction::registerAction("Edit Bookmark..."); + UIAction::registerAction("Remove Bookmark"); #ifdef WIN32 UIAction::registerAction("Record TTD Trace"); @@ -1958,6 +1963,7 @@ void GlobalDebuggerUI::InitializeUI() Sidebar::addSidebarWidgetType(new TTDMemoryWidgetType()); Sidebar::addSidebarWidgetType(new TTDCallsWidgetType()); Sidebar::addSidebarWidgetType(new TTDEventsWidgetType()); + Sidebar::addSidebarWidgetType(new TTDBookmarkWidgetType()); } From b36632112bd9cfeaf0b8c867d78f4b939d9c97bd Mon Sep 17 00:00:00 2001 From: Xusheng Date: Tue, 17 Mar 2026 17:01:38 +0800 Subject: [PATCH 2/2] Add the "Add TTD Bookmark" action to the menu --- ui/ui.cpp | 53 +++++++++++++++++++++++++++++++++++++++++++ ui/uinotification.cpp | 1 + 2 files changed, 54 insertions(+) diff --git a/ui/ui.cpp b/ui/ui.cpp index 401e9f40..2105b729 100644 --- a/ui/ui.cpp +++ b/ui/ui.cpp @@ -1446,6 +1446,59 @@ void GlobalDebuggerUI::SetupMenu(UIContext* context) connectedToTTD)); debuggerMenu->addAction("Navigate to TTD Timestamp...", "TTD"); + context->globalActions()->bindAction("Add TTD Bookmark...", + UIAction( + [=](const UIActionContext& ctxt) { + if (!ctxt.binaryView) + return; + + auto controller = DebuggerController::GetController(ctxt.binaryView); + if (!controller || !controller->IsTTD()) + return; + + TTDPosition currentPos = controller->GetCurrentTTDPosition(); + QString posStr = QString("%1:%2").arg(currentPos.sequence, 0, 16).arg(currentPos.step, 0, 16); + + // Get current view address + uint64_t viewAddress = 0; + ViewFrame* frame = ctxt.context->getCurrentViewFrame(); + if (frame) + viewAddress = frame->getCurrentOffset(); + QString viewAddrStr = viewAddress != 0 ? QString("0x%1").arg(viewAddress, 0, 16) : ""; + + QWidget* parent = ctxt.context->mainWindow(); + TTDBookmarkEditDialog dialog(parent, posStr, "", viewAddrStr); + if (dialog.exec() == QDialog::Accepted) + { + QString editedPosStr = dialog.getPosition(); + QStringList parts = editedPosStr.split(':'); + if (parts.size() == 2) + { + bool ok1, ok2; + uint64_t seq = parts[0].toULongLong(&ok1, 16); + uint64_t stp = parts[1].toULongLong(&ok2, 16); + if (ok1 && ok2) + { + uint64_t addr = 0; + QString addrStr = dialog.getViewAddress(); + if (!addrStr.isEmpty()) + { + QString clean = addrStr.trimmed(); + if (clean.startsWith("0x") || clean.startsWith("0X")) + clean = clean.mid(2); + bool aOk; + addr = clean.toULongLong(&aOk, 16); + if (!aOk) + addr = 0; + } + controller->AddTTDBookmark(TTDPosition(seq, stp), dialog.getNote().toStdString(), addr); + } + } + } + }, + connectedToTTD)); + debuggerMenu->addAction("Add TTD Bookmark...", "TTD"); + UIAction::registerAction("TTD Analysis..."); context->globalActions()->bindAction("TTD Analysis...", UIAction( diff --git a/ui/uinotification.cpp b/ui/uinotification.cpp index 14b26d66..d941e7f4 100644 --- a/ui/uinotification.cpp +++ b/ui/uinotification.cpp @@ -195,6 +195,7 @@ void NotificationListener::OnContextMenuCreated(UIContext *context, View* view, #ifdef WIN32 // TTD Memory Access context menu items menu.addAction("Debugger", "Navigate to TTD Timestamp...", "TTD"); + menu.addAction("Debugger", "Add TTD Bookmark...", "TTD"); menu.addAction("Debugger", "TTD Memory Access\\Read", "TTD"); menu.addAction("Debugger", "TTD Memory Access\\Write", "TTD"); menu.addAction("Debugger", "TTD Memory Access\\Read/Write", "TTD");