Application-wide shortcuts with wxWidgets (on Windows)

Another programming note, this time about wxWidgets and application wide shortcuts. Before going further, I want to state that I’m using the MSW version of wxWidgets. So maybe Gtk or other implementations don’t suffer from the same problem at all. In any case, keep in mind that I’m talking about the MSW implementation of wxWidgets :)

In wx, you can have “window wide” shortcuts through accelerator tables or system wide shortcuts through RegisterHotKey. Problems with accelerator tables is this one: you have a main window with such an accelerator table. Then you add a tool frame, make it floatable. Then the shortcuts stop working if your tool frame is floating and has the focus. It works fine if the tool frame is docked though … And system wide shortcuts should not be used for obvious reasons.

On top of that, there is a big flaw in the way events get processed in wxWidgets. Let’s say you want to add a shortcut on the keyboard key ‘t’ (for translation, for instance) Hell begins. Why ? Because now you can’t enter the letter ‘t’ in any text control of your application: the accelerator table catches it before the text control and process it as a shortcut …

Some might say “don’t you ‘t’, use ‘alt+t’. Yeah right: 3DS Max uses ‘w’ for translation, and you can still write a ‘w’ in the script editor window, obviously… And I don’t want to make a list of all the software that use single letters with no modifier as shortcuts, and still allow using those letter in scripting window, that would be too long. Just to name a few: 3DS Max, Maya, Blender, Photoshop, Visual Studio… well, basically every professional software out there.

Anyway, there is no way to have an application wide shortcut that doesn’t screw text controls, and work also when using floating panels, so enough writing, let’s add such a system !

The first thing is to add a shortcut system to your application:

class Application : public wxApp
{
public:

    //! application init
    virtual bool OnInit();

    //! application uninit
    virtual int OnExit();

    //! register an application wide shortcut
    template< typename T >
    bool RegisterShortcut(int modifier,
                          int key,
                          int id,
                          const T& fctor);

private:

    //! this is where we will filter events before wx can process them
    void OnCharHook(wxKeyEvent& evt);

    //! callback type for application wide events
    typedef boost::function CallbackType;

    //! a shortcut
    struct Shortcut
    {
        //! ctor
        Shortcut(int m, int k, int id, const CallbackType& c);
        //! modifier
        int Modifier;
        //! key code
        int KeyCode;
        //! the wx id
        int ID;
        //! callback
        CallbackType Callback;
        //! equality operator
        bool operator == (const SShortcut& o);
        //! equality operator with an accelerator entry
        bool operator == (const wxAcceleratorEntry& value);

    };

    //! the registered application wide shortcuts
    std::vector m_Shortcuts;
};

template
bool
Application::RegisterShortcut(int modifier,
                              int key,
                              int id,
                              const T& fctor)
{
    // check if the shortcut is not already registered
    Shortcut shortcut(modifier, key, id, fctor);
    if (std::find(m_Shortcuts.begin(), m_Shortcuts.end(), shortcut) != m_Shortcuts.end())
    {
        return false;
    }
    m_Shortcuts.push_back(shortcut);
    return true;
}

inline
Application::Shortcut::SShortcut(int m, int k, int id, const SCallback& c)
    : Modifier(m)
    , KeyCode(k)
    , ID(id)
    , Callback(c)
{
}

inline
bool
Application::Shortcut::operator == (const Shortcut& o)
{
    return o.Modifier == Modifier && o.KeyCode == KeyCode;
}

inline
bool
Application::Shortcut::operator == (const wxAcceleratorEntry& value)
{
    return KeyCode == tolower(value.GetKeyCode()) && Modifier == value.GetFlags();
}

The idea is pretty simple: I created a SShortcut structure which hold a key code, a modifier, a wx id and a callback object. Thanks to boost::function and boost::bind and the templated function to register a shortcut, you can set this to almost anything: a free function, a method of an object, even a lambda if your compiler supports it (well, at least in Visual Studio 2010, it works)

Now the interesting part: how we handle those to avoid conflicts with text controls, and how do we manage to catch those shortcuts even if the focused window is a floating frame ? Like this:

bool
Application::OnInit()
{
    // bind on the char hook event. This event is sent before any
    // other, so we can catch it to intercept our shortcuts
    Bind(wxEVT_CHAR_HOOK, &Application::onCharHook, this);
}

bool
Application::OnExit()
{
    Unbind(wxEVT_CHAR_HOOK, &Application::onCharHook, this);
}

void
Application::OnCharHook(wxKeyEvent& evt)
{
    // dummy shortcut used for search
    Shortcut s(evt.GetModifiers(),
               evt.GetKeyCode(),
               -1,
               CallbackType());
    std::vector::iterator shortcut
        = std::find(m_Shortcuts.begin(), m_Shortcuts.end(), s);

    // this is where we do the job: if we found a shortcut, we need
    // to make sure of a few things. First, that the application has
    // the focus. Then, that the focused control is not a text editing
    // one.
    wxWindow * focused = wxWindow::FindFocus();
     if(shortcut != m_Shortcuts.end()
       && focused
       && !dynamic_cast< wxTextCtrl * >(focused)
       && !dynamic_cast< wxStyledTextCtrl * >(focused))
    {
        // ok, we're clear, we can execute the callback of the shortcut !
        wxCommandEvent e;
        e.SetId(shortcut->ID);
        shortcut->callback(e);
    }
    else
    {
        // let go of the event. If a text control has the focus, let
        // it handle it, etc.
        evt.Skip();
    }
}

So now, imagine that you have an application with a floating panel, in this floating panel you can just do that:

Application * app = static_cast< Application * >(wxApp::GetInstance());
app->RegisterShortcut(wxACCEL_NORMAL,
                      (int)'T', 
                      AnID,
                      boost::bind(&PanelClass::OnTranslate, this, _1));

And here you have it. ‘t’ is now a shortcut (in my case for ‘translation’) This shortcut will work from anywhere in the application, even from a floating frame, and more importantly, will still let you type the letter ‘t’ in text controls.

Note: AnID is the event id which will be sent to the OnTranslate method of the PanelClass class.

Going Further

I tried to keep this article short, so I simplified a lot of things. One of those is how I do when I try to register a shortcut that is already registered by some other part of my application. Their are a lot of ways to take care of that: store the wxWindow pointer which registered the shortcut, then in the OnCharHook you can check if the focused window is one of those, you can also add a simple priority order to the registration. With those 2 you’ll be able to call the correct shortcut whatever the situation.

There is also 1 pitfall with this approach: when you don’t want to make a shortcut application wide, the simplest way is to use accelerators. But with this method, if a focused panel has an accelerator on the letter ‘t’, it’s still the application wide shortcut that will be called. To overcome this, you need to get the focused window, go up its hierarchy, and for each window, get the accelerator table and test it against you current event. This part is a bit hacky on Windows, unfortunately. The “cleanest” way I found was to simply store in the wxAcceleratorTable the array of entries used to create the table (I had to modify the sources of wx) Maybe I’ll write another post about that.

Add scripting to your tool with Lua

It’s been a while since I last wrote a programming article! I’m working on a big visual fx tool for my company, and I recently started to think that it would be nice to be able to script it. For example, create a small script to convert all texture paths of the effect from absolute to relative, etc.

And I found out that it’s really simple. I decided to use Lua which I (re)discovered through the wonderfull Premake project. So first thing first, go to Lua’s website and download it. Then just create a simple console application, and add all the .c files found in lua/src to your project, except lua.c and luac.c !

For my purpose, I just need to be able to execute some Lua, and access my tool’s functionalities from it. The first step is really simple:

// init lua
lua_State* lua = luaL_newstate();
// load the default libs (io, etc.)
luaL_openlibs(lua);

// then you can load and execute an existing script:
luaL_dofile(lua, "test.lua");
// or directly execute something:
luaL_dostring(lua, "io.write(\"Hello World!\")");

// and close
lua_close(lua);

Yes, it’s that simple.

Then the second part is allowing a Lua script to call a C function. This is also pretty simple:

// our function. All functions that will be called from Lua
// need to have the same signature.
int test(lua_State* lua)
{
    // since lua allow calling a function with variable arguments,
    // get the number of arguments it was called with.
    int num_args = lua_gettop(lua);

    // do some tests (we could also just print a message and return)
    // note: arguments are retrieved from the lua state object. Since the
    // very first value is the number of arguments, the arguments start
    // at index 1.
    assert(num_args >= 1 && num_args <= 2);
    assert(lua_isnumber(lua, 1));

    // get the first parameter
    int a = lua_tointeger(lua, 1);

    // check if we need to print
    if(num_args == 2 && lua_isboolean(lua, 2) && lua_toboolean(lua, 2))
    {
        std::cout << "result: " << a * a << std::endl;
    }

    // push as many return values that you want, and return the number of
    // values that you pushed. In our case it's only 1, but it could be
    // really powerful: need to return a list of selected items ? Just
    // push all of them and return the number of items.
    lua_pushnumber(lua, a * a);
    return 1;
}

// somewhere else, start Lua
lua_State* lua = luaL_newstate();
luaL_openlibs(lua);

// push our function on top of the lua state object
lua_pushcfunction(lua, testFunction);
// then make this function globally accessible through the name "test"
lua_setglobal(lua, "test");

// call our function. That's all :)
luaL_dostring(lua, "test(10, true)");
luaL_dostring(lua, "io.write(test(10))");

// close lua
lua_close(lua);

Now if you want to go further, here are a few links:

  • Reference: The Lua 5.2 reference manual.
  • LuaJIT: From what I understood, this is a replacement of the default Lua compiler/VM that is a lot faster and more efficient. I didn’t have time to investigate but it seems really good. Only drawback is that it’s Lua 5.1 only.

Juste du Parkour

Petite découverte Youtube ce midi, une vidéo uniquement Parkour (pas de flips, pas d’accros, etc.) un peu dans la veine de “Parkour, Literally” avec l’1consolable et le Vietnamien Volant (si vous ne connaissez pas, allez vite faire une petite recherche Youtube :)) Et comme les 2 autres, ce traceur est un frenchy ! Cororico ! (ça fait du bien de voir qu’on a encore du gros niveau en France, avec les Russes, Anglais et autres brutasses qui inondent la toile de vidéos de malade, je commençais un peu à me dire que les Français étaient complètement largués :))

ImageViewer version 0.8 : drag & drop et plein d’autres !

Au menu de cette nouvelle version, de gros changements. Le plus gros changement vient du code : j’ai implémenté un système qui permet de récupérer les évènements qui se passent sur le disque dur, et de pouvoir enregistrer divers “écouteurs” sur ces évènements, qui reçoivent donc ensuite de manière automatique des évènements du style “ce fichier a été effacé”, etc.

Le premier changement que ça a entrainé, c’est que toutes les fonctions de couper/copier/coller, drag&drop, suppression de fichier sont devenues beaucoup plus simples à gérer : je m’occupe simplement de faire ce qu’il faut d’un fichier, et ensuite l’interface sera automatiquement notifiée du changement par le système, et se mettra à jour automatiquement. Bref, c’est génial. Et donc, côté utilisateur, ça se traduit par les 2 grosses fonctionnalités suivantes :

  • Le nombre d’images d’un répertoire est maintenant toujours synchronisé. C’est à dire que si vous supprimez des images, ou en ajoutez, le nombre se mettra à jour automatiquement, et ce, même si vous supprimez une image depuis le Finder !
  • Sur le même principe, les onglets sont synchro avec le contenu du disque : si des images sont supprimées du disque, l’ImageViewer se met automatiquement à jour, sans perdre votre sélection (sauf si celle-ci a disparut du disque ^^)
  • copier/coller et couper/coller sont de retour, ainsi que le drag & drop : vous pouvez dragger des images depuis la vue par onglet vers un répertoire de votre choix.
  • Rien à voir avec le système dont j’ai parlé, mais grosse amélioration : on peut maintenant enregistrer l’ImageViewer comme application par défaut pour les images, et donc l’ouvrir directement en plein écran en double cliquant sur une image. Ca manquait, c’est réparé ! ^^

Il y a quelques autres fonctionnalités, mais j’ai la flemme de les détailler ici. Donc comme d’habitude, pour le téléchargement du code source et de l’exécutable, c’est ici que ça se passe : http://blog.pcitron.fr/tools/macosx-imageviewer/

ImageViewer version 0.7

La version 0.7 de mon petit image viewer pour Mac est en ligne ! Comme d’habitude, pour le téléchargement, c’est sur cette page : http://blog.pcitron.fr/tools/macosx-imageviewer/

Au programme de cette version, le panneau des préférences est de retour ! On peut paramétrer :

  • la taille des onglets
  • l’affichage ou non du nom des images sous leur onglet
  • la couleur de fond pour les onglets et l’image courante
  • l’intervalle entre 2 slides, lors du mode “slideshow”
  • est-ce que le slideshow boucle ou pas
  • effaçage permanent des images ou utilisation de la corbeille

Et donc, retour aussi du slideshow, qu’on peut lancer et arrêter avec la touche espace ou P, directement en fullscreen ou non. On peut aussi supprimer les images sélectionnées, que ce soit en fullscreen, ou bien plusieurs images directement dans le browser. D’ailleurs, ajout de fonctions + menu + shortcut pour sélectionner / désélectionner toutes les images du browser. Et pour finir, 2 petits bugfix.

Au programme de la version 0.8, je compte ajouter les fonctionnalités suivantes :

  • possibilité d’ouvrir une image depuis le finder directement avec l’ImageViewer
  • support du copier/couper/coller et du drag&drop pour déplacer les images