RobinSchmidt / RS-MET

Codebase for RS-MET products (Robin Schmidt's Music Engineering Tools)
Other
57 stars 6 forks source link

I found a strategy for complex UI programming / organization #247

Open elanhickler opened 6 years ago

elanhickler commented 6 years ago
  1. Create a .h and .cpp dedicated to resources as static members of a class called EditorResources() or whatever.

    • For example, create Font() and Image() static members for any fonts and images and svg etc that you will be using.
    • Now when you create custom painters (simply overriding paint() on RSlider or whatever), you do something like class MyFontWidget(Font desiredFont) : public RSlider instead of doing over and over again class MyFontWidget(const char data, const size_t size) filling out BinaryData::tekoFont, BinaryData::tekoFontSize
  2. Create a .h and .cpp dedicated to custom classes overriding paint.

    • You will not actually use painters. You will override the paint() method in RSlider or ModulatableSlider rather than use a painter since the mouse handling cannot be customized.
    • The only code that I seem to need to duplicate is the mouse handling, so if we figure out a good strategy to setup mouse handling this would be perfect.
  3. Every unique widget, and I mean every one, create a new class and override RSlider or ModulatableSlider. The only time you should reuse a class is if you're just changing stuff like color, size, font style, rotation, etc. OTHERWISE you should create reusable graphic/path functions that help you do the repetition of creating the paint() method in every new class.

    • for example, my knobs require setting up various arcs of different margins, sizes, thicknesses. I didn't do this yet, but I could create an "arc designer" function that I can reuse in my various paint methods to create the various arc knob styles image
    • Another example of when not to reuse a class: Don't create a mega customizeable button class. Need a button that doesn't change with user interaction? Make a static button. Need a button with two states? Make a two image button. Need a button that blinks? Make a blink button. Need a button that highlights on mouseover? Make a highlight on mouseover button. Do not reuse! Lesson learned.
    • Basically the goal is that you are creating new classes where most of the code is in paint() method. If you're doing code outside of paint method, make another class that helps complete your painting tasks.
  4. When viable, from your various UI classes, create a reusable UI object/template, like one knob that you use multiple times in the UI that has the same color, same size, same everything, so you can just change that one template to quickly affect all ui elements that are exact copies.

  5. Tip: Because you are rapidly creating multiple small UI element classes, it is best not to go crazy and create functions for everything like setFontSize(), setFontColor(), setBackgroundColor(). Instead, get used to accessing the members directly, and use members that are easy to manipulate. font.setHeight(), fontColor = Colour({x,x,x}), backgroundColor = Colour({x,x,x}). Or you could create a custom font class to do stuff like font.setColor(), font.setSize(), and finally use font.draw(g) in your paint methods. Only create functions if you need to do something like call repaint when you change something or if you need special functionality like void doBlink(bool v).

elanhickler commented 6 years ago

Note: We will differentiate between paint and draw. Paint implies repaint() may be called and is a method found in juce::Component. Draw implies this function is called inside a paint method. Resource classes should implement draw()

Basic resources are things like images, fonts, svg that you intend to call draw() inside a paint method.

Resource classes are classes that hold one or more basic resources for a higher level purpose

Resources cannot be interacted with, but may be intended as a paint routine for a Component subclass. Components are meant to be added to parent components (aka editors) so that the parent manages its visibility, repainting, resizing, etc. What you don't want to do is make a component subclass just to call its paint method in the parent component's paint method. A component is for adding to a parent usually via addAndMakeVisible(). I didn't understand this stuff a few days ago. Based on the user interaction of the component you can manipulate the paint method to then change how said resource is drawn to create user interaction with one or more resources.

RobinSchmidt commented 6 years ago

phew - that's a lot to absorb. ...need to read it again... what strikes me a bit odd is that you seem to somehow conflate painting with mouse-handling when you say things like:

You will not actually use painters. You will override the paint() method in RSlider or ModulatableSlider rather than use a painter since the mouse handling cannot be customized.

but i actually think that painting and mouse-handling are two very separate issues that should not be conflated

The only code that I seem to need to duplicate is the mouse handling, so if we figure out a good strategy to setup mouse handling this would be perfect.

i actually have an idea how to do that. client code would then do things like:

mySlider->setPainter(&mySliderPainter);
mySlider->setMouseHandler(&mySliderMouseHandler);

so client code could customize both aspects independently

elanhickler commented 6 years ago

You are right Robin, mouseHandler/Painter sounds like a good strategy. I wrote this more to see if it makes sense. Still figuring out if what I wrote it makes sense. If you skimmed through what I wrote, you probably read enough of it!

elanhickler commented 6 years ago

I have a situation in my plugin where I have both sliders and knobs and it would be confusing for the user to have to drag horizontal for just a few sliders when it's vertical for the majority of the controls. I could make all knobs and sliders respond to just vertical or just horizontal, but that is equally confusing if you are used to one way versus the other.

This solves the issue of having to code vertical or horizontal dragging. This allows for controls to respond to both horizontal and vertical seamlessly. This is how some plugins do it and probably it will become the standard in the future.

if (!e.mods.isRightButtonDown() && !e.mods.isCommandDown())
{
    int newDragDistance = e.getDistanceFromDragStartY() - e.getDistanceFromDragStartX();

    int dragDelta = newDragDistance - oldDragDistance;
    oldDragDistance = newDragDistance;
    dragValue += scale * dragDelta;

    double y = normalizedValueOnMouseDown;
    y -= dragValue;
    y = clip(y, 0, 1);

    setNormalizedValue(y);
}
RobinSchmidt commented 6 years ago

so, the main difference to my RSlider::mouseDrag is this: newDragDistance = e.getDistanceFromDragStartY() - e.getDistanceFromDragStartX(); vs this: newDragDistance = e.getDistanceFromDragStartX(); hmmm...so it would respond equally to both drag dimensions...but - maybe i'm confused - it looks like it reverses the response to horizontal drag due to the minus sign in front of the x-value? shouldn't it be + instead of -?

elanhickler commented 6 years ago

Yeah you can swap around some variables/signs to make the math look better. But the math I gave you is sound.