menu_controller.cpp 14.1 KB
#include "menu_controller.h"
#include "../i18n.h"
#include "../apps_container.h"
#include <assert.h>
#include <escher/metric.h>
#include <ion/events.h>

namespace Code {

MenuController::MenuController(Responder * parentResponder, ScriptStore * scriptStore, ButtonRowController * footer) :
  ViewController(parentResponder),
  ButtonRowDelegate(nullptr, footer),
  m_scriptStore(scriptStore),
  m_addNewScriptCell(I18n::Message::AddScript),
  m_consoleButton(this, I18n::Message::Console, Invocation([](void * context, void * sender) {
    MenuController * menu = (MenuController *)context;
    if (menu->consoleController()->loadPythonEnvironment()) {
      menu->stackViewController()->push(menu->consoleController());
      return;
    }
    //TODO: Pop up warning message: not enough space to load Python
  }, this), KDText::FontSize::Large),
  m_selectableTableView(this, this, 0, 1, 0, 0, 0, 0, this, this, false),
  m_consoleController(parentResponder, m_scriptStore),
  m_scriptParameterController(nullptr, I18n::Message::ScriptOptions, m_scriptStore, this),
  m_editorController(this),
  m_reloadConsoleWhenBecomingFirstResponder(false),
  m_shouldDisplayAddScriptRow(true)
{
  for (int i = 0; i < k_maxNumberOfDisplayableScriptCells; i++) {
    m_scriptCells[i].setParentResponder(&m_selectableTableView);
    m_scriptCells[i].editableTextCell()->textField()->setDelegate(this);
    m_scriptCells[i].editableTextCell()->textField()->setDraftTextBuffer(m_draftTextBuffer);
    m_scriptCells[i].editableTextCell()->textField()->setAlignment(0.0f, 0.5f);
    m_scriptCells[i].editableTextCell()->setMargins(0, 0, 0, Metric::HistoryHorizontalMargin);
  }
}

StackViewController * MenuController::stackViewController() {
  return static_cast<StackViewController *>(parentResponder()->parentResponder());
}

void MenuController::didBecomeFirstResponder() {
  if (m_reloadConsoleWhenBecomingFirstResponder) {
    reloadConsole();
  }
  if (footer()->selectedButton() == 0) {
    assert(m_selectableTableView.selectedRow() < 0);
    app()->setFirstResponder(&m_consoleButton);
    return;
  }
  if (m_selectableTableView.selectedRow() < 0) {
    m_selectableTableView.selectCellAtLocation(0,0);
  }
  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts() + 1);
  app()->setFirstResponder(&m_selectableTableView);
}

void MenuController::viewWillAppear() {
  updateAddScriptRowDisplay();
}

bool MenuController::handleEvent(Ion::Events::Event event) {
  if (event == Ion::Events::Down) {
    m_selectableTableView.deselectTable();
    footer()->setSelectedButton(0);
    return true;
  }
  if (event == Ion::Events::Up) {
    if (footer()->selectedButton() == 0) {
      footer()->setSelectedButton(-1);
      m_selectableTableView.selectCellAtLocation(0, numberOfRows()-1);
      app()->setFirstResponder(&m_selectableTableView);
      return true;
    }
  }
  if (event == Ion::Events::OK || event == Ion::Events::EXE) {
    int selectedRow = m_selectableTableView.selectedRow();
    int selectedColumn = m_selectableTableView.selectedColumn();
    if (selectedRow >= 0 && selectedRow < m_scriptStore->numberOfScripts()) {
      if (selectedColumn == 1) {
        configureScript();
        return true;
      }
      assert(selectedColumn == 0);
      editScriptAtIndex(selectedRow);
      return true;
    } else if (m_shouldDisplayAddScriptRow
        && selectedColumn == 0
        && selectedRow == m_scriptStore->numberOfScripts())
    {
      addScript();
      return true;
    }
  }
  return false;
}

void MenuController::renameSelectedScript() {
  assert(m_selectableTableView.selectedRow() >= 0);
  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
  static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::AlphaLock);
  m_selectableTableView.selectCellAtLocation(0, (m_selectableTableView.selectedRow()));
  EvenOddEditableTextCell * myCell = static_cast<EvenOddEditableTextCell *>(m_selectableTableView.selectedCell());
  app()->setFirstResponder(myCell);
  myCell->setHighlighted(false);
  const char * previousText = myCell->editableTextCell()->textField()->text();
  myCell->editableTextCell()->textField()->setEditing(true);
  myCell->editableTextCell()->textField()->setText(previousText);
  myCell->editableTextCell()->textField()->setCursorLocation(strlen(previousText) - strlen(ScriptStore::k_scriptExtension));
  }

void MenuController::deleteScriptAtIndex(int i) {
  assert(i >= 0);
  assert(i < m_scriptStore->numberOfScripts());
  m_scriptStore->deleteScriptAtIndex(i);
  updateAddScriptRowDisplay();
  m_selectableTableView.reloadData();
}

void MenuController::reloadConsole() {
  m_consoleController.unloadPythonEnvironment();
  m_reloadConsoleWhenBecomingFirstResponder = false;
}

void MenuController::loadPythonIfNeeded() {
  m_consoleController.loadPythonEnvironment(false);
}

void MenuController::openConsoleWithScriptAtIndex(int scriptIndex) {
  reloadConsole();
  if (m_consoleController.loadPythonEnvironment(false)) {
    stackViewController()->push(&m_consoleController);
    m_consoleController.autoImportScriptAtIndex(scriptIndex, true);
  }
  m_reloadConsoleWhenBecomingFirstResponder = true;
}

void MenuController::scriptContentEditionDidFinish(){
  reloadConsole();
}

int MenuController::numberOfRows() {
  return m_scriptStore->numberOfScripts() + m_shouldDisplayAddScriptRow;
}

void MenuController::willDisplayCellAtLocation(HighlightCell * cell, int i, int j) {
  if (i == 0 && j < m_scriptStore->numberOfScripts()) {
    willDisplayScriptTitleCellForIndex(cell, j);
  }
  static_cast<EvenOddCell *>(cell)->setEven(j%2 == 0);
  cell->setHighlighted(i == selectedColumn() && j == selectedRow());
}

KDCoordinate MenuController::columnWidth(int i) {
  switch (i) {
    case 0:
      return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
    case 1:
      return k_parametersColumnWidth;
    default:
      assert(false);
      return 0;
  }
}

KDCoordinate MenuController::cumulatedWidthFromIndex(int i) {
  switch (i) {
    case 0:
      return 0;
    case 1:
      return m_selectableTableView.bounds().width()-k_parametersColumnWidth;
    case 2:
      return m_selectableTableView.bounds().width();
    default:
      assert(false);
      return 0;
  }
}

KDCoordinate MenuController::cumulatedHeightFromIndex(int j) {
  return Metric::StoreRowHeight * j;
}

int MenuController::indexFromCumulatedWidth(KDCoordinate offsetX) {
  if (offsetX <= m_selectableTableView.bounds().width()-k_parametersColumnWidth) {
    return 0;
  }
  if (offsetX <= m_selectableTableView.bounds().width()) {
    return 1;
  }
  else {
    return 2;
  }
  assert(false);
  return 0;
}

int MenuController::indexFromCumulatedHeight(KDCoordinate offsetY) {
  if (Metric::StoreRowHeight == 0) {
    return 0;
  }
  return (offsetY - 1) / Metric::StoreRowHeight;
}

HighlightCell * MenuController::reusableCell(int index, int type) {
  assert(index >= 0);
  if (type == ScriptCellType) {
    assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
    return &m_scriptCells[index];
  }
  if (type == ScriptParameterCellType) {
    assert(index >=0 && index < k_maxNumberOfDisplayableScriptCells);
    return &m_scriptParameterCells[index];
  }
  if (type == AddScriptCellType) {
    assert(index == 0);
    return &m_addNewScriptCell;
  }
  if(type == EmptyCellType) {
    return &m_emptyCell;
  }
  assert(false);
  return nullptr;
}

int MenuController::reusableCellCount(int type) {
  if (type == AddScriptCellType) {
    return 1;
  }
  if (type == ScriptCellType || type == ScriptParameterCellType) {
    return k_maxNumberOfDisplayableScriptCells;
  }
  if (type == EmptyCellType) {
    return 1;
  }
  assert(false);
  return 0;
}

int MenuController::typeAtLocation(int i, int j) {
  assert(i >= 0 && i < numberOfColumns());
  assert(j >= 0 && j < numberOfRows());
  if (i == 0) {
    if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
      return AddScriptCellType;
    }
    return ScriptCellType;
  }
  assert(i == 1);
  if (j == numberOfRows()-1 && m_shouldDisplayAddScriptRow) {
    return EmptyCellType;
  }
  return ScriptParameterCellType;
}

void MenuController::willDisplayScriptTitleCellForIndex(HighlightCell * cell, int index) {
  assert(index >= 0 && index < m_scriptStore->numberOfScripts());
  EditableTextCell * editableTextCell = static_cast<EvenOddEditableTextCell *>(cell)->editableTextCell();
  editableTextCell->textField()->setText(m_scriptStore->scriptAtIndex(index).name());
}

void MenuController::tableViewDidChangeSelection(SelectableTableView * t, int previousSelectedCellX, int previousSelectedCellY) {
  if (selectedRow() == numberOfRows() - 1 && selectedColumn() == 1 && m_shouldDisplayAddScriptRow) {
    t->selectCellAtLocation(0, numberOfRows()-1);
  }
}

bool MenuController::textFieldShouldFinishEditing(TextField * textField, Ion::Events::Event event) {
  return event == Ion::Events::OK || event == Ion::Events::EXE
    || event == Ion::Events::Down || event == Ion::Events::Up;
}

bool MenuController::textFieldDidReceiveEvent(TextField * textField, Ion::Events::Event event) {
  if (event == Ion::Events::Left && textField->isEditing() && textField->cursorLocation() == 0) {
    return true;
  }
  return false;
}

bool MenuController::textFieldDidFinishEditing(TextField * textField, const char * text, Ion::Events::Event event) {
  const char * newName;
  char numberedDefaultName[k_defaultScriptNameMaxSize];
  if (strlen(text) <= strlen(ScriptStore::k_scriptExtension)) {
    // The user entered an empty name. Use a numbered default script name.
    numberedDefaultScriptName(numberedDefaultName);
    newName = const_cast<const char *>(numberedDefaultName);
  } else {
    newName = text;
  }
  if (m_scriptStore->renameScriptAtIndex(m_selectableTableView.selectedRow(), newName)) {
    updateAddScriptRowDisplay();
    textField->setText(newName);
    int currentRow = m_selectableTableView.selectedRow();
    if (event == Ion::Events::Down && currentRow < numberOfRows() - 1) {
      m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow + 1);
    } else if (event == Ion::Events::Up && currentRow > 0) {
      m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), currentRow - 1);
    }
    m_selectableTableView.selectedCell()->setHighlighted(true);
    reloadConsole();
    app()->setFirstResponder(&m_selectableTableView);
    static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
    return true;
  }
  // TODO: add pop up to explain to the user that the name is too long.
  return false;
}

bool MenuController::textFieldDidAbortEditing(TextField * textField, const char * text) {
  if (strlen(text) <= strlen(ScriptStore::k_scriptExtension)) {
    // The previous text was an empty name. Use a numbered default script name.
    char numberedDefaultName[k_defaultScriptNameMaxSize];
    numberedDefaultScriptName(numberedDefaultName);
    m_scriptStore->renameScriptAtIndex(m_selectableTableView.selectedRow(), const_cast<const char *>(numberedDefaultName));
    updateAddScriptRowDisplay();
    m_selectableTableView.reloadData();
  }
  m_selectableTableView.selectCellAtLocation(m_selectableTableView.selectedColumn(), m_selectableTableView.selectedRow());
  app()->setFirstResponder(&m_selectableTableView);
  static_cast<AppsContainer *>(const_cast<Container *>(app()->container()))->setShiftAlphaStatus(Ion::Events::ShiftAlphaStatus::Default);
  return true;
}

bool MenuController::textFieldDidHandleEvent(TextField * textField, Ion::Events::Event event, bool returnValue, bool textHasChanged) {
  int scriptExtensionLength = strlen(ScriptStore::k_scriptExtension);
  if (textField->isEditing() && textField->cursorLocation() > textField->draftTextLength() - scriptExtensionLength) {
    textField->setCursorLocation(textField->draftTextLength() - scriptExtensionLength);
  }
  return returnValue;
}

void MenuController::addScript() {
  if (m_scriptStore->addNewScript()) {
    updateAddScriptRowDisplay();
    m_selectableTableView.reloadData();
    renameSelectedScript();
    return;
  }
  m_selectableTableView.reloadData();
}

void MenuController::configureScript() {
  assert(m_selectableTableView.selectedRow() >= 0);
  assert(m_selectableTableView.selectedRow() < m_scriptStore->numberOfScripts());
  m_scriptParameterController.setScript(m_selectableTableView.selectedRow());
  stackViewController()->push(&m_scriptParameterController);
}

void MenuController::editScriptAtIndex(int scriptIndex) {
  assert(scriptIndex >=0 && scriptIndex < m_scriptStore->numberOfScripts());
  Script script = m_scriptStore->scriptAtIndex(scriptIndex, ScriptStore::EditableZone::Content);
  m_editorController.setScript(script);
  stackViewController()->push(&m_editorController);
}

void MenuController::numberedDefaultScriptName(char * buffer) {
  bool foundNewScriptNumber = false;
  int currentScriptNumber = 1;
  char newName[k_defaultScriptNameMaxSize];
  memcpy(newName, ScriptStore::k_defaultScriptName, strlen(ScriptStore::k_defaultScriptName)+1);
  // We will only name scripts from script1.py to script99.py.
  while (!foundNewScriptNumber && currentScriptNumber < 100) {
    // Change the number in the script name.
    intToText(currentScriptNumber, &newName[strlen(ScriptStore::k_defaultScriptName)-strlen(ScriptStore::k_scriptExtension)]);
    memcpy(&newName[strlen(newName)], ScriptStore::k_scriptExtension, strlen(ScriptStore::k_scriptExtension)+1);
    if (m_scriptStore->scriptNamed(const_cast<const char *>(newName)).isNull()) {
      foundNewScriptNumber = true;
    }
    currentScriptNumber++;
  }
  if (foundNewScriptNumber) {
    memcpy(buffer, newName, strlen(newName)+1);
    return;
  }
  memcpy(buffer, ScriptStore::k_defaultScriptName, strlen(ScriptStore::k_defaultScriptName)+1);
}

void MenuController::intToText(int i, char * buffer) {
  // We only support integers from 0 to 99
  // buffer should have the space for three chars.
  assert(i>=0);
  assert(i<100);
  if (i/10 == 0) {
    buffer[0] = i+'0';
    buffer[1] = 0;
    return;
  }
  buffer[0] = i/10+'0';
  buffer[1] = i-10*(i/10)+'0';
  buffer[2] = 0;
}

void MenuController::updateAddScriptRowDisplay() {
  m_shouldDisplayAddScriptRow = !m_scriptStore->isFull();
}

}