Maintaining a history

To help the user with common phrases and save bandwidth and translation allowance, the app will store a history of requested translations and their results. We'll give the user the option to view them, and use them to avoid duplicate network requests if the user enters text that has already been translated.

Getting ready

We'll store the history file in the app's Documents folder, which sync software generally backs up to the user's computer. Each line will be a JSON-encoded copy of the arrays of strings that holds the original text and the translation.

Open the main.lua file in the TranslationBuddy folder, if you don't already have it open from the previous task.

Getting on with it

The history view and the entry view both need access to the history file, so it's important for them to agree on its location. Lua doesn't have symbolic constants, but we can create a global variable and never change it; both modules can then share it as a common file path.

  1. Near the top of main.lua, before the storyboard calls, add a line to define that location, in the directory that gets backed up by sync software:
    display.setStatusBar(display.DefaultStatusBar)
    
    PATH = system.pathForFile("translation.history", system.DocumentsDirectory)
    
    local storyboard = require "storyboard"
  2. Touch the file path to make sure that the file exists. We don't want to use the file at this time, so we close it as soon as we open it:
    PATH = system.pathForFile("translation.history", system.DocumentsDirectory)
    io.open(PATH, 'a')
      :close()
    
    local storyboard = require "storyboard"

    Tip

    Opening a file in append mode (that's what the 'a' indicates) is the safest way to guarantee that the file exists, because it will create an empty file if there wasn't one, but it will not overwrite or erase the contents of an existing file, which write mode (indicated with a 'w') will.

  3. Now we can leave main.lua for the moment and modify entry.lua to use this file as a cache. Find the scene:userInput function; if we already have the translation saved, there's no need to send it to the translation engine. We'll look at each line, and see if its array starts with the entered text:
        local originalText = event.target.text
        for line in io.lines(PATH) do
          local history = json.decode(line)
          if history[1] == originalText then
            displayResults(history)
            return
          end
        end
        if translator(originalText, 'en', 'ja', output) then

    If we find such an array, which indicates that the text was translated before, we'll simply proceed to the results screen using that saved text, and bail out of the function without calling the translator.

  4. For this to work, we also need to save anything we do get from the translator and add it to the file, so we'll have it next time. We'll do this in the output function that handles translation results:
      storyboard.gotoScene( "result", {effect = 'slideLeft'; params = texts })
    end
    
    local function output(event)
      native.setActivityIndicator(false)
      local texts = {event.source, event.result}
      save(texts, PATH)
      displayResults(texts)
    end
  5. Since the translator is never even called unless the requested text was not in the file, we can simply append it to the end, along with its translation:
    local translator = translation.microsoft(credentials.id, credentials.secret)
    
    local function save(entry, path)
      local log = io.open(path, 'a')
      log:write(json.encode(entry), '
    ')
      log:close()
    end
    
    local function displayResults(texts) 

Viewing the history

At this point, the entry module is silently caching translation results in order to save service costs. However, the design also calls for the app to let the user view this history and review previous translations. This calls for another scene. Save entry.lua, and copy scenetemplate.lua into a new scene file, history.lua. Open this file in your preferred editor.

The history view will use a table view to display the English lines that were sent to the translator. Like the result module, it will create the table fresh each time the scene is launched, but it's even simpler; it needs no extra controls, so no stratum is required:

function scene:enterScene( event )
  local group = self.view

  self.History = widget.newTableView{
    id = "translation_history";
    width = display.contentWidth, height = display.contentHeight;
    topPadding = display.statusBarHeight, bottomPadding = 50;
  }
  group:insert(self.History)
end

-- Called when scene is about to move offscreen:
function scene:didExitScene( event )
  self.History:removeSelf()
end

Similarly to the result module, the history will use an array table to store all the translation requests to display or execute. However, it will use single-line text objects to display just the first part of the English submissions, and each entry will be based on one line in the history file:

  local group = self.view

  self.Data = {}
  local function displayHistory(event)
    display.newText(event.view, self.Data[event.row.index][1], 4, event.row.height * 0.125, native.systemFont, event.row.height * 0.75)
      :setTextColor(0x00)
  end
  self.History = widget.newTableView{
    id = "translation_history";
    width = display.contentWidth, height = display.contentHeight;
    topPadding = display.statusBarHeight, bottomPadding = 50;
    onRowRender = displayHistory;
  }

The table will fill in this array with lines from a text file:

  group:insert(self.History)
  for line in io.lines(PATH) do
    table.insert(self.Data, (json.decode(line)))
    self.History:insertRow{
      height = display.contentHeight * 1/12;
    }
  end
end

Unlike the result view, rows in this table should be selectable to load their contents in the result view. Table views make this easy by taking an onRowTouch handler that processes touch events on the rows and notifies them when they've been pressed, released, or swiped. We're only interested in releases in this case.

The function is basically a copy of the displayResults utility function in entry.lua:

      :setTextColor(0x00)
  end
  local function loadHistory(event)
    if event.phase == 'release' then
      storyboard.gotoScene("result", {effect = 'slideLeft'; params = self.Data[event.row.index]})
    end
  end
  self.History = widget.newTableView{
    id = "translation_history";
    width = display.contentWidth, height = display.contentHeight;
    topPadding = display.statusBarHeight, bottomPadding = 50;
    onRowRender = displayHistory;
    onRowTouch = loadHistory;
  }

Enabling both views

We want users to be able to request translations through either the new entry or the history view; there's an established convention for this in mobile apps, and Corona provides support for it through the tab bar widget. A tab bar usually takes up the bottom of the screen and has a few icons, often with text labels, that can be tapped to switch between different pages of an app's interface. Like the other widgets, the tab bar is highly customizable, but its default appearance is frequently quite adequate, so the only things you have to specify are where to put it and what buttons to put on it.

  1. First, reopen main.lua and load the widget module:
    io.open(PATH, 'a')
      :close()
    
    local widget = require "widget"
    
    local storyboard = require "storyboard"
  2. Visual elements that are loaded after the storyboard is initialized will float over the shared layer that storyboard scenes are loaded into, so we'll create the widget at the bottom of the file:
    storyboard.gotoScene( "entry" )
    
    widget.newTabBar{
      top = display.contentHeight - 50;
      buttons = {
        {
          id = "entry";
          label = "Translate";
          width = 32, height = 32;
          defaultFile = "presentation/translate-up.png", overFile = "presentation/translate-down.png";
          onPress = pickScene;
          selected = true;
        },
      }
    }
    

    The id field for a button can be any Lua value you care to use, and I use the names of the scenes they will load to keep the code simple. The label field is a string that will be displayed under the icon for the tab. It can be blank, but leaving icons unlabeled makes them much less useful; very few are as immediately intuitive as their creators believe. The width and height fields are mandatory in current versions of the widget API, and just specify the dimensions of the button icon. The defaultFile and overFile fields specify the icons to be used for the tab when it is selected (over) or deselected (default). The selected value should only be set on one button, and indicates that that button should use its up image when the bar is loaded. The onPress field should be a function that handles presses on the tab; it can be shared between different buttons if they have some way for the function to distinguish them (like the id value).

  3. The second button will be very similar:
      buttons = {
        {
          id = "entry";
          label = "Translate";
          up = "presentation/translate-up.png", down = "presentation/translate-down.png";
          onPress = pickScene;
          selected = true;
        },
        {
          id = "history";
          label = "History";
          width = 32, height = 32;
          defaultFile = "presentation/history-down.png", overFile = "presentation/history-up.png";
          onPress = pickScene;
        },
      }
  4. Finally, they just need the function that will launch their appropriate scenes. Fill it in just before the widget is created:
    storyboard.gotoScene( "entry" )
    
    local function pickScene(event)
    	storyboard.gotoScene(event.target._id)
    end
    
    widget.newTabBar{

Note

For reasons that aren't clear, current versions of Corona store the IDs assigned to tab bar buttons in the _id field rather than id as previous versions did. This might change in a future version of Corona.

Now you can load the app and switch between the two views. However, unless your history only consists of very small phrases, you may notice an odd glitch when you select a sentence from the history.

Keeping the effects clean

To keep the table short, the history view displays only the beginning of each English sentence in a single-line text object. However, the rest of each text is still hanging off the right edge of the screen. This isn't an issue until the scene slides left to make room for the result display, dragging the extra text across the view. We'll fix this with a mask.

The file presentation/masking-frame.png is pretty simple. Since the screen size is specified in the config.lua file as being 320 x 480, the mask file consists of a white area (which will reveal everything in its target object normally) surrounded by a 4 pixel black border on all sides. Black prevents anything in the target object from showing up at that spot. So, using this mask crops out everything around the edges of the target screen. We'll load the mask with the history module, and attach the mask to the view whenever it's created:

local json = require "json"

local frame = graphics.newMask("presentation/masking-frame.png")

-- Called when the scene's view does not exist:
function scene:createScene( event )
  local group = self.view

  group:setMask(frame)
end

The mask can be positioned on its target using the maskX and maskY values, which specify where to put the center of the mask compared to the masked object's local origin. This is usually the center of the object, but for groups, it's whatever point is defined as the group's point (0, 0). That's frequently the top-left corner, depending on where you placed the group's children inside it. So, we'll move the mask to line up with the center of the screen:

function scene:createScene( event )
  local group = self.view

  group:setMask(frame)
  group.maskX, group.maskY = display.contentCenterX, display.contentCenterY
end

If you try it again, you should not see any more spill-over from the history view into the results as they slide in!

What did we do?

We built a scene that uses a slightly more complex table view. We populated it with data from a file rather than a table, and made it respond to touch actions to select the specified entries. We also created another commonly-used, familiar user interface object to switch between the two related tasks the app can perform; viewing old translations and viewing new translations.

What else do I need to know?

When creating tab bar icons, it's common to leave any part that might be considered the background of the icon, transparent so that it can be superimposed over any bar or background. It's also typical to make some distinctive difference between the selected and unselected forms of an icon, such as grayscale versus color, or flat versus embossed.

To work properly with the graphics rendering engine, images used as masks must have a height and width that are multiples of four, and they should have a border all the way around the edges of black pixels, at least 3 pixels thick. Grays can also be used to make the masked target partially transparent. We'll explore masks in detail in Project 6, Predation

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.39.23