feat(console): right-click "Copy value" for hashes/addresses under the cursor

Right-clicking the output now offers "Copy <value>" when a long alphanumeric token
(txid/blockhash/address, >=16 chars) is under the cursor — resolved via screenToTextPos.
Uses right-click so it never conflicts with the left-drag text selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 16:40:38 -05:00
parent efec3a50a1
commit 5a677f6202
3 changed files with 39 additions and 0 deletions

View File

@@ -549,6 +549,24 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
}
}
namespace {
// Extract a hash/address-like token (a long alphanumeric run, >= 16 chars) around byte
// offset `col` in `text`, for the right-click "Copy value" action. Returns "" if none.
std::string extractCopyableToken(const std::string& text, int col)
{
const int n = static_cast<int>(text.size());
auto isTok = [](unsigned char c) { return std::isalnum(c) != 0; };
int at = col;
if (at >= n || (at >= 0 && !isTok(static_cast<unsigned char>(text[at])))) at--;
if (at < 0 || at >= n || !isTok(static_cast<unsigned char>(text[at]))) return {};
int s = at, e = at;
while (s > 0 && isTok(static_cast<unsigned char>(text[s - 1]))) s--;
while (e + 1 < n && isTok(static_cast<unsigned char>(text[e + 1]))) e++;
std::string tok = text.substr(s, e - s + 1);
return tok.size() >= 16 ? tok : std::string(); // txids/blockhashes/addresses are long
}
} // namespace
void ConsoleTab::renderOutput()
{
using namespace material;
@@ -929,8 +947,27 @@ void ConsoleTab::renderOutput()
ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
}
// Capture the hash/address under the cursor on right-click, for "Copy value".
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
TextPos tp = screenToTextPos(ImGui::GetMousePos(),
output_line_height_ > 0.0f ? output_line_height_ : line_height);
context_token_.clear();
if (tp.line >= 0 && tp.line < static_cast<int>(lines_.size()))
context_token_ = extractCopyableToken(lines_[tp.line].text, tp.col);
}
// Right-click context menu
if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
if (!context_token_.empty()) {
std::string shortTok = context_token_.size() > 22
? context_token_.substr(0, 12) + "\xE2\x80\xA6" + context_token_.substr(context_token_.size() - 6)
: context_token_;
std::string label = std::string(TR("console_copy_value")) + " \"" + shortTok + "\"";
if (ImGui::MenuItem(label.c_str())) {
ImGui::SetClipboardText(context_token_.c_str());
}
ImGui::Separator();
}
if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
std::string selected = getSelectedText();
if (!selected.empty()) {

View File

@@ -153,6 +153,7 @@ private:
// Output filter
char filter_text_[128] = {0};
mutable int filter_match_count_ = 0; // lines matching the text filter (for the toolbar)
std::string context_token_; // hash/address under the cursor at right-click
mutable std::vector<int> visible_indices_; // Cached for selection mapping
// Wrapped line height caching (for variable-height text wrapping)