Add vertical tabs (#3314)

* Add vertical tabs to ribbon widget types

* Update GUI engine for vertical tabs

* Add the ability to get a ribbon's active children number

* Add full looping for vertical tabs
This commit is contained in:
Alayan-stk-2 2018-06-20 03:00:10 +02:00 committed by auriamg
parent 750320fbeb
commit be98a6c4c3
6 changed files with 307 additions and 86 deletions

View File

@ -99,7 +99,8 @@ namespace GUIEngine
\n
\subsection widget1 WTYPE_RIBBON
<em> Names in XML files: </em> \c "ribbon", \c "buttonbar", \c "tabs"
<em> Names in XML files: </em> \c "ribbon", \c "buttonbar", \c "tabs",
\c"vertical-tabs"
Appears as an horizontal bar containing elements laid in a row, each being
and icon and/or a label
@ -351,9 +352,9 @@ namespace GUIEngine
size (parent size means the parent \<div\> or the whole screen if none). A
negative value can also be passed to start coordinate from right and/or
bottom, instead of starting from top-left corner as usual.
Note that in many cases, it is not necessary to manually a position. Div
Note that in many cases, it is not necessary to manually set a position. Div
layouts will often manage that for you (see PROP_LAYOUT). Other widgets will
also automativally manage the position and size of their children, for
also automatically manage the position and size of their children, for
instance ribbons.
\n
@ -367,7 +368,7 @@ namespace GUIEngine
Note that in many cases, it is not necessary to manually a size. Div layouts
will often manage that for you (see PROP_LAYOUT). In addition, sizes are
automatically calculated for widgets made of icons and/or text like labels
and plain icons. Other widgets will also automativally manage the position
and plain icons. Other widgets will also automatically manage the position
and size of their children, for instance ribbons.
Another possible value is "fit", which will make a \<div\> fit to its

View File

@ -433,12 +433,12 @@ void EventHandler::sendNavigationEvent(const NavigationDirection nav, const int
else if (nav == NAV_DOWN)
propagation_state = widget_to_call->downPressed(playerID);
if (propagation_state == EVENT_LET)
sendEventToUser(widget_to_call, widget_to_call->m_properties[PROP_ID], playerID);
if (propagation_state == EVENT_LET || propagation_state == EVENT_BLOCK_BUT_HANDLED)
handled_by_widget = true;
if (propagation_state == EVENT_LET)
sendEventToUser(widget_to_call, widget_to_call->m_properties[PROP_ID], playerID);
if (widget_to_call->m_event_handler == NULL)
break;
@ -476,6 +476,20 @@ void EventHandler::navigate(const NavigationDirection nav, const int playerID)
assert(list != NULL);
list->setSelectionID(nav == NAV_UP ? list->getItemCount() - 1 : 0);
}
// Similar exception for vertical tabs, only apply when entering with down/up
if (closest_widget->m_type == GUIEngine::WTYPE_RIBBON && (nav == NAV_UP || nav == NAV_DOWN))
{
RibbonWidget* ribbon = dynamic_cast<RibbonWidget*>(closest_widget);
assert(ribbon != NULL);
if (ribbon->getRibbonType() == GUIEngine::RibbonType::RIBBON_VERTICAL_TABS)
{
int new_selection = (nav == NAV_UP) ?
ribbon->getActiveChildrenNumber(playerID) - 1 : 0;
ribbon->setSelection(new_selection, playerID);
// The tab selection triggers an action
sendEventToUser(ribbon, ribbon->m_properties[PROP_ID], playerID);
}
}
}
return;
@ -535,6 +549,7 @@ int EventHandler::findIDClosestWidget(const NavigationDirection nav, const int p
}
int offset = 0;
int rightmost = w_test->m_x + w_test->m_w;
if (nav == NAV_UP || nav == NAV_DOWN)
{
@ -564,7 +579,7 @@ int EventHandler::findIDClosestWidget(const NavigationDirection nav, const int p
// we substract the smaller from the bigger
// else, the smaller is 0 and we keep the bigger
int right_offset = std::max(0, w_test->m_x - w->m_x);
int left_offset = std::max(0, (w->m_x + w->m_w) - (w_test->m_x + w_test->m_w));
int left_offset = std::max(0, (w->m_x + w->m_w) - rightmost);
offset = std::max (right_offset - left_offset, left_offset - right_offset);
}
else if (nav == NAV_LEFT || nav == NAV_RIGHT)
@ -572,7 +587,7 @@ int EventHandler::findIDClosestWidget(const NavigationDirection nav, const int p
if (nav == NAV_LEFT)
{
// compare current leftmost point with other widget rightmost
distance = w->m_x - (w_test->m_x + w_test->m_w);
distance = w->m_x - rightmost;
}
else
{
@ -585,9 +600,28 @@ int EventHandler::findIDClosestWidget(const NavigationDirection nav, const int p
int up_offset = std::max(0, (w->m_y + w->m_h) - (w_test->m_y + w_test->m_h));
offset = std::max (down_offset - up_offset, up_offset - down_offset);
// Special case for vertical tabs : select the top element of the body
if (w->m_type == GUIEngine::WTYPE_RIBBON)
{
RibbonWidget* ribbon = dynamic_cast<RibbonWidget*>(w);
if (ribbon->getRibbonType() == GUIEngine::RibbonType::RIBBON_VERTICAL_TABS)
{
offset = w_test->m_y;
offset *= 100;
// Don't count elements above the tabs or too high as valid
if (!(w_test->m_x > (w->m_x + w->m_w) || rightmost < w->m_x) ||
(w_test->m_y + w_test->m_h) < w->m_y)
{
distance = BIG_DISTANCE;
wrapping_distance = BIG_DISTANCE;
}
}
}
// No lateral selection if there is not at least partial alignement
// >= because we don't want it to trigger if two widgets touch each other
if (offset >= w->m_h)
else if (offset >= w->m_h)
{
distance = BIG_DISTANCE;
wrapping_distance = BIG_DISTANCE;
@ -912,4 +946,3 @@ EventPropagation EventHandler::onGUIEvent(const SEvent& event)
}
// -----------------------------------------------------------------------------

View File

@ -102,6 +102,10 @@ void Screen::parseScreenFileDiv(irr::io::IXMLReader* xml, PtrVector<Widget>& app
{
append_to.push_back(new RibbonWidget(RIBBON_TABS));
}
else if (wcscmp(L"vertical-tabs", xml->getNodeName()) == 0)
{
append_to.push_back(new RibbonWidget(RIBBON_VERTICAL_TABS));
}
else if (wcscmp(L"spinner", xml->getNodeName()) == 0)
{
append_to.push_back(new SpinnerWidget());
@ -270,9 +274,10 @@ if(prop_name != NULL) widget.m_properties[prop_flag] = core::stringc(prop_name).
// We're done parsing this 'ribbon', return one step back in
// the recursive call.
if (wcscmp(L"ribbon", xml->getNodeName()) == 0 ||
wcscmp(L"buttonbar", xml->getNodeName()) == 0 ||
wcscmp(L"tabs", xml->getNodeName()) == 0)
if (wcscmp(L"ribbon", xml->getNodeName()) == 0 ||
wcscmp(L"buttonbar", xml->getNodeName()) == 0 ||
wcscmp(L"tabs", xml->getNodeName()) == 0 ||
wcscmp(L"vertical-tabs", xml->getNodeName()) == 0)
return;
}
break;

View File

@ -1003,6 +1003,56 @@ void Skin::drawRibbonChild(const core::recti &rect, Widget* widget,
material2D.UseMipMaps = true;
}
}
/* vertical tab-bar ribbons */
else if (type == RIBBON_VERTICAL_TABS)
{
video::SMaterial& material2D =
irr_driver->getVideoDriver()->getMaterial2D();
for (unsigned int n=0; n<MATERIAL_MAX_TEXTURES; n++)
{
material2D.UseMipMaps = false;
}
const bool mouseIn = rect.isPointInside(irr_driver->getDevice()
->getCursorControl()
->getPosition() );
BoxRenderParams* params;
if (mark_selected && (focused || parent_focused))
params = &SkinConfig::m_render_params["verticalTab::focused"];
else if (parentRibbon->m_mouse_focus == widget && mouseIn)
params = &SkinConfig::m_render_params["verticalTab::focused"];
else if (mark_selected)
params = &SkinConfig::m_render_params["verticalTab::down"];
else
params = &SkinConfig::m_render_params["verticalTab::neutral"];
// automatically guess from position on-screen if tabs go left or right
unsigned int screen_width = irr_driver->getActualScreenSize().Width;
const bool horizontal_flip =
(unsigned int)rect.UpperLeftCorner.X > screen_width/ 2;
params->m_vertical_flip = false;
core::recti rect2 = rect;
if (mark_selected)
{
// the selected tab should be slighlty bigger than others
if (horizontal_flip) rect2.UpperLeftCorner.X -= screen_width/50;
else rect2.LowerRightCorner.X += screen_width/50;
}
drawBoxFromStretchableTexture(widget, rect2, *params,
parentRibbon->m_deactivated ||
widget->m_deactivated);
for (unsigned int n=0; n<MATERIAL_MAX_TEXTURES; n++)
{
material2D.UseMipMaps = true;
}
}
/* icon ribbons */
else
{

View File

@ -115,7 +115,7 @@ void RibbonWidget::add()
if (m_active_children[i].m_type != WTYPE_ICON_BUTTON &&
m_active_children[i].m_type != WTYPE_BUTTON)
{
Log::warn("RiggonWidget", "Ribbon widgets can only have "
Log::warn("RibbonWidget", "Ribbon widgets can only have "
"(icon)button widgets as children");
continue;
}
@ -144,19 +144,26 @@ void RibbonWidget::add()
//int biggest_y = 0;
const int button_y = 10;
const int one_button_space = (subbuttons_amount == 0 ? m_w :
const int one_button_width = (subbuttons_amount == 0 ? m_w :
int(roundf((float)m_w / (float)subbuttons_amount)));
const int one_button_height = (subbuttons_amount == 0 ? m_h :
int(roundf((float)m_h / (float)subbuttons_amount)));
int widget_x = -1;
int widget_y = -1;
// ---- add children
// TODO : the content of the ifs is way too large, separate functions would be better.
// Several pre-loop variables are used inside the ifs,
// so care must be taken to not break things
for (int i=0; i<subbuttons_amount; i++)
{
// ---- tab ribbons
if (getRibbonType() == RIBBON_TABS)
{
const int large_tab = (int)((with_label + without_label)
*one_button_space
*one_button_width
/ (with_label + without_label/2.0f));
const int small_tab = large_tab/2;
@ -277,11 +284,108 @@ void RibbonWidget::add()
if (message.size() == 0) widget_x += small_tab/2;
else widget_x += large_tab/2;
}
} // tabs
// ---- vertical tab ribbons
else if (getRibbonType() == RIBBON_VERTICAL_TABS)
{
const int tab_width = (int)((with_label + without_label)
*m_w
/ (with_label + without_label/2.0f));
stringw& message = m_active_children[i].m_text;
widget_x = tab_width/2;
if (widget_y == -1)
widget_y = 0;
else
widget_y += one_button_height;
IGUIButton * subbtn = NULL;
// The buttons will overlap without this,
// as drawBoxFromStretchableTexture draw the borders outside
// of a widget's reserved area
int VERT_BORDER_MARGIN = 8;
rect<s32> subbtn_rec = rect<s32>(widget_x - tab_width/2+2, widget_y + VERT_BORDER_MARGIN,
widget_x + tab_width/2-2, widget_y - VERT_BORDER_MARGIN + one_button_height);
// TODO Add support for BUTTON type when needed
if (m_active_children[i].m_type == WTYPE_ICON_BUTTON)
{
// The icon will take 1/3rd of the tab width at most, less if height is lacking
int icon_size = std::min(m_w/3, subbtn_rec.getHeight()+2*VERT_BORDER_MARGIN);
rect<s32> icon_part = rect<s32>(5,
one_button_height/2 - icon_size/2-VERT_BORDER_MARGIN,
icon_size+5,
one_button_height/2 + icon_size/2-VERT_BORDER_MARGIN);
// label at the *right* of the icon (for tabs)
rect<s32> label_part = rect<s32>(icon_size+5,
5-VERT_BORDER_MARGIN,
subbtn_rec.getWidth()-5,
one_button_height-5-VERT_BORDER_MARGIN);
// use the same ID for all subcomponents; since event handling
// is done per-ID, no matter which one your hover, this
// widget will get it
int same_id = getNewNoFocusID();
subbtn = GUIEngine::getGUIEnv()->addButton(subbtn_rec, btn,
same_id, L"", L"");
IGUIButton* icon =
GUIEngine::getGUIEnv()->addButton(icon_part, subbtn,
same_id, L"");
icon->setScaleImage(true);
std::string filename = file_manager->getAsset(
m_active_children[i].m_properties[PROP_ICON]);
icon->setImage( irr_driver->getTexture(filename.c_str()) );
icon->setUseAlphaChannel(true);
icon->setDrawBorder(false);
icon->setTabStop(false);
IGUIStaticText* label =
GUIEngine::getGUIEnv()->addStaticText(message.c_str(),
label_part,
false /* border */,
true /* word wrap */,
subbtn, same_id);
if ((int)GUIEngine::getFont()->getDimension(message.c_str())
.Width > label_part.getWidth()&&
message.findFirst(L' ') == -1 &&
message.findFirst(L'\u00AD') == -1 )
{
// if message too long and contains no space and no soft
// hyphen, make the font smaller
label->setOverrideFont(GUIEngine::getSmallFont());
}
label->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER);
label->setTabStop(false);
label->setNotClipped(true);
label->setRightToLeft(translations->isRTLText(message));
m_labels.push_back(label);
subbtn->setTabStop(false);
subbtn->setTabGroup(false);
}
else
{
Log::error("RibbonWidget", "Invalid tab bar contents");
}
m_active_children[i].m_element = subbtn;
} // vertical-tabs
// ---- icon ribbons
else if (m_active_children[i].m_type == WTYPE_ICON_BUTTON)
{
if (widget_x == -1) widget_x = one_button_space/2;
if (widget_x == -1) widget_x = one_button_width/2;
// find how much space to keep for the label under the button.
// consider font size, whether the label is multiline, etc...
@ -316,7 +420,7 @@ void RibbonWidget::add()
float image_h = (float)image->getSize().Height;
float image_w = image_h*imageRatio;
float zoom = (float) (m_h - button_y - needed_space_under_button) / image_h;
float zoom_x = (float) one_button_space / image_w;
float zoom_x = (float) one_button_width / image_w;
if(zoom_x < zoom)
zoom = zoom_x;
@ -338,7 +442,7 @@ void RibbonWidget::add()
if (icon->m_properties[PROP_EXTEND_LABEL].size() == 0)
{
icon->m_properties[PROP_EXTEND_LABEL] =
StringUtils::toString(one_button_space - icon->m_w);
StringUtils::toString(one_button_width - icon->m_w);
}
m_active_children.get(i)->m_parent = btn;
@ -354,7 +458,7 @@ void RibbonWidget::add()
// adds the label outside of the widget area it is assigned to,
// the label will appear in the area we want at the bottom
widget_x += one_button_space;
widget_x += one_button_width;
}
else
{
@ -373,7 +477,7 @@ void RibbonWidget::add()
if (!m_is_visible)
setVisible(false);
} // add
} // add
// ----------------------------------------------------------------------------
@ -460,65 +564,64 @@ void RibbonWidget::select(std::string item, const int mousePlayerID)
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::rightPressed(const int playerID)
{
EventPropagation result = m_ribbon_type != RIBBON_TOOLBAR ? EVENT_LET : EVENT_BLOCK_BUT_HANDLED;
if (m_deactivated) return result;
// empty ribbon, or only one item (can't move right)
if (m_active_children.size() < 2) return result;
m_selection[playerID]++;
if (m_selection[playerID] >= int(m_active_children.size()))
{
if (m_listener != NULL) m_listener->onRibbonWidgetScroll(1);
m_selection[playerID] = m_event_handler ? m_active_children.size()-1 : 0;
}
updateSelection();
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS)
{
const int mousePlayerID = input_manager->getPlayerKeyboardID();
if (playerID == mousePlayerID || playerID == PLAYER_ID_GAME_MASTER)
{
m_mouse_focus = m_active_children.get(m_selection[playerID]);
}
}
// if we reached a filler item, move again (but don't wrap)
if (getSelectionIDString(playerID) == RibbonWidget::NO_ITEM_ID)
{
if (m_selection[playerID] + 1 < int(m_active_children.size()))
{
rightPressed(playerID);
}
}
return result;
return moveToNextItem(/*horizontal*/ true, /*reverse*/ false, playerID);
} // rightPressed
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::leftPressed(const int playerID)
{
EventPropagation result = m_ribbon_type != RIBBON_TOOLBAR ? EVENT_LET : EVENT_BLOCK_BUT_HANDLED;
if (m_deactivated) return result;
// empty ribbon, or only one item (can't move left)
if (m_active_children.size() < 2) return result;
return moveToNextItem(/*horizontal*/ true, /*reverse*/ true, playerID);
} // leftPressed
m_selection[playerID]--;
if (m_selection[playerID] < 0)
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::downPressed(const int playerID)
{
return moveToNextItem(/*horizontal*/ false, /*reverse*/ false, playerID);
} // downPressed
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::upPressed(const int playerID)
{
return moveToNextItem(/*horizontal*/ false, /*reverse*/ true, playerID);
} // upPressed
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::moveToNextItem(const bool horizontally, const bool reverse, const int playerID)
{
EventPropagation result = propagationType(horizontally);
// Do nothing and do not block navigating out of the widget
if (result == EVENT_BLOCK) return result;
if (reverse)
m_selection[playerID]--;
else
m_selection[playerID]++;
if (m_selection[playerID] >= int(m_active_children.size()) || m_selection[playerID] < 0)
{
if (m_listener != NULL) m_listener->onRibbonWidgetScroll(-1);
// In vertical tabs, don't loop when reaching the top or bottom
if (!horizontally)
{
if (reverse)
m_selection[playerID]++;
else
m_selection[playerID]--;
m_selection[playerID] = m_event_handler
? 0
: m_active_children.size()-1;
return EVENT_BLOCK;
}
bool left = (m_selection[playerID] < 0);
if (m_listener != NULL) m_listener->onRibbonWidgetScroll(left ? -1 : 1);
bool select_zero = (m_event_handler && left) || (!m_event_handler && !left);
m_selection[playerID] = select_zero ? 0 : m_active_children.size()-1;
}
updateSelection();
if (m_ribbon_type == RIBBON_COMBO)
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS ||
m_ribbon_type == RIBBON_VERTICAL_TABS)
{
const int mousePlayerID = input_manager->getPlayerKeyboardID();
if (playerID == mousePlayerID || playerID == PLAYER_ID_GAME_MASTER)
@ -530,16 +633,38 @@ EventPropagation RibbonWidget::leftPressed(const int playerID)
// if we reached a filler item, move again (but don't wrap)
if (getSelectionIDString(playerID) == RibbonWidget::NO_ITEM_ID)
{
if (m_selection[playerID] > 0) leftPressed(playerID);
if (((m_selection[playerID] > 0) && reverse ) ||
((m_selection[playerID] + 1 < int(m_active_children.size()))&& !reverse) )
{
moveToNextItem(horizontally, reverse, playerID);
}
}
//if (m_ribbon_type != RIBBON_TOOLBAR)
//{
//GUIEngine::transmitEvent( this, m_properties[PROP_ID], playerID );
//}
return result;
} // moveToNextItem
// ----------------------------------------------------------------------------
EventPropagation RibbonWidget::propagationType(const bool horizontally)
{
EventPropagation result;
if (horizontally)
{
result = m_ribbon_type == RIBBON_VERTICAL_TABS ? EVENT_BLOCK :
m_ribbon_type != RIBBON_TOOLBAR ? EVENT_LET :
EVENT_BLOCK_BUT_HANDLED;
}
else
{
result = m_ribbon_type != RIBBON_VERTICAL_TABS ? EVENT_BLOCK :
EVENT_LET;
}
if (m_deactivated) result = EVENT_BLOCK;
// empty ribbon, or only one item (can't move)
if (m_active_children.size() < 2) result = EVENT_BLOCK;
return result;
} // leftPressed
}
// ----------------------------------------------------------------------------
@ -549,7 +674,8 @@ EventPropagation RibbonWidget::focused(const int playerID)
if (m_active_children.size() < 1) return EVENT_LET; // empty ribbon
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS)
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS ||
m_ribbon_type == RIBBON_VERTICAL_TABS)
{
const int mousePlayerID = input_manager->getPlayerKeyboardID();
if (m_mouse_focus == NULL && m_selection[playerID] != -1 &&
@ -598,7 +724,8 @@ EventPropagation RibbonWidget::mouseHovered(Widget* child,
const int subbuttons_amount = m_active_children.size();
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS)
if (m_ribbon_type == RIBBON_COMBO || m_ribbon_type == RIBBON_TABS ||
m_ribbon_type == RIBBON_VERTICAL_TABS)
{
//Log::info("RibbonWidget", "Setting m_mouse_focus");
m_mouse_focus = child;
@ -765,12 +892,8 @@ int RibbonWidget::findItemNamed(const char* internalName)
{
const int size = m_children.size();
//printf("Ribbon : Looking for %s among %i items\n", internalName, size);
for (int n=0; n<size; n++)
{
//printf(" Ribbon : Looking for %s in item %i : %s\n",
// internalName, n, m_children[n].m_properties[PROP_ID].c_str());
if (m_children[n].m_properties[PROP_ID] == internalName) return n;
}
return -1;

View File

@ -37,7 +37,8 @@ namespace GUIEngine
{
RIBBON_COMBO, //!< select one item out of many, like in a combo box
RIBBON_TOOLBAR, //!< a row of individual buttons
RIBBON_TABS //!< a tab bar
RIBBON_TABS, //!< a tab bar
RIBBON_VERTICAL_TABS //!< a vertical tab bar
};
/** \brief A static text/icons/tabs bar widget.
@ -66,17 +67,21 @@ namespace GUIEngine
int m_selection[MAX_PLAYER_COUNT];
/** The type of this ribbon (toolbar, combo, tabs) */
/** The type of this ribbon (toolbar, combo, tabs, vertical tabs) */
RibbonType m_ribbon_type;
/** Each item within the ribbon holds a flag saying whether it is
* selected or not. This method updates the flag in all of this
* ribbon's children. Called everytime selection changes.*/
void updateSelection();
/** Callbacks */
virtual EventPropagation rightPressed(const int playerID=0) OVERRIDE;
virtual EventPropagation leftPressed(const int playerID=0) OVERRIDE;
virtual EventPropagation upPressed(const int playerID=0) OVERRIDE;
virtual EventPropagation downPressed(const int playerID=0) OVERRIDE;
EventPropagation moveToNextItem(const bool horizontally, const bool reverse, const int playerID);
EventPropagation propagationType(const bool horizontally);
virtual EventPropagation mouseHovered(Widget* child,
const int playerID) OVERRIDE;
virtual EventPropagation transmitEvent(Widget* w,
@ -121,6 +126,10 @@ namespace GUIEngine
* for detailed descriptions) */
RibbonType getRibbonType() const { return m_ribbon_type; }
// --------------------------------------------------------------------
/** Returns the number of active items within the ribbon */
int getActiveChildrenNumber(const int playerID) const
{ return m_active_children.size(); }
// --------------------------------------------------------------------
/** Returns the numerical ID of the selected item within the ribbon */
int getSelection(const int playerID) const
{ return m_selection[playerID]; }