mirror of
https://github.com/JakubMelka/PDF4QT.git
synced 2025-01-30 09:04:48 +01:00
Text selection tool - finishing (without copying text)
This commit is contained in:
parent
95f6135482
commit
12b2f44619
@ -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())
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -164,6 +164,7 @@ private:
|
||||
|
||||
QProgressDialog* m_progressDialog;
|
||||
bool m_isBusy;
|
||||
bool m_isChangingProgressStep;
|
||||
|
||||
pdf::PDFToolManager* m_toolManager;
|
||||
};
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user