mirror of
https://github.com/rizinorg/cutter
synced 2026-08-11 16:23:06 -04:00
Optimize disassembly scroll and selection logic (#3646)
1. Scroll by visual lines instead of disassembly lines (see #3604) Previously whenever a scroll happened, cutter cleared all of the disassembled lines it had and queried rizin again, even if the scroll was only for one instruction. This causes two problems: * All of the lines have to be queried again making scrolling slow. * Since an instruction can have multiple metadata lines attached to it, it meant that whenever a user scrolled a single instruction all of the metadata lines will also be scrolled - making scrolling feel choppy. To solve this we keep a buffer of "max visible lines * 5" which is filled in as user scrolls, meaning at first only "max visible lines" are queried from rizin, if user scrolls upwards the entire "max visible lines" above the current top instruction is fetched and prepended to our "lines" buffer. Since the previous lines are still saved - if the user scrolls back down we can just show those specific lines from our buffer instead of querying again. Lines are erased from start or end based on scroll direction if the buffer exceeds the size cap of "max visible lines * 5" The "lines" buffer is fully cleared if user seeks to some address using some external signal - like the Visual nav bar at the top (basically seeking using any method other than directly clicking on the instruction line itself) 2. Adds infinite selection via mouse and keyboard There was no way in cutter to select more lines than whats currently shown on screen and there was also no way to select text via keyboard. This is done via manually handling the selection instead of letting Qt handle it. The selection is preserved no matter how far user scrolls up or down. 3. Fixes empty space at the bottom of disassembly widget viewport Whenever "max visible lines" were calculated it didn't account for the fact that the last line might be partially visible meaning there is not enough space to display it fully, which created empty space at the bottom 4. Adds a visible/blinking cursor to the disassembly panel Avoids the issue of not knowing which line we are on, if the line contains similar text that is highlighted. Also helps to know where selection will start using physical keys
This commit is contained in:
parent
e01d86b5fa
commit
867c54e838
4 changed files with 848 additions and 191 deletions
|
|
@ -4,19 +4,55 @@
|
|||
|
||||
#include <QAbstractButton>
|
||||
#include <QAbstractItemView>
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QComboBox>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDockWidget>
|
||||
#include <QFileInfo>
|
||||
#include <QFontMetricsF>
|
||||
#include <QMenu>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QString>
|
||||
#include <QTextBlock>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidget>
|
||||
#include <QtCore>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
template<typename TextEditType>
|
||||
int calculateMaxDisplayedLines(TextEditType *textEdit)
|
||||
{
|
||||
qreal lineHeight = 0;
|
||||
const QTextBlock firstBlock = textEdit->document()->begin();
|
||||
if (firstBlock.isValid()) {
|
||||
lineHeight = textEdit->document()->documentLayout()->blockBoundingRect(firstBlock).height();
|
||||
}
|
||||
|
||||
if (lineHeight <= 0) {
|
||||
const QFontMetricsF fm(textEdit->font());
|
||||
lineHeight = fm.lineSpacing();
|
||||
}
|
||||
|
||||
qreal availableHeight = textEdit->viewport()->height();
|
||||
const qreal margin = textEdit->document()->documentMargin();
|
||||
availableHeight -= margin;
|
||||
|
||||
const qreal exactLines = availableHeight / lineHeight;
|
||||
const qreal floorValue = std::floor(exactLines);
|
||||
const qreal fractionalPart = exactLines - floorValue;
|
||||
|
||||
// If the last line is more than 15% visible then count it, otherwise leave it
|
||||
// avoids the issue where there is empty space at the bottom of a textedit widget
|
||||
if (fractionalPart >= 0.15) {
|
||||
return std::ceil(availableHeight / lineHeight);
|
||||
}
|
||||
|
||||
return floorValue;
|
||||
}
|
||||
}
|
||||
|
||||
namespace qhelpers {
|
||||
|
||||
QString formatByteCount(ut64 bytecount)
|
||||
|
|
@ -174,20 +210,12 @@ void SizePolicyMinMax::restoreHeight(QWidget *widget) const
|
|||
|
||||
int getMaxFullyDisplayedLines(QTextEdit *textEdit)
|
||||
{
|
||||
const QFontMetrics fontMetrics(textEdit->document()->defaultFont());
|
||||
return (textEdit->height()
|
||||
- (textEdit->contentsMargins().top() + textEdit->contentsMargins().bottom()
|
||||
+ (int)(textEdit->document()->documentMargin() * 2)))
|
||||
/ fontMetrics.lineSpacing();
|
||||
return calculateMaxDisplayedLines(textEdit);
|
||||
}
|
||||
|
||||
int getMaxFullyDisplayedLines(QPlainTextEdit *plainTextEdit)
|
||||
{
|
||||
const QFontMetrics fontMetrics(plainTextEdit->document()->defaultFont());
|
||||
return (plainTextEdit->height()
|
||||
- (plainTextEdit->contentsMargins().top() + plainTextEdit->contentsMargins().bottom()
|
||||
+ (int)(plainTextEdit->document()->documentMargin() * 2)))
|
||||
/ fontMetrics.lineSpacing();
|
||||
return calculateMaxDisplayedLines(plainTextEdit);
|
||||
}
|
||||
|
||||
QByteArray applyColorToSvg(const QByteArray &data, QColor color)
|
||||
|
|
|
|||
|
|
@ -215,6 +215,10 @@ const QHash<QString, Shortcut> &getDefaultShortcuts()
|
|||
{ { Qt::Key_Space },
|
||||
QT_TRANSLATE_NOOP("DisassemblyWidget", "Switch to Graph"),
|
||||
"DisassemblyWidget" } },
|
||||
{ "Disassembly.moveLeft",
|
||||
{ QList<QKeySequence> { Qt::Key_H }
|
||||
+ QKeySequence::keyBindings(QKeySequence::MoveToPreviousChar),
|
||||
QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Down"), "DisassemblyWidget" } },
|
||||
{ "Disassembly.moveDown",
|
||||
{ QList<QKeySequence> { Qt::Key_J }
|
||||
+ QKeySequence::keyBindings(QKeySequence::MoveToNextLine),
|
||||
|
|
@ -223,6 +227,10 @@ const QHash<QString, Shortcut> &getDefaultShortcuts()
|
|||
{ QList<QKeySequence> { Qt::Key_K }
|
||||
+ QKeySequence::keyBindings(QKeySequence::MoveToPreviousLine),
|
||||
QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Up"), "DisassemblyWidget" } },
|
||||
{ "Disassembly.moveRight",
|
||||
{ QList<QKeySequence> { Qt::Key_L }
|
||||
+ QKeySequence::keyBindings(QKeySequence::MoveToNextChar),
|
||||
QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Up"), "DisassemblyWidget" } },
|
||||
{ "Disassembly.pageDown",
|
||||
{ QKeySequence::keyBindings(QKeySequence::MoveToNextPage),
|
||||
QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Down By Page"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,8 @@ class DisassemblyContextMenu;
|
|||
class DisassemblyLeftPanel;
|
||||
class AddressRangeScrollBar;
|
||||
|
||||
enum class RefreshMode : ut8 { Append, Prepend, Reset, Keep, None };
|
||||
|
||||
/**
|
||||
* @brief Main widget for showing disassembly of a binary
|
||||
*
|
||||
|
|
@ -33,48 +35,67 @@ public:
|
|||
|
||||
static QString getWidgetType();
|
||||
|
||||
QFontMetricsF getFontMetrics();
|
||||
QList<DisassemblyLine> getLines();
|
||||
|
||||
int getStartIndex() const;
|
||||
int getEndIndex() const;
|
||||
|
||||
/**
|
||||
* @brief Updates the offset and character position where the cursor selection ends
|
||||
*/
|
||||
void updateSelectionPos(const QTextCursor &cursor);
|
||||
|
||||
/**
|
||||
* @brief Updates the offset and character position where the cursor selection starts
|
||||
*/
|
||||
void updateSelectionAnchor(const QTextCursor &cursor);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
* @brief Highlights the currently selected line and updates the
|
||||
* highlighting of the same words under the cursor in the visible screen.
|
||||
* This overrides all previous highlighting.
|
||||
* @return List of selections to be highlighted
|
||||
*/
|
||||
void highlightCurrentLine();
|
||||
QList<QTextEdit::ExtraSelection> highlightCurrentLine();
|
||||
/**
|
||||
* @brief Adds the PC line highlighting to the other current highlighting.
|
||||
* This should be called after highlightCurrentLine since that function
|
||||
* overrides all previous highlighting.
|
||||
* This is generally called after highlightCurrentLine
|
||||
* @return List of selections to be highlighted
|
||||
*/
|
||||
void highlightPCLine();
|
||||
QList<QTextEdit::ExtraSelection> highlightPCLine();
|
||||
void showDisasContextMenu(const QPoint &pt);
|
||||
void fontsUpdatedSlot();
|
||||
void colorsUpdatedSlot();
|
||||
void scrollInstructions(int count, bool clampToScrollBarRange = false);
|
||||
void seekPrev();
|
||||
void setPreviewMode(bool previewMode);
|
||||
QFontMetricsF getFontMetrics();
|
||||
QList<DisassemblyLine> getLines();
|
||||
|
||||
/**
|
||||
* @brief Forces the transient vertical scrollbar to appear on scroll
|
||||
*/
|
||||
void showTransientScrollBar();
|
||||
|
||||
void refreshDisasm(RVA offset = RVA_INVALID, RefreshMode mode = RefreshMode::Reset);
|
||||
|
||||
protected slots:
|
||||
void onSeekChanged(RVA offset, CutterCore::SeekHistoryType type);
|
||||
void refreshIfInRange(RVA offset);
|
||||
void instructionChanged(RVA offset);
|
||||
void refreshDisasm(RVA offset = RVA_INVALID);
|
||||
|
||||
bool updateMaxLines();
|
||||
|
||||
void cursorPositionChanged();
|
||||
/**
|
||||
* @brief Copies the currently highlighted disassembly text to the system clipboard
|
||||
*/
|
||||
void copySelection();
|
||||
|
||||
protected:
|
||||
DisassemblyContextMenu *mCtxMenu;
|
||||
DisassemblyScrollArea *mDisasScrollArea;
|
||||
DisassemblyTextEdit *mDisasTextEdit;
|
||||
DisassemblyLeftPanel *leftPanel;
|
||||
QList<DisassemblyLine> lines;
|
||||
|
||||
private:
|
||||
RVA topOffset;
|
||||
|
|
@ -101,7 +122,31 @@ private:
|
|||
int topOffsetHistoryPos = 0;
|
||||
QList<RVA> topOffsetHistory;
|
||||
|
||||
int startIndex = 0;
|
||||
int endIndex = 0;
|
||||
QList<DisassemblyLine> lines;
|
||||
|
||||
// Cursor selection related
|
||||
RVA selectionAnchorRVA = RVA_INVALID;
|
||||
/**
|
||||
* @brief metadata lines attached to instruction have the same offset as the instruction itself,
|
||||
* this keeps track of which metdata line the cursor was at inside the offset block
|
||||
*/
|
||||
int selectionAnchorSubIndex = 0;
|
||||
int selectionAnchorChar = 0;
|
||||
RVA selectionPosRVA = RVA_INVALID;
|
||||
/**
|
||||
* @brief same use-case as @ref selectionAnchorSubIndex but for cursor position instead if
|
||||
* anchor
|
||||
*/
|
||||
int selectionPosSubIndex = 0;
|
||||
int selectionPosChar = 0;
|
||||
|
||||
QList<RVA> breakpoints;
|
||||
/**
|
||||
* @brief Set whenever breakpoints have been updated or the screen has been manually refreshed
|
||||
*/
|
||||
bool breakpointsDirty = true;
|
||||
|
||||
void setupFonts();
|
||||
void setupColors();
|
||||
|
|
@ -110,9 +155,53 @@ private:
|
|||
|
||||
void connectCursorPositionChanged(bool disconnect);
|
||||
|
||||
void moveCursorRelative(bool up, bool page);
|
||||
void moveCursorRelative(QTextCursor::MoveOperation op, bool page);
|
||||
|
||||
void jumpToOffsetUnderCursor(const QTextCursor &);
|
||||
|
||||
/**
|
||||
* @brief Visually highlights the text on screen between the selection start (anchor) and the
|
||||
* current cursor position
|
||||
*/
|
||||
void updateSelection();
|
||||
void updateContextMenuSelection(bool hasSelection);
|
||||
|
||||
/**
|
||||
* @brief Finds the visual line number in the current view for a specific offset
|
||||
* @param offset The memory address (RVA) to look for
|
||||
* @param offsetSubIndex The specific sub-line to target if the instructions has metadata
|
||||
* attached to it
|
||||
* @return The index of the line, or -1 if not found
|
||||
*
|
||||
* @see DisassemblyHelper::getIndexInOffsetGroup()
|
||||
*/
|
||||
int getLineIndex(RVA offset, int offsetSubIndex) const;
|
||||
/**
|
||||
* @brief Refreshes the background colors in the disassembly view.
|
||||
* It applies highlights to both the line currently under the user's cursor and the Program
|
||||
* Counter (PC) line.
|
||||
*/
|
||||
void updateLineHighlights();
|
||||
/**
|
||||
* @brief Clears the current text selection so nothing is highlighted as selected
|
||||
*/
|
||||
void invalidateCursorSelection();
|
||||
|
||||
/**
|
||||
* @brief Metadata lines attached to an instruction have the same offset saved with it as the
|
||||
* instruction, This function returns the index of the current line within that offset
|
||||
* group/block
|
||||
* * e.g:
|
||||
* * @code
|
||||
* ; a comment
|
||||
* ; another comment <------ assume cursor is here
|
||||
* ; void func1()
|
||||
* 0x1000 mov rax, rax
|
||||
* @endcode
|
||||
*
|
||||
* Then the returned index is "1"
|
||||
*/
|
||||
int getIndexInOffsetGroup(const QTextCursor &cursor) const;
|
||||
};
|
||||
|
||||
class DisassemblyScrollArea : public QAbstractScrollArea
|
||||
|
|
@ -142,23 +231,30 @@ class DisassemblyTextEdit : public QPlainTextEdit
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DisassemblyTextEdit(QWidget *parent = nullptr)
|
||||
: QPlainTextEdit(parent), lockScroll(false)
|
||||
{
|
||||
}
|
||||
explicit DisassemblyTextEdit(DisassemblyWidget *disasmWidget = nullptr);
|
||||
|
||||
void setLockScroll(bool lock) { this->lockScroll = lock; }
|
||||
|
||||
qreal textOffset() const;
|
||||
|
||||
void setCursorVisible(bool visible);
|
||||
|
||||
protected:
|
||||
bool viewportEvent(QEvent *event) override;
|
||||
void scrollContentsBy(int dx, int dy) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
bool lockScroll;
|
||||
QTimer *blinkTimer;
|
||||
bool cursorVisible = true;
|
||||
QColor cursorColor;
|
||||
|
||||
DisassemblyWidget *disasmWidget = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue