Introduction
In GUI applications, it is often necessary to prevent the user from being able to carry out some of the actions that the interface offers. This is (often) done by disabling (deactivating) temporarily at run time, groups of controls. This way, we often use the state of GUI elements to synchronize the various tasks of the application. We can speak here without any loss of generality about menus (menu items) as such GUI items; the discussion can easily apply to any type of control. Run-time menu synchronization is usually done by hand in code, in every place we need to have such feature. In GUI applications, this is usually implemented by some enable/disable code spread around the code routines that need to do some from of synchronization. This means that the cost of making a change in such an application, when we need to add a new menu or to define a new logic group, is high. This situation happens often during the development phase and more rarely during maintenance. Below, an automated system is represented which tries to facilitate this kind of task in a GUI application. The logic behind this system is transparent to the programmer.
Analysis
We will use the fact that most of the knowledge about the exclusive groups of menus can be deduced form simple logic statements made about each menu element that is present or is added in a program. For every new menu item added, we need to specify the group of menus where it belongs. For example, let us suppose that we define the first menu item m1
. There are no other menu items before this, thus this forms a group by itself, which we will denote as: {m1}
. Then we add another menu item m2
. This can be in the same group as the first or in a new one. If m2
is in the same group as m1
, then we have still only one group {m1,m2
}. Otherwise, m2
is in a new group, we have two groups {m1
} and {m2
}.
For every two menus, only one of two relations may stand: they are 'friends', that is they belong to the same group, or they are 'enemies' and belong to two different groups. Usually, we do not need to specify the kind of relation for every pair of menus, since this knowledge can be deduced. Let's consider the case when we have three menu items, m1
, m2
, and m3
. If we say that m1
'is friend of' m2
and m2
'is enemy of' m3
, then we would group these three items in two 'action' groups: {m1,m2
} and {m3
}. Thus we need not say explicitly that m1
'is enemy of' m3
.
We can make used of this knowledge to build a system for managing the action groups where the menu relations are deduced based on the relations specified. In such a system, the groups are created automatically and transparently, by examining all of 'friend' and 'enemy' relations defined by the user. The system should then offer the possibility to de/activate the implicit groups, based only on a given menu item, which belongs to one or more of the implicit groups. The details how the implementation of this is done are not important to the user of such system.
In practice, these two relations can be projected in four operations (relations):
- The friend '<+>' operation declares a friendship. If we say that m1<+>m2 after this, we have a group {m1,m2}. Later on, we can say for a new item m3 that either m1<+>m3 or m2<+>m3, and we have {m1, m2, m3}. The friend operation can also merge the groups: if we have groups {m1, m2} and {m3,m4}, then ANY of such declarations m1<+>m3 OR/AND m1<+>m4 OR/AND m2<+>m3 OR/AND m3<+>m1 OR/AND m3<+>m2 OR/AND m4<+>m1 OR/AND m4<+>m2, will bring to this group merge: {m1,m2,m3,m4}. Thus this relation is reflective (m1<+>m1), symmetrical (m1<+>m2 == m2<+>m1) and transitive (m1<+>m2 AND m2<+>m3 => m1<+>m3). It is also associative but this is not needed in the implementation. By definition, m1<+>m1 will create group {m1} if and only if m1 is not already a member of any group.
- The enemy '<->' operation declares a contradiction (negative condition). If we say that m1<->m2, then after this, we have created the groups {m1} and {m2}. In contrary to the 'friend' relation, the '<->' operation is NOT symmetrical. Thus if we have {m1,m2,m3} and we declare m1<->m2, then we will have: {m1} and {m2,m3}. This is different from m2<->m1 which would have resulted in {m2} and {m1,m3} being formed. Thus this operation as defined here can result in a group split. By definition, m1<->m1 will create group {m1} if this group does not exist.
- The mutual friend operation '<*>' declares what we will call directed friendship. It is the same as friend operation, in the fact that it also declares a 'friend' relation, but in difference from the friend operation, the mutual friend operation does NOT result in a group merge. This is required in those cases when we want to declare that a given menu item belongs to more than one action group in a given time. Thus if we have group {m1,m2} and {m3,m4} and we declare m1<*>m3 OR/AND m1<*>m4 we would have {m1,m2} and {m1,m3,m4}. Thus both groups contain m1 as mutual friend, hence the name. This operation is not symmetrical. The operation m1<*>m1 will create group {m1} if this group does not exist.
- The anti-enemy operation '<%>' declares also an enemy-like relation, but changes from operation '<->' in that it does not create a new group for the first operand. Thus if we have {m1,m2,m3} then m1<%>m2 results in {m2,m3} and no group created for m1. If we have {m1,m2} and {m3,m4} then m1<%>m3 OR/AND m1<%>m4 does nothing. By definition m1<%>m1 removes the group {m1} if it exists. This is the main reason for the existence of this operation: given that the enemy operation <-> introduces new groups, there should be then a way to remove them. By definition, m1<%>m2 should create the group {m2} if it does not exist.
The four operations above can be used to change the state of groups (clusters) in any time and a new successive declaration can change the state set by the previous declarations. But note that the order in which the operations are defined is not important.
The system should build the action groups implicitly from these operations. This process should be transparent. However, some means for debugging the state of the manager (the groups formed in a moment of time) would be useful during development, so that it could be implemented and exposed to the users.
Example
// Legend:
// <+> - declareFriend
// <-> - declareEnemy
// <*> - declareMutualFriend
// <%> - declareAntiEnemy
//
m1<+>m2 # this forms group: {m1,m2}
m2<+>m1 # this is the same: {m1,m2}
m2<->m3 # then: {m1,m2};{m3}
m1<+>m3 # this here causes the group merge of {m1,m2} and {m3} so {m1, m2, m3}
m4<+>m2 # {m1,m2,m3,m4};
m4<->m1 # {m1,m2,m3};{m4}
m1<+>m1 # m1 creates a new group if not already a member of another: {m1,m2,m3};{m4}
m1<->m1 # m1 creates a new group if {m1} does not exists: {m1};{m1,m2,m3};{m4}
m5<->m1 # {m1};{m1,m2,m3};{m4};{m5}
m6<->m1 # {m1};{m1,m2,m3};{m4};{m5};{m6}
m6<+>m5 # {m1};{m1,m2,m3};{m4};{m5,m6}
m1<*>m5 # {m1};{m1,m2,m3};{m4};{m1,m5,m6} - mutual friends: m1 belong to two groups.
m1<%>m2 # {m1};{m2,m3};{m4};{m1,m5,m6}
m1<%>m1 # {m2,m3};{m4};{m1,m5,m6}
m1<+>m3 # {m1,m2,m3,m5,m6};{m4}
Implementation and Demo
The main components of the implementation are shown in the figure below. They belong to the namespace com_vpcepa::actionGroup
.
A detailed description follows:
Member<T>
(files: member.h) is a wrapper class around the specific GUI components T
to be synchronized. The T
class is required to have only two methods:
* Each member object of class T must have these methods:
* string (T::*pf)();
* void (T::*pf)(bool);
*
* Also operator << must be defined for specific member objects T.
They will be used like this:
Member<Menu>::setNameMethod(&Menu::getName);
Member<Menu>::setStateMethod(&Menu::setState);
A trivial class Menu
(files: menu.h, menu.cpp) is used as a type T
in demo.
-
Group<T>
(files: group.h) - We save objects not pointers here. This may not be always preferable. The implementation can be changed to make use of pointers, that is to store types of Member<T>*
, instead of Member<T>
as it does now. Since a member item cannot be in a group more than once, then a group is just a set.
Group<T>
class should NOT be accessed directly in code.
-
GroupManager<T>
(files: groupmanager.h, groupmanager.cpp) implements the required logic for clustering the groups of components based on their '<+>' (friend) and '<->' (enemy), '<*>' (mutual friend) and <%> (anti-enemy) operations. The operations can be specified by calling its methods declareFriend()
, declareEnemy()
, declareMutualFriend()
and declareAntiEnemy()
directly in code, or by using an external action file which is the preferred way.
Various '*activate()'
methods of this class are used to de/activate groups.
-
ParseClusters
(files: parseclusters.h, parseclusters.cpp) allows us to initialize a GroupManager<T>
object based on an external action file, not calling thus declareFriend()
, declareEnemy()
, declareMutualFriend()
and declareAntiEnemy()
directly. Only the names of T
objects need to be in this file along with their relations.
The method:
void parseAction(GroupManager<T>&, map<string, Member<T> >&, char *)
is used to initialize a 'GroupManager<T>
' from a action file 'char *
'. The pairs of components (name, Member<T>)
should be provided in a map object.
The format of the actions file is:
# The grammar:
#
# associationsfile := (association_line)*;
# association_line := comment | operation | operation comment | empty;
# comment := '#' + (alfanumeric)*
# operation := name operator name;
# name := (alfanumeric)+;
# operator := '<+>' | '<->' | '<*>' | '<%>'
# alfanumeric := all keyboard chars
# empty := an empty line
#
# Spaces may separate tokens.
To use this feature in code, 'parseclusters.h' should be included and 'parseclusters.cpp' code should be included in the list of to be compiled files.
MemberCollection<T>
(files: membercollection.h, membercollection.cpp) a utility class for using the gmanager
system. The 'gmanager-demo.cpp' uses this class. For more details about manual operations, see 'manager.cpp'. This is the recommended way to use the code functionality.
Various other files are used:
- gmanager-demo.cpp - the main demo of
gmanager
usage in an application. See also 'manager.cpp' for other details. - vutils.h, vutils.cpp - various numeric and
string
routines used here.
To use gmananger
system in another application, you do not need the 'gmanager-demo.cpp' file.
To compile the demo use:
CC gmanager-demo.cpp menu.cpp vutils.cpp parseclusters.cpp membercollection.cpp
Where CC is any C++ compiler, but the code was compiled only by Borland BCC32 5.5.1. The code makes use of C++ exceptions and may not be compiled by all compilers. The exceptions may be omitted, by editing the code.
The demo code is not thread safe. The code needs some critical-session wrapper code to be used in multi-thread applications, that enable/disable controls from many threads.
License
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.