Saturday, September 12, 2009

Tweaker Tutorial: Templating Simple Panels

Something a little different and a little more core to Tweaker today: templates. It's an important feature that I've been meaning to get around to.

My personal UI has a number of different templates, but there are 3 that I'll demonstrate for this tutorial, and they essentially demonstrate the various qualities of templates in Tweaker.

First up is a template for your basic panel. The "base" panel in my UI has a near-black background with a dark gray, square border. It's not a "border" in the traditional sense, but just another panel, really, that's behind the background panel and sticks out over the edges to create the border. On to the code...

Virtual is a special attribute in Tweaker. There is no SetVirtual method in the Widget API, or even a Virtual method. This attribute just tells Tweaker to not render this frame -- thus making it ideal for later inheritance. XML widgets have a similar property and behavior (in fact, I think the property might have the same name, but it's been so long since I worked with XML to create objects), but the Tweaker inheritance behavior goes a bit further, as I'll show in a bit. Also, a panel doesn't need to be declared as Virtual to be inherited, but generally it will be the case.

Also, when working with virtual and inherited widgets, it's common practice to use "$parent" when naming stuff. $parent is replaced with the name of the parent widget upon creation. If this widget wasn't virtual, "$parentBorder" will be renamed as "BasePanelBorder" upon creation. As it stands, if a widget named "MyCoolWidget" inherits BasePanel, then it will get a texture named "MyCoolWidgetBorder" and "MyCoolWidgetBackground" -- this is very important. Had I named it simply "Border" then everything inheriting from BasePanel would have a texture named Border, which would in fact mean that only the first thing using it would have a texture named Border and all other widgets would simply be referencing that. If there's already a UI element named Border, then this would refer that instead of creating a texture. So be very careful to name things properly!

Strata and Level are simply what work with my UI. Adjust as necessary for yours. Points = "ALL" is another special case which tells Tweaker to call SetAllPoints().. which essentially sets TOPLEFT and BOTTOMRIGHT anchors to the parent. Since this frame is virtual, it's not defining a parent -- that'll be left up to the inheriting frames and will surely be different for each one.

The real functionality here is the Textures part. You'll notice that the "Background" texture is actually on the Border layer and the "Border" texture is on the background layer. This is because typical WoW borders are graphics that are laid over top of the background texture around the border of the object. Since I wanted a plain, flat border, it was easier to do it this way than to create an image and apply a vertex color to set the color of it. The Texture attribute defines the color of each and the Points attribute is set to "ALL" so that the background lines up with the parent widget. The border extends out by 3 units in all directions so it's nice and even on all sides.

Another type of panel in my UI is one that has a background image. This is handy for the chat so that it's not just an empty space when there's no text (and I can still read the text over the image when it's there because I've tweaked things beyond the scope of this tutorial). It's also handy for damage meters for when they're emtpy and for wherever you feel like sprucing up your UI a bit with a pretty image. You'll notice later that this is designed so that you can use ImagePanel just to add an image somewhere, it does not need to have the background color and border like the BasePanel has.

There's not much defined here. The ImagePanel is simply to add an image to something, so that's what I've defined. All my images are put on the ARTWORK layer, and they're moved inward on the widget by 1 unit so that, if there's also a background color in play, that will be visible. So far, images are always used in correlation with a BasePanel or some similar panel, so this works well for me to have that small bit of background color before jumping to the border color.

The faded image panel is specifically for panels where you're going to have something on top of the image that will need to be clearly visible. It inherits from the ImagePanel and adds a vertex color to make the image darker so it doesn't obscure things on top of it as much.

You'll see here my first use of the Template attribute. This is another special Tweaker attribute, and it actually pulls double duty. It's first duty is to find any Tweaker widget with a matching name. Template is set to only one widget here, but it can be a space-separated list of widgets to inherit from (demonstrated and discussed in more depth later). It then applies the attributes and values of that widget to the inheriting widget. In this case, it'll take everything from ImagePanel and apply it to FadedImagePanel... namely, the DrawLayer and Points attributes of the $parentImage texture.

The other duty of the Template attribute falls in line with standard Widget API. When creating a new widget, you can supply the name of a widget to inherit attributes from. If you supply to the Template attribute here the name of a widget that's not defined in Tweaker_Data, it will pass this name on to the Widget API's CreateFrame() function and the Widget API will do whatever it does to inherit stuff. I've not yet had a need to use this, so it's currently untested, and I truthfully don't know much about what it does. If you supply multiple names, Tweaker will only send the first one as I'm pretty sure the standard Widget API doesn't support multiple inheritance.

Back on to FadedImagePanel, to summarize, all this does it inherit from ImagePanel and apply a vertex color that will make it 50% darker. Had I done something like 1.0 0.0 0.0 1.0, it would make it red. The parameters are your standard color parameters: rgba -- red, green, blue, alpha. I could have gone with 0 0 0 0.5 to apply a 50% black color instead of 100% of a gray color, but I believe in my testing, that actually faded the image itself 50%.. thus showing the near-black background behind it, thus making it much, much darker than intended. 1 1 1 0.5 may have worked well, but if I ever changed my background color to red, all my images would have a red hue instead of simply looking faded. Feel free to experiment, as always.

Why bother with all this?
Like I hinted to, it makes it easy to change things globally later on. If I changed the background color of BasePanel to 1 0 0 (red), then all my panels that inherit from it would turn red. If I ever wanted to make the border thicker or thinner then I'd only need to change that in one place instead of twenty (or however many) places. It's just a convenience thing, but it's one that's imminently worthwhile, in my opinion, which is why Tweaker supports not only nested inheritance but also multiple inheritance.

Types of Inheritance: Nested
I realize I'm talking about different kinds of inheritance and you may not understand what they are, so I'll explain. Nested inheritance means that something can inherit from something (via the Template attribute) that inherits from something that inherits from something that... well, I hope you get the idea now. For example, I could create a widget that inherits from FadedImagePanel. FadedImagePanel inherits from ImagePanel. So my widget would have properties of all of these things. If you think of nested inheritance as a stack where each inherited widget is placed below the one that's inheriting it, attributes of the topmost widget take precedence over attributes of lower widgets. Or more simply, it works how you'd expect it to work (I think). If I created a widget that inherited from my FadedImagePanel widget above and it's $parentImage had VertexColor = "1.0 0.0 0.0 1.0" then the final widget being created by Tweaker would have that VertexColor instead of the one defined in FadedImagePanel.

Types of Inheritance: Multiple
Multiple inheritance works another way. Instead of stacking widgets on top of each other, I think of them as being laid out in a row, side by side, from left to right in the same order they're defined in the Template attribute, with the widget itself being the leftmost one. And then any attributes not defined by the main widget are absorbed into it through the inheriting widget. So if I created a widget (let's call it SuperWidget) that had Template = "BasePanel ImagePanel" -- my row would be: SuperWidget BasePanel ImagePanel, and I would look at all the attributes of BasePanel first and add on any not defined by SuperWidget (thus defining them for SuperWidget). Then I would look at the attributes of ImagePanel and add on any that still aren't defined by SuperWidget. So if SuperWidget defined height as 10, BasePanel defined height as 5, and ImagePanel defined it as 20, it would be ultimately defined as 10. If SuperWidget didn't define a width, BasePanel defined it as 6, and ImagePanel doesn't define it, then it's 6.

Complicated Inheritance
A more complicated use of inheritance is thus: Template = "FadedImagePanel BasePanel" -- I wish I had images to go along with this and hopefully when I get Tweaker it's own site and spruce all this up, I will, but for now, bare with me here. Now, you have something like this:

SuperWidget is going to inherit any undefined attributes from FadedImagePanel, but it's first going to inherit undefined attributes from ImagePanel for itself. When resolving an attribute for SuperWidget, the hierarchy will look like so: SuperWidget, FadedImagePanel, ImagePanel, BasePanel. Anything defined in SuperWidget itself will take precedence over anything else. The (possibly) tricky part here is that ImagePanel's attributes will take precedence over the attributes of BasePanel because they're rolled into FadedImagePanel and then FadedImagePanel is inherited before BasePanel. If you want BasePanel to take precedence over ImagePanel, it will also need to take precedence over FadedImagePanel, and you'll need to define your attribute as Template = "BasePanel FadedImagePanel"

Generally speaking, nested inheritance is when you want to make a more specific version of something. Above, I have FadedImagePanel as a more specific type of ImagePanel -- one that's faded. You might do RedImagePanel or SparklyImagePanel or whatever. You could go further and create a widget named FadedImagePanelHover, which inherits FadedImagePanel, but adds an effect so that the image is only faded when the mouse is hovering over it. Multiple inheritance is when something is different types of things, but those things are very different from each other and can stand alone. FadedImagePanel, without inheriting from ImagePanel, would not work because the DrawLayer and Points aren't set on it. ImagePanel and BasePanel do work just fine by themselves though. Panels designed for multiple inheritance generally don't define the same attributes either, as in these cases.

Now that my brain is fried trying to (hopefully) explain all this, here are some easy code examples to copy and use and play around with. The only further point I want to make here is that, with all these "virtual" widgets created and ready to be inherited from, creating panels on your existing UI elements is now easy. All you really need to do is give each one a unique name, set its template(s), and set its parent. Some elements, like the Minimap, are a bit odd with their frame strata/level and you may want to do it a little differently. If it's an image panel, you'll also need to set the path to the image and likely the TexCoord to use as well. And if you want a panel that's just a blank panel, just define the Points attribute so it doesn't inherit "ALL" from BasePanel.

No comments:

Post a Comment