Hello and welcome to Scripting for Artists. My name is Sybren.
And in this episode, we'll visit a topic that many of you
have asked for, User Interface. In the previous videos,
we created our own operator, and then turned it into an Addon .
The basics of creating your own User Interface in Blender
is pretty much the same you create a class,
you add some properties to it, you add a function to it,
then you register the thing. So in this video, we'll look at
the following, will be finding existing UI code. So if you're wondering,
how did they do it? After this, you'll know where to find it.
We'll look at the ingredients of panel
before making our own monkey grid panel. What else?
Will be adding operator buttons to the panel, and then,
tweaking the layout a little bit. Then, we'll add seeing an object properties
to it, and we'll also look at conditional drawing.
Because sometimes there is no active object,
so you won't be able to draw properties from the active object.
And finally, we will look at adding your own operators
to existing menus in Blender. Before we make your own,
let's take a look at existing panels in Blender. In the 3D Viewport,
press N to show the panels. And then
let's look at the 3D cursor panel, right-click and choose Edit Source.
If you don't see the Edit Source option,
go to Preferences, Interface, and enable Developer Extras
in Python Tool Tips, and then it should show up.
Let's see what happens if we click it. Blender now tells us here in the corner,
but it's a bit far away because of my scaling.
Basically it tells us to go to a Text Editor, and there
we should find the file with the Source. But there is an easier way.
If you split this window, and make sure that
there is a Text Editor right here, you can click Edit Source,
and there, you have the file. It will be opened for you
and even scrolls to the right line.
Let's scroll through this and see what we can find. As we've seen,
with operators as a class, it has the same kind of naming.
It has the category in capitals and then PT for Panel Type
instead of Operator Type. And then in lower case, the rest of the identifier.
And here, you have the Panel class.
In this case, it's Panel, and not Operator. You would expect
"bpy.type.panel" here instead of just "panel".
We'll see why this is in a bit. Let's look for other commonalities.
First, "bl label", which is the panel's name, the User Interface,
and then we've a Category, a Region type, and a Space Type.
The Space Type and Region Type define
where in the User Interface it will show up. In this case, we're looking at
3D Viewport and Region Type is UI, which means it's right in the 3D Viewport
and not in a header or footer. Then the category is View
which is the name of the tab here. And then, we get to be a label
which is the name of the panel. Where operators had
an Execute function that performs the task of the operators,
here, we have a Draw function that draws the panel.
The drawing itself is done through "self.layout"
which is a UI Layout instance. And you can find this with Help,
and then Python API Reference, and search for UILayout,
one word. There, you'll find the class definition with everything
that is possible. In this video, I will show you the basics still,
and that will get you quite far already.
Now, let's go back to the class definition, because we saw
that panel here and this is a little bit different than we've seen before.
As I said, you would have expected something
like "bpy.type.panel" and not just "panel".
This is because, in this file, is imported in the different way.
Let's scroll to the top and here you can see from "bpy.type import"
Header, Menu, and Panel". Because of this,
you can just type "panel" instead of "bpy.type.panel",
and it would refer to the same thing. Personally, I would say,
be very careful with inputs like this. It can save you some typing
and that may sound like a good thing, but you will read code
way more often than you write it. I would always optimize
for code readability and understandability
rather than saving a few seconds of keystrokes.
The only time where I would use an import like this is where it is very clear
what's going on. In this case, it's Blender's User Interface definition,
and there is only one header, there's only one menu,
there's only one panel and they always come from "bpy.types".
So in this case, I would find it acceptable.
In general though, I would avoid this and always use the full name of the thing
so that once you see it, is clear where it come from.
Now that is out of the way. Let's just jump in
and make your own panel for the monkey grid. Let's take a look at our Addon code
where we left off last time. So here,
we have the "monkey grid.py",
with the monkey grid operator class and the Register
and Unregister functions. Now, to do this, we can add a monkey grid panel.
So, just like before, we create our class, we name it properly.
I kept the category "view 3D" because it's a View 3D panel anyway.
PT, I called it "monkey grid",
and we extend "bpy.type.panel". I just copy these values
from the 3D Cursor panel and into category "monkeys"
so we get our own little tab there. And I labeled it "grid".
And then the only thing that you really need to have is a Draw function.
For it to show up as a panel, it needs to have Draw function,
even if that function does nothing. And because
the function in Python always have to have something,
I put "pass" here, which is Python code for don't do anything.
Every code block after a ":" is indented, and every indent block needs
a little bit of text there in order to make sure that
Python understands that it's indented in the first place.
That's why they have the "pass" key word. Now, the only thing left
is to tell Blender that this class exists in the first place,
which means registering it.
We can copy the Register and Unregister Class function calls
and put in our new class, and that should be it. Let's go to Blender,
see what happens. Now, all we have to do is reload the script.
Go to System, Reload Script, and you'll see "monkeys"
pop up here. This route also gives you access to right-click
and then add to Quick Favorites.
You could just press Q, and Reload Scripts, will give you a fast way
to reload them. When we go to the Monkeys tab, we can see that
our Grid panel is here. It doesn't contain anything yet.
Of course, we don't draw anything there, but at least, it shows up.
Let's put a button there for the operator.
Let's go back to the code and here in "draw" "self.layout"
lets us draw things. And to draw operator,
you type "operator", and then you give the ID name of the operator
you want to have there. Let's copy paste that in.
Go to Blender, reload the script. There, you can see, we have
a monkey grip button in the grid panel, and it actually works.
To customize this button, there's a few possibilities.
Let's go take a look at them. First of all, we can change the text
that is shown on the button.
So let's call it the "default grid". Because pressing this button,
will just give us the default properties. Save the file,
go back to Blender, reload the scripts, and now it's called
"Default Grid". The nice thing is that, if we search for the "monkey grid",
it's still called "monkey grid" in the F3 panel,
because that is the name of the operator.
It is just that on this particular spot in the User Interface,
we've a different text on the button. Finally we can add an icon to it.
Then we have to know the name of that Icon, so you can either
look at existing code to cheat off of that or you can activate
the Icon Viewer Addon. It's bundled with Blender,
but not activated by default. And then, in the Python code,
you get this button called Icon Viewer. You can click on the icon
that you want. Say a monkey. This is copied onto the clipboard,
paste it there, save, go back to Blender, reload the script,
and there you have the icon. In the Tool Tip,
you can also see the description.
And you can see that it will actually call our operator
will all the default parameters. Let's tweak this and make some buttons
that use specific values for these parameters, rather than the default.
So again, we go back to the source.
Let's copy this call, and instead of just having this,
we can say "props = self.layout.operator......"
Let's change this to "Big Grid", "self.layout.operator" is a function
that returns something,
the thing it returns represented properties over an operator.
Here we can say "props.", let's take a look.
We have "count X" "count Y" and "size".
So here we can say "props.count X=10"
and "count y=10", and "size=0.8".
Let's save, go back to Blender, reload the script, we've a new button
"Big Grid", and when you hover over it, you can see that
it has different values for those parameters. Delete the monkey,
delete click on "Big Grid", automatically
it will take those values, of course. To top this off,
can also make a small grid and then we have a few more buttons to play around with.
So for the small grid,
let's set count X is 1, count Y is 1, and keep the size to the default.
So we just don't mention it here.
Save, back to Blender, Reload, then we have a small grid as well.
Which is, of course, just 1. But in the Redo panel,
we can extend this however we want. Now, you can also see,
the value of being able to set the text on that button,
because it's all the same operator, but with different behaviors.
Now, there's one more thing. I don't like how these buttons are spaced
so far apart from each other, because they're basically
doing the same thing. They're creating a monkey grid,
and they should really be grouped together. Fortunately, we can do this
by laying in multi column. I can hear you ask,
but Sybren, they are already in a column, what are you talking about?
Well, a function to create a column has some parameters
which we can use to influence the layout.
So let's take a look at the code again. Right now, each operator
is drawn by calling "self.layout.operator", and we can change it.
We can create a column first,
and then, instead of calling operator on "self.layout",
we call operator on "col",
save the code, go back to Blender, reload the script,
and you can see, they're already squeezed together a little bit more.
And we can go a little bit further. Add "align=true" here,
and then they are exactly as I wanted them to be,
nicely squeezed together. So beside the operators, you can also draw properties
on a panel, and this can be pretty much any property you want.
So you couldt use it to group together these properties
that are strewn throughout Blender's User Interface
but you use a lot. So, for your specific workflow,
you can add them to your own panel and make your life a bit easier.
So let's take two properties, one, a property of Cycles.
Let's say, the number of viewports samples in Cycles.
And the other, I want to have relative
to the currently selected object. So let's draw
the viewport visibility there. So one of the properties is this guy here,
the number of viewports samples, and the other property
we want to add is this one here, Disabling Viewport. Before we go to the code,
let's see what Blender already tells us.
You hover over the Viewport Property. There, it says at the bottom
of the Tool Tip "bpy data scenes.cycle.preview sample"
This is almost what we want.
But if we were to use this, we would hard code the name "scene" in our script.
So if we ever renamed the "scene", then our script is broken already.
And also when you use multiple scenes,
it also wouldn't work anymore. So, rather than using this literally
as it stands there, even though for this case,
it might work, but we'll use the context again, and just get
the current scene with "context.scene". So the property
we want to show is actually "context.scene.cycle.preview sample".
Now, let's go to the code. Let's add another column.
And then we call "col.prop" to render the property for us.
It takes a minimum of two parameters. The first one is
the thing that contains the property, and then it's the name
of the property. So the thing that contains the property is
"context.scene.cycle" everything up to the last point.
And then the name is "preview samples".
Let's save, go back to Blender, reload the script and there
you have it "preview samples 10". So we can change this,
you can see that here, it changes as well. Which indicates that
we have the right property. So here, you can also add
these texts and icon parameters. They work the same as with the operator "col".
So in order to keep this video a bit to the point,
I'll leave that for you to toy with. Now, let's take a look
at the second property we wanted to add,
which is a property of the active object. I want to be able to control
the viewport visibility. So that is this guy over here,
there it says "bpy.data object suzanne.pi viewport".
And again, as with the scene, we don't want to do this
specifically for Suzanne only. We want to do this
for the currently active object, which means "context.active object".
So the property we're going for is "context.active object.hide viewport"
Let's just copy what we had.
Change the thing that contains the property to "context.active object"
and change the property name to "hide viewport". And this should render
that property. Let's take a look. At Blender, reload,
and there is the property we wanted to have.
Now, let's see if it works.
There you go. It's working but it's bit weird,
because it disappears, and here it doesn't. What's going on?
It turns out when you hide an object, it's no longer active.
That means that "context.active object" becomes none instead of Suzanne.
None doesn't have a property hide viewport,
so our code is actually causing problems now. Let's take a look
at the terminal where I started Blender. And here, you see,
rather cryptic error message here at the bottom is
the terminal where I started Blender from.
It's complaining about the "monkey grid.py" file, line 82,
is, of course, the "col" we just added. And it says error
with argument one data, so apparently, this argument is called "data".
"Function.data" does not support a none assignment.
It's a bit vague description, but it doesn't match what I just said.
The active object is none and you cannot draw a property of nothing.
So we have to add a little bit of a guard around this code
to make it always work.
We know that "context.active object" can become none.
So let's check for it. And in that case,
let's just add a label.
Otherwise, we draw the property as we did before. Let's see
how this works now. Reload the script,
and there you have it. It says "no active object" instead of erroring out,
and all the code will keep working.
You can argue that this is a bit of a silly button,
because it only works once and you will be right. But I wanted to have this example
to show you how things can go wrong and how to resolve that.
It's also a demonstration of one thing that I find quite important,
and that is showing why something is not there.
So, rather than not drawing the property at all
and just silently skipping over it, now we draw the text, "no active object",
which indicates to whoever is using your code, why the option is gone.
I think that's quite important. So that's why
I wanted to have this example here anyway,
even though it's rather useless. You now know how to draw your own properties,
so it's up to you to make it useful for your particular workflow.
Before we end this video, I want to show you one more thing,
which is adding operators to menus.
Given what we've seen so far, this is rather easy.
This is why I kept it as the last step. Let's add our monkey grid operator
to the Mesh Add menu. So here, we have Add, Mesh, and would be nice
if underneath "monkey", there was a monkey grid.
This means, we have to add it to a menu and there's a trick for that,
I will show you. But first, we have to know which menu it is.
You can hover here over Add, and it shows "View 3D MT add",
and you probably already guessed MT stands for Menu Type.
Now, the thing is, submenus don't show this.
See, you don't know what the mesh is. But we can look around in Python console
and see if we can find it anyway. It is a Type,
so it is part of the "bpy.types.View3D_MT".
Let's see if we can find something.
There you go, "mesh add". So this is very likely to be our menu.
Calling it here will just give an error, so let's not do that.
But at least, we have the name now, "view 3D MT mesh add"
in "bpy.types". Let's copy this and then go to "code".
As you would have guessed it, we have to do something in the Register
and in the Unregister functions, and this time is quite different
than what we've seen so far. First, we have to make
our own function that will draw whatever we want to add to the menu.
So that looks something like this.
The name that you can think of yourself,
and again, "self, context". Now, let's put "pass" in there
to make Python happy. And this function basically will be called
after the Draw function of that panel. So our own panel
has this Draw function with "self" and "context",
this function will be called with the same "self" and the same "context".
So "self" will refer to that menu in our case.
Now, let's register and unregister this function. This is done
by taking that "bpy.types.view 3D......." and saying ".append(mesh_add_menu_draw)"
The opposite is "remove". So if unregister,
will "remove mesh add menu draw". Now, what to put in here?
That's the same as with a panel. We can just call "self.layout.operator",
hopefully the idea of the operator, and this should work.
Let's save and go back to Blender, reload the script, add mesh,
and there, we have "monkey grid". It doesn't have an icon yet.
But now, you know how to add one, so I'll leave that for you.
Now, you've seen how to add your own panel,
how to add your own operators to existing menus. But most importantly,
you've seen how to look at other code. So when you see something
that's interesting, when you're wondering how did they do that,
you can just right click Edit Source,
and most of the User Interface elements in Blender are written in Python,
and then, this just works. So this is it
for this episode of Scripting for Artists.
If you have any questions or remarks, please leave them in the comment below
and I will see you soon.