]> git.tdb.fi Git - r2c2.git/commitdiff
Add a new remote control program with GLtk-based UI
authorMikko Rasa <tdb@tdb.fi>
Tue, 14 Apr 2015 23:37:43 +0000 (02:37 +0300)
committerMikko Rasa <tdb@tdb.fi>
Tue, 14 Apr 2015 23:37:43 +0000 (02:37 +0300)
17 files changed:
Build
data/remote/connectdialog.ui [new file with mode: 0644]
data/remote/statusbar.ui [new file with mode: 0644]
data/remote/trainpanel.ui [new file with mode: 0644]
data/remote/trainselector.ui [new file with mode: 0644]
source/network/client.h
source/network/train.h
source/remote/connectdialog.cpp [new file with mode: 0644]
source/remote/connectdialog.h [new file with mode: 0644]
source/remote/remote.cpp [new file with mode: 0644]
source/remote/remote.h [new file with mode: 0644]
source/remote/statusbar.cpp [new file with mode: 0644]
source/remote/statusbar.h [new file with mode: 0644]
source/remote/trainpanel.cpp [new file with mode: 0644]
source/remote/trainpanel.h [new file with mode: 0644]
source/remote/trainselector.cpp [new file with mode: 0644]
source/remote/trainselector.h [new file with mode: 0644]

diff --git a/Build b/Build
index 8f0fb18234c5b625b5a5b161ac3958e861886b78..60211baf0d64b655da9ea6f9d36763c50d63367c 100644 (file)
--- a/Build
+++ b/Build
@@ -56,6 +56,17 @@ package "r2c2"
                use "r2c2_net";
        };
 
+       program "remote"
+       {
+               source "source/remote";
+               require "mspgui";
+               require "mspgl";
+               require "mspgltk";
+               require "mspnet";
+               use "r2c2";
+               use "r2c2_net";
+       };
+
        program "serial"
        {
                source "source/serial";
diff --git a/data/remote/connectdialog.ui b/data/remote/connectdialog.ui
new file mode 100644 (file)
index 0000000..3cf45fc
--- /dev/null
@@ -0,0 +1,44 @@
+layout
+{
+       margin
+       {
+               top 2;
+               horizontal 6;
+               bottom 8;
+       };
+};
+
+column
+{
+       label ""
+       {
+               text "Connect to server";
+       };
+
+       grid 2
+       {
+               label ""
+               {
+                       text "Host";
+               };
+
+               entry "ent_host";
+
+               label ""
+               {
+                       text "Port";
+               };
+
+               entry "ent_port";
+       };
+
+       row
+       {
+               split;
+
+               action_button "" 0
+               {
+                       text "Connect";
+               };
+       };
+};
diff --git a/data/remote/statusbar.ui b/data/remote/statusbar.ui
new file mode 100644 (file)
index 0000000..8efe2d0
--- /dev/null
@@ -0,0 +1,45 @@
+layout
+{
+       margin
+       {
+               horizontal 6;
+               vertical 8;
+       };
+};
+
+row
+{
+       grid 3
+       {
+               indicator "ind_power_on"
+               {
+                       style "green";
+               };
+               indicator "ind_power_off"
+               {
+                       style "red";
+               };
+               indicator "ind_halt";
+               button "btn_power_on"
+               {
+                       text "On";
+                       style "green";
+               };
+               button "btn_power_off"
+               {
+                       text "Off";
+                       style "red";
+               };
+               constraint COPY_WIDTH "btn_power_on";
+               button "btn_halt"
+               {
+                       text "Halt";
+               };
+               constraint COPY_WIDTH "btn_power_on";
+       };
+       label "lbl_status"
+       {
+               style "digital";
+       };
+       expand;
+};
diff --git a/data/remote/trainpanel.ui b/data/remote/trainpanel.ui
new file mode 100644 (file)
index 0000000..7b03c96
--- /dev/null
@@ -0,0 +1,59 @@
+layout
+{
+       margin
+       {
+               horizontal 6;
+               vertical 8;
+       };
+};
+
+column
+{
+       row
+       {
+               panel "pnl_functions"
+               {
+                       style "group";
+                       layout
+                       {
+                               margin
+                               {
+                                       horizontal 0;
+                                       vertical 0;
+                               };
+                       };
+               };
+               column
+               {
+                       label "lbl_speed"
+                       {
+                               style "digital";
+                       };
+                       expand true false;
+
+                       vslider "sld_speed";
+                       constraint COPY_WIDTH "pnl_functions";
+                       expand;
+               };
+               column
+               {
+                       indicator "ind_forward";
+                       button "btn_forward"
+                       {
+                               style "arrow_up";
+                       };
+                       constraint COPY_WIDTH "sld_speed";
+                       button "btn_reverse"
+                       {
+                               style "arrow_down";
+                       };
+                       constraint COPY_HEIGHT "btn_forward";
+                       indicator "ind_reverse";
+               };
+       };
+       label "lbl_status"
+       {
+               style "digital";
+       };
+       expand true false;
+};
diff --git a/data/remote/trainselector.ui b/data/remote/trainselector.ui
new file mode 100644 (file)
index 0000000..9261de3
--- /dev/null
@@ -0,0 +1,11 @@
+layout
+{
+       margin
+       {
+               horizontal 6;
+               vertical 8;
+       };
+};
+
+dropdown "drp_trains";
+expand true false;
index 9f142df89e15492a824fd46fbebc726bc39a9f62..94fa78545da9ef52b977acac9084b694256f7636 100644 (file)
@@ -54,6 +54,8 @@ public:
        const std::list<std::string> &get_routes() const { return routes; }
        void set_power(bool);
        void set_halt(bool);
+       bool get_power() const { return power; }
+       bool get_halt() const { return halt; }
        NetTrain &get_train(unsigned) const;
        const std::map<unsigned, NetTrain *> &get_trains() const { return trains; }
 
index 5c938dcbe0afc9517cb565e654912cfb973d4b58..c908beea77c1b8f909e41f191a2597cbdaf50b4f 100644 (file)
@@ -35,6 +35,7 @@ private:
 public:
        NetTrain(Client &, const TrainInfoPacket &);
 
+       Client &get_client() const { return client; }
        const VehicleType &get_loco_type() const { return loco_type; }
        unsigned get_address() const { return address; }
        const std::string &get_name() const { return name; }
@@ -48,6 +49,7 @@ public:
        bool get_function(unsigned i) const { return (functions>>i)&1; }
        void set_route(const std::string &);
        const std::string &get_route() const { return route; }
+       const std::string &get_status() const { return status; }
 
        void process_packet(const TrainControlPacket &);
        void process_packet(const TrainFunctionPacket &);
diff --git a/source/remote/connectdialog.cpp b/source/remote/connectdialog.cpp
new file mode 100644 (file)
index 0000000..40a8bb3
--- /dev/null
@@ -0,0 +1,25 @@
+#include <msp/core/maputils.h>
+#include <msp/net/inet.h>
+#include <msp/net/resolve.h>
+#include "connectdialog.h"
+
+using namespace Msp;
+using namespace R2C2;
+
+ConnectDialog::ConnectDialog(Client &c):
+       client(c)
+{
+       Loader::WidgetMap widgets;
+       DataFile::load(*this, "data/remote/connectdialog.ui", widgets);
+
+       ent_host = dynamic_cast<GLtk::Entry *>(get_item(widgets, "ent_host"));
+       ent_port = dynamic_cast<GLtk::Entry *>(get_item(widgets, "ent_port"));
+       ent_port->set_text("8315");
+}
+
+void ConnectDialog::on_response(int)
+{
+       Net::SockAddr *addr = Net::resolve(ent_host->get_text(), ent_port->get_text(), Net::INET);
+       client.connect(*addr);
+       delete addr;
+}
diff --git a/source/remote/connectdialog.h b/source/remote/connectdialog.h
new file mode 100644 (file)
index 0000000..68ae64b
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef CONNECTDIALOG_H_
+#define CONNECTDIALOG_H_
+
+#include <msp/gltk/dialog.h>
+#include <msp/gltk/entry.h>
+#include "network/client.h"
+
+class ConnectDialog: public Msp::GLtk::Dialog
+{
+private:
+       R2C2::Client &client;
+       Msp::GLtk::Entry *ent_host;
+       Msp::GLtk::Entry *ent_port;
+
+public:
+       ConnectDialog(R2C2::Client &);
+
+private:
+       virtual void on_response(int);
+};
+
+#endif
diff --git a/source/remote/remote.cpp b/source/remote/remote.cpp
new file mode 100644 (file)
index 0000000..f6e8692
--- /dev/null
@@ -0,0 +1,98 @@
+#include <msp/gl/framebuffer.h>
+#include <msp/gltk/layout.h>
+#include <msp/time/utils.h>
+#include "connectdialog.h"
+#include "remote.h"
+
+using namespace std;
+using namespace Msp;
+using namespace R2C2;
+
+Remote::Remote(int, char **):
+       window(1024, 768),
+       client(catalogue),
+       ui_resources("data/r2c2.res"),
+       root(ui_resources, window),
+       root_layout(new GLtk::Layout),
+       status_bar(client)
+{
+       window.signal_close.connect(sigc::bind(sigc::mem_fun(this, &Remote::exit), 0));
+
+       root.set_size(window.get_width()/2, window.get_height()/2);
+       root.set_layout(root_layout);
+       root_layout->set_margin(GLtk::Sides());
+       root_layout->set_spacing(0);
+
+       for(unsigned i=0; i<2; ++i)
+       {
+               selectors[i] = new TrainSelector(client);
+               root.add(*selectors[i]);
+               root_layout->set_gravity(*selectors[i], i*2-1, 1);
+               root_layout->set_expand(*selectors[i], true, false);
+               selectors[i]->signal_train_selected.connect(sigc::bind(sigc::mem_fun(this, &Remote::train_selected), i));
+
+               if(i>0)
+               {
+                       root_layout->add_constraint(*selectors[i], GLtk::Layout::RIGHT_OF, *selectors[i-1]);
+                       root_layout->add_constraint(*selectors[i], GLtk::Layout::COPY_WIDTH, *selectors[i-1]);
+               }
+
+               panels[i] = 0;
+       }
+
+       root.add(status_bar);
+       root_layout->set_gravity(status_bar, -1, -1);
+       root_layout->set_expand(status_bar, true, false);
+
+       catalogue.add_source("data/Märklin/H0");
+
+       client.use_event_dispatcher(ev_disp);
+}
+
+int Remote::main()
+{
+       ConnectDialog *dlg = new ConnectDialog(client);
+       root.add(*dlg);
+       root_layout->set_gravity(*dlg, 0, 0);
+
+       window.show();
+
+       return Application::main();
+}
+
+void Remote::tick()
+{
+       for(unsigned i=0;; ++i)
+       {
+               Time::TimeStamp t = Time::now();
+               if(i>0 && t>=next_frame)
+               {
+                       next_frame = t+Time::sec/30;
+                       break;
+               }
+               ev_disp.tick(max(next_frame-t, Time::zero));
+       }
+
+       window.tick();
+
+       GL::Framebuffer::system().clear(GL::COLOR_BUFFER_BIT);
+       root.render();
+       window.swap_buffers();
+}
+
+void Remote::train_selected(NetTrain *train, unsigned index)
+{
+       delete panels[index];
+       panels[index] = 0;
+
+       if(train)
+       {
+               panels[index] = new TrainPanel(*train);
+               root.add(*panels[index]);
+               root_layout->set_expand(*panels[index], true, true);
+               root_layout->add_constraint(*panels[index], GLtk::Layout::BELOW, *selectors[index]);
+               root_layout->add_constraint(*panels[index], GLtk::Layout::ALIGN_LEFT, *selectors[index]);
+               root_layout->add_constraint(*panels[index], GLtk::Layout::ALIGN_RIGHT, *selectors[index]);
+               root_layout->add_constraint(*panels[index], GLtk::Layout::ABOVE, status_bar);
+       }
+}
diff --git a/source/remote/remote.h b/source/remote/remote.h
new file mode 100644 (file)
index 0000000..bff56ed
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef REMOTE_H_
+#define REMOTE_H_
+
+#include <msp/core/application.h>
+#include <msp/gltk/resources.h>
+#include <msp/gltk/root.h>
+#include <msp/graphics/simplewindow.h>
+#include <msp/io/eventdispatcher.h>
+#include <msp/time/timestamp.h>
+#include "network/client.h"
+#include "statusbar.h"
+#include "trainpanel.h"
+#include "trainselector.h"
+
+class Remote: public Msp::RegisteredApplication<Remote>
+{
+private:
+       Msp::Graphics::SimpleGLWindow window;
+       R2C2::Catalogue catalogue;
+       R2C2::Client client;
+       Msp::IO::EventDispatcher ev_disp;
+       Msp::GLtk::Resources ui_resources;
+       Msp::GLtk::Root root;
+       Msp::GLtk::Layout *root_layout;
+       TrainSelector *selectors[2];
+       TrainPanel *panels[2];
+       StatusBar status_bar;
+       Msp::Time::TimeStamp next_frame;
+
+public:
+       Remote(int, char **);
+
+       virtual int main();
+private:
+       virtual void tick();
+       void train_selected(R2C2::NetTrain *, unsigned);
+};
+
+#endif
diff --git a/source/remote/statusbar.cpp b/source/remote/statusbar.cpp
new file mode 100644 (file)
index 0000000..5feda64
--- /dev/null
@@ -0,0 +1,52 @@
+#include <msp/core/maputils.h>
+#include <msp/gltk/button.h>
+#include "statusbar.h"
+
+using namespace std;
+using namespace Msp;
+using namespace R2C2;
+
+StatusBar::StatusBar(Client &c):
+       client(c)
+{
+       Loader::WidgetMap widgets;
+       DataFile::load(*this, "data/remote/statusbar.ui", widgets);
+
+       Msp::GLtk::Button *btn = dynamic_cast<GLtk::Button *>(get_item(widgets, "btn_power_on"));
+       btn->signal_clicked.connect(sigc::bind(sigc::mem_fun(&client, &Client::set_power), true));
+       btn = dynamic_cast<GLtk::Button *>(get_item(widgets, "btn_power_off"));
+       btn->signal_clicked.connect(sigc::bind(sigc::mem_fun(&client, &Client::set_power), false));
+       btn = dynamic_cast<GLtk::Button *>(get_item(widgets, "btn_halt"));
+       btn->signal_clicked.connect(sigc::mem_fun(this, &StatusBar::ui_halt_clicked));
+
+       ind_power_on = dynamic_cast<GLtk::Indicator *>(get_item(widgets, "ind_power_on"));
+       ind_power_off = dynamic_cast<GLtk::Indicator *>(get_item(widgets, "ind_power_off"));
+       ind_halt = dynamic_cast<GLtk::Indicator *>(get_item(widgets, "ind_halt"));
+
+       lbl_status = dynamic_cast<GLtk::Label *>(get_item(widgets, "lbl_status"));
+
+       client.signal_power_changed.connect(sigc::mem_fun(this, &StatusBar::power_changed));
+       client.signal_halt_changed.connect(sigc::mem_fun(this, &StatusBar::halt_changed));
+       client.signal_emergency.connect(sigc::mem_fun(this, &StatusBar::emergency));
+}
+
+void StatusBar::ui_halt_clicked()
+{
+       //client.set_halt(!client.get_halt());
+}
+
+void StatusBar::power_changed(bool p)
+{
+       ind_power_on->set_active(p);
+       ind_power_off->set_active(!p);
+}
+
+void StatusBar::halt_changed(bool h)
+{
+       ind_halt->set_active(h);
+}
+
+void StatusBar::emergency(const string &e)
+{
+       lbl_status->set_text(e);
+}
diff --git a/source/remote/statusbar.h b/source/remote/statusbar.h
new file mode 100644 (file)
index 0000000..296b019
--- /dev/null
@@ -0,0 +1,30 @@
+#ifndef STATUSBAR_H_
+#define STATUSBAR_H_
+
+#include <msp/gltk/indicator.h>
+#include <msp/gltk/label.h>
+#include <msp/gltk/panel.h>
+#include "network/client.h"
+
+class StatusBar: public Msp::GLtk::Panel
+{
+private:
+       R2C2::Client &client;
+       Msp::GLtk::Indicator *ind_power_on;
+       Msp::GLtk::Indicator *ind_power_off;
+       Msp::GLtk::Indicator *ind_halt;
+       Msp::GLtk::Label *lbl_status;
+
+public:
+       StatusBar(R2C2::Client &);
+
+private:
+       void ui_on_clicked();
+       void ui_off_clicked();
+       void ui_halt_clicked();
+       void power_changed(bool);
+       void halt_changed(bool);
+       void emergency(const std::string &);
+};
+
+#endif
diff --git a/source/remote/trainpanel.cpp b/source/remote/trainpanel.cpp
new file mode 100644 (file)
index 0000000..58dcd67
--- /dev/null
@@ -0,0 +1,98 @@
+#include <msp/core/maputils.h>
+#include <msp/core/raii.h>
+#include <msp/gltk/button.h>
+#include <msp/gltk/column.h>
+#include <msp/gltk/label.h>
+#include <msp/gltk/toggle.h>
+#include <msp/strings/format.h>
+#include "network/client.h"
+#include "trainpanel.h"
+
+using namespace std;
+using namespace Msp;
+using namespace R2C2;
+
+TrainPanel::TrainPanel(NetTrain &t):
+       train(t),
+       updating(false)
+{
+       Loader::WidgetMap widgets;
+       DataFile::load(*this, "data/remote/trainpanel.ui", widgets);
+
+       Msp::GLtk::Button *btn = dynamic_cast<GLtk::Button *>(get_item(widgets, "btn_forward"));
+       btn->signal_clicked.connect(sigc::bind(sigc::mem_fun(&train, &NetTrain::set_reverse), false));
+       btn = dynamic_cast<GLtk::Button *>(get_item(widgets, "btn_reverse"));
+       btn->signal_clicked.connect(sigc::bind(sigc::mem_fun(&train, &NetTrain::set_reverse), true));
+
+       ind_forward = dynamic_cast<GLtk::Indicator *>(get_item(widgets, "ind_forward"));
+       ind_reverse = dynamic_cast<GLtk::Indicator *>(get_item(widgets, "ind_reverse"));
+
+       sld_speed = dynamic_cast<GLtk::Slider *>(get_item(widgets, "sld_speed"));
+       sld_speed->set_range(0, train.get_loco_type().get_maximum_speed()/train.get_client().get_catalogue().get_scale()*3.6);
+       sld_speed->signal_value_changed.connect(sigc::mem_fun(this, &TrainPanel::ui_speed_changed));
+
+       lbl_speed = dynamic_cast<GLtk::Label *>(get_item(widgets, "lbl_speed"));
+
+       lbl_status = dynamic_cast<GLtk::Label *>(get_item(widgets, "lbl_status"));
+       train.signal_status_changed.connect(sigc::mem_fun(this, &TrainPanel::status_changed));
+       lbl_status->set_text(train.get_status());
+
+       GLtk::Panel *pnl_functions = dynamic_cast<GLtk::Panel *>(get_item(widgets, "pnl_functions"));
+       const VehicleType::FunctionMap &functions = train.get_loco_type().get_functions();
+       GLtk::Column column(*pnl_functions->get_layout());
+       for(VehicleType::FunctionMap::const_iterator i=functions.begin(); i!=functions.end(); ++i)
+       {
+               GLtk::Toggle *tgl = new GLtk::Toggle(i->second);
+               pnl_functions->add(*tgl);
+               tgl->set_value(train.get_function(i->first));
+               tgl->signal_toggled.connect(sigc::bind(sigc::mem_fun(this, &TrainPanel::ui_function_toggled), i->first));
+               tgl_functions[i->first] = tgl;
+       }
+
+       train.signal_target_speed_changed.connect(sigc::mem_fun(this, &TrainPanel::update_speed));
+       update_speed(train.get_target_speed());
+       train.signal_reverse_changed.connect(sigc::mem_fun(this, &TrainPanel::update_reverse));
+       update_reverse(train.get_reverse());
+}
+
+void TrainPanel::update_reverse(bool r)
+{
+       ind_forward->set_active(!r);
+       ind_reverse->set_active(r);
+}
+
+void TrainPanel::update_speed(float s)
+{
+       SetFlag setf(updating);
+       float scale_speed = s/train.get_client().get_catalogue().get_scale()*3.6;
+       sld_speed->set_value(scale_speed);
+       lbl_speed->set_text(format("%3.0f", scale_speed));
+}
+
+void TrainPanel::ui_speed_changed(float s)
+{
+       if(!updating)
+       {
+               float real_speed = s*train.get_client().get_catalogue().get_scale()/3.6;
+               train.set_target_speed(real_speed);
+       }
+}
+
+void TrainPanel::function_changed(unsigned f, bool a)
+{
+       SetFlag setf(updating);
+       map<unsigned, GLtk::Toggle *>::iterator i = tgl_functions.find(f);
+       if(i!=tgl_functions.end())
+               get_item(tgl_functions, f)->set_value(a);
+}
+
+void TrainPanel::ui_function_toggled(bool a, unsigned f)
+{
+       if(!updating)
+               train.set_function(f, a);
+}
+
+void TrainPanel::status_changed(const string &s)
+{
+       lbl_status->set_text(s);
+}
diff --git a/source/remote/trainpanel.h b/source/remote/trainpanel.h
new file mode 100644 (file)
index 0000000..02b0b06
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef TRAINPANEL_H_
+#define TRAINPANEL_H_
+
+#include <msp/gltk/indicator.h>
+#include <msp/gltk/panel.h>
+#include <msp/gltk/slider.h>
+#include <msp/gltk/toggle.h>
+#include "network/train.h"
+
+class TrainPanel: public Msp::GLtk::Panel, public sigc::trackable
+{
+private:
+       R2C2::NetTrain &train;
+       Msp::GLtk::Indicator *ind_forward;
+       Msp::GLtk::Indicator *ind_reverse;
+       Msp::GLtk::Slider *sld_speed;
+       Msp::GLtk::Label *lbl_speed;
+       Msp::GLtk::Label *lbl_status;
+       std::map<unsigned, Msp::GLtk::Toggle *> tgl_functions;
+       bool updating;
+
+public:
+       TrainPanel(R2C2::NetTrain &);
+
+private:
+       void update_reverse(bool);
+       void update_speed(float);
+       void ui_speed_changed(float);
+       void function_changed(unsigned, bool);
+       void ui_function_toggled(bool, unsigned);
+       void status_changed(const std::string &);
+};
+
+#endif
diff --git a/source/remote/trainselector.cpp b/source/remote/trainselector.cpp
new file mode 100644 (file)
index 0000000..ca4ef0a
--- /dev/null
@@ -0,0 +1,44 @@
+#include <msp/core/maputils.h>
+#include "network/client.h"
+#include "trainselector.h"
+
+using namespace std;
+using namespace Msp;
+using namespace R2C2;
+
+string train_name(NetTrain *const &train)
+{
+       if(train)
+               return train->get_name();
+       else
+               return "(no train selected)";
+}
+
+TrainSelector::TrainSelector(Client &client):
+       trains(&train_name)
+{
+       Loader::WidgetMap widgets;
+       DataFile::load(*this, "data/remote/trainselector.ui", widgets);
+
+       drp_trains = dynamic_cast<GLtk::Dropdown *>(get_item(widgets, "drp_trains"));
+       drp_trains->set_data(trains);
+       drp_trains->signal_item_selected.connect(sigc::mem_fun(this, &TrainSelector::train_selected));
+
+       client.signal_train_added.connect(sigc::mem_fun(this, &TrainSelector::train_added));
+
+       trains.append(0);
+       drp_trains->set_selected_index(0);
+       const map<unsigned, NetTrain *> &ctrains = client.get_trains();
+       for(map<unsigned, NetTrain *>::const_iterator i=ctrains.begin(); i!=ctrains.end(); ++i)
+               trains.append(i->second);
+}
+
+void TrainSelector::train_added(NetTrain &train)
+{
+       trains.append(&train);
+}
+
+void TrainSelector::train_selected(unsigned index)
+{
+       signal_train_selected.emit(trains.get(index));
+}
diff --git a/source/remote/trainselector.h b/source/remote/trainselector.h
new file mode 100644 (file)
index 0000000..00f46dd
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef TRAINSELECTOR_H_
+#define TRAINSELECTOR_H_
+
+#include <msp/gltk/dropdown.h>
+#include <msp/gltk/listdata.h>
+#include <msp/gltk/panel.h>
+#include "network/train.h"
+
+class TrainSelector: public Msp::GLtk::Panel
+{
+public:
+       sigc::signal<void, R2C2::NetTrain *> signal_train_selected;
+
+private:
+       Msp::GLtk::Dropdown *drp_trains;
+       Msp::GLtk::FunctionListData<R2C2::NetTrain *> trains;
+
+public:
+       TrainSelector(R2C2::Client &);
+
+private:
+       void train_added(R2C2::NetTrain &);
+       void train_selected(unsigned);
+};
+
+#endif