mirror of
https://github.com/UpsilonNumworks/Upsilon.git
synced 2026-01-19 00:37:25 +01:00
289 lines
11 KiB
C++
289 lines
11 KiB
C++
#include <escher/layout_field.h>
|
|
#include <apps/i18n.h>
|
|
#include <escher/clipboard.h>
|
|
#include <escher/text_field.h>
|
|
#include <poincare/expression.h>
|
|
#include <poincare/horizontal_layout_node.h>
|
|
#include <assert.h>
|
|
#include <string.h>
|
|
|
|
using namespace Poincare;
|
|
|
|
LayoutField::ContentView::ContentView() :
|
|
m_cursor(),
|
|
m_expressionView(0.0f, 0.5f, KDColorBlack, KDColorWhite),
|
|
m_cursorView(),
|
|
m_isEditing(false)
|
|
{
|
|
clearLayout();
|
|
}
|
|
|
|
void LayoutField::ContentView::layoutCursorSubview() {
|
|
if (!m_isEditing) {
|
|
m_cursorView.setFrame(KDRectZero);
|
|
return;
|
|
}
|
|
KDPoint expressionViewOrigin = m_expressionView.absoluteDrawingOrigin();
|
|
LayoutRef pointedLayoutR = m_cursor.layoutReference();
|
|
LayoutCursor::Position cursorPosition = m_cursor.position();
|
|
LayoutCursor eqCursor = pointedLayoutR.equivalentCursor(&m_cursor);
|
|
if (eqCursor.isDefined() && pointedLayoutR.hasChild(eqCursor.layoutReference())) {
|
|
pointedLayoutR = eqCursor.layoutReference();
|
|
cursorPosition = eqCursor.position();
|
|
}
|
|
KDPoint cursoredExpressionViewOrigin = pointedLayoutR.absoluteOrigin();
|
|
KDCoordinate cursorX = expressionViewOrigin.x() + cursoredExpressionViewOrigin.x();
|
|
if (cursorPosition == LayoutCursor::Position::Right) {
|
|
cursorX += pointedLayoutR.layoutSize().width();
|
|
}
|
|
KDPoint cursorTopLeftPosition(cursorX, expressionViewOrigin.y() + cursoredExpressionViewOrigin.y() + pointedLayoutR.baseline() - m_cursor.baseline());
|
|
m_cursorView.setFrame(KDRect(cursorTopLeftPosition, LayoutCursor::k_cursorWidth, m_cursor.cursorHeight()));
|
|
}
|
|
|
|
void LayoutField::reload() {
|
|
KDSize previousSize = minimalSizeForOptimalDisplay();
|
|
layoutRef().invalidAllSizesPositionsAndBaselines();
|
|
KDSize newSize = minimalSizeForOptimalDisplay();
|
|
if (m_delegate && previousSize.height() != newSize.height()) {
|
|
m_delegate->layoutFieldDidChangeSize(this);
|
|
}
|
|
m_contentView.cursorPositionChanged();
|
|
scrollToCursor();
|
|
markRectAsDirty(bounds());
|
|
}
|
|
|
|
bool LayoutField::handleEventWithText(const char * text, bool indentation, bool forceCursorRightOfText) {
|
|
if (text[0] == 0) {
|
|
// The text is empty
|
|
return true;
|
|
}
|
|
|
|
int currentNumberOfLayouts = m_contentView.expressionView()->numberOfLayouts();
|
|
if (currentNumberOfLayouts >= k_maxNumberOfLayouts - 6) {
|
|
/* We add -6 because in some cases (Ion::Events::Division,
|
|
* Ion::Events::Exp...) we let the layout cursor handle the layout insertion
|
|
* and these events may add at most 6 layouts (e.g *10^•). */
|
|
return true;
|
|
}
|
|
|
|
// Handle special cases
|
|
if (strcmp(text, Ion::Events::Division.text()) == 0) {
|
|
m_contentView.cursor()->addFractionLayoutAndCollapseSiblings();
|
|
} else if (strcmp(text, Ion::Events::Exp.text()) == 0) {
|
|
m_contentView.cursor()->addEmptyExponentialLayout();
|
|
} else if (strcmp(text, Ion::Events::Power.text()) == 0) {
|
|
m_contentView.cursor()->addEmptyPowerLayout();
|
|
} else if (strcmp(text, Ion::Events::Sqrt.text()) == 0) {
|
|
m_contentView.cursor()->addEmptySquareRootLayout();
|
|
} else if (strcmp(text, Ion::Events::Square.text()) == 0) {
|
|
m_contentView.cursor()->addEmptySquarePowerLayout();
|
|
} else if (strcmp(text, Ion::Events::EE.text()) == 0) {
|
|
m_contentView.cursor()->addEmptyTenPowerLayout();
|
|
} else if ((strcmp(text, "[") == 0) || (strcmp(text, "]") == 0)) {
|
|
m_contentView.cursor()->addEmptyMatrixLayout();
|
|
} else {
|
|
Expression resultExpression = Expression::parse(text);
|
|
if (resultExpression.isUninitialized()) {
|
|
m_contentView.cursor()->insertText(text);
|
|
} else {
|
|
LayoutRef resultLayoutRef = resultExpression.createLayout(Poincare::Preferences::sharedPreferences()->displayMode(), Poincare::PrintFloat::k_numberOfStoredSignificantDigits);
|
|
if (currentNumberOfLayouts + resultLayoutRef.numberOfDescendants(true) >= k_maxNumberOfLayouts) {
|
|
return true;
|
|
}
|
|
// Find the pointed layout.
|
|
LayoutRef pointedLayoutRef;
|
|
if (strcmp(text, I18n::translate(I18n::Message::RandomCommandWithArg)) == 0) {
|
|
/* Special case: if the text is "random()", the cursor should not be set
|
|
* inside the parentheses. */
|
|
pointedLayoutRef = resultLayoutRef;
|
|
} else if (resultLayoutRef.isHorizontal()) {
|
|
/* If the layout is horizontal, pick the first open parenthesis. For now,
|
|
* all horizontal layouts in MathToolbox have parentheses. */
|
|
for (int i = 0; i < resultLayoutRef.numberOfChildren(); i++) {
|
|
LayoutRef l = resultLayoutRef.childAtIndex(i);
|
|
if (l.isLeftParenthesis()) {
|
|
pointedLayoutRef = l;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/* Insert the layout. If pointedLayout is uninitialized, the cursor will
|
|
* be on the right of the inserted layout. */
|
|
insertLayoutAtCursor(resultLayoutRef, pointedLayoutRef, forceCursorRightOfText);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool LayoutField::handleEvent(Ion::Events::Event event) {
|
|
bool didHandleEvent = false;
|
|
bool shouldRecomputeLayout = m_contentView.cursor()->showEmptyLayoutIfNeeded();
|
|
bool moveEventChangedLayout = false;
|
|
if (privateHandleMoveEvent(event, &moveEventChangedLayout)) {
|
|
if (!isEditing()) {
|
|
setEditing(true);
|
|
}
|
|
shouldRecomputeLayout = shouldRecomputeLayout || moveEventChangedLayout;
|
|
didHandleEvent = true;
|
|
} else if (privateHandleEvent(event)) {
|
|
shouldRecomputeLayout = true;
|
|
didHandleEvent = true;
|
|
}
|
|
if (didHandleEvent) {
|
|
shouldRecomputeLayout = m_contentView.cursor()->hideEmptyLayoutIfNeeded() || shouldRecomputeLayout;
|
|
if (!shouldRecomputeLayout) {
|
|
m_contentView.cursorPositionChanged();
|
|
scrollToCursor();
|
|
} else {
|
|
reload();
|
|
}
|
|
return true;
|
|
}
|
|
m_contentView.cursor()->hideEmptyLayoutIfNeeded();
|
|
return false;
|
|
}
|
|
|
|
bool LayoutField::privateHandleEvent(Ion::Events::Event event) {
|
|
if (m_delegate && m_delegate->layoutFieldDidReceiveEvent(this, event)) {
|
|
return true;
|
|
}
|
|
if (Responder::handleEvent(event)) {
|
|
/* The only event Responder handles is 'Toolbox' displaying. In that case,
|
|
* the LayoutField is forced into editing mode. */
|
|
if (!isEditing()) {
|
|
setEditing(true);
|
|
}
|
|
return true;
|
|
}
|
|
if (isEditing() && m_delegate->layoutFieldShouldFinishEditing(this, event)) { //TODO use class method?
|
|
setEditing(false);
|
|
if (m_delegate->layoutFieldDidFinishEditing(this, layoutRef(), event)) {
|
|
clearLayout();
|
|
}
|
|
return true;
|
|
}
|
|
if ((event == Ion::Events::OK || event == Ion::Events::EXE) && !isEditing()) {
|
|
setEditing(true);
|
|
m_contentView.cursor()->setLayoutReference(layoutRef());
|
|
m_contentView.cursor()->setPosition(LayoutCursor::Position::Right);
|
|
return true;
|
|
}
|
|
if (event == Ion::Events::Back && isEditing()) {
|
|
clearLayout();
|
|
setEditing(false);
|
|
m_delegate->layoutFieldDidAbortEditing(this);
|
|
return true;
|
|
}
|
|
if (event.hasText() || event == Ion::Events::Paste || event == Ion::Events::Backspace) {
|
|
if (!isEditing()) {
|
|
setEditing(true);
|
|
}
|
|
if (event.hasText()) {
|
|
handleEventWithText(event.text());
|
|
} else if (event == Ion::Events::Paste) {
|
|
handleEventWithText(Clipboard::sharedClipboard()->storedText(), false, true);
|
|
} else {
|
|
assert(event == Ion::Events::Backspace);
|
|
m_contentView.cursor()->performBackspace();
|
|
}
|
|
return true;
|
|
}
|
|
if (event == Ion::Events::Clear && isEditing()) {
|
|
clearLayout();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool LayoutField::privateHandleMoveEvent(Ion::Events::Event event, bool * shouldRecomputeLayout) {
|
|
LayoutCursor result;
|
|
if (event == Ion::Events::Left) {
|
|
result = m_contentView.cursor()->cursorAtDirection(LayoutCursor::MoveDirection::Left, shouldRecomputeLayout);
|
|
} else if (event == Ion::Events::Right) {
|
|
result = m_contentView.cursor()->cursorAtDirection(LayoutCursor::MoveDirection::Right, shouldRecomputeLayout);
|
|
} else if (event == Ion::Events::Up) {
|
|
result = m_contentView.cursor()->cursorAtDirection(LayoutCursor::MoveDirection::Up, shouldRecomputeLayout);
|
|
} else if (event == Ion::Events::Down) {
|
|
result = m_contentView.cursor()->cursorAtDirection(LayoutCursor::MoveDirection::Down, shouldRecomputeLayout);
|
|
} else if (event == Ion::Events::ShiftLeft) {
|
|
*shouldRecomputeLayout = true;
|
|
if (m_contentView.cursor()->layoutReference().removeGreySquaresFromAllMatrixAncestors()) {
|
|
*shouldRecomputeLayout = true;
|
|
}
|
|
result.setLayoutReference(layoutRef());
|
|
result.setPosition(LayoutCursor::Position::Left);
|
|
} else if (event == Ion::Events::ShiftRight) {
|
|
if (m_contentView.cursor()->layoutReference().removeGreySquaresFromAllMatrixAncestors()) {
|
|
*shouldRecomputeLayout = true;
|
|
}
|
|
result.setLayoutReference(layoutRef());
|
|
result.setPosition(LayoutCursor::Position::Right);
|
|
}
|
|
if (result.isDefined()) {
|
|
m_contentView.setCursor(result);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void LayoutField::scrollRightOfLayout(LayoutRef layoutR) {
|
|
KDRect layoutRect(layoutR.absoluteOrigin().translatedBy(m_contentView.expressionView()->drawingOrigin()), layoutR.layoutSize());
|
|
scrollToBaselinedRect(layoutRect, layoutR.baseline());
|
|
}
|
|
|
|
void LayoutField::scrollToBaselinedRect(KDRect rect, KDCoordinate baseline) {
|
|
scrollToContentRect(rect, true);
|
|
// Show the rect area around its baseline
|
|
KDCoordinate underBaseline = rect.height() - baseline;
|
|
KDCoordinate minAroundBaseline = min(baseline, underBaseline);
|
|
minAroundBaseline = min(minAroundBaseline, bounds().height() / 2);
|
|
KDRect balancedRect(rect.x(), rect.y() + baseline - minAroundBaseline, rect.width(), 2 * minAroundBaseline);
|
|
scrollToContentRect(balancedRect, true);
|
|
}
|
|
|
|
void LayoutField::insertLayoutAtCursor(LayoutRef layoutR, LayoutRef pointedLayoutR, bool forceCursorRightOfLayout) {
|
|
if (layoutR.isUninitialized()) {
|
|
return;
|
|
}
|
|
|
|
// Handle empty layouts
|
|
m_contentView.cursor()->showEmptyLayoutIfNeeded();
|
|
|
|
bool layoutWillBeMerged = layoutR.isHorizontal();
|
|
LayoutRef lastMergedLayoutChild = layoutWillBeMerged ? layoutR.childAtIndex(layoutR.numberOfChildren()-1) : LayoutRef();
|
|
|
|
// Add the layout
|
|
m_contentView.cursor()->addLayoutAndMoveCursor(layoutR);
|
|
|
|
// Move the cursor if needed
|
|
if(!forceCursorRightOfLayout) {
|
|
if (!pointedLayoutR.isUninitialized() && (!layoutWillBeMerged || pointedLayoutR != layoutR)) {
|
|
// Make sure the layout was inserted (its parent is not uninitialized)
|
|
m_contentView.cursor()->setLayoutReference(pointedLayoutR);
|
|
m_contentView.cursor()->setPosition(LayoutCursor::Position::Right);
|
|
} else if (!layoutWillBeMerged) {
|
|
m_contentView.cursor()->setLayoutReference(layoutR.layoutToPointWhenInserting());
|
|
m_contentView.cursor()->setPosition(LayoutCursor::Position::Right);
|
|
}
|
|
} else if (!layoutWillBeMerged) {
|
|
m_contentView.cursor()->setLayoutReference(layoutR);
|
|
m_contentView.cursor()->setPosition(LayoutCursor::Position::Right);
|
|
}
|
|
|
|
// Handle matrices
|
|
m_contentView.cursor()->layoutReference().addGreySquaresToAllMatrixAncestors();
|
|
|
|
// Handle empty layouts
|
|
m_contentView.cursor()->hideEmptyLayoutIfNeeded();
|
|
|
|
// Reload
|
|
reload();
|
|
if (!layoutWillBeMerged) {
|
|
scrollRightOfLayout(layoutR);
|
|
} else {
|
|
assert(!lastMergedLayoutChild.isUninitialized());
|
|
scrollRightOfLayout(lastMergedLayoutChild);
|
|
}
|
|
scrollToCursor();
|
|
}
|