Issue #118: Refactoring of forms

This commit is contained in:
Jakub Melka
2023-12-06 20:58:35 +01:00
parent edd100d3b8
commit b4d123baac
8 changed files with 2204 additions and 2116 deletions

View File

@@ -57,7 +57,6 @@ add_library(Pdf4QtLibCore SHARED
sources/pdfsignaturehandler.cpp
sources/pdfsnapper.cpp
sources/pdfstructuretree.cpp
sources/pdftexteditpseudowidget.cpp
sources/pdftextlayout.cpp
sources/pdftransparencyrenderer.cpp
sources/pdfutils.cpp

File diff suppressed because it is too large Load Diff

View File

@@ -485,56 +485,10 @@ private:
PDFWidgetToFormFieldMapping m_widgetToFormField;
};
/// Base class for editors of form fields. It enables editation
/// of form fields, such as entering text, clicking on check box etc.
class PDFFormFieldWidgetEditor
{
public:
explicit PDFFormFieldWidgetEditor(PDFFormManager* formManager, PDFFormWidget formWidget);
virtual ~PDFFormFieldWidgetEditor() = default;
virtual void shortcutOverrideEvent(QWidget* widget, QKeyEvent* event);
virtual void keyPressEvent(QWidget* widget, QKeyEvent* event);
virtual void keyReleaseEvent(QWidget* widget, QKeyEvent* event);
virtual void mousePressEvent(QWidget* widget, QMouseEvent* event, const QPointF& mousePagePosition);
virtual void mouseDoubleClickEvent(QWidget* widget, QMouseEvent* event, const QPointF& mousePagePosition);
virtual void mouseReleaseEvent(QWidget* widget, QMouseEvent* event, const QPointF& mousePagePosition);
virtual void mouseMoveEvent(QWidget* widget, QMouseEvent* event, const QPointF& mousePagePosition);
virtual void wheelEvent(QWidget* widget, QWheelEvent* event, const QPointF& mousePagePosition);
virtual void reloadValue() { }
virtual bool isEditorDrawEnabled() const { return false; }
virtual QRectF getActiveEditorRectangle() const { return QRectF(); }
const PDFFormWidget* getFormWidget() const { return &m_formWidget; }
PDFFormField* getFormField() const { return m_formWidget.getParent(); }
PDFObjectReference getWidgetAnnotation() const { return m_formWidget.getWidget(); }
void setFocus(bool hasFocus);
/// Draw form field widget using given parameters. It is used, when
/// we want to draw editor contents on the painter using parameters.
/// Parameter \p edit decides, if editor is drawn, or static contents
/// based on field value is drawn.
/// \param parameters Parameters
/// \param edit Draw editor or static contents
virtual void draw(AnnotationDrawParameters& parameters, bool edit) const;
protected:
/// This function is called every time, the focus state changes
/// \param focused If editor was focused, or not
virtual void setFocusImpl(bool focused) { Q_UNUSED(focused); }
void performKeypadNavigation(QWidget* widget, QKeyEvent* event);
PDFFormManager* m_formManager;
PDFFormWidget m_formWidget;
bool m_hasFocus;
};
/// Form manager. Manages all form widgets functionality - triggers actions,
/// edits fields, updates annotation appearances, etc. Valid pointer to annotation
/// manager is requirement.
class PDF4QTLIBCORESHARED_EXPORT PDFFormManager : public QObject, public IDrawWidgetInputInterface
class PDF4QTLIBCORESHARED_EXPORT PDFFormManager : public QObject
{
Q_OBJECT
@@ -542,7 +496,7 @@ private:
using BaseClass = QObject;
public:
explicit PDFFormManager(PDFDrawWidgetProxy* proxy, QObject* parent);
explicit PDFFormManager(QObject* parent);
virtual ~PDFFormManager() override;
enum FormAppearanceFlag
@@ -599,20 +553,6 @@ public:
/// \param functor Functor to apply
void modify(const std::function<void(PDFFormField*)>& functor) const;
/// Sets focus to the editor. Is is allowed to pass nullptr to this
/// function, it means that no editor is focused.
/// \param editor Editor to be focused
void setFocusToEditor(PDFFormFieldWidgetEditor* editor);
/// Tries to set focus on next/previous form field in the focus chain.
/// Function returns true, if focus has been successfully set.
/// \param next Focus next (true) or previous (false) widget
bool focusNextPrevFormField(bool next);
/// Returns true, if widget is focused.
/// \param widget Widget annotation reference
bool isFocused(PDFObjectReference widget) const;
/// Tries to find appropriate action and returns it. If action is not found,
/// then nullptr is returned.
/// \param actionType Action to be performed
@@ -622,36 +562,12 @@ public:
/// Returns default form apperance flags
static constexpr FormAppearanceFlags getDefaultApperanceFlags() { return FormAppearanceFlags(HighlightFields | HighlightRequiredFields); }
struct MouseEventInfo
{
/// Form field under mouse event, nullptr, if
/// no form field is under mouse button.
PDFFormField* formField = nullptr;
/// Form field widget editor, which is associated
/// with given form field.
PDFFormFieldWidgetEditor* editor = nullptr;
/// Mouse position in form field coordinate space
QPointF mousePosition;
/// Matrix, which maps from device space to widget space
QTransform deviceToWidget;
/// Returns true, if mouse event info is valid, i.e.
/// mouse event occurs above some form field.
bool isValid() const { return editor != nullptr; }
};
/// Tries to set value to the form field
void setFormFieldValue(PDFFormField::SetValueParameters parameters);
/// Get widget rectangle (from annotation)
QRectF getWidgetRectangle(const PDFFormWidget& widget) const;
/// Returns editor for form field
PDFFormFieldWidgetEditor* getEditor(const PDFFormField* formField) const;
/// Is committing data disabled?
bool isCommitDisabled() const { return m_isCommitDisabled; }
@@ -660,23 +576,6 @@ public:
/// \param action Reset action
void performResetAction(const PDFActionResetForm* action);
// interface IDrawWidgetInputInterface
virtual void shortcutOverrideEvent(QWidget* widget, QKeyEvent* event) override;
virtual void keyPressEvent(QWidget* widget, QKeyEvent* event) override;
virtual void keyReleaseEvent(QWidget* widget, QKeyEvent* event) override;
virtual void mousePressEvent(QWidget* widget, QMouseEvent* event) override;
virtual void mouseDoubleClickEvent(QWidget* widget, QMouseEvent* event) override;
virtual void mouseReleaseEvent(QWidget* widget, QMouseEvent* event) override;
virtual void mouseMoveEvent(QWidget* widget, QMouseEvent* event) override;
virtual void wheelEvent(QWidget* widget, QWheelEvent* event) override;
/// Returns tooltip generated from annotation
virtual QString getTooltip() const override { return QString(); }
/// Returns current cursor
virtual const std::optional<QCursor>& getCursor() const override;
/// Draws XFA form, or does nothing, if XFA form is not present
/// \param pagePointToDevicePointMatrix Page point to device point matrix
/// \param page Page
@@ -692,44 +591,14 @@ public:
/// is emitted.
void performPaging();
virtual int getInputPriority() const override { return FormPriority; }
protected:
virtual void updateFieldValues();
signals:
void actionTriggered(const pdf::PDFAction* action);
void documentModified(pdf::PDFModifiedDocument document);
private:
void updateFormWidgetEditors();
void updateFieldValues();
MouseEventInfo getMouseEventInfo(QWidget* widget, QPoint point);
struct MouseGrabInfo
{
MouseEventInfo info;
int mouseGrabNesting = 0;
bool isMouseGrabbed() const { return mouseGrabNesting > 0; }
};
bool isMouseGrabbed() const { return m_mouseGrabInfo.isMouseGrabbed(); }
/// Grabs mouse input, if mouse is already grabbed, or if event
/// is accepted. When mouse is grabbed, then mouse input events
/// are sent to active editor and are automatically accepted.
/// \param info Mouse event info
/// \param event Mouse event
void grabMouse(const MouseEventInfo& info, QMouseEvent* event);
/// Release mouse input
/// \param info Mouse event info
/// \param event Mouse event
void ungrabMouse(const MouseEventInfo& info, QMouseEvent* event);
/// Releases all form widget editors
void clearEditors();
PDFDrawWidgetProxy* m_proxy;
PDFAnnotationManager* m_annotationManager;
const PDFDocument* m_document;
FormAppearanceFlags m_flags;
@@ -737,11 +606,6 @@ private:
bool m_isCommitDisabled;
PDFXFAEngine m_xfaEngine;
std::vector<PDFFormFieldWidgetEditor*> m_widgetEditors;
PDFFormFieldWidgetEditor* m_focusedEditor;
MouseGrabInfo m_mouseGrabInfo;
std::optional<QCursor> m_mouseCursor;
};
} // namespace pdf

View File

@@ -1,938 +0,0 @@
// Copyright (C) 2022 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#include "pdftexteditpseudowidget.h"
#include "pdfpainterutils.h"
#include <QStyle>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QCoreApplication>
#include <QClipboard>
namespace pdf
{
PDFTextEditPseudowidget::PDFTextEditPseudowidget(PDFFormField::FieldFlags flags) :
m_flags(flags),
m_selectionStart(0),
m_selectionEnd(0),
m_positionCursor(0),
m_maxTextLength(0)
{
m_textLayout.setCacheEnabled(true);
m_passwordReplacementCharacter = QChar(QCoreApplication::style()->styleHint(QStyle::SH_LineEdit_PasswordCharacter));
}
void PDFTextEditPseudowidget::shortcutOverrideEvent(QWidget* widget, QKeyEvent* event)
{
Q_UNUSED(widget);
constexpr QKeySequence::StandardKey acceptedKeys[] = { QKeySequence::Delete, QKeySequence::Cut, QKeySequence::Copy, QKeySequence::Paste,
QKeySequence::SelectAll, QKeySequence::MoveToNextChar, QKeySequence::MoveToPreviousChar,
QKeySequence::MoveToNextWord, QKeySequence::MoveToPreviousWord, QKeySequence::MoveToNextLine,
QKeySequence::MoveToPreviousLine, QKeySequence::MoveToStartOfLine, QKeySequence::MoveToEndOfLine,
QKeySequence::MoveToStartOfBlock, QKeySequence::MoveToEndOfBlock, QKeySequence::MoveToStartOfDocument,
QKeySequence::MoveToEndOfDocument, QKeySequence::SelectNextChar, QKeySequence::SelectPreviousChar,
QKeySequence::SelectNextWord, QKeySequence::SelectPreviousWord, QKeySequence::SelectNextLine,
QKeySequence::SelectPreviousLine, QKeySequence::SelectStartOfLine, QKeySequence::SelectEndOfLine,
QKeySequence::SelectStartOfBlock, QKeySequence::SelectEndOfBlock, QKeySequence::SelectStartOfDocument,
QKeySequence::SelectEndOfDocument, QKeySequence::DeleteStartOfWord, QKeySequence::DeleteEndOfWord,
QKeySequence::DeleteEndOfLine, QKeySequence::Deselect, QKeySequence::DeleteCompleteLine, QKeySequence::Backspace };
if (std::any_of(std::begin(acceptedKeys), std::end(acceptedKeys), [event](QKeySequence::StandardKey standardKey) { return event == standardKey; }))
{
event->accept();
return;
}
switch (event->key())
{
case Qt::Key_Direction_L:
case Qt::Key_Direction_R:
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_Left:
case Qt::Key_Right:
event->accept();
break;
default:
break;
}
if (!event->text().isEmpty())
{
event->accept();
for (const QChar& character : event->text())
{
if (!character.isPrint())
{
event->ignore();
break;
}
}
}
}
void PDFTextEditPseudowidget::keyPressEvent(QWidget* widget, QKeyEvent* event)
{
Q_UNUSED(widget);
/*
We will support following key sequences:
Delete
Cut,
Copy,
Paste,
SelectAll,
MoveToNextChar,
MoveToPreviousChar,
MoveToNextWord,
MoveToPreviousWord,
MoveToNextLine,
MoveToPreviousLine,
MoveToStartOfLine,
MoveToEndOfLine,
MoveToStartOfBlock,
MoveToEndOfBlock,
MoveToStartOfDocument,
MoveToEndOfDocument,
SelectNextChar,
SelectPreviousChar,
SelectNextWord,
SelectPreviousWord,
SelectNextLine,
SelectPreviousLine,
SelectStartOfLine,
SelectEndOfLine,
SelectStartOfBlock,
SelectEndOfBlock,
SelectStartOfDocument,
SelectEndOfDocument,
DeleteStartOfWord,
DeleteEndOfWord,
DeleteEndOfLine,
Deselect,
DeleteCompleteLine,
Backspace,
* */
event->accept();
if (event == QKeySequence::Delete)
{
performDelete();
}
else if (event == QKeySequence::Cut)
{
performCut();
}
else if (event == QKeySequence::Copy)
{
performCopy();
}
else if (event == QKeySequence::Paste)
{
performPaste();
}
else if (event == QKeySequence::SelectAll)
{
setSelection(0, getTextLength());
}
else if (event == QKeySequence::MoveToNextChar)
{
setCursorPosition(getCursorCharacterForward(), false);
}
else if (event == QKeySequence::MoveToPreviousChar)
{
setCursorPosition(getCursorCharacterBackward(), false);
}
else if (event == QKeySequence::MoveToNextWord)
{
setCursorPosition(getCursorWordForward(), false);
}
else if (event == QKeySequence::MoveToPreviousWord)
{
setCursorPosition(getCursorWordBackward(), false);
}
else if (event == QKeySequence::MoveToNextLine)
{
setCursorPosition(getCursorLineDown(), false);
}
else if (event == QKeySequence::MoveToPreviousLine)
{
setCursorPosition(getCursorLineUp(), false);
}
else if (event == QKeySequence::MoveToStartOfLine || event == QKeySequence::MoveToStartOfBlock)
{
setCursorPosition(getCursorLineStart(), false);
}
else if (event == QKeySequence::MoveToEndOfLine || event == QKeySequence::MoveToEndOfBlock)
{
setCursorPosition(getCursorLineEnd(), false);
}
else if (event == QKeySequence::MoveToStartOfDocument)
{
setCursorPosition(getCursorDocumentStart(), false);
}
else if (event == QKeySequence::MoveToEndOfDocument)
{
setCursorPosition(getCursorDocumentEnd(), false);
}
else if (event == QKeySequence::SelectNextChar)
{
setCursorPosition(getCursorCharacterForward(), true);
}
else if (event == QKeySequence::SelectPreviousChar)
{
setCursorPosition(getCursorCharacterBackward(), true);
}
else if (event == QKeySequence::SelectNextWord)
{
setCursorPosition(getCursorWordForward(), true);
}
else if (event == QKeySequence::SelectPreviousWord)
{
setCursorPosition(getCursorWordBackward(), true);
}
else if (event == QKeySequence::SelectNextLine)
{
setCursorPosition(getCursorLineDown(), true);
}
else if (event == QKeySequence::SelectPreviousLine)
{
setCursorPosition(getCursorLineUp(), true);
}
else if (event == QKeySequence::SelectStartOfLine || event == QKeySequence::SelectStartOfBlock)
{
setCursorPosition(getCursorLineStart(), true);
}
else if (event == QKeySequence::SelectEndOfLine || event == QKeySequence::SelectEndOfBlock)
{
setCursorPosition(getCursorLineEnd(), true);
}
else if (event == QKeySequence::SelectStartOfDocument)
{
setCursorPosition(getCursorDocumentStart(), true);
}
else if (event == QKeySequence::SelectEndOfDocument)
{
setCursorPosition(getCursorDocumentEnd(), true);
}
else if (event == QKeySequence::DeleteStartOfWord)
{
if (!isReadonly())
{
setCursorPosition(getCursorWordBackward(), true);
performRemoveSelectedText();
}
}
else if (event == QKeySequence::DeleteEndOfWord)
{
if (!isReadonly())
{
setCursorPosition(getCursorWordForward(), true);
performRemoveSelectedText();
}
}
else if (event == QKeySequence::DeleteEndOfLine)
{
if (!isReadonly())
{
setCursorPosition(getCursorLineEnd(), true);
performRemoveSelectedText();
}
}
else if (event == QKeySequence::Deselect)
{
clearSelection();
}
else if (event == QKeySequence::DeleteCompleteLine)
{
if (!isReadonly())
{
m_selectionStart = getCurrentLineTextStart();
m_selectionEnd = getCurrentLineTextEnd();
performRemoveSelectedText();
}
}
else if (event == QKeySequence::Backspace || event->key() == Qt::Key_Backspace)
{
performBackspace();
}
else if (event->key() == Qt::Key_Direction_L)
{
QTextOption option = m_textLayout.textOption();
option.setTextDirection(Qt::LeftToRight);
m_textLayout.setTextOption(qMove(option));
updateTextLayout();
}
else if (event->key() == Qt::Key_Direction_R)
{
QTextOption option = m_textLayout.textOption();
option.setTextDirection(Qt::LeftToRight);
m_textLayout.setTextOption(qMove(option));
updateTextLayout();
}
else if (event->key() == Qt::Key_Up)
{
setCursorPosition(getCursorLineUp(), event->modifiers().testFlag(Qt::ShiftModifier));
}
else if (event->key() == Qt::Key_Down)
{
setCursorPosition(getCursorLineDown(), event->modifiers().testFlag(Qt::ShiftModifier));
}
else if (event->key() == Qt::Key_Left)
{
const int position = (event->modifiers().testFlag(Qt::ControlModifier)) ? getCursorWordBackward() : getCursorCharacterBackward();
setCursorPosition(position, event->modifiers().testFlag(Qt::ShiftModifier));
}
else if (event->key() == Qt::Key_Right)
{
const int position = (event->modifiers().testFlag(Qt::ControlModifier)) ? getCursorWordForward() : getCursorCharacterForward();
setCursorPosition(position, event->modifiers().testFlag(Qt::ShiftModifier));
}
else if (isMultiline() && (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return))
{
performInsertText(QString::fromUtf16(u"\u2028"));
}
else
{
QString text = event->text();
if (!text.isEmpty())
{
performInsertText(text);
}
else
{
event->ignore();
}
}
}
void PDFTextEditPseudowidget::setSelection(int startPosition, int selectionLength)
{
if (selectionLength > 0)
{
// We are selecting to the right
m_selectionStart = startPosition;
m_selectionEnd = qMin(startPosition + selectionLength, getTextLength());
m_positionCursor = m_selectionEnd;
}
else if (selectionLength < 0)
{
// We are selecting to the left
m_selectionStart = qMax(startPosition + selectionLength, 0);
m_selectionEnd = startPosition;
m_positionCursor = m_selectionStart;
}
else
{
// Clear the selection
m_selectionStart = 0;
m_selectionEnd = 0;
m_positionCursor = startPosition;
}
}
void PDFTextEditPseudowidget::setCursorPosition(int position, bool select)
{
if (select)
{
const bool isTextSelected = this->isTextSelected();
const bool isCursorAtStartOfSelection = isTextSelected && m_selectionStart == m_positionCursor;
const bool isCursorAtEndOfSelection = isTextSelected && m_selectionEnd == m_positionCursor;
// Do we have selected text, and cursor is at the end of selected text?
// In this case, we must preserve start of the selection (we are manipulating
// with the end of selection.
if (isCursorAtEndOfSelection)
{
m_selectionStart = qMin(m_selectionStart, position);
m_selectionEnd = qMax(m_selectionStart, position);
}
else if (isCursorAtStartOfSelection)
{
// We must preserve end of the text selection, because we are manipulating
// with start of text selection.
m_selectionStart = qMin(m_selectionEnd, position);
m_selectionEnd = qMax(m_selectionEnd, position);
}
else
{
// Otherwise we are manipulating with cursor
m_selectionStart = qMin(m_positionCursor, position);
m_selectionEnd = qMax(m_positionCursor, position);
}
}
// Why we are clearing text selection, even if we doesn't have it?
// We can have, for example m_selectionStart == m_selectionEnd == 3,
// and we want to have it both zero.
if (!select || !isTextSelected())
{
clearSelection();
}
m_positionCursor = position;
}
void PDFTextEditPseudowidget::setText(const QString& text)
{
clearSelection();
m_editText = text;
setCursorPosition(getPositionEnd(), false);
updateTextLayout();
}
void PDFTextEditPseudowidget::setAppearance(const PDFAnnotationDefaultAppearance& appearance, Qt::Alignment textAlignment, QRectF rect, int maxTextLength)
{
// Set appearance
qreal fontSize = appearance.getFontSize();
if (qFuzzyIsNull(fontSize))
{
fontSize = rect.height();
}
QFont font(appearance.getFontName());
font.setHintingPreference(QFont::PreferNoHinting);
font.setPixelSize(qCeil(fontSize));
font.setStyleStrategy(QFont::ForceOutline);
m_textLayout.setFont(font);
QTextOption option = m_textLayout.textOption();
option.setWrapMode(isMultiline() ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap);
option.setAlignment(textAlignment);
option.setUseDesignMetrics(true);
m_textLayout.setTextOption(option);
m_textColor = appearance.getFontColor();
if (!m_textColor.isValid())
{
m_textColor = Qt::black;
}
m_maxTextLength = maxTextLength;
m_widgetRect = rect;
updateTextLayout();
}
void PDFTextEditPseudowidget::setAppearance(const QFont& font,
Qt::Alignment textAlignment,
QRectF rect,
int maxTextLength,
QColor textColor)
{
// Set appearance
m_textLayout.setFont(font);
QTextOption option = m_textLayout.textOption();
option.setWrapMode(isMultiline() ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap);
option.setAlignment(textAlignment);
option.setUseDesignMetrics(true);
m_textLayout.setTextOption(option);
m_textColor = textColor;
if (!m_textColor.isValid())
{
m_textColor = Qt::black;
}
m_maxTextLength = maxTextLength;
m_widgetRect = rect;
updateTextLayout();
}
void PDFTextEditPseudowidget::performCut()
{
if (isReadonly())
{
return;
}
performCopy();
performRemoveSelectedText();
}
void PDFTextEditPseudowidget::performCopy()
{
if (isTextSelected() && !isPassword())
{
QApplication::clipboard()->setText(getSelectedText(), QClipboard::Clipboard);
}
}
void PDFTextEditPseudowidget::performPaste()
{
// We always insert text, even if it is empty. Because we want
// to erase selected text.
performInsertText(QApplication::clipboard()->text(QClipboard::Clipboard));
}
void PDFTextEditPseudowidget::performClear()
{
if (isReadonly())
{
return;
}
performSelectAll();
performRemoveSelectedText();
}
void PDFTextEditPseudowidget::performSelectAll()
{
m_selectionStart = getPositionStart();
m_selectionEnd = getPositionEnd();
}
void PDFTextEditPseudowidget::performBackspace()
{
if (isReadonly())
{
return;
}
// If we have selection, then delete selected text. If we do not have
// selection, then we delete previous character.
if (!isTextSelected())
{
setCursorPosition(m_textLayout.previousCursorPosition(m_positionCursor, QTextLayout::SkipCharacters), true);
}
performRemoveSelectedText();
}
void PDFTextEditPseudowidget::performDelete()
{
if (isReadonly())
{
return;
}
// If we have selection, then delete selected text. If we do not have
// selection, then we delete previous character.
if (!isTextSelected())
{
setCursorPosition(m_textLayout.nextCursorPosition(m_positionCursor, QTextLayout::SkipCharacters), true);
}
performRemoveSelectedText();
}
void PDFTextEditPseudowidget::performRemoveSelectedText()
{
if (isTextSelected())
{
m_editText.remove(m_selectionStart, getSelectionLength());
setCursorPosition(m_selectionStart, false);
clearSelection();
updateTextLayout();
}
}
void PDFTextEditPseudowidget::performInsertText(const QString& text)
{
if (isReadonly())
{
return;
}
// Insert text at the cursor
performRemoveSelectedText();
m_editText.insert(m_positionCursor, text);
setCursorPosition(m_positionCursor + text.length(), false);
updateTextLayout();
}
QTransform PDFTextEditPseudowidget::createTextBoxTransformMatrix(bool edit) const
{
QTransform matrix;
matrix.translate(m_widgetRect.left(), m_widgetRect.bottom());
matrix.scale(1.0, -1.0);
if (edit && !isComb() && m_textLayout.isValidCursorPosition(m_positionCursor))
{
// Jakub Melka: we must scroll the control, so cursor is always
// visible in the widget area. If we are not editing, this not the
// case, because we always show the text from the beginning.
const QTextLine line = m_textLayout.lineForTextPosition(m_positionCursor);
if (line.isValid())
{
const qreal xCursorPosition = line.cursorToX(m_positionCursor);
if (xCursorPosition >= m_widgetRect.width())
{
const qreal delta = xCursorPosition - m_widgetRect.width();
matrix.translate(-delta, 0.0);
}
// Check, if we aren't outside of y position
const qreal lineSpacing = line.leadingIncluded() ? line.height() : line.leading() + line.height();
const qreal lineBottom = lineSpacing * (line.lineNumber() + 1);
if (lineBottom >= m_widgetRect.height())
{
const qreal delta = lineBottom - m_widgetRect.height();
matrix.translate(0.0, -delta);
}
}
}
if (!isMultiline() && !isComb())
{
// If text is single line, then adjust text position to the vertical center
QTextLine textLine = m_textLayout.lineAt(0);
if (textLine.isValid())
{
const qreal lineSpacing = textLine.leadingIncluded() ? textLine.height() : textLine.leading() + textLine.height();
const qreal textBoxHeight = m_widgetRect.height();
if (lineSpacing < textBoxHeight)
{
const qreal delta = (textBoxHeight - lineSpacing) * 0.5;
matrix.translate(0.0, delta);
}
}
}
return matrix;
}
std::vector<int> PDFTextEditPseudowidget::getCursorPositions() const
{
std::vector<int> result;
result.reserve(m_editText.length());
result.push_back(0);
int currentPos = 0;
while (currentPos < m_editText.length())
{
currentPos = m_textLayout.nextCursorPosition(currentPos, QTextLayout::SkipCharacters);
result.push_back(currentPos);
}
return result;
}
void PDFTextEditPseudowidget::draw(AnnotationDrawParameters& parameters, bool edit) const
{
pdf::PDFPainterStateGuard guard(parameters.painter);
if (parameters.annotation)
{
parameters.boundingRectangle = parameters.annotation->getRectangle();
}
QPalette palette = QApplication::palette();
QPainter* painter = parameters.painter;
if (edit)
{
pdf::PDFPainterStateGuard guard2(painter);
painter->setPen(parameters.colorConvertor.convert(Qt::black, false, true));
painter->setBrush(Qt::NoBrush);
painter->drawRect(parameters.boundingRectangle);
}
painter->setClipRect(parameters.boundingRectangle, Qt::IntersectClip);
painter->setWorldTransform(QTransform(createTextBoxTransformMatrix(edit)), true);
painter->setPen(parameters.colorConvertor.convert(Qt::black, false, true));
if (isComb())
{
const qreal combCount = qMax(m_maxTextLength, 1);
qreal combWidth = m_widgetRect.width() / combCount;
QRectF combRect(0.0, 0.0, combWidth, m_widgetRect.height());
painter->setFont(m_textLayout.font());
QColor textColor = parameters.colorConvertor.convert(m_textColor, false, true);
QColor highlightTextColor = parameters.colorConvertor.convert(palette.color(QPalette::HighlightedText), false, true);
QColor highlightColor = parameters.colorConvertor.convert(palette.color(QPalette::Highlight), false, false);
std::vector<int> positions = getCursorPositions();
for (size_t i = 1; i < positions.size(); ++i)
{
if (positions[i - 1] >= m_selectionStart && positions[i] - 1 < m_selectionEnd)
{
// We are in text selection
painter->fillRect(combRect, highlightColor);
painter->setPen(highlightTextColor);
}
else
{
// We are not in text selection
painter->setPen(textColor);
}
int length = positions[i] - positions[i - 1];
QString text = m_displayText.mid(positions[i - 1], length);
painter->drawText(combRect, Qt::AlignCenter, text);
// Draw the cursor?
if (edit && m_positionCursor >= positions[i - 1] && m_positionCursor < positions[i])
{
QRectF cursorRect(combRect.left(), combRect.bottom() - 1, combRect.width(), 1);
painter->fillRect(cursorRect, textColor);
}
combRect.translate(combWidth, 0.0);
}
// Draw the cursor onto next unfilled cell?
if (edit && m_positionCursor == getPositionEnd())
{
QRectF cursorRect(combRect.left(), combRect.bottom() - 1, combRect.width(), 1);
painter->fillRect(cursorRect, textColor);
}
}
else
{
QVector<QTextLayout::FormatRange> selections;
QTextLayout::FormatRange defaultFormat;
defaultFormat.start = getPositionStart();
defaultFormat.length = getTextLength();
defaultFormat.format.clearBackground();
defaultFormat.format.setForeground(QBrush(parameters.colorConvertor.convert(m_textColor, false, true), Qt::SolidPattern));
// If we are editing, draw selections
if (edit && isTextSelected())
{
QTextLayout::FormatRange before = defaultFormat;
QTextLayout::FormatRange after = defaultFormat;
before.start = getPositionStart();
before.length = m_selectionStart;
after.start = m_selectionEnd;
after.length = getTextLength() - m_selectionEnd;
QTextLayout::FormatRange selectedFormat = defaultFormat;
selectedFormat.start = m_selectionStart;
selectedFormat.length = getSelectionLength();
selectedFormat.format.setForeground(QBrush(parameters.colorConvertor.convert(palette.color(QPalette::HighlightedText), false, true), Qt::SolidPattern));
selectedFormat.format.setBackground(QBrush(parameters.colorConvertor.convert(palette.color(QPalette::Highlight), false, false), Qt::SolidPattern));
selections = { before, selectedFormat, after};
}
else
{
selections.push_back(defaultFormat);
}
// Draw text
m_textLayout.draw(painter, QPointF(0.0, 0.0), selections, QRectF());
// If we are editing, also draw text
if (edit && !isReadonly())
{
m_textLayout.drawCursor(painter, QPointF(0.0, 0.0), m_positionCursor);
}
}
}
int PDFTextEditPseudowidget::getCursorPositionFromWidgetPosition(const QPointF& point, bool edit) const
{
QTransform textBoxSpaceToPageSpace = createTextBoxTransformMatrix(edit);
QTransform pageSpaceToTextBoxSpace = textBoxSpaceToPageSpace.inverted();
QPointF textBoxPoint = pageSpaceToTextBoxSpace.map(point);
if (isComb())
{
// If it is comb, then characters are spaced equidistantly
const qreal x = qBound(0.0, textBoxPoint.x(), m_widgetRect.width());
const size_t position = qFloor(x * qreal(m_maxTextLength) / qreal(m_widgetRect.width()));
std::vector<int> positions = getCursorPositions();
if (position < positions.size())
{
return positions[position];
}
return positions.back();
}
else if (m_textLayout.lineCount() > 0)
{
QTextLine line;
qreal yPos = 0.0;
// Find line under cursor
for (int i = 0; i < m_textLayout.lineCount(); ++i)
{
QTextLine currentLine = m_textLayout.lineAt(i);
const qreal lineSpacing = currentLine.leadingIncluded() ? currentLine.height() : currentLine.leading() + currentLine.height();
const qreal yNextPos = yPos + lineSpacing;
if (textBoxPoint.y() >= yPos && textBoxPoint.y() < yNextPos)
{
line = currentLine;
break;
}
yPos = yNextPos;
}
// If no line is found, select last line
if (!line.isValid())
{
if (textBoxPoint.y() < 0.0)
{
line = m_textLayout.lineAt(0);
}
else
{
line = m_textLayout.lineAt(m_textLayout.lineCount() - 1);
}
}
return line.xToCursor(textBoxPoint.x(), QTextLine::CursorBetweenCharacters);
}
return 0;
}
void PDFTextEditPseudowidget::updateTextLayout()
{
// Prepare display text
if (isPassword())
{
m_displayText.resize(m_editText.length(), m_passwordReplacementCharacter);
}
else
{
m_displayText = m_editText;
}
// Perform text layout
m_textLayout.clearLayout();
m_textLayout.setText(m_displayText);
m_textLayout.beginLayout();
QPointF textLinePosition(0.0, 0.0);
while (true)
{
QTextLine textLine = m_textLayout.createLine();
if (!textLine.isValid())
{
// We are finished with layout
break;
}
textLinePosition.ry() += textLine.leading();
textLine.setLineWidth(m_widgetRect.width());
textLine.setPosition(textLinePosition);
textLinePosition.ry() += textLine.height();
}
m_textLayout.endLayout();
// Check length
if (m_maxTextLength > 0)
{
int length = 0;
int currentPos = 0;
while (currentPos < m_editText.length())
{
currentPos = m_textLayout.nextCursorPosition(currentPos, QTextLayout::SkipCharacters);
++length;
if (length == m_maxTextLength)
{
break;
}
}
if (currentPos < m_editText.length())
{
m_editText = m_editText.left(currentPos);
m_positionCursor = qBound(getPositionStart(), m_positionCursor, getPositionEnd());
updateTextLayout();
}
}
}
int PDFTextEditPseudowidget::getSingleStepForward() const
{
// If direction is right-to-left, then move backward (because
// text is painted from right to left.
return (m_textLayout.textOption().textDirection() == Qt::RightToLeft) ? -1 : 1;
}
int PDFTextEditPseudowidget::getNextPrevCursorPosition(int referencePosition, int steps, QTextLayout::CursorMode mode) const
{
int cursor = referencePosition;
if (steps > 0)
{
for (int i = 0; i < steps; ++i)
{
cursor = m_textLayout.nextCursorPosition(cursor, mode);
}
}
else if (steps < 0)
{
for (int i = 0; i < -steps; ++i)
{
cursor = m_textLayout.previousCursorPosition(cursor, mode);
}
}
return cursor;
}
int PDFTextEditPseudowidget::getCurrentLineTextStart() const
{
return m_textLayout.lineForTextPosition(m_positionCursor).textStart();
}
int PDFTextEditPseudowidget::getCurrentLineTextEnd() const
{
QTextLine textLine = m_textLayout.lineForTextPosition(m_positionCursor);
return textLine.textStart() + textLine.textLength();
}
int PDFTextEditPseudowidget::getCursorLineUp() const
{
QTextLine line = m_textLayout.lineForTextPosition(m_positionCursor);
const int lineIndex = line.lineNumber() - 1;
if (lineIndex >= 0)
{
QTextLine upLine = m_textLayout.lineAt(lineIndex);
return upLine.xToCursor(line.cursorToX(m_positionCursor), QTextLine::CursorBetweenCharacters);
}
return m_positionCursor;
}
int PDFTextEditPseudowidget::getCursorLineDown() const
{
QTextLine line = m_textLayout.lineForTextPosition(m_positionCursor);
const int lineIndex = line.lineNumber() + 1;
if (lineIndex < m_textLayout.lineCount())
{
QTextLine downLine = m_textLayout.lineAt(lineIndex);
return downLine.xToCursor(line.cursorToX(m_positionCursor), QTextLine::CursorBetweenCharacters);
}
return m_positionCursor;
}
} // namespace pdf

View File

@@ -1,217 +0,0 @@
// Copyright (C) 2022 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFTEXTEDITPSEUDOWIDGET_H
#define PDFTEXTEDITPSEUDOWIDGET_H
#include "pdfglobal.h"
#include "pdfform.h"
#include <QRectF>
#include <QColor>
class QWidget;
class QKeyEvent;
namespace pdf
{
/// "Pseudo" widget, which is emulating text editor, which can be single line, or multiline.
/// Passwords can also be edited and editor can be read only.
class PDFTextEditPseudowidget
{
public:
explicit PDFTextEditPseudowidget(PDFFormField::FieldFlags flags);
void shortcutOverrideEvent(QWidget* widget, QKeyEvent* event);
void keyPressEvent(QWidget* widget, QKeyEvent* event);
inline bool isReadonly() const { return m_flags.testFlag(PDFFormField::ReadOnly); }
inline bool isMultiline() const { return m_flags.testFlag(PDFFormField::Multiline); }
inline bool isPassword() const { return m_flags.testFlag(PDFFormField::Password); }
inline bool isFileSelect() const { return m_flags.testFlag(PDFFormField::FileSelect); }
inline bool isComb() const { return m_flags.testFlag(PDFFormField::Comb); }
inline bool isEmpty() const { return m_editText.isEmpty(); }
inline bool isTextSelected() const { return !isEmpty() && getSelectionLength() > 0; }
inline bool isWholeTextSelected() const { return !isEmpty() && getSelectionLength() == m_editText.length(); }
inline int getTextLength() const { return m_editText.length(); }
inline int getSelectionLength() const { return m_selectionEnd - m_selectionStart; }
inline int getPositionCursor() const { return m_positionCursor; }
inline int getPositionStart() const { return 0; }
inline int getPositionEnd() const { return getTextLength(); }
inline void clearSelection() { m_selectionStart = m_selectionEnd = 0; }
inline const QString& getText() const { return m_editText; }
inline QString getSelectedText() const { return m_editText.mid(m_selectionStart, getSelectionLength()); }
/// Sets (updates) text selection
/// \param startPosition From where we are selecting text
/// \param selectionLength Selection length (positive - to the right, negative - to the left)
void setSelection(int startPosition, int selectionLength);
/// Moves cursor position. It behaves as usual in text boxes,
/// when user moves the cursor. If \p select is true, then
/// selection is updated.
/// \param position New position of the cursor
/// \param select Select text when moving the cursor?
void setCursorPosition(int position, bool select);
/// Sets text content of the widget. This functions sets the text,
/// even if widget is readonly.
/// \param text Text to be set
void setText(const QString& text);
/// Sets widget appearance, such as font, font size, color, text alignment,
/// and rectangle, in which widget resides on page (in page coordinates)
/// \param appearance Appearance
/// \param textAlignment Text alignment
/// \param rect Widget rectangle in page coordinates
/// \param maxTextLength Maximal text length
void setAppearance(const PDFAnnotationDefaultAppearance& appearance,
Qt::Alignment textAlignment,
QRectF rect,
int maxTextLength);
/// Sets widget appearance, such as font, font size, color, text alignment,
/// and rectangle, in which widget resides on page (in page coordinates)
/// \param font Font
/// \param textAlignment Text alignment
/// \param rect Widget rectangle in page coordinates
/// \param maxTextLength Maximal text length
/// \param textColor Color
void setAppearance(const QFont& font,
Qt::Alignment textAlignment,
QRectF rect,
int maxTextLength,
QColor textColor);
void performCut();
void performCopy();
void performPaste();
void performClear();
void performSelectAll();
void performBackspace();
void performDelete();
void performRemoveSelectedText();
void performInsertText(const QString& text);
/// Draw text edit using given parameters
/// \param parameters Parameters
void draw(AnnotationDrawParameters& parameters, bool edit) const;
/// Returns valid cursor position retrieved from position in the widget.
/// \param point Point in page coordinate space
/// \param edit Are we using edit transformations?
/// \returns Cursor position
int getCursorPositionFromWidgetPosition(const QPointF& point, bool edit) const;
inline int getCursorForward(QTextLayout::CursorMode mode) const { return getNextPrevCursorPosition(getSingleStepForward(), mode); }
inline int getCursorBackward(QTextLayout::CursorMode mode) const { return getNextPrevCursorPosition(getSingleStepBackward(), mode); }
inline int getCursorCharacterForward() const { return getCursorForward(QTextLayout::SkipCharacters); }
inline int getCursorCharacterBackward() const { return getCursorBackward(QTextLayout::SkipCharacters); }
inline int getCursorWordForward() const { return getCursorForward(QTextLayout::SkipWords); }
inline int getCursorWordBackward() const { return getCursorBackward(QTextLayout::SkipWords); }
inline int getCursorDocumentStart() const { return (getSingleStepForward() > 0) ? getPositionStart() : getPositionEnd(); }
inline int getCursorDocumentEnd() const { return (getSingleStepForward() > 0) ? getPositionEnd() : getPositionStart(); }
inline int getCursorLineStart() const { return (getSingleStepForward() > 0) ? getCurrentLineTextStart() : getCurrentLineTextEnd(); }
inline int getCursorLineEnd() const { return (getSingleStepForward() > 0) ? getCurrentLineTextEnd() : getCurrentLineTextStart(); }
inline int getCursorNextLine() const { return getCurrentLineTextEnd(); }
inline int getCursorPreviousLine() const { return getNextPrevCursorPosition(getCurrentLineTextStart(), -1, QTextLayout::SkipCharacters); }
const QRectF& getWidgetRect() const { return m_widgetRect; }
QFont getFont() const { return m_textLayout.font(); }
QColor getFontColor() const { return m_textColor; }
private:
/// This function does following things:
/// 1) Clamps edit text to fit maximum length
/// 2) Creates display string from edit string
/// 3) Updates text layout
void updateTextLayout();
/// Returns single step forward, which is determined
/// by cursor move style and layout direction.
int getSingleStepForward() const;
/// Returns single step backward, which is determined
/// by cursor move style and layout direction.
int getSingleStepBackward() const { return -getSingleStepForward(); }
/// Returns next/previous position, by number of steps,
/// using given cursor mode (skipping characters or whole words).
/// \param steps Number of steps to proceed (can be negative number)
/// \param mode Skip mode - letters or words?
int getNextPrevCursorPosition(int steps, QTextLayout::CursorMode mode) const { return getNextPrevCursorPosition(m_positionCursor, steps, mode); }
/// Returns next/previous position from reference cursor position, by number of steps,
/// using given cursor mode (skipping characters or whole words).
/// \param referencePosition Reference cursor position
/// \param steps Number of steps to proceed (can be negative number)
/// \param mode Skip mode - letters or words?
int getNextPrevCursorPosition(int referencePosition, int steps, QTextLayout::CursorMode mode) const;
/// Returns current line text start position
int getCurrentLineTextStart() const;
/// Returns current line text end position
int getCurrentLineTextEnd() const;
/// Creates text box transform matrix, which transforms from
/// widget space to page space.
/// \param edit Create matrix for text editing?
QTransform createTextBoxTransformMatrix(bool edit) const;
/// Returns vector of cursor positions (which may be not equal
/// to edit string length, because edit string can contain surrogates,
/// or graphemes, which are single glyphs, but represented by more
/// 16-bit unicode codes).
std::vector<int> getCursorPositions() const;
int getCursorLineUp() const;
int getCursorLineDown() const;
PDFFormField::FieldFlags m_flags;
/// Text edited by the user
QString m_editText;
/// Text, which is displayed. It can differ from text
/// edited by user, in case password is being entered.
QString m_displayText;
/// Text layout
QTextLayout m_textLayout;
/// Character for displaying passwords
QChar m_passwordReplacementCharacter;
int m_selectionStart;
int m_selectionEnd;
int m_positionCursor;
int m_maxTextLength;
QRectF m_widgetRect;
QColor m_textColor;
};
} // namespace pdf
#endif // PDFTEXTEDITPSEUDOWIDGET_H