diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp
index 63672bfbe4..cab7f03451 100644
--- a/src/Gui/MainWindow.cpp
+++ b/src/Gui/MainWindow.cpp
@@ -813,14 +813,6 @@ QMenu* MainWindow::createPopupMenu ()
populateDockWindowMenu(menu);
menu->addSeparator();
populateToolBarMenu(menu);
- QMenu *undockMenu = new QMenu(menu);
- ToolBarManager::getInstance()->populateUndockMenu(undockMenu);
- if (undockMenu->actions().isEmpty()) {
- delete undockMenu;
- }
- else {
- menu->addMenu(undockMenu);
- }
menu->addSeparator();
Workbench* wb = WorkbenchManager::instance()->active();
if (wb) {
diff --git a/src/Gui/ToolBarAreaWidget.cpp b/src/Gui/ToolBarAreaWidget.cpp
index d3a80ae6f5..21c03a3318 100644
--- a/src/Gui/ToolBarAreaWidget.cpp
+++ b/src/Gui/ToolBarAreaWidget.cpp
@@ -27,6 +27,7 @@
#include "MainWindow.h"
#include "ToolBarAreaWidget.h"
+#include "ToolBarManager.h"
#include
using namespace Gui;
@@ -53,6 +54,10 @@ void ToolBarAreaWidget::addWidget(QWidget* widget)
return;
}
+ if (auto toolbar = qobject_cast(widget)) {
+ toolbar->updateCustomGripVisibility();
+ }
+
_layout->addWidget(widget);
adjustParent();
@@ -80,6 +85,10 @@ void ToolBarAreaWidget::insertWidget(int index, QWidget* widget)
_layout->insertWidget(index, widget);
+ if (auto toolbar = qobject_cast(widget)) {
+ toolbar->updateCustomGripVisibility();
+ }
+
adjustParent();
saveState();
}
@@ -88,6 +97,10 @@ void ToolBarAreaWidget::removeWidget(QWidget* widget)
{
_layout->removeWidget(widget);
+ if (auto toolbar = qobject_cast(widget)) {
+ toolbar->updateCustomGripVisibility();
+ }
+
QString name = widget->objectName();
if (!name.isEmpty()) {
Base::ConnectionBlocker block(_conn);
diff --git a/src/Gui/ToolBarManager.cpp b/src/Gui/ToolBarManager.cpp
index 221bde476d..eb8c4d50f4 100644
--- a/src/Gui/ToolBarManager.cpp
+++ b/src/Gui/ToolBarManager.cpp
@@ -22,13 +22,17 @@
#include "PreCompiled.h"
#ifndef _PreComp_
-#include
-#include
-#include
-#include
-#include
-#include
-#include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
+# include
#endif
#include
@@ -41,6 +45,7 @@
#include "Command.h"
#include "MainWindow.h"
#include "OverlayWidgets.h"
+#include "WidgetFactory.h"
using namespace Gui;
@@ -162,6 +167,219 @@ QList ToolBarItem::getItems() const
// -----------------------------------------------------------
+ToolBar::ToolBar()
+ : QToolBar()
+{
+ setupConnections();
+}
+
+ToolBar::ToolBar(QWidget* parent)
+ : QToolBar(parent)
+{
+ setupConnections();
+}
+
+void ToolBar::undock()
+{
+ {
+ // We want to block only some signals - topLevelChanged should still be propagated
+ QSignalBlocker blocker(this);
+
+ if (auto area = ToolBarManager::getInstance()->toolBarAreaWidget(this)) {
+ area->removeWidget(this);
+ getMainWindow()->addToolBar(this);
+ }
+
+ setWindowFlags(Qt::Tool
+ | Qt::FramelessWindowHint
+ | Qt::X11BypassWindowManagerHint);
+ adjustSize();
+ setVisible(true);
+ }
+
+ Q_EMIT topLevelChanged(true);
+}
+
+void ToolBar::updateCustomGripVisibility()
+{
+ auto area = ToolBarManager::getInstance()->toolBarAreaWidget(this);
+ auto grip = findChild();
+
+ auto customGripIsRequired = isMovable() && area;
+
+ if (grip && !customGripIsRequired) {
+ grip->detach();
+ grip->deleteLater();
+ } else if (!grip && customGripIsRequired) {
+ grip = new ToolBarGrip(this);
+ grip->attach();
+ } else {
+ // either grip is present and should be present
+ // or is not present and should not be - nothing to do
+ return;
+ }
+}
+
+void Gui::ToolBar::setupConnections()
+{
+ connect(this, &QToolBar::topLevelChanged, this, &ToolBar::updateCustomGripVisibility);
+ connect(this, &QToolBar::movableChanged, this, &ToolBar::updateCustomGripVisibility);
+}
+
+// -----------------------------------------------------------
+
+ToolBarGrip::ToolBarGrip(QToolBar * parent)
+ : QWidget(parent)
+{
+ updateSize();
+}
+
+void ToolBarGrip::attach()
+{
+ if (isAttached()) {
+ return;
+ }
+
+ auto parent = qobject_cast(parentWidget());
+
+ if (!parent) {
+ return;
+ }
+
+ auto actions = parent->actions();
+
+ _action = parent->insertWidget(
+ // ensure that grip is always placed as the first widget in the toolbar
+ actions.isEmpty() ? nullptr : actions[0],
+ this
+ );
+
+ setCursor(Qt::OpenHandCursor);
+ setMouseTracking(true);
+ setVisible(true);
+}
+
+void ToolBarGrip::detach()
+{
+ if (!isAttached()) {
+ return;
+ }
+
+ auto parent = qobject_cast(parentWidget());
+
+ if (!parent) {
+ return;
+ }
+
+ parent->removeAction(_action);
+}
+
+bool ToolBarGrip::isAttached() const
+{
+ return _action != nullptr;
+}
+
+void ToolBarGrip::paintEvent(QPaintEvent*)
+{
+ QPainter painter(this);
+
+ if (auto toolbar = qobject_cast(parentWidget())) {
+ QStyle *style = toolbar->style();
+ QStyleOptionToolBar opt;
+
+ toolbar->initStyleOption(&opt);
+
+ opt.features = QStyleOptionToolBar::Movable;
+ opt.rect = rect();
+
+ style->drawPrimitive(QStyle::PE_IndicatorToolBarHandle, &opt, &painter, toolbar);
+ }
+}
+
+void ToolBarGrip::mouseMoveEvent(QMouseEvent *me)
+{
+ auto toolbar = qobject_cast(parentWidget());
+ if (!toolbar) {
+ return;
+ }
+
+ auto area = ToolBarManager::getInstance()->toolBarAreaWidget(toolbar);
+ if (!area) {
+ return;
+ }
+
+ QPoint pos = me->globalPos();
+ QRect rect(toolbar->mapToGlobal(QPoint(0,0)), toolbar->size());
+
+ // if mouse did not leave the area of toolbar do not continue with undocking it
+ if (rect.contains(pos)) {
+ return;
+ }
+
+ toolbar->undock();
+
+ // After removing from area, this grip will be deleted. In order to
+ // continue toolbar dragging (because the mouse button is still pressed),
+ // we fake mouse events and send to toolbar. For some reason,
+ // send/postEvent() does not work, only timer works.
+ QPointer tb(toolbar);
+ QTimer::singleShot(0, [tb] {
+ auto modifiers = QApplication::queryKeyboardModifiers();
+ auto buttons = QApplication::mouseButtons();
+ if (buttons != Qt::LeftButton
+ || QWidget::mouseGrabber()
+ || modifiers != Qt::NoModifier
+ || !tb) {
+ return;
+ }
+
+ QPoint pos(10, 10);
+ QPoint globalPos(tb->mapToGlobal(pos));
+ QMouseEvent mouseEvent(
+ QEvent::MouseButtonPress,
+ pos, globalPos, Qt::LeftButton, buttons, modifiers);
+ QApplication::sendEvent(tb, &mouseEvent);
+
+ // Mouse follow the mouse press event with mouse move with some offset
+ // in order to activate toolbar dragging.
+ QPoint offset(30, 30);
+ QMouseEvent mouseMoveEvent(
+ QEvent::MouseMove,
+ pos+offset, globalPos+offset,
+ Qt::LeftButton, buttons, modifiers);
+ QApplication::sendEvent(tb, &mouseMoveEvent);
+ });
+}
+
+void ToolBarGrip::mousePressEvent(QMouseEvent *)
+{
+ setCursor(Qt::ClosedHandCursor);
+}
+
+void ToolBarGrip::mouseReleaseEvent(QMouseEvent *)
+{
+ setCursor(Qt::OpenHandCursor);
+}
+
+void ToolBarGrip::updateSize()
+{
+ auto parent = qobject_cast(parentWidget());
+
+ if (!parent) {
+ return;
+ }
+
+ QStyle *style = parent->style();
+ QStyleOptionToolBar opt;
+
+ parent->initStyleOption(&opt);
+ opt.features = QStyleOptionToolBar::Movable;
+
+ setFixedWidth(style->subElementRect(QStyle::SE_ToolBarHandle, &opt, parent).width() + 4);
+}
+
+// -----------------------------------------------------------
+
ToolBarManager* ToolBarManager::_instance = nullptr; // NOLINT
ToolBarManager* ToolBarManager::getInstance()
@@ -189,6 +407,8 @@ ToolBarManager::ToolBarManager()
setupConnection();
setupTimer();
setupMenuBarTimer();
+
+ setupWidgetProducers();
}
ToolBarManager::~ToolBarManager() = default;
@@ -304,6 +524,11 @@ void ToolBarManager::setupMenuBarTimer()
});
}
+void Gui::ToolBarManager::setupWidgetProducers()
+{
+ new WidgetProducer;
+}
+
ToolBarArea ToolBarManager::toolBarArea(QWidget *widget) const
{
if (auto toolBar = qobject_cast(widget)) {
@@ -327,15 +552,24 @@ ToolBarArea ToolBarManager::toolBarArea(QWidget *widget) const
}
}
- for (auto &areaWidget : { statusBarAreaWidget, menuBarLeftAreaWidget, menuBarRightAreaWidget }) {
- if (areaWidget->indexOf(widget) >= 0) {
- return areaWidget->area();
- }
+ if (auto areaWidget = toolBarAreaWidget(widget)) {
+ return areaWidget->area();
}
return ToolBarArea::NoToolBarArea;
}
+ToolBarAreaWidget* ToolBarManager::toolBarAreaWidget(QWidget* widget) const
+{
+ for (auto &areaWidget : { statusBarAreaWidget, menuBarLeftAreaWidget, menuBarRightAreaWidget }) {
+ if (areaWidget->indexOf(widget) >= 0) {
+ return areaWidget;
+ }
+ }
+
+ return nullptr;
+}
+
namespace {
QPointer createActionWidget()
{
@@ -427,23 +661,29 @@ void ToolBarManager::setup(ToolBarItem* toolBarItems)
int top_width = 0;
bool nameAsToolTip = App::GetApplication().GetUserParameter().GetGroup("BaseApp")
- ->GetGroup("Preferences")->GetGroup("MainWindow")->GetBool("ToolBarNameAsToolTip",true);
+ ->GetGroup("Preferences")
+ ->GetGroup("MainWindow")
+ ->GetBool("ToolBarNameAsToolTip", true);
+
QList items = toolBarItems->getItems();
- QList toolbars = toolBars();
+ QList toolbars = toolBars();
+
for (ToolBarItem* it : items) {
// search for the toolbar
QString name = QString::fromUtf8(it->command().c_str());
this->toolbarNames << name;
- QToolBar* toolbar = findToolBar(toolbars, name);
+ ToolBar* toolbar = findToolBar(toolbars, name);
std::string toolbarName = it->command();
bool toolbar_added = false;
if (!toolbar) {
- toolbar = getMainWindow()->addToolBar(
- QApplication::translate("Workbench",
- toolbarName.c_str())); // i18n
+ toolbar = new ToolBar(getMainWindow());
+ toolbar->setWindowTitle(QApplication::translate("Workbench", toolbarName.c_str()));
toolbar->setObjectName(name);
- if (nameAsToolTip){
+
+ getMainWindow()->addToolBar(toolbar);
+
+ if (nameAsToolTip) {
auto tooltip = QChar::fromLatin1('[')
+ QApplication::translate("Workbench", toolbarName.c_str())
+ QChar::fromLatin1(']');
@@ -470,7 +710,8 @@ void ToolBarManager::setup(ToolBarItem* toolBarItems)
// Enable automatic handling of visibility via, for example, (contextual) menu
toolbar->toggleViewAction()->setVisible(true);
}
- else { // ToolBarItem::DefaultVisibility::Unavailable
+ else {
+ // ToolBarItem::DefaultVisibility::Unavailable
// Prevent that the action to show/hide a toolbar appears on the (contextual) menus.
// This is also managed by the client code for a toolbar with custom policy
toolbar->toggleViewAction()->setVisible(false);
@@ -595,9 +836,10 @@ void ToolBarManager::saveState() const
return value == ToolBarItem::DefaultVisibility::Unavailable;
};
- QList toolbars = toolBars();
+ QList toolbars = toolBars();
for (const QString& it : toolbarNames) {
- QToolBar* toolbar = findToolBar(toolbars, it);
+ ToolBar* toolbar = findToolBar(toolbars, it);
+
if (toolbar) {
if (ignoreSave(toolbar->toggleViewAction())) {
continue;
@@ -614,7 +856,7 @@ void ToolBarManager::restoreState() const
std::map sbToolBars;
std::map mbRightToolBars;
std::map mbLeftToolBars;
- QList toolbars = toolBars();
+ QList toolbars = toolBars();
for (const QString& it : toolbarNames) {
QToolBar* toolbar = findToolBar(toolbars, it);
if (toolbar) {
@@ -782,67 +1024,9 @@ bool ToolBarManager::addToolBarToArea(QObject *source, QMouseEvent *ev)
return false;
}
-void ToolBarManager::populateUndockMenu(QMenu *menu, ToolBarAreaWidget *area)
-{
- menu->setTitle(tr("Undock toolbars"));
- auto tooltip = QObject::tr("Undock from toolbar area");
-
- auto addMenuUndockItem = [&](QToolBar *toolbar, int, ToolBarAreaWidget *area) {
- auto toggleViewAction = toolbar->toggleViewAction();
- auto undockAction = new QAction(menu);
-
- undockAction->setText(toggleViewAction->text());
- undockAction->setToolTip(tooltip);
-
- menu->addAction(undockAction);
- QObject::connect(undockAction, &QAction::triggered, [area, toolbar]() {
- if (toolbar->parentWidget() == getMainWindow()) {
- return;
- }
-
- auto pos = toolbar->mapToGlobal(QPoint(0, 0));
- auto yOffset = toolbar->height();
-
- // if widget is on the bottom move it up instead
- if (area->area() == Gui::ToolBarArea::StatusBarToolBarArea) {
- yOffset *= -1;
- }
-
- {
- // Block signals caused by manually floating the widget
- QSignalBlocker blocker(toolbar);
-
- area->removeWidget(toolbar);
- getMainWindow()->addToolBar(toolbar);
-
- // this will make toolbar floating, there is no better way to do that.
- toolbar->setWindowFlags(Qt::Tool
- | Qt::FramelessWindowHint
- | Qt::X11BypassWindowManagerHint);
- toolbar->move(pos.x(), pos.y() + yOffset);
- toolbar->adjustSize();
- toolbar->setVisible(true);
- }
-
- // but don't block actual information about widget being floated
- Q_EMIT toolbar->topLevelChanged(true);
- });
- };
-
- if (area) {
- area->foreachToolBar(addMenuUndockItem);
- }
- else {
- statusBarAreaWidget->foreachToolBar(addMenuUndockItem);
- menuBarLeftAreaWidget->foreachToolBar(addMenuUndockItem);
- menuBarRightAreaWidget->foreachToolBar(addMenuUndockItem);
- }
-}
-
bool ToolBarManager::showContextMenu(QObject *source)
{
QMenu menu;
- QMenu menuUndock;
QLayout* layout = nullptr;
ToolBarAreaWidget* area = nullptr;
if (getMainWindow()->statusBar() == source) {
@@ -872,12 +1056,7 @@ bool ToolBarManager::showContextMenu(QObject *source)
}
area->foreachToolBar(addMenuVisibleItem);
- populateUndockMenu(&menuUndock, area);
- if (!menuUndock.actions().empty()) {
- menu.addSeparator();
- menu.addMenu(&menuUndock);
- }
menu.exec(QCursor::pos());
return true;
}
@@ -990,8 +1169,8 @@ bool ToolBarManager::eventFilter(QObject *source, QEvent *ev)
void ToolBarManager::retranslate() const
{
- QList toolbars = toolBars();
- for (QToolBar* it : toolbars) {
+ QList toolbars = toolBars();
+ for (ToolBar* it : toolbars) {
QByteArray toolbarName = it->objectName().toUtf8();
it->setWindowTitle(QApplication::translate("Workbench", (const char*)toolbarName));
}
@@ -1009,16 +1188,17 @@ void Gui::ToolBarManager::setToolBarsLocked(bool locked) const
setMovable(!locked);
}
-void Gui::ToolBarManager::setMovable(bool moveable) const
+void Gui::ToolBarManager::setMovable(bool movable) const
{
for (auto& tb : toolBars()) {
- tb->setMovable(moveable);
+ tb->setMovable(movable);
+ tb->updateCustomGripVisibility();
}
}
-QToolBar* ToolBarManager::findToolBar(const QList& toolbars, const QString& item) const
+ToolBar* ToolBarManager::findToolBar(const QList& toolbars, const QString& item) const
{
- for (QToolBar* it : toolbars) {
+ for (ToolBar* it : toolbars) {
if (it->objectName() == item) {
return it;
}
@@ -1038,12 +1218,14 @@ QAction* ToolBarManager::findAction(const QList& acts, const QString&
return nullptr; // no item with the user data found
}
-QList ToolBarManager::toolBars() const
+QList ToolBarManager::toolBars() const
{
auto mw = getMainWindow();
- QList tb;
- QList bars = getMainWindow()->findChildren();
- for (QToolBar* it : bars) {
+
+ QList tb;
+ QList bars = getMainWindow()->findChildren();
+
+ for (ToolBar* it : bars) {
auto parent = it->parentWidget();
if (parent == mw
|| parent == mw->statusBar()
diff --git a/src/Gui/ToolBarManager.h b/src/Gui/ToolBarManager.h
index e245a6660b..43f2dcfad7 100644
--- a/src/Gui/ToolBarManager.h
+++ b/src/Gui/ToolBarManager.h
@@ -28,6 +28,7 @@
#include
#include
+#include
#include
#include
#include
@@ -88,6 +89,53 @@ private:
QList _items;
};
+class ToolBarGrip: public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ToolBarGrip(QToolBar *);
+
+ void attach();
+ void detach();
+
+ bool isAttached() const;
+
+protected:
+ void paintEvent(QPaintEvent*);
+ void mouseMoveEvent(QMouseEvent *);
+ void mousePressEvent(QMouseEvent *);
+ void mouseReleaseEvent(QMouseEvent *);
+
+ void updateSize();
+
+private:
+ QPointer _action = nullptr;
+};
+
+/**
+ * QToolBar from Qt lacks few abilities like ability to float toolbar from code.
+ * This class allows us to provide custom behaviors for toolbars if needed.
+ */
+class GuiExport ToolBar: public QToolBar
+{
+ Q_OBJECT
+
+ friend class ToolBarGrip;
+
+public:
+ ToolBar();
+ explicit ToolBar(QWidget* parent);
+
+ virtual ~ToolBar() = default;
+
+ void undock();
+ void updateCustomGripVisibility();
+
+protected:
+ void setupConnections();
+};
+
/**
* The ToolBarManager class is responsible for the creation of toolbars and appending them
* to the main window.
@@ -121,13 +169,12 @@ public:
void setState(const QList& names, State state);
void setState(const QString& name, State state);
-
- int toolBarIconSize(QWidget *widget=nullptr) const;
+
+ int toolBarIconSize(QWidget *widget = nullptr) const;
void setupToolBarIconSize();
- void populateUndockMenu(QMenu *menu, ToolBarAreaWidget *area = nullptr);
-
ToolBarArea toolBarArea(QWidget* toolBar) const;
+ ToolBarAreaWidget* toolBarAreaWidget(QWidget* toolBar) const;
protected:
void setup(ToolBarItem*, QToolBar*) const;
@@ -145,8 +192,8 @@ protected:
bool eventFilter(QObject *source, QEvent *ev) override;
/** Returns a list of all currently existing toolbars. */
- QList toolBars() const;
- QToolBar* findToolBar(const QList&, const QString&) const;
+ QList toolBars() const;
+ ToolBar* findToolBar(const QList&, const QString&) const;
QAction* findAction(const QList&, const QString&) const;
ToolBarManager();
~ToolBarManager() override;
@@ -160,6 +207,8 @@ private:
void setupSizeTimer();
void setupResizeTimer();
void setupMenuBarTimer();
+ void setupWidgetProducers();
+
void addToMenu(QLayout* layout, QWidget* area, QMenu* menu);
QLayout* findLayoutOfObject(QObject* source, QWidget* area) const;
ToolBarAreaWidget* findToolBarAreaWidget() const;
diff --git a/src/Gui/resource.cpp b/src/Gui/resource.cpp
index d7961ab35d..ee3ff5d8ab 100644
--- a/src/Gui/resource.cpp
+++ b/src/Gui/resource.cpp
@@ -59,6 +59,7 @@
#include "InputField.h"
#include "QuantitySpinBox.h"
#include "PrefWidgets.h"
+#include "ToolBarManager.h"
using namespace Gui;
using namespace Gui::Dialog;
diff --git a/src/Mod/BIM/BimStatusBar.py b/src/Mod/BIM/BimStatusBar.py
index e9f329cc48..0a361999c9 100644
--- a/src/Mod/BIM/BimStatusBar.py
+++ b/src/Mod/BIM/BimStatusBar.py
@@ -117,7 +117,7 @@ def setStatusIcons(show=True):
if statuswidget:
statuswidget.show()
else:
- statuswidget = QtGui.QToolBar()
+ statuswidget = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
statuswidget.setObjectName("BIMStatusWidget")
s = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/General").GetInt("ToolbarIconSize", 24)
statuswidget.setIconSize(QtCore.QSize(s,s))
diff --git a/src/Mod/BIM/bimcommands/BimIfcExplorer.py b/src/Mod/BIM/bimcommands/BimIfcExplorer.py
index 30997d22cd..76cb68e543 100644
--- a/src/Mod/BIM/bimcommands/BimIfcExplorer.py
+++ b/src/Mod/BIM/bimcommands/BimIfcExplorer.py
@@ -106,7 +106,7 @@ class BIM_IfcExplorer:
self.dialog.setObjectName("IfcExplorer")
self.dialog.setWindowTitle(translate("BIM", "Ifc Explorer"))
self.dialog.resize(720, 540)
- toolbar = QtGui.QToolBar()
+ toolbar = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
layout = QtGui.QVBoxLayout(self.dialog)
layout.addWidget(toolbar)
diff --git a/src/Mod/BIM/bimcommands/BimViews.py b/src/Mod/BIM/bimcommands/BimViews.py
index 85ecc03056..ba04e0b648 100644
--- a/src/Mod/BIM/bimcommands/BimViews.py
+++ b/src/Mod/BIM/bimcommands/BimViews.py
@@ -81,7 +81,7 @@ class BIM_Views:
size = FreeCAD.ParamGet(
"User parameter:BaseApp/Preferences/General"
).GetInt("ToolbarIconSize", 24)
- toolbar = QtGui.QToolBar()
+ toolbar = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
toolbar.setIconSize(QtCore.QSize(size, size))
dialog.horizontalLayout.addWidget(toolbar)
for button in [
diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py
index c5fc3c2d33..487d8f703d 100644
--- a/src/Mod/Draft/DraftGui.py
+++ b/src/Mod/Draft/DraftGui.py
@@ -205,7 +205,7 @@ class DraftToolBar:
# add only a dummy widget, since widgets are created on demand
self.baseWidget = DraftBaseWidget()
- self.tray = QtWidgets.QToolBar(None)
+ self.tray = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
self.tray.setObjectName("Draft tray")
self.tray.setWindowTitle("Draft tray")
self.toptray = self.tray