functions with different arguments same typedef ? polymorphism?

I am implementing Listener threads that read sensor values in parallel to execution of robot movement and I need to influence the robot movement based on the sensor values ( sonar indicates wall, stop the robot movement). But I also need to be able to tell the sonar listener to only fire the appropriate facing sonar based on wether robot speed is positive, negative( moving backward ), or zero.


I want to be able to pass l_input which is either an l_package or l_package_t to keyboard_interrupt and similar types of functions. In my actual project, keyboard_interrupt and sonar_interrupt are functions assigned to a Listener class object. I needed to maintain some homogeneity so all Listener class objects should have the function pointer assigned to their main_l be of the following signature :

typedef int(Robot::*m_listener)(l_package&);

In the Listener class object, I have member:

m_listener main_l;

And then I assign a function pointer that was passed through the Listener constructor such that :

main_l=&Robot::keyboard_interrupt;

Obviously, if I change l_package& function parameter to l_package_t&, the typedef would no longer apply and the assignments to main_l will fail for those functions that are to receive the l_package_t&.

I need some functions to have the function argument of l_package_t because I need those functions to react to some global Robot:: variable. For example, sonar_interrupt should fire the front sonars if speed >0 otherwise, rear sonars if speed <0 or none with speed=0.

So I am assigning a global Robot variable (ex: speed, which I find incidentally needs to be a static class variable) to the reference variable trigger in l_package_t. Keyboard_interrupt wont care about speed, so it should have the regular l_package.

My question is, how do I implement variable function parameter types (at least l_package_t and l_package) in the listener functions while maintaining that typedef main_l applies to both functions that take in a l_package and l_package_t?

I thought I would be able to use some polymorphism here but I cant seem to get it to work.

Any ideas?

Thanks
Chris

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#include <cstdlib>
#include <memory>
#include <thread>
#include <deque>
#include <iostream>
#include <vector>
#include <atomic>
#include <unistd.h>

#include <ctime>


#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

int kbhit(void)
{
  struct termios oldt, newt;
  int ch;
  int oldf;
 
  tcgetattr(STDIN_FILENO, &oldt);
  newt = oldt;
  newt.c_lflag &= ~(ICANON | ECHO);
  tcsetattr(STDIN_FILENO, TCSANOW, &newt);
  oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
  fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
 
  ch = getchar();
 
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
  fcntl(STDIN_FILENO, F_SETFL, oldf);
 
  if(ch != EOF)
  {
    ungetc(ch, stdin);
    return 1;
  }
 
  return 0;
}

class Robot;

class l_package {
public:
      l_package(){};
      l_package(int r) : r_flag(r){};
      int l_flag=1; 
      int r_flag=0; 
};

class l_package_t :public l_package {
public:
    l_package_t();
    
    l_package_t(int &i_trigger): l_package(0), trigger(i_trigger) {};
    int &trigger;
};








typedef int(Robot::*m_behavior)(int);
typedef int(Robot::*m_listener)(l_package&);

class thread_item;





class Behavior {
public:
Behavior(Robot &o, m_behavior b_have, std::string i_id);
void run_listeners();

int exec(int flag);

l_package k_package;
l_package s_package;

l_package_t s_proposed;  


m_behavior main_b;
std::string id;
std::vector<std::shared_ptr<thread_item> >thread_list;
Robot &r;

};

struct State {
    std::shared_ptr<Behavior> current_behavior;
    int current_param;
};


class Stack {
    public:
    Stack() {};
    void empty(){
         state.clear();
    };
    
    State pop_out(){  
        State last=state.front();
        state.pop_front();
        return last;
    };
    
    void push_b(std::shared_ptr<Behavior> i_behavior, int iparam) { 
        State temp;
             temp.current_behavior=i_behavior;
             temp.current_param=iparam;        
             state.push_back(temp);
    };
    
    
    void push_f(std::shared_ptr<Behavior> i_behavior, int iparam) { 
        State temp;
              temp.current_behavior=i_behavior;
              temp.current_param=iparam;        
	      state.push_front(temp);
    };
    
    std::deque<State> state;
};



class Robot {
public:
    static std::atomic<bool> behavior_status;
    Robot() {  
    std::shared_ptr<Behavior> move=std::make_shared<Behavior>(*this,&Robot::move_and_scan,"M");
    run_stack->push_b(move,20);
    };

    int keyboard_interrupt(l_package &l_input){
      std::cout << "Keyboard running with R flag " <<l_input.r_flag <<std::endl;
      //std::cout << "Sproposed in keyboard" << l_input.trigger << std::endl;
      int c;
        for (;;) {
            if (l_input.l_flag !=1)
                break;
            
            if (kbhit()) {  
               if (getchar() == 'c') {
                  std::cout << "Key c pressed, stopping move_and_scan" << std::endl;
                  behavior_status=false;
               }
            }   
        }
      std::cout << "Keyboard thread ending " << l_input.l_flag << std::endl;
    }
    
    
    
    int sonar_interrupt( l_package &l_input ){
        std::cout << "Sonar running with R flag " << l_input.r_flag <<std::endl;
        int c;
        l_input.r_flag=true;
        for (;;) {
            if (l_input.l_flag!=1)
               break;
            else {
                usleep(1000000);
            } 
        }
        
        l_input.r_flag=false;
        std::cout << "Sonar thread ending " << l_input.l_flag <<  std::endl; 
    }
    
    int move_and_scan(int x){
        bool status=true;
        std::cout << "Move starting " << std::endl;
        behavior_status=true;
        int start_time=clock();
        int elapsed_time=0;
        do {
            if (!behavior_status) {
                std::cout << "Moved was stopped " << std::endl;
                status=false;
                break;
            }
            elapsed_time=clock() - start_time;
        } while (elapsed_time < (x*1000000));
        std::cout << "Move stopping with result "<< status << std::endl;
    }
    
    void run_state() {
           if (run_stack->state.size() > 0) {
                std::cout << "Run stack size is  " << run_stack->state.size() << std::endl;
                s=run_stack->pop_out();
                int outcome=s.current_behavior->exec(s.current_param);
           }
    }
           std::shared_ptr<Stack> run_stack=std::make_shared<Stack>();
           State s;
           static int speed;
};

int Robot::speed=50;

l_package_t::l_package_t():trigger(Robot::speed){};
std::atomic<bool> Robot::behavior_status{0};

class thread_item {
    public:
        thread_item(Robot &o, m_listener m_list, std::string i_id, l_package &i_pack) : r(o), id(i_id), l_pack(i_pack) {
            thr=std::make_shared<std::thread>(m_list,r,std::ref(l_pack));
        }
        
        std::string id;
        l_package &l_pack;
        std::shared_ptr<std::thread> thr;
        Robot &r;
    
};


Behavior::Behavior(Robot &o, m_behavior b_have, std::string i_id): r(o), main_b(b_have),id(i_id) {  
	   k_package=l_package(5); 
	   s_package=l_package(6); 
	   };

void Behavior::run_listeners() {
    
    std::shared_ptr<thread_item>m=std::make_shared<thread_item>(r,&Robot::keyboard_interrupt,"KL",std::ref(k_package));
    thread_list.push_back(m);
    std::shared_ptr<thread_item>x=std::make_shared<thread_item>(r,&Robot::sonar_interrupt,"SL",std::ref(s_package));
    thread_list.push_back(x);
   
}


int Behavior::exec(int flag){
	   l_package_t s_proposed(Robot::speed);
       run_listeners();
       std::cout << "Executing behavior "  << this->id << std::endl;  
       std::cout << "Trigger "<< r.speed << std::endl;
       std::cout << "s_proposed " << s_proposed.trigger << std::endl;
       
       bool result = (r.*(this->main_b))(flag);
       
       for ( auto &l: thread_list ) {
           l->l_pack.l_flag=0;
       }
       
       for ( auto &l: thread_list ) {
         l->thr->join();
       }
} 

int main(int argc, char** argv) {
    Robot create;
    
    
    
    //for (;;) {
        create.run_state();
    //}
    
    return 0;
}
This sounds like a design problem. Instead of all those function pointer make classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class robot
{
  int get_speed() const
  {
    return ...;
  }
  void stop()
  {
    ...
  }
};

class thread_item
{
public:
  virtual void process() = 0;
};

class thread_list
{
  std::list<std::thread> m_List;

  void add(thread_item &ti)
  {
    m_List.emplace_back(&thread_item::process, &ti);
    ...
  }
};

class sensor : public thread_item
{
protected:
  std::shared_ptr<robot> m_Robot;

public:
  sensor(std::shared_ptr<robot> r) : m_Robot(r)
  {
  }
};

class rear_sonar : public sensor
{
public:
  rear_sonar(std::shared_ptr<robot> r) : sensor(r)
  {
  }

public:
  virtual void process()
  {
    if(obstacle_detected() and (m_Robot->get_speed() < 0))
      m_Robot->stop();
  }
}


int main(int argc, char** argv) {
  std::shared_ptr<robot> r(new robot());
  rear_sonar rs(r);
  thread_list tl;
  tl.add(rs);

...
}
Note that you should of cours use better variable name.
I was able to solve the problem. I forgot something really basic. Base references dont have access to derived reference members. I needed to create a getter function. Program works now and I get the trigger value in line 171. I have a few questions.

Q1. Is there a better way of instantiating l_packet_t such that I wont need to instantiate it with a default constructor first skipping line 89 and the necessity to define the default constructor in line 217? Basically, is there anywhere in the code where :
 
	   l_package_t s_proposed(Robot::speed);  


will work?

Q2. On line 53, I defined a virtual getter function that does nothing so I can instantiate a base class. However this breaks rules since it does not really return anything. Is there a way to create a base class getter that returns directly the value of a derived class member?

Thanks for the ideas. The code above is a test code that runs similar to my project. I wanted to be able to post runnable code but I cant post my project code as it is too big. I have been using classes to encapsulate the function pointers and turn them into objects.

Thanks
Chris

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279

#include <cstdlib>
#include <memory>
#include <thread>
#include <deque>
#include <iostream>
#include <vector>
#include <atomic>
#include <unistd.h>

#include <ctime>


#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

int kbhit(void)
{
  struct termios oldt, newt;
  int ch;
  int oldf;
 
  tcgetattr(STDIN_FILENO, &oldt);
  newt = oldt;
  newt.c_lflag &= ~(ICANON | ECHO);
  tcsetattr(STDIN_FILENO, TCSANOW, &newt);
  oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
  fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
 
  ch = getchar();
 
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
  fcntl(STDIN_FILENO, F_SETFL, oldf);
 
  if(ch != EOF)
  {
    ungetc(ch, stdin);
    return 1;
  }
 
  return 0;
}

class Robot;

class l_package {
public:
      l_package(){};
      l_package(int r) : r_flag(r){};
      int l_flag=1; 
      int r_flag=0; 
      virtual int& get(){};  //virtual getter returns nothing
};

class l_package_t :public l_package {
public:
    l_package_t();
    
    l_package_t(int &i_trigger): l_package(99), trigger(i_trigger) {};
    virtual int& get() {
		return trigger;
	};
    int &trigger;
};




typedef int(Robot::*m_behavior)(int);
typedef int(Robot::*m_listener)(l_package&);

class thread_item;





class Behavior {
public:
Behavior(Robot &o, m_behavior b_have, std::string i_id);
void run_listeners();

int exec(int flag);

l_package k_package;
l_package s_package;

l_package_t s_proposed;  


m_behavior main_b;
std::string id;
std::vector<std::shared_ptr<thread_item> >thread_list;
Robot &r;

};

struct State {
    std::shared_ptr<Behavior> current_behavior;
    int current_param;
};


class Stack {
    public:
    Stack() {};
    void empty(){
         state.clear();
    };
    
    State pop_out(){  
        State last=state.front();
        state.pop_front();
        return last;
    };
    
    void push_b(std::shared_ptr<Behavior> i_behavior, int iparam) { 
        State temp;
             temp.current_behavior=i_behavior;
             temp.current_param=iparam;        
             state.push_back(temp);
    };
    
    
    void push_f(std::shared_ptr<Behavior> i_behavior, int iparam) { 
        State temp;
              temp.current_behavior=i_behavior;
              temp.current_param=iparam;        
	      state.push_front(temp);
    };
    
    std::deque<State> state;
};



class Robot {
public:
    static std::atomic<bool> behavior_status;
    Robot() {  
    std::shared_ptr<Behavior> move=std::make_shared<Behavior>(*this,&Robot::move_and_scan,"M");
    run_stack->push_b(move,20);
    };

    int keyboard_interrupt(l_package &l_input){
      std::cout << "Keyboard running with R flag " <<l_input.r_flag <<std::endl;
      
      
      
      int c;
        for (;;) {
            if (l_input.l_flag !=1)
                break;
            
            if (kbhit()) {  
               if (getchar() == 'c') {
                  std::cout << "Key c pressed, stopping move_and_scan" << std::endl;
                  behavior_status=false;
               }
            }   
        }
      std::cout << "Keyboard thread ending " << l_input.l_flag << std::endl;
    }
    
    
    
    int sonar_interrupt( l_package &l_input ){
        std::cout << "Sonar running with R flag " << l_input.r_flag <<std::endl;
        
        std::cout << "s_proposed in sonar should give the trigger value of 50 =>" << l_input.get() << std::endl;
        int c;
        l_input.r_flag=true;
        for (;;) {
            if (l_input.l_flag!=1)
               break;
            else {
                usleep(1000000);
            } 
        }
        
        l_input.r_flag=false;
        std::cout << "Sonar thread ending " << l_input.l_flag <<  std::endl; 
    }
    
    int move_and_scan(int x){
        bool status=true;
        std::cout << "Move starting " << std::endl;
        behavior_status=true;
        int start_time=clock();
        int elapsed_time=0;
        do {
            if (!behavior_status) {
                std::cout << "Moved was stopped " << std::endl;
                status=false;
                break;
            }
            elapsed_time=clock() - start_time;
        } while (elapsed_time < (x*1000000));
        std::cout << "Move stopping with result "<< status << std::endl;
    }
    
    void run_state() {
           if (run_stack->state.size() > 0) {
                std::cout << "Run stack size is  " << run_stack->state.size() << std::endl;
                s=run_stack->pop_out();
                int outcome=s.current_behavior->exec(s.current_param);
           }
    }
           std::shared_ptr<Stack> run_stack=std::make_shared<Stack>();
           State s;
           static int speed;
};

int Robot::speed=50;

l_package_t::l_package_t():l_package(66),trigger(Robot::speed){};  //default l_package_t constructor requires initialization of trigger with some variable
std::atomic<bool> Robot::behavior_status{0};

class thread_item {
    public:
        thread_item(Robot &o, m_listener m_list, std::string i_id, l_package &i_pack) : r(o), id(i_id), l_pack(i_pack) {
            thr=std::make_shared<std::thread>(m_list,r,std::ref(l_pack));
        }
        
        std::string id;
        l_package &l_pack;
        std::shared_ptr<std::thread> thr;
        Robot &r;
    
};


Behavior::Behavior(Robot &o, m_behavior b_have, std::string i_id): r(o), main_b(b_have),id(i_id) {  
	   k_package=l_package(5); 
	   s_package=l_package(6); 
	   };

void Behavior::run_listeners() {
    
    std::shared_ptr<thread_item>m=std::make_shared<thread_item>(r,&Robot::keyboard_interrupt,"KL",std::ref(k_package));
    thread_list.push_back(m);
    //std::shared_ptr<thread_item>x=std::make_shared<thread_item>(r,&Robot::sonar_interrupt,"SL",std::ref(s_package));  //OLD package
    std::shared_ptr<thread_item>x=std::make_shared<thread_item>(r,&Robot::sonar_interrupt,"SL",std::ref(s_proposed));  //NEW package with trigger
    thread_list.push_back(x);
   
}


int Behavior::exec(int flag){
	   
       run_listeners();
       std::cout << "Executing behavior "  << this->id << std::endl;  
       
       
       bool result = (r.*(this->main_b))(flag);
       
       for ( auto &l: thread_list ) {
           l->l_pack.l_flag=0;
       }
       
       for ( auto &l: thread_list ) {
         l->thr->join();
       }
} 

int main(int argc, char** argv) {
    Robot create;
    
    
    
    //for (;;) {
        create.run_state();
    //}
    
    return 0;
}

Is there a way to create a base class getter that returns directly the value of a derived class member?

First of all, let's be clear that for this to work by any method, you must be certain that the function is always called with an argument that is a derived class instance and not just a base class instance. If it's ever called with an instance of the base class, then there won't be any derived class member to access.

If you're certain that the method will always be called with an instance of derived class then just cast it. I think it's like this:

1
2
3
4
void func(Base &base) {
    Derived &der(dynamic_cast<Derived&>(base);
    ...
}


Last edited on
Q1. Create a constructor for l_package_t that takes such a function pointer.

Q2. No. A base class does not know anything about the derived class.
You can however introduce a member variable that the derived class manipulates and the base class just returns.

As NecipUP stated: I would strongly discourage you frrom continuing like that. Consider an alternative without function pointer and references which causes a lot of trouble. Let alone the race condition caused by this threading approach.

threads are an expensive resource especially for an arduino. Consider polling instead since you use the hardware exclusively.
Using function pointers was a design decision because I found that I was repeating a lot of things in different places. I thought it would be nice if I could discretize things such that the unit behaviors did only something unique so I created a behavior scripting backend using behavior objects tied to specific functions (via function pointers). By enforcing compliance to a typedef and creating a decision matrix for transitioning to other behaviors, i was able to create macrobehavior chains from unit behaviors and other macrobehavior chains. I found myself writing the behavior chain in specific notation on paper and so I converted that to a type of intuitive behavior scripting. I then moved from pollng to using threads to further discretize the unit behaviors ( remove the sensor component) so I could just attach the sensor components and even user interfaces ( keyboard and then in future http) via listener threads. Next on my list is locking down the serial communication with a mutex as that seems to be a necessary consequence of the thread approach. And then building more behaviors. I am just doing this for fun. It keeps me interested. Because of my lack of experience, I like to post in the forums when I encounter technical blocks, to learn and to get exposed to new ideas and new directions I may not have considered. I am running this on an irobot create using a raspberry Pi 2 so I got plenty of horsepower to spare.

Thanks for your interest and time.
Chris
I thought it would be nice if I could discretize things such that the unit behaviors did only something unique
You are on the right track. OOP is introduced for this. It is just that you mix the functional and the object oriented approach.

https://en.wikipedia.org/wiki/Object-oriented_programming
https://en.wikipedia.org/wiki/Functional_programming

functional programming is acutally stateless (i.e. the next value is always calculated from the previous). c++ is not functional but object oriented. So you rather pass pointer to objects (variables at the lowest level) than pointer to functions.

Objects (classes) can store states while functions cannot.

OOP allows an unlimited level of abstraction (what you call 'macrobehavior chains') since objects can contain objects which can contain object...

In OOP when you use the keyword virtual you can actually change or extend the behavior of the subclass.

With function pointers this will become difficult.

then in future http
There are already libraries for this like http://libcurl.org/ or https://pocoproject.org/

Next on my list is locking down the serial communication with a mutex as that seems to be a necessary consequence of the thread approach.
Mutexes are used to protect shared resources generally. These resources are usually shared variable.

I am running this on an irobot create using a raspberry Pi 2 so I got plenty of horsepower to spare.
Even the rasberry is limited (compared to a PC).

The key to master threads is using them sparingly and wisely (reduce the shared variables as much as possible).
This is a good distinction to make between functional and OOP. I wasn't really thinking in those matters though I was superficially aware of the concepts. I set out to create an OOP application. However, one of the classes got so big (Robot class) and got cluttered with co dependent functions (calling each other and relying on global at least in Robot class scope) variables. I wanted to preserve the functionality but transition into something more coherently scriptable. So my solution was to encapsulate the functions into objects of Behavior class ( with the complication that in order to import function pointers, I also needed to import a reference of the robot instance to execute those functions ). So the application can use the "OLD" way of doing things by calling functions directly and can also use the "NEW" way of creating instances of MicroBehaviors or MacroBehaviors (which is just a chain of macros and micros) and calling the exec function on those. In fact, I spent the last few weeks rebuilding the old functionality using Behavior objects. My intention is to fully be using the NEW system at some point in the future.

A lot of things need to happen yet to fully transition to the new system such as a way to consistently pass information between Behavior objects to eliminate the need for global variables. Currently, in a chain of Behaviors, I have one Behavior passing its result ( int ) to the next Behavior. I am also constrained by my reluctance to change the function signature. I already feel bad about Listeners having to take an l_package instead of an int. I think they will get an int and an l_package because I need to pass a parameter to sonar_listener that tells it how far to look ahead. So Behaviors are constrained to have int parameters and int return values which is what they expect if called directly in Robot class. But the complication arises when the result of Behavior 1 needs to be passed to the input of Behavior 3 and so I gave them all emails so the Behaviors can email each other and check their inboxes and read their emails before they execute. An then what If I need Behavior 3 to ignore Behavior 2's output and instead take Behavior 1's explicit input which may not necessarily be Behavior 1's output. And all this without making the scripting interface overly nonintuitive. Because it is supposed to be an improvement. It tends to write itself, making its own design decisions. And thankfully, I have no deadlines.

I guess to make it fully OOP instead of a hybrid, I need to define the functions inside each Behavior object after I break their dependence from global variables and each other through a consistent behavior to behavior communication model. Then create an instance of each Behavior in Robot class and script their interactions there like lego blocks.

I dont think the application ( or I ) can handle another paradigm shift at the moment but if I ever finish this project and find myself one afternoon thinking "What can I do today?", I might change it again.

Thanks for your insights
Chris


Topic archived. No new replies allowed.