The Making of Pro Office Calculator, Part 1 - Fragment-Based Architecture

2019-07-04

This is the first in a series of articles on the making of Pro Office Calculator. The series will delve into the creative and technical aspects of the project.

Introduction

Some time in mid-2017 I got the urge to start working on a new project. At the time, my day job involved mostly front-end web development, which was getting boring. I missed writing C++. I had the idea to produce some kind of abstract puzzle game using a GUI toolkit.

The initial idea was for the game to start off as something simple, perhaps just a single button. It would then transform into something else a puzzle of some kind and when you'd solved that puzzle there'd be another, and then another, and so on.

I then decided it should actually be less abstract. I wanted it to start off looking like something ordinary, like a calculator. A calculator seemed like the ideal choice because I could make a fully functioning calculator quite easily and I liked the thought of someone using it for years without ever knowing it was something more. This revived an old idea of mine. I used to like finding Easter eggs in old office software and once thought it would be cool to find an Easter egg that goes deeper and deeper until you'd forgotten all about what the app originally looked like its original form being only a facade to hide the secret layer beneath.

In terms of design, this was as far as I'd gotten before starting to write the code. I intended to make it up as I went along.

Fragment Based Architecture

The Problem

Early on I made the decision that the game would be level based with no user data persisting from one level to the next. The save file would therefore just contain a number representing which level the player had attained. In addition, I decided each level should be standalone, so the player needn't remember anything from previous levels to complete the current one.

Whilst some levels, I decided, would differ from the previous one in very subtle ways different text in the About dialog, or different symbols on some of the buttons others would differ in more dramatic ways, bearing little to no resemblance to the previous level. This presented a technical challenge.

Generally in GUI applications, each piece of the UI will be represented by a class. For example, you might have a main window containing a frame (representing, say, a toolbar), which in turn contains a text box and a button.
class MainWindow : public QMainWindow {
  Q_OBJECT

  public:
    // ...

  private:
    ToolsWidget* m_tools;
};

class ToolsWidget : public QFrame {
  Q_OBJECT

  public:
    // ...

  private slots:
    void onButtonClicked();

  private:
    QGridLayout* m_grid;
    QPushButton* m_button;
    QPlainTextEdit* m_textEdit;
};
Now let's say on level 1 the text box is coloured blue, on level 2 it's larger and coloured yellow, on levels 3 and 5 it's disabled, on levels 4, 6, and 7 it's absent entirely, and on level 8 the frame containing the text box doesn't even exist. Likewise, we can imagine just as many configurations for the button not just for its appearance, but its behaviour also. Representing this in code the naive way would quickly become unmanageable.
ToolsWidget::ToolsWidget()
  : QFrame() {

  // Setup widgets

  m_button = new QPushButton(this);

  switch (currentLevel) {
    case 1:
    case 2: {
      m_button->setText("Click me!");
      break;
    }
    default: {
      m_button->setText("Cancel");
      break;
    }
  }

  m_textEdit = new QPlainTextEdit(this);
  m_textEdit->setFixedSize(80, 50);

  switch (currentLevel) {
    case 1: {
      m_textEdit->setStyleSheet("background-color: blue");
      break;
    }
    case 2: {
      m_textEdit->setFixedSize(120, 50);
      m_textEdit->setStyleSheet("background-color: yellow");
      break;
    }
    case 5:
    case 3: {
      m_textEdit->setDisabled(true);
      break;
    }
    case 4:
    case 6:
    case 7: {
      delete m_textEdit;
      m_textEdit = nullptr;
      break;
    }
  }

  // etc ...
}

void ToolsWidget::onButtonClick() {
  switch (currentLevel) {
    case 1: {
      // Do something
      break;
    }
    case 2:
    case 3: {
      // Do something else
      break;
    }
    case 4:
    case 5:
    case 6: {
      // Do something else
      break;
    }
    case 7: {
      // Do something else
      break;
    }
  }
}
Not only did I need a better way of organising the logic for each level, I wanted to enable a seamless transition from one level to the next without the user having to close and reopen the application. In other words, the application should be able to completely transform itself before one's eyes.

The Solution

I spent a day thinking through this problem, sketching diagrams, writing prototypes, etc. to come up with an architecture that could accommodate the full range of examples I was able to think up. As I hadn't yet thought of what the levels of the game would actually be, or even how many there would be, extensibility was a key requirement. I don't remember the exact steps that lead me to the final design, so I'll just describe to you the end result.

The idea is to organise the application as a hierarchy, or a tree, of fragments. A fragment may be a piece of UI, a piece of logic, or whatever it's simply a class that inherits the Fragment base class. At any given moment, only a subtree of these fragments is instantiated. By switching fragments on and off we can alter the appearance and behaviour of the application.


Some fragments may appear in the tree multiple times (like fragment F in the diagram above). I call these relocatable fragments.

Each fragment class is accompanied by a fragment spec a struct containing the fragment's configuration (e.g. its background colour, dimensions, or any other properties by which we might wish to parameterise the fragment). It also specifies what children can be attached to the fragment by including each child's fragment spec as a property. Thus, the fragment specs form a tree. This is a nice declarative way of specifying the fragment structure of the application, as it's relatively easy to change these structs and the way in which they're nested together. The above tree can be represented like so. (In practice, each spec resides in its own file.)
struct FragASpec : public FragmentSpec {
  FragBSpec fragBSpec;
  FragCSpec fragCSpec;

  std::string someParam;
};

struct FragBSpec : public FragmentSpec {
  FragDSpec fragDSpec;
  FragESpec fragESpec;
  FragFSpec fragFSpec;

  int someParam;
  std::string someOtherParam;
};

struct FragCSpec : public FragmentSpec {
  FragFSpec fragFSpec;

  double someParam1;
  double someParam2;
};

struct FragDSpec : public FragmentSpec {
  FragGSpec fragGSpec;
};

struct FragESpec : public FragmentSpec {
  double someParam1;
  std::string someParam2;
};

struct FragFSpec : public FragmentSpec {
  FragHSpec fragHSpec;
};

struct FragGSpec : public FragmentSpec {
  FragFSpec fragFSpec;

  double someParam;
};
We can then specify the application state by filling out a FragmentASpec instance and passing it into FragmentA's rebuild method.
FragASpec* constructLevel1AppState() {
  FragASpec* fragASpec = new FragASpec;
  fragASpec->someParam = "hello world";
  fragASpec->fragBSpec.setEnabled(true);
  fragASpec->fragBSpec.someParam = 123;
  fragASpec->fragBSpec.someOtherParam = "blah";
  fragASpec->fragBSpec.fragDSpec.setEnabled(true);
  fragASpec->fragBSpec.fragDSpec.fragGSpec.setEnabled(true);
  fragASpec->fragBSpec.fragDSpec.fragGSpec.someParam = 12.3;
  fragASpec->fragBSpec.fragFSpec.setEnabled(true);

  return fragASpec;
}

int main(int argc, char** argv) {
  FragASpec* rootSpec = constructLevel1AppState();

  FragmentA rootFrag;
  rootFrag.rebuild(*rootSpec);

  // ...
}
This would yield the following fragment tree, where the nodes highlighted in red are the active fragments.


Each fragment inherits the Fragment class and implements the reload and cleanUp methods. I'll explain these in more detail later. The rebuild method mentioned above is provided by the Fragment base class.
class FragmentA : public Fragment {
  public:
    // ...

    // Inherited Fragment class defines rebuild() method

    void reload(const FragmentSpec& spec) override {
      // Initialise fragment and modify parent
    }

    void cleanUp() override {
      // Reverse modifications to parent
    }

    // ...
};
The code samples so far have been slightly simplified. This isn't meant to be a tutorial, just a description of how the architecture works and the rationale behind it.

In implementing this architecture I had to slightly bend the rules of good software engineering practice. Usually, if you have a class that uses another class say, a dialog containing a widget the widget class should not have to know anything about the dialog class. It should be agnostic about the context in which it's used so that it can be used in many places. I turned this idea on its head. You can think of a fragment like a parasite that latches onto its parent and tampers with its innards by adding or removing widgets, altering its properties, etc. When the fragment is disabled, it reverts all changes it made to its parent. The child fragment knows everything about its parent; indeed, it has an explicit dependency on it by including the parent's header file. The parent fragment, on the other hand, is written without any knowledge of its children. As you'll see, this is how I was able to avoid the switch statement mess I showed earlier.

Here is a summary of the components that make up fragment based architecture with links to their actual implementations in Pro Office Calculator.
The fragment tree for Pro Office Calculator is roughly reflected in the directory structure of the source directory. I say roughly because some fragments appear in the tree in multiple places, so I've put their source files in the relocatable directory.
rob@rob-desktop:~/code/projects/pro_office_calc/src$ tree -d
.
├── fragments
│   ├── f_main
│   │   ├── f_app_dialog
│   │   │   ├── f_console
│   │   │   ├── f_doomsweeper
│   │   │   ├── f_file_system
│   │   │   ├── f_file_system_2
│   │   │   ├── f_mail_client
│   │   │   ├── f_minesweeper
│   │   │   ├── f_procalc_setup
│   │   │   ├── f_server_room
│   │   │   └── f_text_editor
│   │   ├── f_countdown_to_start
│   │   ├── f_desktop
│   │   │   └── f_server_room_init
│   │   ├── f_login_screen
│   │   ├── f_maze_3d
│   │   ├── f_settings_dialog
│   │   │   ├── f_config_maze
│   │   │   └── f_loading_screen
│   │   ├── f_shuffled_calc
│   │   └── f_troubleshooter_dialog
│   └── relocatable
│       ├── f_calculator
│       │   ├── f_normal_calc_trigger
│       │   └── f_partial_calc
│       ├── f_glitch
│       └── f_tetrominos
├── raycast
└── states

30 directories
Here is the spec for the root fragment FMain.
struct FMainSpec : public FragmentSpec {
  // ...

  FGlitchSpec glitchSpec;
  FCalculatorSpec calculatorSpec;
  FShuffledCalcSpec shuffledCalcSpec;
  FLoginScreenSpec loginScreenSpec;
  FDesktopSpec desktopSpec;
  FCountdownToStartSpec countdownToStartSpec;
  FSettingsDialogSpec settingsDialogSpec;
  FAppDialogSpec appDialogSpec0;
  FAppDialogSpec appDialogSpec1;
  FAppDialogSpec appDialogSpec2;
  FAppDialogSpec appDialogSpec3;
  FAppDialogSpec appDialogSpec4;
  FTroubleshooterDialogSpec troubleshooterDialogSpec;
  FTetrominosSpec tetrominosSpec;
  FMaze3dSpec maze3dSpec;

  QString windowTitle = "Pro Office Calculator";
  int width = 400;
  int height = 300;
  QColor bgColour = QColor(240, 240, 240);
  QString backgroundImage;
  QString fileLabel = "File";
  QString quitLabel = "Quit";
  QString helpLabel = "Help";
  QString aboutLabel = "About";
  QString aboutDialogTitle = "About";
  QString aboutDialogText;
};
So the state of the application is specified by filling out this struct, which when passed into the root fragment's rebuild method triggers a rebuild of the whole fragment tree. For each level of the game we fill out the struct to describe which fragments should be enabled and how they should be configured. Here is the application state for level 1.
namespace st_normal_calc {


FMainSpec* makeFMainSpec(const AppConfig& appConfig) {
  FMainSpec* mainSpec = new FMainSpec;
  mainSpec->calculatorSpec.setEnabled(true);
  mainSpec->aboutDialogText = QString{} + "<p align='center'><big>Pro Office Calculator</big>"
    "<br>Version " + appConfig.version.c_str() + "</p>"
    "<p align='center'>Copyright (c) 2018 Rob Jinman. All rights reserved.</p>"
    "<i>" + QString::number(10 - appConfig.stateId) + "</i>";
  mainSpec->countdownToStartSpec.setEnabled(true);
  mainSpec->countdownToStartSpec.stateId = appConfig.stateId;

  return mainSpec;
}


}
When in this state, the application looks like a normal calculator except for a number on the About dialog that decrements each time you close and reopen the app until it hits zero (this is the first puzzle). As you can see, it enables the FCalculator and FCountdownToStart fragments, so the fragment tree for level 1 looks like this.
f_main
├── f_countdown_to_start
└── f_calculator
And the app just resembles a normal calculator.


At the top level of the application (in the main function), we listen for requestStateChange events from the event system and rebuild the fragment tree with the new state.
int main(int argc, char** argv) {
  // ...

  unique_ptr<FMainSpec> mainSpec(makeFMainSpec(appConfig));

  FMain mainFragment({appConfig, *eventSystem, updateLoop});
  mainFragment.rebuild(*mainSpec, false);
  mainFragment.show();

  EventHandle hStateChange = eventSystem->listen("requestStateChange", [&](const Event& e_) {
    const RequestStateChangeEvent& e = dynamic_cast<const RequestStateChangeEvent&>(e_);
    appConfig.stateId = e.stateId;

    mainSpec.reset(makeFMainSpec(appConfig));
    mainFragment.rebuild(*mainSpec, e.hardReset);
  });

  // ...
}

Adding a New Fragment

Let's imagine we want to create a level where the 5 and 6 buttons are swapped around and coloured green. We would first create a directory for our new fragment in the appropriate location, src/fragments/relocatable/f_calculator/f_swapped_buttons. Inside this directory, we create a fragment spec, f_swapped_buttons_spec.hpp. Let's make the colour configurable. It will be red by default.
#ifndef __PROCALC_FRAGMENTS_F_SWAPPED_BUTTONS_SPEC_HPP__
#define __PROCALC_FRAGMENTS_F_SWAPPED_BUTTONS_SPEC_HPP__


#include <QColor>
#include "fragment_spec.hpp"


struct FSwappedButtonsSpec : public FragmentSpec {
  FSwappedButtonsSpec()
    : FragmentSpec("FSwappedButtons", {}) {}

  QColor colour{255, 0, 0};
};


#endif
We want to attach this fragment to FCalculator, so we add its spec to FCalculatorSpec.
#ifndef __PROCALC_FRAGMENTS_F_CALCULATOR_SPEC_HPP__
#define __PROCALC_FRAGMENTS_F_CALCULATOR_SPEC_HPP__


#include <QColor>
#include "fragment_spec.hpp"
#include "fragments/relocatable/f_calculator/f_normal_calc_trigger/f_normal_calc_trigger_spec.hpp"
#include "fragments/relocatable/f_calculator/f_partial_calc/f_partial_calc_spec.hpp"
// Include the spec's header file
#include "fragments/relocatable/f_calculator/f_swapped_buttons/f_swapped_buttons_spec.hpp"


struct FCalculatorSpec : public FragmentSpec {
  FCalculatorSpec()
    : FragmentSpec("FCalculator", {
        &normalCalcTriggerSpec,
        &partialCalcSpec,
        &swappedButtonsSpec // Pass a pointer to the spec to the base class constructor
      }) {}

  FNormalCalcTriggerSpec normalCalcTriggerSpec;
  FPartialCalcSpec partialCalcSpec;
  // Add the spec as a public data member
  FSwappedButtonsSpec swappedButtonsSpec;

  QColor displayColour = QColor(255, 255, 255);
};


#endif
Now let's implement the fragment itself. First, the header file.
#ifndef __PROCALC_FRAGMENTS_F_SWAPPED_BUTTONS_HPP__
#define __PROCALC_FRAGMENTS_F_SWAPPED_BUTTONS_HPP__


#include <QPalette>
#include "fragment.hpp"


struct FSwappedButtonsData : public FragmentData {};

class FSwappedButtons : public Fragment {
  public:
    FSwappedButtons(Fragment& parent, FragmentData& parentData, const CommonFragData& commonData);

    virtual void reload(const FragmentSpec& spec) override;
    virtual void cleanUp() override;

    virtual ~FSwappedButtons() override;

  private:
    FSwappedButtonsData m_data;

    // Remember the original colour of the buttons, so we can change it back
    QPalette m_originalPalette;
};


#endif
And the source file.
#include "fragments/relocatable/f_calculator/f_swapped_buttons/f_swapped_buttons.hpp"
#include "fragments/relocatable/f_calculator/f_swapped_buttons/f_swapped_buttons_spec.hpp"
#include "fragments/relocatable/f_calculator/f_calculator.hpp"
#include "utils.hpp"


//===========================================
// FSwappedButtons::FSwappedButtons
//===========================================
FSwappedButtons::FSwappedButtons(Fragment& parent_, FragmentData& parentData_,
  const CommonFragData& commonData)
  : Fragment("FSwappedButtons", parent_, parentData_, m_data, commonData) {

  DBG_PRINT("FSwappedButtons::FSwappedButtons\n");
}

//===========================================
// doSwap
//===========================================
static void doSwap(ButtonGrid& wgtButtonGrid) {
  QWidget* btnA = wgtButtonGrid.grid->itemAtPosition(2, 1)->widget();
  QWidget* btnB = wgtButtonGrid.grid->itemAtPosition(2, 2)->widget();

  wgtButtonGrid.grid->removeWidget(btnA);
  wgtButtonGrid.grid->removeWidget(btnB);

  wgtButtonGrid.grid->addWidget(btnB, 2, 1);
  wgtButtonGrid.grid->addWidget(btnA, 2, 2);
}

//===========================================
// setColour
//===========================================
static void setColour(ButtonGrid& wgtButtonGrid, const QPalette& palette) {
  QWidget* btnA = wgtButtonGrid.grid->itemAtPosition(2, 1)->widget();
  QWidget* btnB = wgtButtonGrid.grid->itemAtPosition(2, 2)->widget();

  btnA->setPalette(palette);
  btnB->setPalette(palette);
}

//===========================================
// FSwappedButtons::reload
//===========================================
void FSwappedButtons::reload(const FragmentSpec& spec) {
  DBG_PRINT("FSwappedButtons::reload\n");

  auto& spec_ = dynamic_cast<const FSwappedButtonsSpec&>(spec);
  auto& parentData = parentFragData<FCalculatorData>();
  auto& wgtButtonGrid = *parentData.wgtCalculator->wgtButtonGrid;

  doSwap(wgtButtonGrid);

  m_originalPalette = wgtButtonGrid.grid->itemAtPosition(2, 1)->widget()->palette();

  QPalette palette = m_originalPalette;
  palette.setColor(QPalette::Button, spec_.colour);

  setColour(wgtButtonGrid, palette);
}

//===========================================
// FSwappedButtons::cleanUp
//===========================================
void FSwappedButtons::cleanUp() {
  DBG_PRINT("FSwappedButtons::cleanUp\n");

  auto& parentData = parentFragData<FCalculatorData>();
  auto& wgtButtonGrid = *parentData.wgtCalculator->wgtButtonGrid;

  doSwap(wgtButtonGrid);
  setColour(wgtButtonGrid, m_originalPalette);
}

//===========================================
// FSwappedButtons::~FSwappedButtons
//===========================================
FSwappedButtons::~FSwappedButtons() {
  DBG_PRINT("FSwappedButtons::~FSwappedButtons\n");
}
We need to make sure it can be instantiated by name by adding a new else-if block to the fragment factory function, which gets called by Fragment::rebuild().
#include "fragment_factory.hpp"
#include "exception.hpp"
#include "utils.hpp"
#include "fragments/relocatable/f_glitch/f_glitch.hpp"

// ...

#include "fragments/relocatable/f_calculator/f_normal_calc_trigger/f_normal_calc_trigger.hpp"
#include "fragments/relocatable/f_calculator/f_partial_calc/f_partial_calc.hpp"
// Include the spec's header file
#include "fragments/relocatable/f_calculator/f_swapped_buttons/f_swapped_buttons.hpp"
#include "fragments/f_main/f_shuffled_calc/f_shuffled_calc.hpp"


using std::string;


//===========================================
// constructFragment
//===========================================
Fragment* constructFragment(const string& name, Fragment& parent, FragmentData& parentData,
  const CommonFragData& commonData) {

  DBG_PRINT("constructFragment(), name=" << name << "\n");

  if (name == "FGlitch") {
    return new FGlitch(parent, parentData, commonData);
  }
  else if (name == "FCalculator") {
    return new FCalculator(parent, parentData, commonData);
  }
  else if (name == "FNormalCalcTrigger") {
    return new FNormalCalcTrigger(parent, parentData, commonData);
  }
  else if (name == "FPartialCalc") {
    return new FPartialCalc(parent, parentData, commonData);
  }

  // ...

  else if (name == "FShuffledCalc") {
    return new FShuffledCalc(parent, parentData, commonData);
  }
  // Construct our new fragment here
  else if (name == "FSwappedButtons") {
    return new FSwappedButtons(parent, parentData, commonData);
  }

  EXCEPTION("Cannot construct fragment with unrecognised name '" << name << "'\n");
}
Finally, we enable and configure the fragment in the relevant app state.
FMainSpec* makeFMainSpec(const AppConfig& appConfig) {
  FMainSpec* mainSpec = new FMainSpec;
  mainSpec->calculatorSpec.setEnabled(true);
  // Enable our new fragment
  mainSpec->calculatorSpec.swappedButtonsSpec.setEnabled(true);
  mainSpec->calculatorSpec.swappedButtonsSpec.colour = QColor(100, 255, 100);
  mainSpec->aboutDialogText = "";
  mainSpec->aboutDialogText += QString() + "<p align='center'><big>Pro Office Calculator</big>"
    "<br>Version " + appConfig.version.c_str() + "</p>"
    "<p align='center'>Copyright (c) 2018 Rob Jinman. All rights reserved.</p>"
    "<i>" + QString::number(10 - appConfig.stateId) + "</i>";
  mainSpec->countdownToStartSpec.setEnabled(true);
  mainSpec->countdownToStartSpec.stateId = appConfig.stateId;

  return mainSpec;
}
And here is the final result.


This architecture served me well for the duration of the project and never became overly burdensome. While the example above may seem like a lot of code for such little functionality, overall I think it greatly simplified things. It enabled a practically unlimited degree of extensibility. I believe I could have continued to add app states and fragments indefinitely without major problems.