Revisiting Configurations and the Command Line: GUI Context
Applying the previously developed configuration and command-line handlers to a graphical context resulted in a number of small modifications.
The issue is not the nature of the graphical interface as such: it's that many choices which might otherwise get passed at the command line are more naturally chosen by the user after the application has started up. The various factories I had developed depended, at least in part, on the command-line handler (admittedly, via an interface).
The ICommandLineOptions calls, though, are not necessarily fitted only for command-line derived data. Other classes could implement the interface. Should I go that route, producing a custom set of options for the application which could be passed to the factories?
I decided that it made more sense to extend the capabilities of the options class itself.
Not, though, the interface. The interface as provided is immutable; extending that interface would mean passing a mutable object to contexts which had no business in making any changes.
So I created a new interface:
class IUpdateableOptions
{
public:
virtual ~IUpdateableOptions();
virtual void addOption(const std::string &inName, const std::string &inValue)
= 0;
virtual void addPositionalArgument(const int inPosition,
const std::string &inValue)
= 0;
virtual void removeOption(const std::string &inName) = 0;
};
CommandLineOptions now inherits from this interface as well as from ICommandLineOptions. The concrete class is now mutable, but in most cases it won't be visible, only the immutable interface.
The implementations are straightforward:
void addOption(const std::string &inName,
const std::string &inValue) override
{
m_args.insert_or_assign(inName, inValue);
}
void removeOption(const std::string &inName) override
{
m_args.erase(inName);
}
void addPositionalArgument(const int inPosition,
const std::string &inValue) override
{
m_positionalArgs.insert_or_assign(inPosition, inValue);
}
I have deliberately omitted the ability to remove a positional argument (though it can be reset to an empty string, which has the same effect).
Implementing in a GTK context.
The Singleton is broadly considered an antipattern. I generally concur (though I once wrote a post on the Singleton as Band-Aid arguing for a particular use in some sorts of legacy systems where one is in effect trying to drain a swamp). However, the GTK API (reflecting the underlying X11 model) uses straight C callbacks; moreover, although the callbacks very properly provide user data arguments, sometimes getting the resources you want to where they can be passed as user data can be an onerous task.
I had another problem as well.
I wanted to capture argv and argc from the command-line for my CommandLineOptions instance, and that has to take place in main() just before the GTK application is set running. This would normally be a one-line function:
int main(const int argc, char **argv)
{
return g_application_run(G_APPLICATION(breviary_disp_app_new()), argc, argv);
}
and it would normally be alone in the file.
So the options object was going to be a global object in any case, whether I liked it or not (or it might have been a static object in a different file set and accessed by free-standing C-style functions, but that's essentially the use case for a Singleton).
The other factor is that the object itself will never be updated or accessed in a thread-conflict context. It gets initialized before any threading begins, options are reset in one phase of the program, and then read in another. So problems with Singleton lifetimes don't occur.
Very well.
class SingletonCommandLineOptions : public JSBUtil::ICommandLineOptions,
public JSBUtil::IUpdateableOptions
{
public:
static SingletonCommandLineOptions &Instance() { return s_Instance; }
~SingletonCommandLineOptions() override;
void set(const int argc, const char **argv);
void process(std::function<void(const std::string &, const std::string &)>
inFunc) const override
{
m_delegate->process(inFunc);
}
bool isSingleArg(const std::string &inVal) const override
{
return m_delegate->isSingleArg(inVal);
}
const std::string &getDoubleArg(const std::string &inVal) const override
{
return m_delegate->getDoubleArg(inVal);
}
const std::string &getPositionalArg(const int inVal) const override
{
return m_delegate->getPositionalArg(inVal);
}
const std::string &getAppName() const override
{
return m_delegate->getAppName();
}
void getPositionalArgs(std::map<int, std::string> &outVals) const override
{
m_delegate->getPositionalArgs(outVals);
}
void addOption(const std::string &inName,
const std::string &inValue) override
{
m_updateableInterface->addOption(inName, inValue);
}
void addPositionalArgument(const int inPosition,
const std::string &inValue) override
{
m_updateableInterface->addPositionalArgument(inPosition, inValue);
}
void removeOption(const std::string &inName) override {
m_updateableInterface->removeOption(inName);
}
void clear();
private:
static SingletonCommandLineOptions s_Instance;
SingletonCommandLineOptions()
{
JSBUtil::NullCommandLineOptions *ptr
= new JSBUtil::NullCommandLineOptions();
m_updateableInterface = ptr;
m_delegate.reset(ptr);
}
std::unique_ptr<ICommandLineOptions> m_delegate;
IUpdateableOptions *m_updateableInterface;
};
Although in practice the first call sets the arguments, we use a null delegate to provide lifetime safety until the set() function is called:
void SingletonCommandLineOptions::set(const int argc, const char **argv)
{
JSBUtil::CommandLineOptions *ptr
= new JSBUtil::CommandLineOptions(argc, argv);
m_updateableInterface = ptr;
m_delegate.reset(ptr);
}
The first call is then:
int main(const int argc, char **argv)
{
SingletonCommandLineOptions::Instance().set(argc, const_cast<const char**>(argv));
return g_application_run(G_APPLICATION(breviary_disp_app_new()), argc, argv);
}
The callbacks that are registered with the GTK when editing options are then straightforward:
void set_is_priest(GtkWidget *widget, gpointer data)
{
if (gtk_toggle_button_get_active(
reinterpret_cast<GtkToggleButton *>(widget)))
SingletonCommandLineOptions::Instance().addOption("P", "");
else
SingletonCommandLineOptions::Instance().removeOption("P");
}
void set_office_type(GtkWidget *widget, gpointer data)
{
const char *cp
= gtk_combo_box_get_active_id(reinterpret_cast<GtkComboBox *>(widget));
if ((cp == nullptr) || (std::strcmp(cp, "All Offices") == 0))
{
SingletonCommandLineOptions::Instance().removeOption("x");
SingletonCommandLineOptions::Instance().removeOption("O");
return;
}
if (std::string s(cp); s == "Current Office")
SingletonCommandLineOptions::Instance().addOption("x", "");
else
SingletonCommandLineOptions::Instance().addOption("O", s);
}
void set_year(GtkWidget *widget, gpointer data)
{
const char *cp
= gtk_combo_box_get_active_id(reinterpret_cast<GtkComboBox *>(widget));
if (cp == nullptr)
SingletonCommandLineOptions::Instance().removeOption("y");
else
SingletonCommandLineOptions::Instance().addOption("y", cp);
}
gboolean set_date(GtkWidget *widget, GdkEventFocus inEvent, gpointer data)
{
const char * cp = gtk_entry_get_text(reinterpret_cast<GtkEntry *>(widget));
if (cp == nullptr)
SingletonCommandLineOptions::Instance().removeOption("D");
else
SingletonCommandLineOptions::Instance().addOption("D", cp);
return GDK_EVENT_PROPAGATE;
}
Configuration Files
No such global data issues affect the configuration file model. We do actually have a static value in the window initialization file so that a reader can be initialized once, but referenced every time the display_office() callback is executed from a GTK context. (Again, we can pass user data through a pointer, but the object has to have a static lifetime (hence either static, global, or dynamically allocated) and might be used outside that context (hence can't simply be passed to the callback registration as owner).
The object also has to extend the BaseConfigFileReader because in a richer environment there are additional things to be configured. We provide a new interface for this new set of options;
class IExtendedConfigOptions
{
public:
virtual ~IExtendedConfigOptions();
virtual std::string getFormattingType() const = 0;
virtual bool suppressPsalmNumbers() const = 0;
};
and we extend the BaseConfigFileReader with the two new functions:
class GtkConfigFileReader : public BreviaryLib::BaseConfigFileReader,
public IExtendedConfigOptions
{
public:
GtkConfigFileReader(std::istream &inStream, const std::string &inTopElement):
BreviaryLib::BaseConfigFileReader(inStream, inTopElement)
{ }
~GtkConfigFileReader() override;
std::string getFormattingType() const override
{
return m_config.getValue("Formatting");
}
bool suppressPsalmNumbers() const override
{
return m_config.getValue("SuppressPsalmVerses") == "TRUE";
}
};
These get used in the GTK formatting code, whose topic is for another day.
We have a pointer to this at file static level:
std::unique_ptr<GtkConfigFileReader> theConfigFile;
and initialize it via a free-standing function
bool initConfigFile()
{
std::string configFile = BreviaryLib::CommandLineArgs(SingletonCommandLineOptions::Instance()).getConfig();
if (configFile.empty())
configFile = "/home/james/breviary/gtk_breviary_app.xml";
std::ifstream cf(configFile);
if (!cf)
{
theLoggerFactory.createLogger(GtkLoggerFactory::Popup)->log(Log::Severity::Error,
"Unable to open configuration file " + configFile);
return false;
}
theConfigFile = std::make_unique<GtkConfigFileReader>(cf, "GtkBreviaryApp");
return true;
}
Note that the configuration file is not one of the options which can be reset interactively -- if it were, it would have to be read in only in the specific display context after the options had been set. As it's a comparatively expensive operation, getting it out of the way early is useful.
initConfigFile() is called during the callback made at the GTK initialization level of
void breviary_disp_app_window_init(BreviaryDisplayAppWindow *app);
This means that anything it accesses has to be static or global data (except for data it creates on the heap and passes into other contexts via e.g. call back registrations). It also means that the static unique_ptr gets initialized before anything else can make use of it.
Comments
Post a Comment