Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best way to share actions between separate GUI classes (menu, toolbars, etc.)

I have several sets of actions (e.g., copy, paste, undo, redo, show dockable window XYZ, zoom, etc.) that I don't want to duplicate in multiple locations but that are shared by different parts of the GUI like the main menu, a toolbar, and right-click menus.

What is the best way to share them? I'm using Qt 5.3 with C++, but this is mostly independent from any specific GUI framework or language.

Some possibilities:

  1. Designate one central place, say the main window, to create all of them with their text, icon, and callback. Then:

    1. Pass in the actions to the constructor when creating the subcomponents of the GUI. This can make the constructor param lists rather long.

    2. Call setters on the subcomponents of the GUI after the subcomponent is constructed and pass in all necessary actions. This makes the constructor shorter but isn't much prettier in the end.

    3. Supply getters from the main window and have the subcomponents get the actions they want. The subcomponents generally have a pointer to the main window already. This makes it so the main window is ignorant of who cares about which action, but it also exposes a bunch of public members (unless I use the Attorney-Client idiom or similar).

    4. Add them to a separate, global-ish repository where the main window adds them and users look them up by name or key or something, as needed. This is similar to the other options but separates the concerns a little better and exposes only a single parameterized getter rather than a bunch of specific getters. Downside: This adds a global-ish object that everyone accesses.

  2. Define the actions in the "main" use case, say, the main menu, and then have getters for everyone else. This locates them in one place and would mean the main window would need to supply a single getter function for the main menu instead. But it still exposes a bunch of internal members as public.

What's the best approach? Is there something better that I haven't listed here?

like image 323
metal Avatar asked Jul 07 '14 19:07

metal


Video Answer


4 Answers

Let us take a step back, and look at the software being made. It is usually a good practice to have customizable UI, so that the users could create/modify application menus and toolbars. This requires toolbars/menus to be created using a config file (.xml or .cfg), which simply binds menu/toolbar items to actions.

Hence, it is required for Actions to have unique names/action-codes using which they could be referred.

I would therefore recommend 1.4. You could either create actions on-demand using an ActionFactory that receives an action name/code as argument, or you could make it mandatory for Actions to register themselves with an ActionRegistry (global!) from which they could be looked up.

PS: Another use-case for named Actions is that if your software has an SDK, you can easily provide automation APIs (e.g. ApiExecuteAction(Actions.COPY)).

like image 189
dhaundy Avatar answered Nov 15 '22 20:11

dhaundy


Are they optional, or does your class require them?

if your class requires them pass them in to the constructor, you say here the argument list would become long? then why not package the actions into and object first and use a factory method to create said object.

If they're not pass them in with with setters and getters.

Something you could read into and consider is the pattern of dependency injection which you can read more about here:

What is dependency injection?

like image 39
Sam Fare Avatar answered Nov 15 '22 21:11

Sam Fare


The problem of reusing QActions in some dialogs is the reconnection of the signals.

You can avoid this problem creating a set of classes to store a group of signals. Something like this:

template < class T >
class EditionSet
{
    T* parent;

  public:

    EditionSet( T* parent )
      : parent( parent )
    {
      cutAction = new QAction( "Cut", parent );
      copyAction = new QAction( "Copy", parent );
      pasteAction = new QAction( "Paste", parent );

      QObject::connect( cutAction, SIGNAL( triggered( ) ),
                        parent, SLOT( CutActionTriggered( ) ) );

      QObject::connect( copyAction, SIGNAL( triggered( ) ),
                        parent, SLOT( CopyActionTriggered( ) ) );

      QObject::connect( pasteAction, SIGNAL( triggered( ) ),
                        parent, SLOT( PasteActionTriggered( ) ) );
    }

    ~EditionSet( )
    {
      QObject::disconnect( cutAction, SIGNAL( triggered( ) ),
                           parent, SLOT( CutActionTriggered( ) ) );

      QObject::disconnect( copyAction, SIGNAL( triggered( ) ),
                           parent, SLOT( CopyActionTriggered( ) ) );

      QObject::disconnect( pasteAction, SIGNAL( triggered( ) ),
                           parent, SLOT( PasteActionTriggered( ) ) );

      delete cutAction;
      delete copyAction;
      delete pasteAction;
    }

    QAction* cutAction;
    QAction* copyAction;
    QAction* pasteAction;
};

class dialog : public QDialog
{
    Q_OBJECT

  public:

    dialog::dialog( QWidget* parent )
      : QDialog( parent ),
        ui( new Ui::dialog ),
        editionSet( EditionSet< dialog >( this ) )
    {
      // ...

      ui->mainToolBar->addAction( editionSet.cutAction );
      ui->mainToolBar->addAction( editionSet.copyAction );
      ui->mainToolBar->addAction( editionSet.pasteAction );
    }

  private:

    EditionSet< dialog > editionSet;
};

If actions will always be inserted in the same order, you can improve this class to allow "auto insertion".

template < class T >
class EditionSet
{
    T* parent;
    QAction* cutAction;
    QAction* copyAction;
    QAction* pasteAction;

  public:

    EditionSet( T* parent )
      : parent( parent )
    {
      cutAction = new QAction( "Cut", parent );
      copyAction = new QAction( "Copy", parent );
      pasteAction = new QAction( "Paste", parent );

      QObject::connect( cutAction, SIGNAL( triggered( ) ),
                        parent, SLOT( CutActionTriggered( ) ) );

      QObject::connect( copyAction, SIGNAL( triggered( ) ),
                        parent, SLOT( CopyActionTriggered( ) ) );

      QObject::connect( pasteAction, SIGNAL( triggered( ) ),
                        parent, SLOT( PasteActionTriggered( ) ) );

    }

    ~EditionSet( )
    {
      QObject::disconnect( cutAction, SIGNAL( triggered( ) ),
                           parent, SLOT( CutActionTriggered( ) ) );

      QObject::disconnect( copyAction, SIGNAL( triggered( ) ),
                           parent, SLOT( CopyActionTriggered( ) ) );

      QObject::disconnect( pasteAction, SIGNAL( triggered( ) ),
                           parent, SLOT( PasteActionTriggered( ) ) );

      delete cutAction;
      delete copyAction;
      delete pasteAction;
    }

    void AddActionsTo( QWidget* container )
    {
      container->addAction( cutAction );
      container->addAction( copyAction );
      container->addAction( pasteAction );
    }
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

  public:
    MainWindow(QWidget *parent = 0)
      : QMainWindow( parent ),
        ui( new Ui::MainWindow )
        editionSet( EditionSet< MainWindow >( this ) )
    {
      ui->setupUi(this);

      editionSet.AddActionsTo( ui->mainToolBar );
      editionSet.AddActionsTo( ui->menuBar );
    }

  private:

    EditionSet< MainWindow > editionSet;
};
like image 20
eferion Avatar answered Nov 15 '22 22:11

eferion


I strongly recommend 1.1 or 1.2 as the least grievous solution.

1.3 greatly extends the public interface of the window class in a very tedious way.

1.4 off-loads the problem of global uniqueness to a new "name space"--the one where the repository of commands lives.

2.0 exposes a lot of private information, it seems the worst to me.

BTW, if you have not read up on the Command pattern, I recommend it.

like image 24
Dwayne Towell Avatar answered Nov 15 '22 20:11

Dwayne Towell