#include "history_view_cell.h" #include "app.h" #include "../constant.h" #include "selectable_table_view.h" #include #include #include #include namespace Calculation { /* HistoryViewCellDataSource */ void HistoryViewCellDataSource::setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedCellX, int previousSelectedCellY) { HistoryViewCell * selectedCell = nullptr; HistoryViewCell * previouslySelectedCell = nullptr; SubviewType previousSubviewType = m_selectedSubviewType; m_selectedSubviewType = subviewType; /* We need to notify the whole table that the selection changed if it * involves the selection/deselection of an output. Indeed, only them can * trigger change in the displayed expressions. */ historyViewCellDidChangeSelection(&selectedCell, &previouslySelectedCell, previousSelectedCellX, previousSelectedCellY, subviewType, previousSubviewType); previousSubviewType = sameCell ? previousSubviewType : SubviewType::None; if (selectedCell) { selectedCell->reloadSubviewHighlight(); selectedCell->cellDidSelectSubview(subviewType, previousSubviewType); Container::activeApp()->setFirstResponder(selectedCell); } if (previouslySelectedCell) { previouslySelectedCell->cellDidSelectSubview(SubviewType::Input); } } /* HistoryViewCell */ KDCoordinate HistoryViewCell::Height(Calculation * calculation, bool expanded) { HistoryViewCell cell(nullptr); cell.setCalculation(calculation, expanded, true); KDRect ellipsisFrame = KDRectZero; KDRect inputFrame = KDRectZero; KDRect outputFrame = KDRectZero; cell.computeSubviewFrames(Ion::Display::Width, KDCOORDINATE_MAX, &ellipsisFrame, &inputFrame, &outputFrame); return k_margin + inputFrame.unionedWith(outputFrame).height() + k_margin; } HistoryViewCell::HistoryViewCell(Responder * parentResponder) : Responder(parentResponder), m_calculationCRC32(0), m_calculationDisplayOutput(Calculation::DisplayOutput::Unknown), m_calculationAdditionInformation(Calculation::AdditionalInformationType::None), m_inputView(this, k_inputViewHorizontalMargin, k_inputOutputViewsVerticalMargin), m_scrollableOutputView(this), m_calculationExpanded(false), m_calculationSingleLine(false) { } void HistoryViewCell::setEven(bool even) { EvenOddCell::setEven(even); m_inputView.setBackgroundColor(backgroundColor()); m_scrollableOutputView.setBackgroundColor(backgroundColor()); m_scrollableOutputView.evenOddCell()->setEven(even); m_ellipsis.setEven(even); } void HistoryViewCell::setHighlighted(bool highlight) { if (m_highlighted == highlight) { return; } m_highlighted = highlight; reloadSubviewHighlight(); // Re-layout as the ellispsis subview might have appear/disappear layoutSubviews(); } void HistoryViewCell::reloadSubviewHighlight() { assert(m_dataSource); m_inputView.setExpressionBackgroundColor(backgroundColor()); m_scrollableOutputView.evenOddCell()->setHighlighted(false); m_ellipsis.setHighlighted(false); if (isHighlighted()) { if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) { m_inputView.setExpressionBackgroundColor(Palette::Select); } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) { m_scrollableOutputView.evenOddCell()->setHighlighted(true); } else { assert(m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Ellipsis); m_ellipsis.setHighlighted(true); } } } Poincare::Layout HistoryViewCell::layout() const { assert(m_dataSource); if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) { return m_inputView.layout(); } else { return m_scrollableOutputView.layout(); } } void HistoryViewCell::reloadScroll() { m_inputView.reloadScroll(); m_scrollableOutputView.reloadScroll(); } void HistoryViewCell::reloadOutputSelection(HistoryViewCellDataSource::SubviewType previousType) { /* Select the right output according to the calculation display output. This * will reload the scroll to display the selected output. */ if (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate) { m_scrollableOutputView.setSelectedSubviewPosition( previousType == HistoryViewCellDataSource::SubviewType::Ellipsis ? Shared::ScrollableTwoExpressionsView::SubviewPosition::Right : Shared::ScrollableTwoExpressionsView::SubviewPosition::Center ); } else { assert((m_calculationDisplayOutput == Calculation::DisplayOutput::ApproximateOnly) || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle) || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactOnly)); m_scrollableOutputView.setSelectedSubviewPosition(Shared::ScrollableTwoExpressionsView::SubviewPosition::Right); } } void HistoryViewCell::cellDidSelectSubview(HistoryViewCellDataSource::SubviewType type, HistoryViewCellDataSource::SubviewType previousType) { // Init output selection if (type == HistoryViewCellDataSource::SubviewType::Output) { reloadOutputSelection(previousType); } // Update m_calculationExpanded m_calculationExpanded = (type == HistoryViewCellDataSource::SubviewType::Output && m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle); /* The selected subview has changed. The displayed outputs might have changed. * For example, for the calculation 1.2+2 --> 3.2, selecting the output would * display 1.2+2 --> 16/5 = 3.2. */ m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded); /* The displayed outputs have changed. We need to re-layout the cell * and re-initialize the scroll. */ layoutSubviews(); reloadScroll(); } View * HistoryViewCell::subviewAtIndex(int index) { /* The order of the subviews should not matter here as they don't overlap. * However, the order determines the order of redrawing as well. For several * reasons listed after, changing subview selection often redraws the entire * m_scrollableOutputView even if it seems unecessary: * - Before feeding new Layouts to ExpressionViews, we reset the hold layouts * in order to empty the Poincare pool and have more space to compute new * layouts. * - Even if we did not do that, ExpressionView::setLayout doesn't avoid * redrawing when the previous expression is identical (for reasons * explained in expression_view.cpp) * - Because of the toggling burger view, ExpressionViews often have the same * absolute frame but a different relative frame which leads to redrawing * them anyway. * All these reasons cause a blinking which can be avoided if we redraw the * output view before the input view (starting with redrawing the more * complex view enables to redraw it before the vblank thereby preventing * blinking). * TODO: this is a dirty hack which should be fixed! */ View * views[3] = {&m_scrollableOutputView, &m_inputView, &m_ellipsis}; return views[index]; } bool HistoryViewCell::ViewsCanBeSingleLine(KDCoordinate inputViewWidth, KDCoordinate outputViewWidth) { // k_margin is the separation between the input and output. return (inputViewWidth + k_margin + outputViewWidth) < Ion::Display::Width - Metric::EllipsisCellWidth; } void HistoryViewCell::layoutSubviews(bool force) { KDRect frameBounds = bounds(); if (bounds().width() <= 0 || bounds().height() <= 0) { // TODO Make this behaviour in a non-virtual layoutSublviews, and all layout subviews should become privateLayoutSubviews return; } KDRect ellipsisFrame = KDRectZero; KDRect inputFrame = KDRectZero; KDRect outputFrame = KDRectZero; computeSubviewFrames(frameBounds.width(), frameBounds.height(), &ellipsisFrame, &inputFrame, &outputFrame); m_ellipsis.setFrame(ellipsisFrame, force); // Required even if ellipsisFrame is KDRectZero, to mark previous rect as dirty m_inputView.setFrame(inputFrame,force); m_scrollableOutputView.setFrame(outputFrame, force); } void HistoryViewCell::computeSubviewFrames(KDCoordinate frameWidth, KDCoordinate frameHeight, KDRect * ellipsisFrame, KDRect * inputFrame, KDRect * outputFrame) { assert(ellipsisFrame != nullptr && inputFrame != nullptr && outputFrame != nullptr); if (displayedEllipsis()) { *ellipsisFrame = KDRect(frameWidth - Metric::EllipsisCellWidth, 0, Metric::EllipsisCellWidth, frameHeight); frameWidth -= Metric::EllipsisCellWidth; } else { *ellipsisFrame = KDRectZero; } KDSize inputSize = m_inputView.minimalSizeForOptimalDisplay(); KDSize outputSize = m_scrollableOutputView.minimalSizeForOptimalDisplay(); /* To compute if the calculation is on a single line, use the expanded width * if there is both an exact and an approximate layout. */ m_calculationSingleLine = ViewsCanBeSingleLine(inputSize.width(), m_scrollableOutputView.minimalSizeForOptimalDisplayFullSize().width()); KDCoordinate inputY = k_margin; KDCoordinate outputY = k_margin; if (m_calculationSingleLine && !m_inputView.layout().isUninitialized()) { KDCoordinate inputBaseline = m_inputView.layout().baseline(); KDCoordinate outputBaseline = m_scrollableOutputView.baseline(); KDCoordinate baselineDifference = outputBaseline - inputBaseline; if (baselineDifference > 0) { inputY += baselineDifference; } else { outputY += -baselineDifference; } } else { outputY += inputSize.height(); } *inputFrame = KDRect( 0, inputY, std::min(frameWidth, inputSize.width()), inputSize.height()); *outputFrame = KDRect( std::max(0, frameWidth - outputSize.width()), outputY, std::min(frameWidth, outputSize.width()), outputSize.height()); } void HistoryViewCell::resetMemoization() { // Clean the layouts to make room in the pool // TODO: maybe do this only when the layout won't change to avoid blinking m_inputView.setLayout(Poincare::Layout()); m_scrollableOutputView.setLayouts(Poincare::Layout(), Poincare::Layout(), Poincare::Layout()); m_calculationCRC32 = 0; } void HistoryViewCell::setCalculation(Calculation * calculation, bool expanded, bool canChangeDisplayOutput) { uint32_t newCalculationCRC = Ion::crc32Byte((const uint8_t *)calculation, ((char *)calculation->next()) - ((char *) calculation)); if (newCalculationCRC == m_calculationCRC32 && m_calculationExpanded == expanded) { return; } Poincare::Context * context = App::app()->localContext(); // TODO: maybe do this only when the layout won't change to avoid blinking resetMemoization(); // Memoization m_calculationCRC32 = newCalculationCRC; m_calculationExpanded = expanded && calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactAndApproximateToggle; m_calculationAdditionInformation = calculation->additionalInformationType(context); m_inputView.setLayout(calculation->createInputLayout()); /* All expressions have to be updated at the same time. Otherwise, * when updating one layout, if the second one still points to a deleted * layout, calling to layoutSubviews() would fail. */ // Create the exact output layout Poincare::Layout exactOutputLayout = Poincare::Layout(); if (Calculation::DisplaysExact(calculation->displayOutput(context))) { bool couldNotCreateExactLayout = false; exactOutputLayout = calculation->createExactOutputLayout(&couldNotCreateExactLayout); if (couldNotCreateExactLayout) { if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ExactOnly) { calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly); } else { /* We should only display the exact result, but we cannot create it * -> raise an exception. */ Poincare::ExceptionCheckpoint::Raise(); } } } // Create the approximate output layout Poincare::Layout approximateOutputLayout; if (calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactOnly) { approximateOutputLayout = exactOutputLayout; } else { bool couldNotCreateApproximateLayout = false; approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout); if (couldNotCreateApproximateLayout) { if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ApproximateOnly) { /* Set the display output to ApproximateOnly, make room in the pool by * erasing the exact layout, and retry to create the approximate layout */ calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly); exactOutputLayout = Poincare::Layout(); couldNotCreateApproximateLayout = false; approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout); if (couldNotCreateApproximateLayout) { Poincare::ExceptionCheckpoint::Raise(); } } else { Poincare::ExceptionCheckpoint::Raise(); } } } m_calculationDisplayOutput = calculation->displayOutput(context); // We must set which subviews are displayed before setLayouts to mark the right rectangle as dirty m_scrollableOutputView.setDisplayableCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle); m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded); m_scrollableOutputView.setLayouts(Poincare::Layout(), exactOutputLayout, approximateOutputLayout); I18n::Message equalMessage = calculation->exactAndApproximateDisplayedOutputsAreEqual(context) == Calculation::EqualSign::Equal ? I18n::Message::Equal : I18n::Message::AlmostEqual; m_scrollableOutputView.setEqualMessage(equalMessage); /* The displayed input and outputs have changed. We need to re-layout the cell * and re-initialize the scroll. */ layoutSubviews(); reloadScroll(); } void HistoryViewCell::didBecomeFirstResponder() { assert(m_dataSource); if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) { Container::activeApp()->setFirstResponder(&m_inputView); } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) { Container::activeApp()->setFirstResponder(&m_scrollableOutputView); } } bool HistoryViewCell::handleEvent(Ion::Events::Event event) { assert(m_dataSource != nullptr); HistoryViewCellDataSource::SubviewType type = m_dataSource->selectedSubviewType(); assert(type != HistoryViewCellDataSource::SubviewType::None); HistoryViewCellDataSource::SubviewType otherSubviewType = HistoryViewCellDataSource::SubviewType::None; if (m_calculationSingleLine) { static_assert( static_cast(HistoryViewCellDataSource::SubviewType::None) == 0 && static_cast(HistoryViewCellDataSource::SubviewType::Input) == 1 && static_cast(HistoryViewCellDataSource::SubviewType::Output) == 2 && static_cast(HistoryViewCellDataSource::SubviewType::Ellipsis) == 3, "The array types is not well-formed anymore"); HistoryViewCellDataSource::SubviewType types[] = { HistoryViewCellDataSource::SubviewType::None, HistoryViewCellDataSource::SubviewType::Input, HistoryViewCellDataSource::SubviewType::Output, displayedEllipsis() ? HistoryViewCellDataSource::SubviewType::Ellipsis : HistoryViewCellDataSource::SubviewType::None, HistoryViewCellDataSource::SubviewType::None, }; if (event == Ion::Events::Right || event == Ion::Events::Left) { otherSubviewType = types[static_cast(type) + (event == Ion::Events::Right ? 1 : -1)]; } } else if ((event == Ion::Events::Down && type == HistoryViewCellDataSource::SubviewType::Input) || (event == Ion::Events::Left && type == HistoryViewCellDataSource::SubviewType::Ellipsis)) { otherSubviewType = HistoryViewCellDataSource::SubviewType::Output; } else if (event == Ion::Events::Up && type == HistoryViewCellDataSource::SubviewType::Output) { otherSubviewType = HistoryViewCellDataSource::SubviewType::Input; } else if (event == Ion::Events::Right && type != HistoryViewCellDataSource::SubviewType::Ellipsis && displayedEllipsis()) { otherSubviewType = HistoryViewCellDataSource::SubviewType::Ellipsis; } if (otherSubviewType == HistoryViewCellDataSource::SubviewType::None) { return false; } m_dataSource->setSelectedSubviewType(otherSubviewType, true); return true; } }