Revisiting Formatters: GTK

 Most of the factories used in the text version of the breviary application, and provided in the library, needed no revisiting for the GUI.  However, the core formatting functionality is radically different when working with a GTKTextBuffer.

What we had was:

class OfficeFormatterFactory

{

public:

  OfficeFormatterFactory(const IConfigSource &inConfig, const JSBUtil::ICommandLineOptions &inOptions);

  std::unique_ptr<IEncapsulatedOfficeFormatter> create();

private:

  std::string m_outputFileName;

  int m_lineLength;

  Use m_use;

};

OfficeFormatterFactory::OfficeFormatterFactory(const IConfigSource &inConfig,

                                               const JSBUtil::ICommandLineOptions &inOptions):

    m_lineLength(inConfig.getLineLength()),

    m_use(inConfig.getUse()),

    m_office(inOptions.getDoubleArg("O")),

    m_outputFileName(inOptions.getDoubleArg("f")),

    m_config(inConfig)    

{

  if (inOptions.isSingleArg("x"))

    {

      std::time_t c_now = std::time(nullptr);

      std::tm  tstruct;

      localtime_r(&c_now, &tstruct);

      if (tstruct.tm_hour < 8)

m_office = "LAUDS";

      else if (tstruct.tm_hour < 9)

m_office = "PRIME";

      else if (tstruct.tm_hour < 11)

m_office = "TERCE";

      else if (tstruct.tm_hour < 14)

m_office = "SEXT";

      else if (tstruct.tm_hour < 17)

m_office = "NONE";

      else if (tstruct.tm_hour < 20)

m_office = "VESPERS";

      else

m_office = "COMPLINE";

    }

}

std::unique_ptr<IEncapsulatedOfficeFormatter> OfficeFormatterFactory::create()

{

  if (m_office.empty())

    {

      if (m_outputFileName.empty())

        return std::make_unique<TextEncapsulatedOfficeFormatter>(std::cout, m_config);

      else

        {

          theStream.open(m_outputFileName);

          if (!theStream)

            throw std::runtime_error("Could not open output file "

                                     + m_outputFileName + " for writing");

          return std::make_unique<TextEncapsulatedOfficeFormatter>(theStream, m_config);

        }

    }

  else

    {

      if (m_outputFileName.empty())

        return std::make_unique<SingleOfficeTextEncapsulatedOfficeFormatter>(

            m_office, std::cout, m_config);

      else

        {

          theStream.open(m_outputFileName);

          if (!theStream)

            throw std::runtime_error("Could not open output file "

                                     + m_outputFileName + " for writing");

          return std::make_unique<SingleOfficeTextEncapsulatedOfficeFormatter>(

              m_office, theStream, m_config);

        }

    }

}

with its associated concrete formatters.  The text mode formatters can be used if you want unformatted text in GTK or other GUI applications (essentially, format to a stringstream and insert the output into a buffer).  But to make use of actual GUI formatting you need a different substructure.

One option would have been simply to write another completely different concrete class for the factory as well as different formatters.  The streaming formatters used for text would not normally be used for the GUI context (unless one wanted a save to file option, but in that case it would probably be buried in a Composite pattern (which needs a new class in any case) or a completely different context (uses only file output and can use the old class unchanged)).

However, there is some useful functionality which would still be wanted, in particular that conversion of the current time into an office name.  That could have been moved elsewhere, i.e. completely out of the class, and then reused in that way.  There were, however, some other competing concerns.  That formatter generation logic is already, locally, somewhat messy, with two levels of nested if/else tests.

If we move the logic into a strategy, we gain the necessary flexibility and can clean up the base class as well.

So, we create a family of inner classes:

  class IStrategy

  {

  public:

    virtual ~IStrategy();

    virtual std::unique_ptr<IEncapsulatedOfficeFormatter>

    create(const std::string &inOffice, const std::string &inFilename,

           const IConfigSource &inConfig)

        = 0;

  };


  class ConsoleStrategy : public IStrategy

  {

  public:

    ~ConsoleStrategy() override;

    std::unique_ptr<IEncapsulatedOfficeFormatter>

    create(const std::string &inOffice, const std::string &inFilename,

           const IConfigSource &inConfig) override;

  };


  class FileStrategy : public IStrategy

  {

  public:

    ~FileStrategy() override;

    std::unique_ptr<IEncapsulatedOfficeFormatter>

    create(const std::string &inOffice, const std::string &inFilename,

           const IConfigSource &inConfig) override;

  };


std::unique_ptr<IEncapsulatedOfficeFormatter>

OfficeFormatterFactory::FileStrategy::create(const std::string &inOffice,

                                             const std::string &inFilename,

                                             const IConfigSource &inConfig)

{

  theStream.open(inFilename);

  if (!theStream)

    throw std::runtime_error("Could not open output file " + inFilename

                             + " for writing");

  if (inOffice.empty())

    {

      return std::make_unique<TextEncapsulatedOfficeFormatter>(theStream,

                                                               inConfig);

    }

  else

    {

      return std::make_unique<SingleOfficeTextEncapsulatedOfficeFormatter>(

          inOffice, theStream, inConfig);

    }

}

std::unique_ptr<IEncapsulatedOfficeFormatter>

OfficeFormatterFactory::ConsoleStrategy::create(const std::string &inOffice,

                                                const std::string &inFilename,

                                                const IConfigSource &inConfig)

{

  if (inOffice.empty())

    {

      return std::make_unique<TextEncapsulatedOfficeFormatter>(std::cout,

                                                               inConfig);

    }

  else

    {

      return std::make_unique<SingleOfficeTextEncapsulatedOfficeFormatter>(

          inOffice, std::cout, inConfig);

    }

}

This reduces the parent class create() to:

std::unique_ptr<IEncapsulatedOfficeFormatter>

OfficeFormatterFactory::create() const

{

  return m_strategy->create(m_office, m_outputFileName, m_config);

}

once we initialize the strategy properly:

auto

OfficeFormatterFactory::CreateStrategy(const std::string &inFilename) -> std::unique_ptr<IStrategy>

{

  if (inFilename.empty())

    return std::make_unique<ConsoleStrategy>();

  else

    return std::make_unique<FileStrategy>();

}

The public constructor now has:

OfficeFormatterFactory::OfficeFormatterFactory(

    const IConfigSource &inConfig,

    const JSBUtil::ICommandLineOptions &inOptions):

    m_outputFileName(inOptions.getDoubleArg("f")),

    m_strategy(CreateStrategy(m_outputFileName)),

    m_office(inOptions.getDoubleArg("O")), m_config(inConfig)

{

  if (m_office.empty() && (inOptions.isSingleArg("x")))

    m_office = GetOfficeFromCurrentTime();

}

(after moving m_outputFilename up to the top of the data members).

There is now a protected constructor for downstream use:

OfficeFormatterFactory::OfficeFormatterFactory(

    const IConfigSource &inConfig,

    const JSBUtil::ICommandLineOptions &inOptions,

    std::unique_ptr<IStrategy> &&inStrategy):

    m_outputFileName(inOptions.getDoubleArg("f")),

    m_strategy(std::move(inStrategy)), m_office(inOptions.getDoubleArg("O")),

    m_config(inConfig)

{

  if (m_office.empty() && (inOptions.isSingleArg("x")))

    m_office = GetOfficeFromCurrentTime();

}

(That logic for determining the office names has been moved into a separate function on DRY principles.)

So that's the pure refactoring part of the job. Now for new features.

GTK Formatters.

I deliberately designed the formatter interface not do assume anything about the output mechanisms.  So I can now create a formatter which uses GTK capabilities to provide markup.

class FullTextOfficeFormatter

    : public BreviaryLib::IEncapsulatedOfficeFormatter

{

public:

  FullTextOfficeFormatter(GtkTextBuffer *inBuffer,

                          const BreviaryLib::IConfigSource &inConfig,

                          const bool inSuppressPsalmNumbers):

      m_buffer(inBuffer),

      m_config(inConfig), m_suppressPsalmNumbers(inSuppressPsalmNumbers)

  {

  }

  ~FullTextOfficeFormatter() override;

  void formatAntiphon(const std::string &inAntiphon,

                      const bool inIsAtEnd) const override;

  void formatDayTitle(const std::string &inHeader) const override;

  void formatOfficeTitle(const std::string &inHeader) const override;

  void formatHeading(const std::string &inHeader,

                     const bool inItalic = false) const override;

  void formatPsalmHeading(const std::string &inHeader,

                          const std::string &inTag) const override;

  void formatResponsory(const std::string &inVersicle,

                        const std::string &inResponse,

                        const bool inEasterResponsory = false,

                        const bool inPassiontideResponsory

                        = false) const override;

  void formatCanticleVerse(const std::string &inVerse) const override;

  void formatPsalmVerse(uint16_t inNumber,

                        std::string_view inVerse) const override;

  void formatChapter(const std::string &inText,

                     const std::string &inSource) const override;

  void formatCollect(const std::string &inText) const override;

  void formatHymn(const std::string &inText,

                  const std::string &inTag) const override;

  void formatGloriaPatri() const override;

  void formatKyrie(const bool inThreefold = true) const override;

  void formatPaterNoster() const override;

  void formatDismissal(const bool inIsPriest) const override;

  void formatCreed() const override;

  void formatConfiteor() const override;

  void formatInvocation(const bool inIsAfterSeptuagesima) const override;

  void formatGreeting(const bool inIsPriest) const override;

  void formatLaudsBeginningResponsory(

      const std::pair<std::string, std::string> &inSarumResponses,

      const bool inIsLent) const override;

  void formatEpiscopalPetitions() const override;

private:

  GtkTextBuffer *m_buffer;

  mutable GtkTextIter m_iter;

  const BreviaryLib::IConfigSource &m_config;

  bool m_suppressPsalmNumbers;

  mutable bool m_addAlleluiaToGloriaPatri = false;

  mutable bool m_psalm119 = false;

  void insertPlainText(const std::string &inValue) const

  {

    gtk_text_buffer_get_end_iter(m_buffer, &m_iter);

    gtk_text_buffer_insert(m_buffer, &m_iter, inValue.c_str(), -1);

  }

  void insertItalicText(const std::string &inValue) const

  {

    gtk_text_buffer_get_end_iter(m_buffer, &m_iter);

    gtk_text_buffer_insert_with_tags_by_name(m_buffer, &m_iter,

                                             inValue.c_str(), inValue.length(),

                                             "italic", nullptr);

  }

  void insertLargeText(const std::string &inValue) const

  {

    gtk_text_buffer_get_end_iter(m_buffer, &m_iter);

    gtk_text_buffer_insert_with_tags_by_name(m_buffer, &m_iter,

                                             inValue.c_str(), inValue.length(),

                                             "large", nullptr);

  }

  void insertRubricText(const std::string &inValue) const

  {

    gtk_text_buffer_get_end_iter(m_buffer, &m_iter);

    gtk_text_buffer_insert_with_tags_by_name(m_buffer, &m_iter,

                                             inValue.c_str(), inValue.length(),

                                             "italic", "error", nullptr);

  }

};

Implementations are along the lines of:

void FullTextOfficeFormatter::formatPsalmHeading(

    const std::string &inHeader, const std::string &inTag) const

{

  if (inHeader.starts_with("Psalm 119"))

    m_psalm119 = true;

  else if (inHeader.starts_with("Psalm"))

    m_psalm119 = false;

  insertPlainText("           " + inHeader + " ");

  insertItalicText(inTag);

  insertPlainText("\n");

}

If I were concerned with discussions of the GTK mechanisms and limitations, I would provide more of the implementations.  I'll skip that here, only to note that you can't turn on and off centring as you insert text; you can make an entire view centred, but not a line.  The solution to this is to use a grid, with centred lines getting their own grid cells.  For my current purposes, this looked like entirely too much work, and not even to expand my acquaintance with the GTK formatting because I've done lots of things with grid before.  So there are a number of places, as above, where I've formatted headings differently for GTK presentation, usually by indenting them or doing other small reformattings.

That new configuration value for suppressing psalm numbers gets used here as well:

void FullTextOfficeFormatter::formatPsalmVerse(uint16_t inNumber,

                                               std::string_view inVerse) const

{

  if ((inNumber == 1) || (m_psalm119 && (inNumber % 8) == 1))

    insertPlainText("\n");

  if (!m_suppressPsalmNumbers)

    insertItalicText(boost::lexical_cast<std::string>(inNumber) + " ");

  insertPlainText(std::string(inVerse) + "\n");

  if (m_psalm119

      && ((inNumber % 8) == 0)

      && ((inNumber % 16) != 0))

    insertPlainText("\n");

}

There is a single office formatter which uses the same delegation model that the stream version did.  There is also  a version (planned to be used to display exactly what was written out if I add a print-to-file capability) which just delegates all formatting to a stream formatter and then inserts the output into a text buffer: PlainTextOfficeFormatter.  It's not very efficient, but there is no perceptible delay when it's used.

That leads us to the new factory:

class GtkOfficeFormatterFactory : public BreviaryLib::OfficeFormatterFactory

{

  class TextStrategy : public BreviaryLib::OfficeFormatterFactory::IStrategy

  {

  public:

    TextStrategy(GtkTextBuffer *inBuffer): m_buffer(inBuffer) {}

    ~TextStrategy() override;

    std::unique_ptr<BreviaryLib::IEncapsulatedOfficeFormatter>

    create(const std::string &inOffice, const std::string &inFilename,

           const BreviaryLib::IConfigSource &inConfig) override;

  private:

    GtkTextBuffer *m_buffer;

  };

  class FormattedStrategy

      : public BreviaryLib::OfficeFormatterFactory::IStrategy

  {

  public:

    FormattedStrategy(const IExtendedConfigOptions &inExtendedOptions,

                      GtkTextBuffer *inBuffer):

        m_buffer(inBuffer),

        m_extendedOptions(inExtendedOptions)

    { }

    ~FormattedStrategy() override;

    std::unique_ptr<BreviaryLib::IEncapsulatedOfficeFormatter>

    create(const std::string &inOffice, const std::string &inFilename,

           const BreviaryLib::IConfigSource &inConfig) override;

  private:

    GtkTextBuffer *m_buffer;

    const IExtendedConfigOptions &m_extendedOptions;

  };

public:

  GtkOfficeFormatterFactory(const IExtendedConfigOptions &inExtendedOptions,

                            GtkTextBuffer *inBuffer,

                            const BreviaryLib::IConfigSource &inConfig,

                            const JSBUtil::ICommandLineOptions &inOptions):

      BreviaryLib::OfficeFormatterFactory(

          inConfig, inOptions, GetStrategy(inExtendedOptions, inBuffer))

  { }

  ~GtkOfficeFormatterFactory() override;

private:

  static std::unique_ptr<OfficeFormatterFactory::IStrategy>

  GetStrategy(const IExtendedConfigOptions &inExtendedOptions,

              GtkTextBuffer *inBuffer);

};

As with the base class, this defines a couple of strategies, which are injected at construction time into the base class.

The strategy implementations are straightforward:

std::unique_ptr<BreviaryLib::IEncapsulatedOfficeFormatter>

GtkOfficeFormatterFactory::TextStrategy::create(

    const std::string &inOffice, const std::string &inFilename,

    const BreviaryLib::IConfigSource &inConfig)

{

  if (inOffice.empty())

      return std::make_unique<PlainTextOfficeFormatter>(m_buffer, inConfig);

  else

      return std::make_unique<PlainTextOfficeFormatter>(inOffice, m_buffer,

                                                        inConfig);

}

std::unique_ptr<BreviaryLib::IEncapsulatedOfficeFormatter>

GtkOfficeFormatterFactory::FormattedStrategy::create(

    const std::string &inOffice, const std::string &inFilename,

    const BreviaryLib::IConfigSource &inConfig)

{

  if (inOffice.empty())

      return std::make_unique<FullTextOfficeFormatter>(

          m_buffer, inConfig, m_extendedOptions.suppressPsalmNumbers());

  else

      return std::make_unique<SingleOfficeTextOfficeFormatter>(

          inOffice, m_buffer, inConfig,

          m_extendedOptions.suppressPsalmNumbers());

}

It is true that in theory one could create four strategies rather than two, dividing on the presence or absence of the office specification.  That can, so to speak, be left as an exercise for the reader.  Personally, I don't find one level of if/else logic a problem, unless it is confusing for other reasons, or potentially a performance issue. (We're already calling the strategy through a virtual function; eliminating an extra if/else test is at worst neutral (if the factory's create() is called only once, though that is the normal case here; but there's no perceptual delay between clicking on display and seeing the window pop up, fully formatted, so no reason to address performance).)  So I'm leaving it where it is, with two create() functions which are four lines long each.

Comments

Popular posts from this blog

Boundaries

State Machines

Considerations on an Optimization