3

I'm trying to write a user interface in Maya, and it's getting incredibly confusing with multiple levels of parents and no indents. The basic code (without any functionality) is currently around 400 lines and it's taking a while to find the bits I need.

For example take this following code without comments:

#Earlier user interface

py.rowColumnLayout( numberOfColumns = 5 )
py.text( label="", width = 1 )
py.text( label="Column 1", enable = False, width = 250 )
py.text( label="", width = 1 )
py.text( label="Column 2" enable = False, width = 250 )
py.text( label="", width = 1 )

py.text( label="" )
py.rowColumnLayout( numberOfColumns = 4 )
py.text( label="   Input data:", align="left" )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.textField( text = "Text here" )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.setParent( ".." )

py.text( label="" )
py.rowColumnLayout( numberOfColumns = 4 )
py.rowColumnLayout( numberOfColumns = 5 )
py.radioButton( label = "Read file from path", width = 100 )
py.text( label="" )
py.button( label = "Browse" )
py.text( label="" )
py.button( label = "Validate" )
py.setParent( ".." )
py.text( label="" )
py.text( label="" )
py.text( label="" )
py.setParent( ".." )
py.setParent( ".." )

However, this is how it'd look with indents

py.rowColumnLayout( numberOfColumns = 5 )
    py.text( label="", width = 1 )
    py.text( label="Column 1", enable = False, width = 250 )
    py.text( label="", width = 1 )
    py.text( label="Column 2" enable = False, width = 250 )
    py.text( label="", width = 1 )

    py.text( label="" )
    py.rowColumnLayout( numberOfColumns = 4 )
        py.text( label="   Input data:", align="left" )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.textField( text = "Text here" )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.setParent( ".." )

    py.text( label="" )
    py.rowColumnLayout( numberOfColumns = 4 )
        py.rowColumnLayout( numberOfColumns = 5 )
            py.radioButton( label = "Read file from path", width = 100 )
            py.text( label="" )
            py.button( label = "Browse" )
            py.text( label="" )
            py.button( label = "Validate" )
            py.setParent( ".." )
        py.text( label="" )
        py.text( label="" )
        py.text( label="" )
        py.setParent( ".." )
    py.setParent( ".." )

Is there any way at all I'd be able to write it with indents but make it ignore them all on execution? I saw the question asking if you can write python without indents, but I'm kind of needing the opposite.

Note: The output values of some of the py.* functions will also need to be assigned to variables, just haven't been yet as the layout needs to get sorted first.

kartikg3
  • 2,590
  • 1
  • 16
  • 23
Peter
  • 3,186
  • 3
  • 26
  • 59
  • 7
    Use functions. And if you want more separation, use more functions. And modules. And classes. And all that stuff. – matsjoyce Jan 19 '15 at 18:31
  • Why do you want that indentation? Even if the second is legal, first code looks better.. – Maroun Jan 19 '15 at 18:33
  • Consider writing all of this in something other than Python. I'm sure there are tools out there which let you design GUIs in some other format and use those GUIs in Python programs. – Tanner Swett Jan 19 '15 at 18:34
  • 1
    If you really want indentation, you can just add if True blocks. Functions, etc. are a much cleaner way to go in the long run, though. – AMacK Jan 19 '15 at 18:35
  • 1
    I suggest using something like [Glade](https://glade.gnome.org/), or the equivalent for whatever GUI tool kit you are using. This looks more like data than like code, so just make it data. – Sven Marnach Jan 19 '15 at 18:36
  • Thanks for the replies guys. I'm wanting the indentation as it's really hard to figure out where you are once the code gets longer, and I'm writing it for Maya so ideally need to keep it to the inbuilt UI code. It sounds as if the 'if True' method would be best for now, eventually I'll move to functions but it's a lot easier having all the code together for easy editing :) – Peter Jan 19 '15 at 19:17
  • What's the `if True` method? Try my method below. It parse the script to remove indentation and executes a new file without editing your current code. – Malik Brahimi Jan 19 '15 at 19:21
  • The one AMacK suggested, by putting `if True:` above something you can indent it without breaking anything. Your way is good but does require an extra file :P – Peter Jan 19 '15 at 19:28
  • A great question. Especially relevant for Maya developers. – kartikg3 Jan 19 '15 at 22:00
  • @Peter Try my solution below. It's something I use from time to time while developing UI in Maya. – kartikg3 Jan 19 '15 at 22:23
  • As a side note, I would recommend you to dive into PyMel, PySide or PyQt to create your UI. I'm currently learning PyQt and the development of UI is way faster with it. This also gives you more widgets that you can customize. You can apply easily an equivalent of CSS to your UI (I don't like maya colors) and so on. – DrHaze Jan 20 '15 at 09:15
  • Yeah your method seems great, I got a question I'll post as a comment :) DrHaze I'm currently using pymel haha (hence the `py.` at the start of everything, I use py instead of pm), I did once try PyQt but it requires the separate install which I'd like to avoid, and the lack of tutorials made it a bit tricky to figure out – Peter Jan 20 '15 at 11:33

7 Answers7

7

This is an excellent use case that technical artists like us face everyday while building UI in Maya.

For PyMEL based UI:

This comes built into PyMEL. You don't have to create a context manager. The layout commands themselves are context managers. You only have to add a with keyword before every layout command call like so:

# Do this when using PyMEL for your UI code
import pymel.core as pm

# ...

with pm.rowColumnLayout( numberOfColumns = 5 ):
    pm.text( label="", width = 1 )
    pm.text( label="Column 1", enable = False, width = 250 )
    pm.text( label="", width = 1 )
    pm.text( label="Column 2", enable = False, width = 250 )
    pm.text( label="", width = 1 )

    pm.text( label="" )

    with pm.rowColumnLayout( numberOfColumns = 4 ):
        pm.text( label="   Input data:", align="left" )
        pm.text( label="" )
        pm.text( label="" )
        pm.text( label="" )
        pm.textField( text = "Text here" )
        pm.text( label="" )
        pm.text( label="" )
        pm.text( label="" )        

    pm.text( label="" )
    with pm.rowColumnLayout( numberOfColumns = 4 ):
        with pm.rowColumnLayout( numberOfColumns = 5 ):
            pm.radioButton( label = "Read file from path", width = 100 )
            pm.text( label="" )
            pm.button( label = "Browse" )
            pm.text( label="" )
            pm.button( label = "Validate" )

        pm.text( label="" )
        pm.text( label="" )
        pm.text( label="" )

For maya.cmds based UI:

One quick solution would be to make a dummy context manager. You could do something like this

# Do this when using Maya's cmds for your UI code
import maya.cmds as cmds

# ...

from contextlib import contextmanager
@contextmanager
def neat_indent():
    # OPTIONAL: This is also an opportunity to do something before the block of code runs!
    try:
        # During this is where your indented block will execute
        # Leave it empty
        yield
    finally:
        # OPTIONAL: This is where you can write code that executes AFTER your indented block executes.
        pass

This way your code doesn't have to change too much. Just add your context manager function with the with keyword in the beginning of every intended indent!

cmds.rowColumnLayout( numberOfColumns = 5 )
with neat_indent():
    cmds.text( label="", width = 1 )
    cmds.text( label="Column 1", enable = False, width = 250 )
    cmds.text( label="", width = 1 )
    cmds.text( label="Column 2", enable = False, width = 250 )
    cmds.text( label="", width = 1 )

    cmds.text( label="" )

    cmds.rowColumnLayout( numberOfColumns = 4 )
    with neat_indent():
        cmds.text( label="   Input data:", align="left" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.textField( text = "Text here" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.setParent( ".." )

    cmds.text( label="" )
    cmds.rowColumnLayout( numberOfColumns = 4 )
    with neat_indent():
        cmds.rowColumnLayout( numberOfColumns = 5 )
        with neat_indent():
            cmds.radioButton( label = "Read file from path", width = 100 )
            cmds.text( label="" )
            cmds.button( label = "Browse" )
            cmds.text( label="" )
            cmds.button( label = "Validate" )
            cmds.setParent( ".." )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.setParent( ".." )
    cmds.setParent( ".." )

The context manager we created, neat_indent(), also gives you the opportunity to write code that wraps your indent blocks. A practical example here, is that in the end of every indent you find yourself writing py.setParent(".."). You can just throw this into the finally section of the context manager:

from contextlib import contextmanager
@contextmanager
def neat_indent(parent=None):
    # OPTIONAL: This is also an opportunity to do something before the block of code runs!
    try:
        # During this is where your indented block will execute
        # Leave it empty
        yield
    finally:
        # OPTIONAL: This is where you can write code that executes AFTER your indented block executes.
        if parent:
            cmds.setParent(parent)

Your code will make more sense now:

cmds.rowColumnLayout( numberOfColumns = 5 )
with neat_indent(".."):
    cmds.text( label="", width = 1 )
    cmds.text( label="Column 1", enable = False, width = 250 )
    cmds.text( label="", width = 1 )
    cmds.text( label="Column 2", enable = False, width = 250 )
    cmds.text( label="", width = 1 )

    cmds.text( label="" )

    cmds.rowColumnLayout( numberOfColumns = 4 )
    with neat_indent(".."):
        cmds.text( label="   Input data:", align="left" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.textField( text = "Text here" )
        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )        

    cmds.text( label="" )
    cmds.rowColumnLayout( numberOfColumns = 4 )
    with neat_indent(".."):
        cmds.rowColumnLayout( numberOfColumns = 5 )
        with neat_indent(".."):
            cmds.radioButton( label = "Read file from path", width = 100 )
            cmds.text( label="" )
            cmds.button( label = "Browse" )
            cmds.text( label="" )
            cmds.button( label = "Validate" )

        cmds.text( label="" )
        cmds.text( label="" )
        cmds.text( label="" )

Context managers are powerful. In this post I have used the contextmanager decorator from contextlib standard library module. You can read about this technique here. About with in general here.

Also, for this very purpose (one of the purposes) of making UI development in Maya cleaner and more Pythonic @theodox created the mGui module. Check it out.

Peter
  • 3,186
  • 3
  • 26
  • 59
kartikg3
  • 2,590
  • 1
  • 16
  • 23
  • 1
    This was a highly educational post for me and IMO the best possible solution to the OP's problem should indeed involve `contextmanager` objects in some way. – jez Jan 19 '15 at 22:46
  • Yeah thanks a lot for this haha, it does look like it's the best way, got 2 questions though (sorry) as this method is pretty new to me. The top block of code seems to work better on first glance, and as you said parts of the code are already contextmanagers, what's the advantage of a custom contextmanager in this case? The first class defined on your `with` link also seems fine aside from the error handling, is it simply just neater to use a contextmanager instead of that, or is there any other bonus? – Peter Jan 20 '15 at 11:55
  • Ah hit the 5 minute edit limit, I was about to reword the first question - is the only advantage of custom contextmanagers being that you can add extra code if needed, or is there anything else I'm missing too? – Peter Jan 20 '15 at 12:03
  • 1
    The first code block applies if you are using PyMEL. PyMEL already has the `with` construct built in for layouts. The second code block is an alternative solution for when you are using `maya.cmds` to make the UI. `maya.cmds` is less Pythonic, i.e. does not have the `with` construct built in. So we need to work around that by making our own context manager. Context managers are meant to be used and facilitate `with` constructs. – kartikg3 Jan 20 '15 at 12:03
  • 1
    If you are using PyMEL, then you only need to see the first code block and ignore the rest. The rest is only extra information if you are interested, mainly for the maya.cmds route. I hope I answered your questions. – kartikg3 Jan 20 '15 at 12:05
  • I have added some comments to the code blocks to make this clearer. – kartikg3 Jan 20 '15 at 12:07
  • In the case of PyMEL, you don't have to use your own custom context managers. You CAN if you want to. The advantage of using your custom context managers is that you can add your own DO-THIS-BEFORE and DO-THIS-AFTER code if needed. Check out those links that I linked in the answer. They would show some real world advantages of using your own context managers. But again, if you are using PyMEL, you dont have to worry about doing that, for this use-case. – kartikg3 Jan 20 '15 at 12:10
  • So far, I have found context managers very useful in nested UI layout code like this, for stuff that needs to be initiated and closed properly, like files, threads, databases, connections etc. – kartikg3 Jan 20 '15 at 12:13
  • Ohh right thanks again, I just got a bit confused when you'd said with `maya.cmds` without changing any of the prefixes haha. I know about using `while` when reading files, so I'll keep my eye out for other situations where it'd come in useful too :) – Peter Jan 20 '15 at 16:27
  • @Peter Thanks for the edit. Yes I always use pm or pmc for pymel.core and cmds for maya.cmds. I kept it py to match your code to make it easier for you to test. – kartikg3 Jan 20 '15 at 19:52
1

You could preprocess the lines of code before executing them as illustrated:

mycode = """\
    print "something"
        print "something else"
      print 42
    """

exec('\n'.join(line.lstrip() for line in mycode.splitlines()))

Output:

something
something else
42

It could even be made into a "one-liner":

exec('\n'.join(line.lstrip() for line in """\
    print "something"
        print "something else"
      print 42
    """.splitlines()))

You could keep the code in a separate file (which would enable it to be syntax-lighted by your editor) by doing it this way:

File mycode.py:

print "something"
    print "something else"
  var = 42

Separate-file version:

with open('mycode.py') as code:
    exec(''.join(line.lstrip() for line in code))
print 'var:', var

Output:

something
something else
var: 42

Caveat: I should point out that these all remove all indenting from each line which would mess-up any multiline Python code (like an if/else) encountered — which might limit its usefulness depending on exactly what you're doing.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Ah thanks, that's a really good idea that hadn't crossed my mind. I may try this and the 'if True' method, this would seem neater but the lack of colour coding could possibly make some other parts hard to read – Peter Jan 19 '15 at 19:19
1

@Kartik's answer covers the bases nicely. I'd point out that you can clean up the layout code a little more by allowing the context manager to declare layouts (rowLayout, columnLayout etc) in-line, which makes it even easier:

class uiCtx(object):
   '''
   quickie layouthelper: automatically setParents after a layout is finished
   '''
   def __init__(self, uiClass, *args, **kwargs):
        self.Control = uiClass(*args, **kwargs)

    def __enter__(self):
        return self

    def __exit__(self, tb, val, traceback):
        cmds.setParent("..")

    def __repr__(self):
        return self.Control

will call a maya.cmds layout function when it's encountered, and then close up the parenting at the end of an indented block, so you can do the layout call as you go as in this snippet

    with uiCtx(cmds.rowLayout, **layout_options) as widget:
        self.Toggle = cmds.checkBox('', v = self.Module.enabled, cc = self._state_changed)
        with uiCtx(cmds.columnLayout, w=self.COLUMNS[1], cal='left') as details:
            cmds.text(l = self.ModuleKey, fn = "boldLabelFont")
            cmds.text(l = self.Module.path, fn = "smallObliqueLabelFont")
        cmds.button("edit",  c=self._edit)
        cmds.button("show", c=self._show)
    return widget

Adding the __repr__ to the uiCtx lets you treat it as if it returned a string the way ordinary maya layout commands do, so in that example 'widget' can be queried in the usual way.

The whole thing is up on GitHub to see it in context. As Kartik also pointed out there's a more elaborate declarative UI option in the form of mGui, which looks like this in practice:

with gui.Window('window', title = 'fred') as example_window:
    with BindingContext() as bind_ctx:
        with VerticalForm('main') as main:
            Text(None, label = "The following items don't have vertex colors")
            lists.VerticalList('lister' ).Collection < bind() < bound  
            with HorizontalStretchForm('buttons'):
                Button('refresh', l='Refresh')
                Button('close', l='Close')

# show the window
example_window.show()

There's some more maya layout related info here

theodox
  • 12,028
  • 3
  • 23
  • 36
1

I've been using this for years, it works brilliantly:

if True:
    # Indented code.
    print("This entire code block is indented.")
Contango
  • 76,540
  • 58
  • 260
  • 305
0

Create another program to parse and execute your script.py:

parse.py

text = open('script.py').readlines()
text = [i.strip() for i in text]

edit = open('new.py', 'w')
for line in text:
    edit.write(line + '\n')
edit.close()

execfile('new.py')

This file creates a modified new.py which will be executed.

Malik Brahimi
  • 16,341
  • 7
  • 39
  • 70
0

I can think of two possibilities that allow you to write code directly without having to parse it from text (syntax coloring intact, no temporary files).

The first would be to prefix a truism to the beginning of each line, like if True: as suggested above or, equivalently, maybe something like this:

# ...

if 'level 1':  a = py.text( label="" )
if 'level 2':      b = py.rowColumnLayout( numberOfColumns = 4 )
if 'level 3':          c = py.rowColumnLayout( numberOfColumns = 5 )
if 'level 4':              d = py.radioButton( label = "Read file from path", width = 100 )

# ...

Provided you don't use a string that evaluates to False (like '') you can use these strings as comments, to help you remember which part of the GUI you're constructing on any given line (padded with the appropriate number of spaces to ensure things still line up, of course).

The second idea would be to create a list of containers containing callables and their arguments, format that list however you like, and execute each one at the end:

objects = {}
commands = [

    # ...

    dict( name = 'spam', func = py.text, label="" ),
    dict( name = 'ham', func = py.rowColumnLayout, numberOfColumns = 4 ),
        dict( name = 'eggs', func = py.rowColumnLayout, numberOfColumns = 5 ),
            dict( name = 'beans', func = py.radioButton, label = "Read file from path", width = 100 ),

    # ...
]

for d in commands:
    objects[ d.pop( 'name' ) ] = d.pop( 'func' )( **d )
jez
  • 14,867
  • 5
  • 37
  • 64
  • Very confusing and hard to understand. Try my approach. – Malik Brahimi Jan 19 '15 at 18:53
  • Obviously, if like @Malik you find the approach confusing, then you shouldn't use it. Particularly if you find it confusing *and* hard to understand. – jez Jan 19 '15 at 19:15
  • Thanks, it's an interesting answer haha. The downside seems to be you can't then easily assign the function outputs to variables though, which I will need to do, just haven't yet due to only blocking out the UI without anything linked to it :) – Peter Jan 19 '15 at 19:25
  • @Peter, ok, then let's get the best of both worlds with `if 'comment': variable = function( arguments)` (see edited answer) – jez Jan 19 '15 at 19:28
  • @Peter assignment can even be done in the second form (again, see edit). But it is a bit more of a stretch, admittedly. – jez Jan 19 '15 at 19:39
  • Ah thanks again, I wasn't aware you could do that with if statements so it's useful to see :) I think I may stick with the `if True` method though, just to avoid having to write it out for every line. Btw, I'll decide on the best answer later, I did vote yours up but obviously it's just gone back up to zero ;p – Peter Jan 19 '15 at 19:47
0

Use a declarative approach:

interface = [
  [py.rowColumnLayout, [], dict(numberOfColumns=5)],
     [py.text, [], dict(label="", width=1)],
     [py.text, [], dict(label="Column 1", enable=False, width=250)],
     ...
     [py.setParent, [".."], {}],
]    

for callable, args, kwargs in interface:
    callable(*args, **kwargs)

Inside () or [] indentation does not matter so you are free to organize the lines as you see fit.

Paulo Scardine
  • 73,447
  • 11
  • 124
  • 153