Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Supported by

[solved] plug-in GUI documentation

edited April 2013 in OpenSesame

Hi Sebastiaan,

Might it be a good idea to add a page to the documentation site on GUI programming for plug-ins? To me, creating a plugin is all fun 'n games until the GUI part comes along, where I don't really have a clear understanding of what exactly is going on. Just copying GUI functionality from other plugins and built-in items (and nosing around in qtplugin.py) does the job as well, but the learning curve is steeper and it's still quite hard to fully understand what is going on without previous knowledge of PyQt4. Of course, one could simple suggest that users should learn some basic PyQt4 then, but I do not think it is realistic to ask this from all users (even though they might be able to create the prepare and run phases of a plugin).

I suspect this could be the case for a lot of other people as well, making it unnecessarily hard for user to create their own plugins. Is it possible that some kind of documentation page is added with most of the GUI functions? (e.g. add_line_edit_control, add_checkbox_control, and stuff like setDisabled as well)

Best,

Edwin

Comments

  • edited March 2013

    To be a bit more specific: I'm currently trying to use the imgpath_filepool_control function, but I have no clue what the 'click_func' argument should be. I've tried the following:

    def browse_img(self):
    
        """Present a file dialog to browse for the image"""
        
        s = pool_widget.select_from_pool(self.experiment.main_window)
        if unicode(s) == "":
            return          
        self.imgpath_filepool_control.setText(s)
        self.apply_edit_changes()
    

    and

    self.imgpath_filepool_control = self.add_filepool_control("imgpath", "Image file", self.img_path(), tooltip = "The image that you want to present")
    

    But this function gets called immediately upon adding my plugin to a sequence, in stead of upon clicking the 'Browse' button...

  • edited March 2013

    Yes, adding documentation for creating plug-ins is certainly on my TODO list. For now, the best references are other plug-ins and the qtplugin source code. All the functions that are called add_*() can be used to add controls to your plug-in.

    In your specific case, I think your're pretty much there. The problem is that you are calling the function and passing the return value as a parameter, rather than passing the function itself as a parameter:

    self.imgpath_filepool_control = self.add_filepool_control("imgpath", "Image file", self.browse_img, tooltip="The image that you want to present")
    

    Does this work for you?

    Cheers!

  • edited March 2013

    Thanks a bunch, you're right! That was a bit of a silly mistake, my bad.

    The code still doesn't function as it is now, but this is due to the fact that add_filepool_control doesn't return anything (the same goes for add_slider_control and add_editor_control, btw). This is a problem and it could be solved in a number of ways, which are presented below (I've added the add_filepool_control code to illustrate)

    add_filepool_control (from libqtopensesame.items.qtplugin)

        def add_filepool_control(self, var, label, click_func, tooltip=None, \
            default=None):
    
            """
            Adds a control to select a file from the file pool. This is not fully
            automated, and a function has to specified that is called when a file
            is selected.
    
            Arguments:
            var -- name of the associated variable
            label -- a label
            click_func -- a function to be called when a file is selected
    
            Keyword arguments:
            tooltip -- a tooltip (default=None)
            default -- a default value (default=None)
            """
    
            edit = QtGui.QLineEdit()        
            edit.setText(self.unistr(self.get_check(var, u'', _eval=False)))
            edit.editingFinished.connect(self.apply_edit_changes)
            if var != None:
                self.auto_line_edit[var] = edit
            button = QtGui.QPushButton(self.experiment.icon(u'browse'), u'Browse')
            button.setIconSize(QtCore.QSize(16, 16))
            button.clicked.connect(click_func)
            hbox = QtGui.QHBoxLayout()
            hbox.setMargin(0)
            hbox.addWidget(edit)
            hbox.addWidget(button)
            widget = QtGui.QWidget()
            widget.setLayout(hbox)
            self.add_control(label, widget, tooltip)
    

    I assume it to be used as follows (where browse_img is the same function as in my earlier post):

    self.my_control = self.add_filepool_control("varname", "Var label", browse_img, tooltip="Tooltip for var")
    

    1. Adding return widget

    This would allow for use of setDisabled: self.my_control.setDisabled(self.get("my_var") == 'no'). However, setText will not work, as self.my_control is now a QWidget object, which does not have a setText method (and, as far as I could tell from the Qt online documentation, no method with a similar function). This is a problem, since it would make sense to be able to set a text in the QLineEdit part of the widget.

    2. Adding return edit

    This would allow for both the use of setDisabled and setText, but only for the QLineEdit part of the widget. The rest would be inaccessible.

    3. Adding return edit, button or return edit, button, widget

    In this case, every part of the widget would be accessible, but now we have two or three returned values, which would mess up the uniformity of the add_* methods of qtplugin.

    I think I prefer option 2, but you're way more experienced with this sort of thing, so I'm curious what you think.

  • edited 2:11AM

    The code still doesn't function as it is now, but this is due to the fact that add_filepool_control doesn't return anything (the same goes for add_slider_control and add_editor_control, btw).

    Right, this isn't consistent. All the add_(...) functions should return a QWidget. And I agree, returning edit makes the most sense.

    Also, while we're at it, it's a bit awkward that you need to implement your own function to show the file pool dialog, and process the selection. Ideally, this would be fully automated, so that you just need to say self.add_filepool_control(...), and don't need to worry about anything else (unless you want to do more advanced things, such as graying out controls under certain conditions).

    Are you planning to work on this?

  • edited 2:11AM

    Guess I am now :p

  • edited 2:11AM

    It turned out to be surprisingly simple, so I suppose it's fixed now (did a commit + pull request on github). See here for the changed files.

  • edited 2:11AM

    Thanks! I merged it into the Playground. I was also playing around with a more user friendly API for creating plug-ins. Not instead of the current way, but an extension that allows you to define the controls using a json file.

    The idea is that you have a configuration file like this, in which you can specify all kinds of attributes for your plug-in, and also the controls, like this:

    {
        "version"   : 1.0,
        "author"    : "Sebastiaan Mathôt",
        "url"       : "http://osdoc.cogsci.nl/",
        "date"      : "2013",
        "controls" : [
            {
                "type"      : "color_edit",         
                "var"       : "color",
                "label"     : "Color",
                "tooltip"   : "What color should the fixation dot have?"
            },
            {
                "type"      : "spinbox",
                "var"       : "duration",
                "label"     : "Duration",
                "min_val"   : 0,
                "max_val"   : 100000,
                "suffix"    : " ms",
                "tooltip"   : "For how long should the fixation dot be shown?"
            }
        ]
    }
    

    The plug-in developer doesn't have to bother with the GUI at all, and only defines the runtime part of the plug-in.

    What do you think?

  • edited 2:11AM

    Look a lot easier! One detail, though: I think the "controls" list should be a dict, where the keys are the names of the widgets and these should be accessible somehow (i.e. as a dict of widget object in the plugin's script). Then, a user would be able to use the names of these names to manipulate single widgets (e.g. disabling them). But this is something that shouldn't be too hard with the above setup, so I am definitely in favour!

  • edited March 2013

    I just committed the new plug-in API, and added a page to the documentation:

    One detail, though: I think the "controls" list should be a dict, where the keys are the names of the widgets and these should be accessible somehow (i.e. as a dict of widget object in the plugin's script). Then, a user would be able to use the names of these names to manipulate single widgets (e.g. disabling them).

    Good point. You can now specify a name for each control, which will be used to attach the control widget as a property to the plug-in. So say that you have defined this control ...

    ...
        "controls" : [
            {
                "type"      : "checkbox",
                "var"       : "_checkbox",
                "label"     : "Example checkbox",
                "name"      : "checkbox_widget",
            },
    ...
    

    ... then the resulting QCheckBox will be accessible as self.checkbox_widget in the plug-in.

    Hopefully this new implementation will make it more attractive (i.e. easier) for people to develop their own plug-ins. Let me know what you think, and feel free to polish the documentation, etc.!

  • edited 2:11AM

    I kinda forgot about this until now, but that new documentation is very good! Especially the auto_example is a very nice addition. I'll definitively use it as a template for every new project.

    Small question: I take it that the answer is yes, but older plugins will still be compatible, right?

    Cheers!

  • edited 2:11AM

    Small question: I take it that the answer is yes, but older plugins will still be compatible, right?

    Yep! It's just a layer on top of the old system really.

Sign In or Register to comment.