Text selection tool - finishing (without copying text)

This commit is contained in:
Jakub Melka 2020-01-26 17:06:50 +01:00
parent 95f6135482
commit 12b2f44619
9 changed files with 286 additions and 20 deletions

View File

@ -355,6 +355,58 @@ PDFTextLayoutGetter PDFAsynchronousTextLayoutCompiler::getTextLayoutLazy(PDFInte
return PDFTextLayoutGetter(nullptr, pageIndex);
}
PDFTextSelection PDFAsynchronousTextLayoutCompiler::getTextSelectionAll(QColor color) const
{
PDFTextSelection result;
if (m_textLayouts)
{
const PDFTextLayoutStorage& textLayouts = *m_textLayouts;
QMutex mutex;
PDFIntegerRange<size_t> pageRange(0, textLayouts.getCount());
auto selectPageText = [this, &mutex, &textLayouts, &result, color](PDFInteger pageIndex)
{
PDFTextLayout textLayout = textLayouts.getTextLayout(pageIndex);
PDFTextSelectionItems items;
const PDFTextBlocks& blocks = textLayout.getTextBlocks();
for (size_t blockId = 0, blockCount = blocks.size(); blockId < blockCount; ++blockId)
{
const PDFTextBlock& block = blocks[blockId];
const PDFTextLines& lines = block.getLines();
if (!lines.empty())
{
const PDFTextLine& lastLine = lines.back();
Q_ASSERT(!lastLine.getCharacters().empty());
PDFCharacterPointer ptrStart;
ptrStart.pageIndex = pageIndex;
ptrStart.blockIndex = blockId;
ptrStart.lineIndex = 0;
ptrStart.characterIndex = 0;
PDFCharacterPointer ptrEnd;
ptrEnd.pageIndex = pageIndex;
ptrEnd.blockIndex = blockId;
ptrEnd.lineIndex = lines.size() - 1;
ptrEnd.characterIndex = lastLine.getCharacters().size() - 1;
items.emplace_back(ptrStart, ptrEnd);
}
}
QMutexLocker lock(&mutex);
result.addItems(qMove(items), color);
};
PDFExecutionPolicy::execute(PDFExecutionPolicy::Scope::Page, pageRange.begin(), pageRange.end(), selectPageText);
}
result.build();
return result;
}
void PDFAsynchronousTextLayoutCompiler::makeTextLayout()
{
if (m_state != State::Active || !m_proxy->getDocument())

View File

@ -130,6 +130,10 @@ public:
/// \param pageIndex Page index
PDFTextLayoutGetter getTextLayoutLazy(PDFInteger pageIndex);
/// Select all texts on all pages using \p color color.
/// \param color Color to be used for text selection
PDFTextSelection getTextSelectionAll(QColor color) const;
/// Create text layout for the document. Function is asynchronous,
/// it returns immediately. After text layout is created, signal
/// \p textLayoutChanged is emitted.

View File

@ -362,6 +362,127 @@ bool PDFTextLayout::isHoveringOverTextBlock(const QPointF& point) const
return false;
}
PDFTextSelection PDFTextLayout::createTextSelection(PDFInteger pageIndex, const QPointF& point1, const QPointF& point2)
{
PDFTextSelection selection;
// Jakub Melka: We must treat each block in its own coordinate system. Because texts can
// have different angles, we will treat each block separately.
size_t blockId = 0;
for (PDFTextBlock& block : m_blocks)
{
QMatrix angleMatrix;
angleMatrix.rotate(block.getAngle());
block.applyTransform(angleMatrix);
QPointF pointA = angleMatrix.map(point1);
QPointF pointB = angleMatrix.map(point2);
const qreal xMin = qMin(pointA.x(), pointB.x());
const qreal yMin = qMin(pointA.y(), pointB.y());
const qreal xMax = qMax(pointA.x(), pointB.x());
const qreal yMax = qMax(pointA.y(), pointB.y());
QRectF rect(xMin, yMin, xMax - xMin, yMax - yMin);
QPainterPath rectPath;
rectPath.addRect(rect);
QPainterPath intersectionPath = block.getBoundingBox().intersected(rectPath);
if (!intersectionPath.isEmpty())
{
QRectF intersectionRect = intersectionPath.boundingRect();
Q_ASSERT(intersectionRect.isValid());
const PDFTextLines& lines = block.getLines();
auto itLineA = std::find_if(lines.cbegin(), lines.cend(), [pointA](const PDFTextLine& line) { return line.getBoundingBox().contains(pointA); });
auto itLineB = std::find_if(lines.cbegin(), lines.cend(), [pointB](const PDFTextLine& line) { return line.getBoundingBox().contains(pointB); });
if (itLineA == itLineB && itLineA != lines.cend())
{
// Both points are in the same line. We consider point with lesser
// horizontal coordinate as start selection point, and point with greater
// horizontal coordinate as end selection point.
if (pointA.x() > pointB.x())
{
std::swap(pointA, pointB);
}
}
else
{
// Otherwise points are not in the same line. Then start point will be
// point top of the second point. Bottom point will mark end of selection.
if (pointA.y() > pointB.y())
{
std::swap(pointA, pointB);
}
}
// Now, we have pointA as start point and pointB as end point. We must found
// nearest character to the right of point A, and nearest character to the
// left of point B (with respect to point A/B).
qreal maxDistanceA = std::numeric_limits<qreal>::infinity();
qreal maxDistanceB = std::numeric_limits<qreal>::infinity();
PDFCharacterPointer ptrA;
PDFCharacterPointer ptrB;
for (size_t lineId = 0, linesCount = lines.size(); lineId < linesCount; ++lineId)
{
const PDFTextLine& line = lines[lineId];
const TextCharacters& characters = line.getCharacters();
for (size_t characterId = 0, characterCount = characters.size(); characterId < characterCount; ++characterId)
{
const TextCharacter& character = characters[characterId];
QPointF characterCenter = character.boundingBox.boundingRect().center();
qreal distanceA = QLineF(pointA, characterCenter).length();
qreal distanceB = QLineF(pointB, characterCenter).length();
if (distanceA < maxDistanceA && characterCenter.x() > pointA.x())
{
maxDistanceA = distanceA;
ptrA.pageIndex = pageIndex;
ptrA.blockIndex = blockId;
ptrA.lineIndex = lineId;
ptrA.characterIndex = characterId;
}
if (distanceB < maxDistanceB && characterCenter.x() < pointB.x())
{
maxDistanceB = distanceB;
ptrB.pageIndex = pageIndex;
ptrB.blockIndex = blockId;
ptrB.lineIndex = lineId;
ptrB.characterIndex = characterId;
}
}
}
// If we have filled the pointers, add them to the selection
if (ptrA.isValid() && ptrB.isValid())
{
if (ptrA < ptrB)
{
selection.addItems({ PDFTextSelectionItem(ptrA, ptrB) }, Qt::yellow);
}
else
{
selection.addItems({ PDFTextSelectionItem(ptrB, ptrA) }, Qt::yellow);
}
}
}
// Increment block index
++blockId;
// Apply backward transformation to restore original coordinate system
block.applyTransform(angleMatrix.inverted());
}
selection.build();
return selection;
}
QDataStream& operator>>(QDataStream& stream, PDFTextLayout& layout)
{
stream >> layout.m_characters;

View File

@ -180,7 +180,7 @@ struct PDFCharacterPointer
/// Returns true, if character belongs to same line
bool hasSameLine(const PDFCharacterPointer& other) const;
int pageIndex = -1;
PDFInteger pageIndex = -1;
size_t blockIndex = 0;
size_t lineIndex = 0;
size_t characterIndex = 0;
@ -324,7 +324,7 @@ public:
/// Adds character to the layout
void addCharacter(const PDFTextCharacterInfo& info);
/// Perorms text layout algorithm
/// Performs text layout algorithm
void perform();
/// Optimizes layout memory allocation to contain less space
@ -339,6 +339,14 @@ public:
/// Returns true, if given point is pointing to some text block
bool isHoveringOverTextBlock(const QPointF& point) const;
/// Creates text selection. This function needs to modify the layout contents,
/// so do not use this function from multiple threads (it is not thread-safe).
/// Text selection is created from rectangle using two points.
/// \param pageIndex Page index
/// \param point1 First point
/// \param point2 Second point
PDFTextSelection createTextSelection(PDFInteger pageIndex, const QPointF& point1, const QPointF& point2);
friend QDataStream& operator<<(QDataStream& stream, const PDFTextLayout& layout);
friend QDataStream& operator>>(QDataStream& stream, PDFTextLayout& layout);
@ -354,7 +362,7 @@ private:
/// Applies transform to text characters (positions and bounding boxes)
/// \param characters Characters
/// \param matrix Transform matrix
void applyTransform(TextCharacters& characters, const QMatrix& matrix);
static void applyTransform(TextCharacters& characters, const QMatrix& matrix);
TextCharacters m_characters;
std::set<PDFReal> m_angles;
@ -458,6 +466,9 @@ public:
/// \param flowFlags Text flow flags
PDFFindResults find(const QRegularExpression& expression, PDFTextFlow::FlowFlags flowFlags) const;
/// Returns number of pages
size_t getCount() const { return m_offsets.size(); }
private:
std::vector<int> m_offsets;
QByteArray m_textLayouts;

View File

@ -446,15 +446,32 @@ PDFTextSelection PDFFindTextTool::getTextSelectionImpl() const
return result;
}
PDFSelectTextTool::PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* selectAllAction, QAction* deselectAction, QObject* parent) :
PDFSelectTextTool::PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* copyTextAction, QAction* selectAllAction, QAction* deselectAction, QObject* parent) :
BaseClass(proxy, action, parent),
m_copyTextAction(copyTextAction),
m_selectAllAction(selectAllAction),
m_deselectAction(deselectAction),
m_isCursorOverText(false)
{
connect(copyTextAction, &QAction::triggered, this, &PDFSelectTextTool::onActionCopyText);
connect(selectAllAction, &QAction::triggered, this, &PDFSelectTextTool::onActionSelectAll);
connect(deselectAction, &QAction::triggered, this, &PDFSelectTextTool::onActionDeselect);
updateActions();
}
void PDFSelectTextTool::drawPage(QPainter* painter,
PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const
{
Q_UNUSED(compiledPage);
pdf::PDFTextSelectionPainter textSelectionPainter(&m_textSelection);
textSelectionPainter.draw(painter, pageIndex, layoutGetter, pagePointToDevicePointMatrix);
}
void PDFSelectTextTool::mousePressEvent(QWidget* widget, QMouseEvent* event)
{
Q_UNUSED(widget);
@ -493,6 +510,8 @@ void PDFSelectTextTool::mouseReleaseEvent(QWidget* widget, QMouseEvent* event)
if (m_selectionInfo.pageIndex == pageIndex)
{
// Jakub Melka: handle the selection
PDFTextLayout textLayout = getProxy()->getTextLayoutCompiler()->getTextLayoutLazy(pageIndex);
setSelection(textLayout.createTextSelection(pageIndex, m_selectionInfo.selectionStartPoint, pagePoint));
}
else
{
@ -510,26 +529,29 @@ void PDFSelectTextTool::mouseMoveEvent(QWidget* widget, QMouseEvent* event)
{
Q_UNUSED(widget);
// We must make text layout. This is fast, because text layout is being
// created only, if it doesn't exist. This function is also called only,
// if tool is active.
getProxy()->getTextLayoutCompiler()->makeTextLayout();
QPointF pagePoint;
const PDFInteger pageIndex = getProxy()->getPageUnderPoint(event->pos(), &pagePoint);
PDFTextLayout textLayout = getProxy()->getTextLayoutCompiler()->getTextLayoutLazy(pageIndex);
m_isCursorOverText = textLayout.isHoveringOverTextBlock(pagePoint);
if (event->button() == Qt::LeftButton)
if (m_selectionInfo.pageIndex != -1)
{
if (m_selectionInfo.pageIndex != -1)
if (m_selectionInfo.pageIndex == pageIndex)
{
if (m_selectionInfo.pageIndex == pageIndex)
{
// Jakub Melka: handle the selection
}
else
{
setSelection(pdf::PDFTextSelection());
}
event->accept();
// Jakub Melka: handle the selection
setSelection(textLayout.createTextSelection(pageIndex, m_selectionInfo.selectionStartPoint, pagePoint));
}
else
{
setSelection(pdf::PDFTextSelection());
}
event->accept();
}
updateCursor();
@ -556,8 +578,11 @@ void PDFSelectTextTool::updateActions()
{
BaseClass::updateActions();
m_selectAllAction->setEnabled(isActive());
m_deselectAction->setEnabled(isActive() && !m_textSelection.isEmpty());
const bool isActive = this->isActive();
const bool hasSelection = !m_textSelection.isEmpty();
m_selectAllAction->setEnabled(isActive);
m_deselectAction->setEnabled(isActive && hasSelection);
m_copyTextAction->setEnabled(isActive && hasSelection);
}
void PDFSelectTextTool::updateCursor()
@ -575,6 +600,27 @@ void PDFSelectTextTool::updateCursor()
}
}
void PDFSelectTextTool::onActionCopyText()
{
}
void PDFSelectTextTool::onActionSelectAll()
{
if (isActive())
{
setSelection(getProxy()->getTextLayoutCompiler()->getTextSelectionAll(Qt::yellow));
}
}
void PDFSelectTextTool::onActionDeselect()
{
if (isActive())
{
setSelection(pdf::PDFTextSelection());
}
}
void PDFSelectTextTool::setSelection(PDFTextSelection&& textSelection)
{
if (m_textSelection != textSelection)
@ -590,7 +636,7 @@ PDFToolManager::PDFToolManager(PDFDrawWidgetProxy* proxy, Actions actions, QObje
m_predefinedTools()
{
m_predefinedTools[FindTextTool] = new PDFFindTextTool(proxy, actions.findPrevAction, actions.findNextAction, this, parentDialog);
m_predefinedTools[SelectTextTool] = new PDFSelectTextTool(proxy, actions.selectTextToolAction, actions.selectAllAction, actions.deselectAction, this);
m_predefinedTools[SelectTextTool] = new PDFSelectTextTool(proxy, actions.selectTextToolAction, actions.copyTextAction, actions.selectAllAction, actions.deselectAction, this);
for (PDFWidgetTool* tool : m_predefinedTools)
{

View File

@ -193,8 +193,18 @@ private:
public:
/// Construct new text selection tool
/// \param proxy Draw widget proxy
/// \param action Tool activation action
/// \param copyTextAction Copy text action
/// \param selectAllAction Select all text action
/// \param deselectAction Deselect text action
/// \param parent Parent object
explicit PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* selectAllAction,QAction* deselectAction, QObject* parent);
explicit PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* copyTextAction, QAction* selectAllAction, QAction* deselectAction, QObject* parent);
virtual void drawPage(QPainter* painter,
PDFInteger pageIndex,
const PDFPrecompiledPage* compiledPage,
PDFTextLayoutGetter& layoutGetter,
const QMatrix& pagePointToDevicePointMatrix) const override;
virtual void mousePressEvent(QWidget* widget, QMouseEvent* event) override;
virtual void mouseReleaseEvent(QWidget* widget, QMouseEvent* event) override;
@ -206,6 +216,9 @@ protected:
private:
void updateCursor();
void onActionCopyText();
void onActionSelectAll();
void onActionDeselect();
void setSelection(pdf::PDFTextSelection&& textSelection);
struct SelectionInfo
@ -214,6 +227,7 @@ private:
QPointF selectionStartPoint;
};
QAction* m_copyTextAction;
QAction* m_selectAllAction;
QAction* m_deselectAction;
pdf::PDFTextSelection m_textSelection;
@ -239,6 +253,7 @@ public:
QAction* selectTextToolAction = nullptr;
QAction* selectAllAction = nullptr;
QAction* deselectAction = nullptr;
QAction* copyTextAction = nullptr;
};
/// Construct new text search tool

View File

@ -82,6 +82,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
m_progressTaskbarIndicator(nullptr),
m_progressDialog(nullptr),
m_isBusy(false),
m_isChangingProgressStep(false),
m_toolManager(nullptr)
{
ui->setupUi(this);
@ -104,6 +105,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
ui->actionFindNext->setShortcut(QKeySequence::FindNext);
ui->actionSelectTextAll->setShortcut(QKeySequence::SelectAll);
ui->actionDeselectText->setShortcut(QKeySequence::Deselect);
ui->actionCopyText->setShortcut(QKeySequence::Copy);
connect(ui->actionOpen, &QAction::triggered, this, &PDFViewerMainWindow::onActionOpenTriggered);
connect(ui->actionClose, &QAction::triggered, this, &PDFViewerMainWindow::onActionCloseTriggered);
@ -229,6 +231,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
actions.selectTextToolAction = ui->actionSelectText;
actions.selectAllAction = ui->actionSelectTextAll;
actions.deselectAction = ui->actionDeselectText;
actions.copyTextAction = ui->actionCopyText;
m_toolManager = new pdf::PDFToolManager(m_pdfWidget->getDrawWidgetProxy(), actions, this, this);
m_pdfWidget->setToolManager(m_toolManager);
@ -575,6 +578,13 @@ void PDFViewerMainWindow::onProgressStarted(pdf::ProgressStartupInfo info)
void PDFViewerMainWindow::onProgressStep(int percentage)
{
if (m_isChangingProgressStep)
{
return;
}
pdf::PDFTemporaryValueChange guard(&m_isChangingProgressStep, true);
if (m_progressDialog)
{
m_progressDialog->setValue(percentage);

View File

@ -164,6 +164,7 @@ private:
QProgressDialog* m_progressDialog;
bool m_isBusy;
bool m_isChangingProgressStep;
pdf::PDFToolManager* m_toolManager;
};

View File

@ -105,6 +105,7 @@
<addaction name="actionFindNext"/>
<addaction name="separator"/>
<addaction name="actionSelectText"/>
<addaction name="actionCopyText"/>
<addaction name="actionSelectTextAll"/>
<addaction name="actionDeselectText"/>
<addaction name="separator"/>
@ -403,6 +404,11 @@
<string>Deselect</string>
</property>
</action>
<action name="actionCopyText">
<property name="text">
<string>Copy text</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>